Skip to content

Commit

Permalink
fix: fixed many issues in oneOf/anyOf functions (rjsf-team#3392)
Browse files Browse the repository at this point in the history
* fix: fixed several issue in oneOf/anyOf functions
Fixes rjsf-team#2944, rjsf-team#3236, rjsf-team#2978 and possibly others
- In `@rjsf/utils`, added new `getClosestMatchingOption()`, `getFirstMatchingOption()` and `sanitizeDataForNewSchema()` schema-based utility functions
  - Deprecated `getMatchingOption()` and updated all calls to it in other utility functions to use `getFirstMatchingOption()`
  - Added 100% unit tests for all new functions, renaming the old `getMatchingOptionsTest.ts` file to `getFirstMatchingOptionsTest.ts`
  - Updated `createSchemaUtils()` and it's associated type to add the three new functions
- In `@rjsf/validator-ajv6` and `@rjsf/validator-ajv8`, updated the `schema.tests.ts` to add the new tests for the new schema-based utility functions
- In `@rjsf/core`, updated the `MultiSchemaField` to use the new `getClosestMatchingOption()` and `sanitizeDataForNewSchema()` utility functions
  - Also updated the render to properly pass props to the widget and the schema field
- In `@rjsf/playground`, updated `onFormDataEdited()` to only change the formData in the state if the `JSON.stringify()` of the old and new values are different
  - Also updated the `npm start` command to add the `--force` option to avoid issues where changes made to other packages weren't getting picked up due to `vite` caching
- Updated the `utility-functions.md` file to document the new schema-based functions and to fix up incorrect strike-through caused by the unescaped `<S>` generic
- Updated the `5.x upgrade guide.md` file to document the new utility functions and the deprecation of `getMatchingOption()`

* - Fixed a few small issues exposed by trying to use the playground in rjsf-team#2375
- Also updated the `CHANGELOG.md` to include all of the fixed issues

* - Fix rjsf-team#2538 by fixing additionalProperties to deal with allOf/anyOf/oneOf

* - Updated `getSchemaType()` to grab the type of the first element of a `oneOf`/`anyOf`

* - Allow `formData` in `getClosestMatchingOption()` to accept `undefined`

* - Responded to reviewer feedback

* - Deal with sanitizing data when both `array.items` elements are booleans and have the same value

* - Fixed issue with const being assigned default value incorrectly and handle readOnly default values like const
- Updated some documentation in the types and createSchemaUtils
  • Loading branch information
heath-freenome authored and shijistar committed Jun 8, 2023
1 parent d542fbf commit fa3213e
Show file tree
Hide file tree
Showing 31 changed files with 2,213 additions and 121 deletions.
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,38 @@ it according to semantic versioning. For example, if your PR adds a breaking cha
should change the heading of the (upcoming) version to include a major version bump.
-->
# 5.0.0-beta-18

## @rjsf/core
- Updated `MultiSchemaField` to utilize the new `getClosestMatchingOption()` and `sanitizeDataForNewSchema()` functions, fixing the following issues:
- [#3236](https://github.com/rjsf-team/react-jsonschema-form/issues/3236)
- [#2978](https://github.com/rjsf-team/react-jsonschema-form/issues/2978)
- [#2944](https://github.com/rjsf-team/react-jsonschema-form/issues/2944)
- [#2202](https://github.com/rjsf-team/react-jsonschema-form/issues/2202)
- [#2183](https://github.com/rjsf-team/react-jsonschema-form/issues/2183)
- [#2086](https://github.com/rjsf-team/react-jsonschema-form/issues/2086)
- [#2069](https://github.com/rjsf-team/react-jsonschema-form/issues/2069)
- [#1661](https://github.com/rjsf-team/react-jsonschema-form/issues/1661)
- And probably others
- Updated `ObjectField` to deal with `additionalProperties` with `oneOf`/`anyOf`, fixing [#2538](https://github.com/rjsf-team/react-jsonschema-form/issues/2538)

## @rjsf/material-ui
- Fix shrinking of `SelectWidget` label only if value is not empty, fixing [#3369](https://github.com/rjsf-team/react-jsonschema-form/issues/3369)

## @rjsf/mui
- Fix shrinking of `SelectWidget` label only if value is not empty, fixing [#3369](https://github.com/rjsf-team/react-jsonschema-form/issues/3369)

## @rjsf/utils
- Added new `getClosestMatchingOption()`, `getFirstMatchingOption()` and `sanitizeDataForNewSchema()` schema-based utility functions
- Deprecated `getMatchingOption()` and updated all calls to it in other utility functions to use `getFirstMatchingOption()`
- Updated `stubExistingAdditionalProperties()` to deal with `additionalProperties` with `oneOf`/`anyOf`, fixing [#2538](https://github.com/rjsf-team/react-jsonschema-form/issues/2538)
- Updated `getSchemaType()` to grab the type of the first element of a `oneOf`/`anyOf`, fixing [#1654](https://github.com/rjsf-team/react-jsonschema-form/issues/1654)

## Dev / docs / playground
- Updated the playground to `onFormDataEdited()` to only change the formData in the state if the `JSON.stringify()` of the old and new values are different, partially fixing [#3236](https://github.com/rjsf-team/react-jsonschema-form/issues/3236)
- Updated the playground `npm start` command to always use the `--force` option to avoid issues where changes made to other packages weren't getting picked up due to `vite` caching
- Updated the documentation for `utility-functions` and the `5.x upgrade guide` to add the new utility functions and to document the deprecation of `getMatchingOption()`

# 5.0.0-beta-17

## @rjsf/antd
Expand Down
8 changes: 7 additions & 1 deletion docs/5.x upgrade guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Unfortunately, there is required work pending to properly support React 18, so u
There are four new packages added in RJSF version 5:

- `@rjsf/utils`: All of the [utility functions](https://react-jsonschema-form.readthedocs.io/en/stable/api-reference/utiltity-functions) previously imported from `@rjsf/core/utils` as well as the Typescript types for RJSF version 5.
- The following new utility functions were added: `createSchemaUtils()`, `getInputProps()`, `mergeValidationData()` and `processSelectValue()`
- The following new utility functions were added: `ariaDescribedByIds()`, `createSchemaUtils()`, `descriptionId()`, `enumOptionsDeselectValue()`, `enumOptionsSelectValue()`, `errorId()`, `examplesId()`, `getClosestMatchingOption()`, `getFirstMatchingOption()`, `getInputProps()`, `helpId()`, `mergeValidationData()`, `optionId()`, `processSelectValue()`, `sanitizeDataForNewSchema()` and `titleId()`
- `@rjsf/validator-ajv6`: The [ajv](https://github.com/ajv-validator/ajv)-v6-based validator refactored out of `@rjsf/core@4.x`, that implements the `ValidatorType` interface defined in `@rjsf/utils`.
- `@rjsf/validator-ajv8`: The [ajv](https://github.com/ajv-validator/ajv)-v8-based validator that is an upgrade of the `@rjsf/validator-ajv6`, that implements the `ValidatorType` interface defined in `@rjsf/utils`. See the ajv 6 to 8 [migration guide](https://ajv.js.org/v6-to-v8-migration.html) for more information.
- `@rjsf/mui`: Previously `@rjsf/material-ui/v5`, now provided as its own theme.
Expand Down Expand Up @@ -231,6 +231,7 @@ render((
In version 5, all the utility functions that were previously accessed via `import { utils } from '@rjsf/core';` are now available via `import utils from '@rjsf/utils';`.
Because of the decoupling of validation from `@rjsf/core` there is a breaking change for all the [validator-based utility functions](https://react-jsonschema-form.readthedocs.io/en/stable/api-reference/utiltity-functions#validator-based-utility-functions), since they now require an additional `ValidatorType` parameter.
More over, one previously exported function `resolveSchema()` is no longer exposed in the `@rjsf/utils`, so use `retrieveSchema()` instead.
Finally, the function `getMatchingOption()` has been deprecated in favor of `getFirstMatchingOption()`.

If you have built custom fields or widgets that utilized any of these breaking-change functions, don't worry, there is a quick and easy solution for you.
The `registry` has a breaking-change which removes the previously deprecated `definitions` property while adding the new `schemaUtils` property.
Expand Down Expand Up @@ -259,8 +260,10 @@ import { RJSFSchema, WidgetProps, getUiOptions } from '@rjsf/utils';
function YourWidget(props: WidgetProps) {
const { registry, uiSchema } = props;
const { schemaUtils } = registry;
// const matchingOption = getMatchingOption({}, options, rootSchema); <- version 4
// const isMultiSelect = isMultiSelect(schema, rootSchema); <- version 4
// const newSchema = resolveSchema(schema, formData, rootSchema); <- version 4
const matchingOption = schemaUtils.getFirstMatchingOption({}, options);
const isMultiSelect = schemaUtils.isMultiSelect(schema);
const newSchema: RJSFSchema = schemaUtils.retrieveSchema(schema, formData);
const options = getUiOptions(uiSchema);
Expand Down Expand Up @@ -399,6 +402,9 @@ From v5, the child fields will correctly use the parent id when generating its o

#### Deprecations added in v5

##### getMatchingOption()
The utility function `getMatchingOption()` was deprecated in favor of the more aptly named `getFirstMatchingOption()` which has the exact same implementation.

##### Non-standard `enumNames` property

`enumNames` is a non-standard JSON Schema field that was deprecated in version 5.
Expand Down
62 changes: 54 additions & 8 deletions docs/api-reference/utility-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,22 +99,22 @@ Return a consistent `id` for the field description element.
Removes the `value` from the currently `selected` list of values.

#### Parameters
- value: EnumOptionsType<S>["value"] - The value that should be selected
- selected: EnumOptionsType<S>["value"][] - The current list of selected values
- value: EnumOptionsType\<S>["value"] - The value that should be selected
- selected: EnumOptionsType\<S>["value"][] - The current list of selected values

#### Returns
- EnumOptionsType<S>["value"][]: The updated `selected` list with the `value` removed from it
- EnumOptionsType\<S>["value"][]: The updated `selected` list with the `value` removed from it

### enumOptionsSelectValue\<S extends StrictRJSFSchema = RJSFSchema>()
Add the `value` to the list of `selected` values in the proper order as defined by `allEnumOptions`.

#### Parameters
- value: EnumOptionsType<S>["value"] - The value that should be selected
- selected: EnumOptionsType<S>["value"][] - The current list of selected values
- allEnumOptions: EnumOptionsType<S>[] - The list of all the known enumOptions
- value: EnumOptionsType\<S>["value"] - The value that should be selected
- selected: EnumOptionsType\<S>["value"][] - The current list of selected values
- allEnumOptions: EnumOptionsType\<S>[] - The list of all the known enumOptions

#### Returns
- EnumOptionsType<S>["value"][]: The updated list of selected enum values with `value` added to it in the proper location
- EnumOptionsType\<S>["value"][]: The updated list of selected enum values with `value` added to it in the proper location

### errorId<T = any>()
Return a consistent `id` for the field error element.
Expand Down Expand Up @@ -344,7 +344,7 @@ Return a consistent `id` for the `option`s of a `Radio` or `Checkboxes` widget

#### Parameters
- id: string - The id of the parent component for the option
- option: EnumOptionsType<S> - The option for which the id is desired
- option: EnumOptionsType\<S> - The option for which the id is desired

#### Returns
- string: An id for the option based on the parent `id`
Expand Down Expand Up @@ -517,8 +517,38 @@ Determines whether the combination of `schema` and `uiSchema` properties indicat
#### Returns
- boolean: True if the label should be displayed or false if it should not

### getClosestMatchingOption<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>()
Determines which of the given `options` provided most closely matches the `formData`.
Returns the index of the option that is valid and is the closest match, or 0 if there is no match.

The closest match is determined using the number of matching properties, and more heavily favors options with matching readOnly, default, or const values.

#### Parameters
- validator: ValidatorType<T, S, F> - An implementation of the `ValidatorType` interface that will be used when necessary
- rootSchema: S - The root schema, used to primarily to look up `$ref`s
- formData: T | undefined - The current formData, if any, used to figure out a match
- options: S[] - The list of options to find a matching options from
- [selectedOption=-1]: number - The index of the currently selected option, defaulted to -1 if not specified

#### Returns
- number: The index of the option that is the closest match to the `formData` or the `selectedOption` if no match

### getFirstMatchingOption<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>()
Given the `formData` and list of `options`, attempts to find the index of the first option that matches the data.
Always returns the first option if there is nothing that matches.

#### Parameters
- validator: ValidatorType<T, S, F> - An implementation of the `ValidatorType` interface that will be used when necessary
- formData: T | undefined - The current formData, if any, used to figure out a match
- options: S[] - The list of options to find a matching options from
- rootSchema: S - The root schema, used to primarily to look up `$ref`s

#### Returns
- number: The index of the first matched option or 0 if none is available

### getMatchingOption<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>()
Given the `formData` and list of `options`, attempts to find the index of the option that best matches the data.
Deprecated, use `getFirstMatchingOption()` instead.

#### Parameters
- validator: ValidatorType<T, S, F> - An implementation of the `ValidatorType` interface that will be used when necessary
Expand Down Expand Up @@ -589,6 +619,22 @@ potentially recursive resolution.
#### Returns
- RJSFSchema: The schema having its conditions, additional properties, references and dependencies resolved

### sanitizeDataForNewSchema<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>()
Sanitize the `data` associated with the `oldSchema` so it is considered appropriate for the `newSchema`.
If the new schema does not contain any properties, then `undefined` is returned to clear all the form data.
Due to the nature of schemas, this sanitization happens recursively for nested objects of data.
Also, any properties in the old schema that are non-existent in the new schema are set to `undefined`.

#### Parameters
- validator: ValidatorType<T, S, F> - An implementation of the `ValidatorType` interface that will be used when necessary
- rootSchema: S - The root JSON schema of the entire form
- [newSchema]: S - The new schema for which the data is being sanitized
- [oldSchema]: S - The old schema from which the data originated
- [data={}]: any - The form data associated with the schema, defaulting to an empty object when undefined

#### Returns
- T: The new form data, with all the fields uniquely associated with the old schema set to `undefined`. Will return `undefined` if the new schema is not an object containing properties.

### toIdSchema<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>()
Generates an `IdSchema` object for the `schema`, recursively

Expand Down
129 changes: 55 additions & 74 deletions packages/core/src/components/fields/MultiSchemaField.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import React, { Component } from "react";
import get from "lodash/get";
import isEmpty from "lodash/isEmpty";
import omit from "lodash/omit";
import {
getUiOptions,
getWidget,
guessType,
deepEquals,
FieldProps,
FormContextType,
RJSFSchema,
StrictRJSFSchema,
ERRORS_KEY,
} from "@rjsf/utils";
import has from "lodash/has";
import unset from "lodash/unset";

/** Type used for the state of the `AnyOfField` component */
type AnyOfFieldState = {
Expand Down Expand Up @@ -83,8 +84,12 @@ class AnyOfField<
getMatchingOption(selectedOption: number, formData: T, options: S[]) {
const { schemaUtils } = this.props.registry;

const option = schemaUtils.getMatchingOption(formData, options);
if (option !== 0) {
const option = schemaUtils.getClosestMatchingOption(
formData,
options,
selectedOption
);
if (option > 0) {
return option;
}
// If the form data matches none of the options, use the currently selected
Expand All @@ -98,53 +103,40 @@ class AnyOfField<
*
* @param option -
*/
onOptionChange = (option: any) => {
const selectedOption = parseInt(option, 10);
onOptionChange = (option?: string) => {
const { selectedOption } = this.state;
const { formData, onChange, options, registry } = this.props;
const { schemaUtils } = registry;
const newOption = schemaUtils.retrieveSchema(
options[selectedOption],
const intOption = option !== undefined ? parseInt(option, 10) : -1;
if (intOption === selectedOption) {
return;
}
const newOption =
intOption >= 0
? schemaUtils.retrieveSchema(options[intOption], formData)
: undefined;
const oldOption =
selectedOption >= 0
? schemaUtils.retrieveSchema(options[selectedOption], formData)
: undefined;

let newFormData = schemaUtils.sanitizeDataForNewSchema(
newOption,
oldOption,
formData
);

// If the new option is of type object and the current data is an object,
// discard properties added using the old option.
let newFormData: T | undefined = undefined;
if (
guessType(formData) === "object" &&
(newOption.type === "object" || newOption.properties)
) {
newFormData = Object.assign({}, formData);

const optionsToDiscard = options.slice();
optionsToDiscard.splice(selectedOption, 1);

// Discard any data added using other options
for (const option of optionsToDiscard) {
if (option.properties) {
for (const key in option.properties) {
if (has(newFormData, key)) {
unset(newFormData, key);
}
}
}
}
}
// Call getDefaultFormState to make sure defaults are populated on change. Pass "excludeObjectChildren"
// so that only the root objects themselves are created without adding undefined children properties
onChange(
schemaUtils.getDefaultFormState(
options[selectedOption],
if (newFormData && newOption) {
// Call getDefaultFormState to make sure defaults are populated on change. Pass "excludeObjectChildren"
// so that only the root objects themselves are created without adding undefined children properties
newFormData = schemaUtils.getDefaultFormState(
newOption,
newFormData,
"excludeObjectChildren"
) as T,
undefined,
this.getFieldId()
);
) as T;
}
onChange(newFormData, undefined, this.getFieldId());

this.setState({
selectedOption: parseInt(option, 10),
});
this.setState({ selectedOption: intOption });
};

getFieldId() {
Expand All @@ -158,19 +150,11 @@ class AnyOfField<
*/
render() {
const {
name,
baseType,
disabled = false,
readonly = false,
hideError = false,
errorSchema = {},
formData,
formContext,
idPrefix,
idSeparator,
idSchema,
onBlur,
onChange,
onFocus,
options,
registry,
Expand All @@ -180,8 +164,16 @@ class AnyOfField<
const { widgets, fields } = registry;
const { SchemaField: _SchemaField } = fields;
const { selectedOption } = this.state;
const { widget = "select", ...uiOptions } = getUiOptions<T, S, F>(uiSchema);
const {
widget = "select",
placeholder,
autofocus,
autocomplete,
...uiOptions
} = getUiOptions<T, S, F>(uiSchema);
const Widget = getWidget<T, S, F>({ type: "number" }, widget, widgets);
const rawErrors = get(errorSchema, ERRORS_KEY, []);
const fieldErrorSchema = omit(errorSchema, [ERRORS_KEY]);

const option = options[selectedOption] || null;
let optionSchema;
Expand All @@ -208,33 +200,22 @@ class AnyOfField<
onChange={this.onOptionChange}
onBlur={onBlur}
onFocus={onFocus}
disabled={disabled || isEmpty(enumOptions)}
multiple={false}
rawErrors={rawErrors}
errorSchema={fieldErrorSchema}
value={selectedOption}
options={{ enumOptions }}
options={{ enumOptions, ...uiOptions }}
registry={registry}
formContext={formContext}
{...uiOptions}
placeholder={placeholder}
autocomplete={autocomplete}
autofocus={autofocus}
label=""
/>
</div>
{option !== null && (
<_SchemaField
name={name}
schema={optionSchema}
uiSchema={uiSchema}
errorSchema={errorSchema}
idSchema={idSchema}
idPrefix={idPrefix}
idSeparator={idSeparator}
formData={formData}
formContext={formContext}
onChange={onChange}
onBlur={onBlur}
onFocus={onFocus}
registry={registry}
disabled={disabled}
readonly={readonly}
hideError={hideError}
/>
<_SchemaField {...this.props} schema={optionSchema} />
)}
</div>
);
Expand Down
Loading

0 comments on commit fa3213e

Please sign in to comment.