From f6c5bf753b5604c1dfee813f99f28833b135f52b Mon Sep 17 00:00:00 2001 From: Appie Date: Mon, 21 Oct 2024 18:35:11 +0200 Subject: [PATCH] Make fields with const pre-fiiled and readonly #3843 (#4326) * Make fields with const pre-fiiled and readonl * fixed issue with default on root level. * fixed array const populate values * fixed array issue and written tests to cover the behavior * updated changeLog * fixed issue with core failing tests. * changed changeLog to rerun tests * improvement based on feedback * Update packages/utils/test/schema/getDefaultFormStateTest.ts Adding default that should not be used * Update packages/utils/test/schema/getDefaultFormStateTest.ts Fix linter --------- Co-authored-by: Abdallah Al-Soqatri Co-authored-by: Heath C <51679588+heath-freenome@users.noreply.github.com> --- CHANGELOG.md | 3 +- .../src/components/fields/ObjectField.tsx | 6 +- .../src/components/fields/SchemaField.tsx | 4 +- .../utils/src/schema/getDefaultFormState.ts | 44 ++++-- .../test/schema/getDefaultFormStateTest.ts | 146 ++++++++++++++++++ 5 files changed, 188 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1597d24fb8..c23540c7d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,10 +16,11 @@ should change the heading of the (upcoming) version to include a major version b --> -# 5.21.3 +# 5.22.0 ## @rjsf/utils +- Made fields with const property pre-filled and readonly, fixing [#2600](https://github.com/rjsf-team/react-jsonschema-form/issues/2600) - Added `experimental_customMergeAllOf` option to `retrieveSchema` to allow custom merging of `allOf` schemas # 5.21.2 diff --git a/packages/core/src/components/fields/ObjectField.tsx b/packages/core/src/components/fields/ObjectField.tsx index 8399429da5..fd884efc7d 100644 --- a/packages/core/src/components/fields/ObjectField.tsx +++ b/packages/core/src/components/fields/ObjectField.tsx @@ -202,15 +202,18 @@ class ObjectField(schema, uiOptions, idSchema, registry); const disabled = Boolean(uiOptions.disabled ?? props.disabled); - const readonly = Boolean(uiOptions.readonly ?? (props.readonly || props.schema.readOnly || schema.readOnly)); + const readonly = Boolean( + uiOptions.readonly ?? (props.readonly || props.schema.const || props.schema.readOnly || schema.readOnly) + ); const uiSchemaHideError = uiOptions.hideError; // Set hideError to the value provided in the uiSchema, otherwise stick with the prop to propagate to children const hideError = uiSchemaHideError === undefined ? props.hideError : Boolean(uiSchemaHideError); diff --git a/packages/utils/src/schema/getDefaultFormState.ts b/packages/utils/src/schema/getDefaultFormState.ts index 2cbbde4b4a..f51d41d04a 100644 --- a/packages/utils/src/schema/getDefaultFormState.ts +++ b/packages/utils/src/schema/getDefaultFormState.ts @@ -3,6 +3,7 @@ import isEmpty from 'lodash/isEmpty'; import { ANY_OF_KEY, + CONST_KEY, DEFAULT_KEY, DEPENDENCIES_KEY, PROPERTIES_KEY, @@ -30,6 +31,8 @@ import { } from '../types'; import isMultiSelect from './isMultiSelect'; import retrieveSchema, { resolveDependencies } from './retrieveSchema'; +import isConstant from '../isConstant'; +import { JSONSchema7Object } from 'json-schema'; /** Enum that indicates how `schema.additionalItems` should be handled by the `getInnerSchemaForArrayItem()` function. */ @@ -93,6 +96,7 @@ export function getInnerSchemaForArrayItem( obj: GenericObjectType, @@ -101,10 +105,13 @@ function maybeAddDefaultToObject( includeUndefinedValues: boolean | 'excludeObjectChildren', isParentRequired?: boolean, requiredFields: string[] = [], - experimental_defaultFormStateBehavior: Experimental_DefaultFormStateBehavior = {} + experimental_defaultFormStateBehavior: Experimental_DefaultFormStateBehavior = {}, + isConst = false ) { const { emptyObjectFields = 'populateAllDefaults' } = experimental_defaultFormStateBehavior; - if (includeUndefinedValues) { + if (includeUndefinedValues || isConst) { + // If includeUndefinedValues + // Or if the schema has a const property defined, then we should always return the computedDefault since it's coming from the const. obj[key] = computedDefault; } else if (emptyObjectFields !== 'skipDefaults') { if (isObject(computedDefault)) { @@ -194,7 +201,9 @@ export function computeDefaults(validator, schema, rootSchema, formData, experimental_customMergeAllOf) : schema; + const parentConst = retrievedSchema[CONST_KEY]; const objectDefaults = Object.keys(retrievedSchema.properties || {}).reduce( (acc: GenericObjectType, key: string) => { + const propertySchema = get(retrievedSchema, [PROPERTIES_KEY, key]); + // Check if the parent schema has a const property defined, then we should always return the computedDefault since it's coming from the const. + const hasParentConst = isObject(parentConst) && (parentConst as JSONSchema7Object)[key] !== undefined; + const hasConst = (isObject(propertySchema) && CONST_KEY in propertySchema) || hasParentConst; // 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]), { + const computedDefault = computeDefaults(validator, propertySchema, { rootSchema, _recurseList, experimental_defaultFormStateBehavior, @@ -345,7 +359,8 @@ export function getObjectDefaults { expect(() => getDefaultFormState(testValidator, null as unknown as RJSFSchema)).toThrowError('Invalid schema:'); }); + it('test an object const value merge with formData', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + localConst: { + type: 'string', + const: 'local', + }, + RootConst: { + type: 'object', + properties: { + attr1: { + type: 'number', + }, + attr2: { + type: 'boolean', + }, + }, + const: { + attr1: 1, + attr2: true, + }, + }, + RootAndLocalConst: { + type: 'string', + const: 'FromLocal', + }, + fromFormData: { + type: 'string', + }, + }, + const: { + RootAndLocalConst: 'FromRoot', + }, + }; + expect( + getDefaultFormState( + testValidator, + schema, + { + fromFormData: 'fromFormData', + }, + schema, + false, + { emptyObjectFields: 'skipDefaults' } + ) + ).toEqual({ + localConst: 'local', + RootConst: { + attr1: 1, + attr2: true, + }, + RootAndLocalConst: 'FromLocal', + fromFormData: 'fromFormData', + }); + }); it('getInnerSchemaForArrayItem() item of type boolean returns empty schema', () => { expect(getInnerSchemaForArrayItem({ items: [true] }, AdditionalItemsHandling.Ignore, 0)).toEqual({}); }); @@ -48,6 +104,20 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType foo: 42, }); }); + it('test computeDefaults that is passed a schema with a const property', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + test: { + type: 'string', + const: 'test', + }, + }, + }; + expect(computeDefaults(testValidator, schema, { rootSchema: schema })).toEqual({ + test: 'test', + }); + }); it('test an object with an optional property that has a nested required property', () => { const schema: RJSFSchema = { type: 'object', @@ -848,6 +918,59 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType requiredProperty: 'foo', }); }); + it('test an object const value populate as field defaults', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + localConst: { + type: 'string', + const: 'local', + }, + RootConst: { + type: 'object', + properties: { + attr1: { + type: 'number', + }, + attr2: { + type: 'boolean', + }, + }, + const: { + attr1: 1, + attr2: true, + }, + }, + fromFormData: { + type: 'string', + default: 'notUsed', + }, + RootAndLocalConst: { + type: 'string', + const: 'FromLocal', + }, + }, + const: { + RootAndLocalConst: 'FromRoot', + }, + }; + expect( + getObjectDefaults(testValidator, schema, { + rootSchema: schema, + experimental_defaultFormStateBehavior: { emptyObjectFields: 'skipDefaults' }, + rawFormData: { + fromFormData: 'fromFormData', + }, + }) + ).toEqual({ + localConst: 'local', + RootConst: { + attr1: 1, + attr2: true, + }, + RootAndLocalConst: 'FromLocal', + }); + }); it('test an object with an additionalProperties', () => { const schema: RJSFSchema = { type: 'object', @@ -1065,6 +1188,29 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType ) ).toEqual(['Raphael', 'Michaelangelo', 'Unknown', 'Unknown']); }); + it('test an array const value populate as defaults', () => { + const schema: RJSFSchema = { + type: 'array', + minItems: 4, + const: ['ConstFromRoot', 'ConstFromRoot'], + items: { + type: 'string', + const: 'Constant', + }, + }; + + expect( + getArrayDefaults( + testValidator, + schema, + { + rootSchema: schema, + includeUndefinedValues: 'excludeObjectChildren', + }, + ['ConstFromRoot', 'ConstFromRoot'] + ) + ).toEqual(['ConstFromRoot', 'ConstFromRoot', 'Constant', 'Constant']); + }); it('test an array with no defaults', () => { const schema: RJSFSchema = { type: 'array',