Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding feature to support const as default bug fix seeming like a regression #4381

Merged
merged 1 commit into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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