Skip to content

Commit

Permalink
Adding feature to support const as default bug fix seeming like a reg…
Browse files Browse the repository at this point in the history
…ression (#4381)

Fixes #4344, #4361 and #4377
- In `@rjsf/utils`:
  - Updated the `Experimental_DefaultFormStateBehavior` type to add new optional `constAsDefaults` prop with three choices
  - Updated `getDefaultFormState()` to respond to the new `constAsDefaults` feature to limit `const` as defaults based on the `never` or `skipOneOf` choices
  - Added tests for `getDefaultFormState()` to verify the new feature
- In `@rjsf/core`:
  - Updated `SchemaField` to remove making the field readonly when const
- In `playground`:
  - Updated `Header` to add support for `constAsDefaults`
- Updated the `CHANGELOG.md` accordingly
  • Loading branch information
heath-freenome authored Nov 15, 2024
1 parent 1e12bfe commit e71cb8d
Show file tree
Hide file tree
Showing 7 changed files with 305 additions and 10 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions packages/core/src/components/fields/SchemaField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,7 @@ function SchemaFieldRender<T = any, S extends StrictRJSFSchema = RJSFSchema, F e

const FieldComponent = getFieldComponent<T, S, F>(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);
Expand Down
12 changes: 12 additions & 0 deletions packages/docs/docs/api-reference/form-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
22 changes: 22 additions & 0 deletions packages/playground/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
30 changes: 23 additions & 7 deletions packages/utils/src/schema/getDefaultFormState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -199,9 +200,10 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
let defaults: T | T[] | undefined = parentDefaults;
// If we get a new schema, then we need to recompute defaults again for the new schema found.
let schemaToCompute: S | null = null;
let experimental_dfsb_to_compute = experimental_defaultFormStateBehavior;
let updatedRecurseList = _recurseList;

if (isConstant(schema)) {
if (schema[CONST_KEY] && experimental_defaultFormStateBehavior?.constAsDefaults !== 'never') {
defaults = schema.const as unknown as T;
} else if (isObject(defaults) && isObject(schema.default)) {
// For object defaults, only override parent defaults that are defined in
Expand Down Expand Up @@ -250,6 +252,15 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
return undefined;
}
const discriminator = getDiscriminatorFieldFromSchema<S>(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<T, S, F>(
validator,
Expand Down Expand Up @@ -285,7 +296,7 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
rootSchema,
includeUndefinedValues,
_recurseList: updatedRecurseList,
experimental_defaultFormStateBehavior,
experimental_defaultFormStateBehavior: experimental_dfsb_to_compute,
parentDefaults: defaults as T | undefined,
rawFormData: formData as T,
required,
Expand Down Expand Up @@ -337,9 +348,12 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
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.
// 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<T, S, F>(validator, propertySchema, {
Expand Down Expand Up @@ -481,8 +495,10 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
}
}

// Check if the schema has a const property defined, then we should always return the computedDefault since it's coming from the const.
const hasConst = isObject(schema) && CONST_KEY in schema;
// Check if the 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 hasConst =
isObject(schema) && CONST_KEY in schema && experimental_defaultFormStateBehavior?.constAsDefaults !== 'never';
if (hasConst === false) {
if (neverPopulate) {
return defaults ?? emptyDefault;
Expand Down
12 changes: 12 additions & 0 deletions packages/utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,18 @@ export type Experimental_DefaultFormStateBehavior = {
* default value instead
*/
mergeDefaultsIntoFormData?: 'useFormDataIfPresent' | 'useDefaultIfFormDataUndefined';
/** 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.
* - `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
*
*/
constAsDefaults?: 'always' | 'skipOneOf' | 'never';
};

/** Optional function that allows for custom merging of `allOf` schemas
Expand Down
Loading

0 comments on commit e71cb8d

Please sign in to comment.