diff --git a/CHANGELOG.md b/CHANGELOG.md index 0265353174..fc45839147 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,21 @@ should change the heading of the (upcoming) version to include a major version b --> +# 5.23.0 + +## @rjsf/core + +- Updated `SchemaField` to no longer make schema fields with const read-only by default, partially fixing [#4344](https://github.com/rjsf-team/react-jsonschema-form/issues/4344) + +## @rjsf/utils + +- Updated `Experimental_DefaultFormStateBehavior` to add a new `constAsDefaults` option +- Updated `getDefaultFormState()` to use the new `constAsDefaults` option to control how const is used for defaulting, fixing [#4344](https://github.com/rjsf-team/react-jsonschema-form/issues/4344), [#4361](https://github.com/rjsf-team/react-jsonschema-form/issues/4361) and [#4377](https://github.com/rjsf-team/react-jsonschema-form/issues/4377) + +## Dev / docs / playground + +- Updated the playground to add a selector for the `constAsDefaults` option + # 5.22.4 ## @rjsf/utils diff --git a/packages/core/src/components/fields/SchemaField.tsx b/packages/core/src/components/fields/SchemaField.tsx index b8397532b7..c7b8db77e5 100644 --- a/packages/core/src/components/fields/SchemaField.tsx +++ b/packages/core/src/components/fields/SchemaField.tsx @@ -151,9 +151,7 @@ function SchemaFieldRender(schema, uiOptions, idSchema, registry); const disabled = Boolean(uiOptions.disabled ?? props.disabled); - const readonly = Boolean( - uiOptions.readonly ?? (props.readonly || props.schema.const || props.schema.readOnly || schema.readOnly) - ); + const readonly = Boolean(uiOptions.readonly ?? (props.readonly || 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/docs/docs/api-reference/form-props.md b/packages/docs/docs/api-reference/form-props.md index e3aa52215a..8c14cd92ff 100644 --- a/packages/docs/docs/api-reference/form-props.md +++ b/packages/docs/docs/api-reference/form-props.md @@ -251,6 +251,18 @@ render( ); ``` +### constAsDefaults + +Optional enumerated flag controlling how const values are merged into the form data as defaults when dealing with undefined values, defaulting to `always`. +The defaulting behavior for this flag will always be controlled by the `emptyObjectField` flag value. +For instance, if `populateRequiredDefaults` is set and the const value is not required, it will not be set. + +| Flag Value | Description | +| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `always` | A const value will always be merged into the form as a default. If there is are const values in a `oneOf` (for instance to create an enumeration with title different from the values), the first const value will be defaulted | +| `skipOneOf` | If const is in a `oneOf` it will NOT pick the first value as a default | +| `never` | A const value will never be used as a default | + ### mergeDefaultsIntoFormData Optional enumerated flag controlling how the defaults are merged into the form data when dealing with undefined values, defaulting to `useFormDataIfPresent`. diff --git a/packages/playground/src/components/Header.tsx b/packages/playground/src/components/Header.tsx index a2c77deac3..873666c6e3 100644 --- a/packages/playground/src/components/Header.tsx +++ b/packages/playground/src/components/Header.tsx @@ -128,6 +128,28 @@ const liveSettingsSelectSchema: RJSFSchema = { }, ], }, + constAsDefaults: { + type: 'string', + title: 'const as default behavior', + default: 'always', + oneOf: [ + { + type: 'string', + title: 'A const value will always be merged into the form as a default', + enum: ['always'], + }, + { + type: 'string', + title: 'If const is in a `oneOf` it will NOT pick the first value as a default', + enum: ['skipOneOf'], + }, + { + type: 'string', + title: 'A const value will never be used as a default', + enum: ['never'], + }, + ], + }, emptyObjectFields: { type: 'string', title: 'Object fields default behavior', diff --git a/packages/utils/src/schema/getDefaultFormState.ts b/packages/utils/src/schema/getDefaultFormState.ts index b765da2fab..580e9f8d98 100644 --- a/packages/utils/src/schema/getDefaultFormState.ts +++ b/packages/utils/src/schema/getDefaultFormState.ts @@ -31,9 +31,10 @@ import { } from '../types'; import isMultiSelect from './isMultiSelect'; import retrieveSchema, { resolveDependencies } from './retrieveSchema'; -import isConstant from '../isConstant'; import { JSONSchema7Object } from 'json-schema'; +const PRIMITIVE_TYPES = ['string', 'number', 'integer', 'boolean', 'null']; + /** Enum that indicates how `schema.additionalItems` should be handled by the `getInnerSchemaForArrayItem()` function. */ export enum AdditionalItemsHandling { @@ -199,9 +200,10 @@ export function computeDefaults(schema); + const { type = 'null' } = remaining; + if ( + !Array.isArray(type) && + PRIMITIVE_TYPES.includes(type) && + experimental_dfsb_to_compute?.constAsDefaults === 'skipOneOf' + ) { + // If we are in a oneOf of a primitive type, then we want to pass constAsDefaults as 'never' for the recursion + experimental_dfsb_to_compute = { ...experimental_dfsb_to_compute, constAsDefaults: 'never' }; + } schemaToCompute = oneOf![ getClosestMatchingOption( validator, @@ -285,7 +296,7 @@ export function computeDefaults { 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. + // Check if the parent schema has a const property defined AND we are supporting const as defaults, 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; + const hasConst = + ((isObject(propertySchema) && CONST_KEY in propertySchema) || hasParentConst) && + experimental_defaultFormStateBehavior?.constAsDefaults !== 'never'; // Compute the defaults for this node, with the parent defaults we might // have from a previous run: defaults[key]. const computedDefault = computeDefaults(validator, propertySchema, { @@ -481,8 +495,10 @@ export function getArrayDefaults { + 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', constAsDefaults: 'never' } + ) + ).toEqual({ + fromFormData: 'fromFormData', + }); + }); it('test an object with deep nested dependencies with formData', () => { const schema: RJSFSchema = { type: 'object', @@ -289,6 +339,106 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType test: 'test', }); }); + it('test computeDefaults that is passed a schema with a const property and constAsDefaults is never', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + test: { + type: 'string', + const: 'test', + }, + }, + }; + expect( + computeDefaults(testValidator, schema, { + rootSchema: schema, + experimental_defaultFormStateBehavior: { constAsDefaults: 'never' }, + }) + ).toEqual({}); + }); + it('test oneOf with const values and constAsDefaults is always', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + oneOfField: { + title: 'One Of Field', + type: 'string', + oneOf: [ + { + const: 'username', + title: 'Username and password', + }, + { + const: 'secret', + title: 'SSO', + }, + ], + }, + }, + required: ['oneOfField'], + }; + expect( + computeDefaults(testValidator, schema, { + rootSchema: schema, + experimental_defaultFormStateBehavior: { constAsDefaults: 'always' }, + }) + ).toEqual({ oneOfField: 'username' }); + }); + it('test oneOf with const values and constAsDefaults is skipOneOf', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + oneOfField: { + title: 'One Of Field', + type: 'string', + oneOf: [ + { + const: 'username', + title: 'Username and password', + }, + { + const: 'secret', + title: 'SSO', + }, + ], + }, + }, + required: ['oneOfField'], + }; + const result = computeDefaults(testValidator, schema, { + rootSchema: schema, + experimental_defaultFormStateBehavior: { constAsDefaults: 'skipOneOf' }, + }); + expect(result).toEqual({}); + }); + it('test oneOf with const values and constAsDefaults is never', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + oneOfField: { + title: 'One Of Field', + type: 'string', + oneOf: [ + { + const: 'username', + title: 'Username and password', + }, + { + const: 'secret', + title: 'SSO', + }, + ], + }, + }, + required: ['oneOfField'], + }; + expect( + computeDefaults(testValidator, schema, { + rootSchema: schema, + experimental_defaultFormStateBehavior: { constAsDefaults: 'never' }, + }) + ).toEqual({}); + }); it('test an object with an optional property that has a nested required property', () => { const schema: RJSFSchema = { type: 'object', @@ -1257,6 +1407,52 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType RootAndLocalConst: 'FromLocal', }); }); + it('test an object const value NOT populate as field defaults when constAsDefault is never', () => { + 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', constAsDefaults: 'never' }, + rawFormData: { + fromFormData: 'fromFormData', + }, + }) + ).toEqual({}); + }); it('test an object with an additionalProperties', () => { const schema: RJSFSchema = { type: 'object', @@ -1497,6 +1693,30 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType ) ).toEqual(['ConstFromRoot', 'ConstFromRoot', 'Constant', 'Constant']); }); + it('test an array const value NOT populate as defaults when constAsDefaults is never', () => { + const schema: RJSFSchema = { + type: 'array', + minItems: 4, + const: ['ConstFromRoot', 'ConstFromRoot'], + items: { + type: 'string', + const: 'Constant', + }, + }; + + expect( + getArrayDefaults( + testValidator, + schema, + { + rootSchema: schema, + includeUndefinedValues: 'excludeObjectChildren', + experimental_defaultFormStateBehavior: { constAsDefaults: 'never' }, + }, + ['ConstFromRoot', 'ConstFromRoot'] + ) + ).toEqual(['ConstFromRoot', 'ConstFromRoot']); + }); it('test an array with no defaults', () => { const schema: RJSFSchema = { type: 'array',