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

fix 4215 and 4260 by updating optionsList() to take a uiSchema #4263

Merged
merged 2 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ should change the heading of the (upcoming) version to include a major version b
## @rjsf/core

- Support allowing raising errors from within a custom Widget [#2718](https://github.com/rjsf-team/react-jsonschema-form/issues/2718)
- Updated `ArrayField`, `BooleanField` and `StringField` to call `optionsList()` with the additional `UiSchema` parameter, fixing [#4215](https://github.com/rjsf-team/react-jsonschema-form/issues/4215) and [#4260](https://github.com/rjsf-team/react-jsonschema-form/issues/4260)

## @rjsf/utils

- Updated the `WidgetProps` type to add `es?: ErrorSchema<T>, id?: string` to the params of the `onChange` handler function
- Updated `UIOptionsBaseType` to add the new `enumNames` prop to support an alternate way to provide labels for `enum`s in a schema, fixing [#4215](https://github.com/rjsf-team/react-jsonschema-form/issues/4215)
- Updated `optionsList()` to take an optional `uiSchema` that is used to extract alternate labels for `enum`s or `oneOf`/`anyOf` in a schema, fixing [#4215](https://github.com/rjsf-team/react-jsonschema-form/issues/4215) and [#4260](https://github.com/rjsf-team/react-jsonschema-form/issues/4260)
- NOTE: The generics for `optionsList()` were expanded from `<S extends StrictRJSFSchema = RJSFSchema>` to `<S extends StrictRJSFSchema = RJSFSchema, T = any, F extends FormContextType = any>` to support the `UiSchema`.

## Dev / docs / playground

Expand All @@ -41,7 +45,7 @@ should change the heading of the (upcoming) version to include a major version b

- Updated the `ValidatorType` interface to add an optional `reset?: () => void` prop that can be implemented to reset a validator back to initial constructed state
- Updated the `ParserValidator` to provide a `reset()` function that clears the schema map
- Also updated the default translatable string to use `Markdown` rather than HTML tags since we now render them with `Markdown`
- Also updated the default translatable string to use `Markdown` rather than HTML tags since we now render them with `Markdown`

## @rjsf/validator-ajv8

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/components/fields/ArrayField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,7 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
} = this.props;
const { widgets, schemaUtils, formContext, globalUiOptions } = registry;
const itemsSchema = schemaUtils.retrieveSchema(schema.items as S, items);
const enumOptions = optionsList(itemsSchema);
const enumOptions = optionsList<S, T[], F>(itemsSchema, uiSchema);
const { widget = 'select', title: uiTitle, ...options } = getUiOptions<T[], S, F>(uiSchema, globalUiOptions);
const Widget = getWidget<T[], S, F>(schema, widget, widgets);
const label = uiTitle ?? schema.title ?? name;
Expand Down
42 changes: 24 additions & 18 deletions packages/core/src/components/fields/BooleanField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,22 @@ function BooleanField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extend
let enumOptions: EnumOptionsType<S>[] | undefined;
const label = uiTitle ?? schemaTitle ?? title ?? name;
if (Array.isArray(schema.oneOf)) {
enumOptions = optionsList<S>({
oneOf: schema.oneOf
.map((option) => {
if (isObject(option)) {
return {
...option,
title: option.title || (option.const === true ? yes : no),
};
}
return undefined;
})
.filter((o: any) => o) as S[], // cast away the error that typescript can't grok is fixed
} as unknown as S);
enumOptions = optionsList<S, T, F>(
{
oneOf: schema.oneOf
.map((option) => {
if (isObject(option)) {
return {
...option,
title: option.title || (option.const === true ? yes : no),
};
}
return undefined;
})
.filter((o: any) => o) as S[], // cast away the error that typescript can't grok is fixed
} as unknown as S,
uiSchema
);
} else {
// We deprecated enumNames in v5. It's intentionally omitted from RSJFSchema type, so we need to cast here.
const schemaWithEnumNames = schema as S & { enumNames?: string[] };
Expand All @@ -81,11 +84,14 @@ function BooleanField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extend
},
];
} else {
enumOptions = optionsList<S>({
enum: enums,
// NOTE: enumNames is deprecated, but still supported for now.
enumNames: schemaWithEnumNames.enumNames,
} as unknown as S);
enumOptions = optionsList<S, T, F>(
{
enum: enums,
// NOTE: enumNames is deprecated, but still supported for now.
enumNames: schemaWithEnumNames.enumNames,
} as unknown as S,
uiSchema
);
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/components/fields/StringField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function StringField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends
} = props;
const { title, format } = schema;
const { widgets, formContext, schemaUtils, globalUiOptions } = registry;
const enumOptions = schemaUtils.isSelect(schema) ? optionsList(schema) : undefined;
const enumOptions = schemaUtils.isSelect(schema) ? optionsList<S, T, F>(schema, uiSchema) : undefined;
let defaultWidget = enumOptions ? 'select' : 'text';
if (format && hasWidget<T, S, F>(schema, format, widgets)) {
defaultWidget = format;
Expand Down
33 changes: 28 additions & 5 deletions packages/core/test/BooleanField.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ describe('BooleanField', () => {

const labels = [].map.call(node.querySelectorAll('.field-radio-group label'), (label) => label.textContent);
expect(labels).eql(['Yes', 'No']);
expect(console.warn.calledWithMatch(/The enumNames property is deprecated/)).to.be.true;
expect(console.warn.calledWithMatch(/The "enumNames" property in the schema is deprecated/)).to.be.true;
});

it('should support oneOf titles for radio widgets', () => {
Expand All @@ -413,6 +413,29 @@ describe('BooleanField', () => {
expect(labels).eql(['Yes', 'No']);
});

it('should support oneOf titles for radio widgets, overrides in uiSchema', () => {
const { node } = createFormComponent({
schema: {
type: 'boolean',
oneOf: [
{
const: true,
title: 'Yes',
},
{
const: false,
title: 'No',
},
],
},
formData: true,
uiSchema: { 'ui:widget': 'radio', oneOf: [{ 'ui:title': 'Si!' }, { 'ui:title': 'No!' }] },
});

const labels = [].map.call(node.querySelectorAll('.field-radio-group label'), (label) => label.textContent);
expect(labels).eql(['Si!', 'No!']);
});

it('should preserve oneOf option ordering for radio widgets', () => {
const { node } = createFormComponent({
schema: {
Expand Down Expand Up @@ -495,19 +518,19 @@ describe('BooleanField', () => {
expect(onBlur.calledWith(element.id, false)).to.be.true;
});

it('should support enumNames for select', () => {
it('should support enumNames for select, with overrides in uiSchema', () => {
const { node } = createFormComponent({
schema: {
type: 'boolean',
enumNames: ['Yes', 'No'],
},
formData: true,
uiSchema: { 'ui:widget': 'select' },
uiSchema: { 'ui:widget': 'select', 'ui:enumNames': ['Si!', 'No!'] },
});

const labels = [].map.call(node.querySelectorAll('.field option'), (label) => label.textContent);
expect(labels).eql(['', 'Yes', 'No']);
expect(console.warn.calledWithMatch(/The enumNames property is deprecated/)).to.be.true;
expect(labels).eql(['', 'Si!', 'No!']);
expect(console.warn.calledWithMatch(/TThe "enumNames" property in the schema is deprecated/)).to.be.false;
});

it('should handle a focus event with checkbox', () => {
Expand Down
16 changes: 16 additions & 0 deletions packages/docs/docs/api-reference/uiSchema.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,22 @@ const uiSchema: UiSchema = {
};
```

### enumNames

Allows a user to provide a list of labels for enum values in the schema.

```tsx
import { RJSFSchema, UiSchema } from '@rjsf/utils';

const schema: RJSFSchema = {
type: 'number',
enum: [1, 2, 3],
};
const uiSchema: UiSchema = {
'ui:enumNames': ['one', 'two', 'three'],
};
```

### filePreview

The `FileWidget` can be configured to show a preview of an image or a download link for non-images using this flag.
Expand Down
14 changes: 9 additions & 5 deletions packages/docs/docs/api-reference/utility-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -621,17 +621,21 @@ Return a consistent `id` for the `optionIndex`s of a `Radio` or `Checkboxes` wid

- string: An id for the option index based on the parent `id`

### optionsList&lt;S extends StrictRJSFSchema = RJSFSchema>()
### optionsList&lt;S extends StrictRJSFSchema = RJSFSchema, T = any, F extends FormContextType = any>()

Gets the list of options from the schema. If the schema has an enum list, then those enum values are returned.
The labels for the options will be extracted from the non-standard `enumNames` if it exists otherwise will be the same as the `value`.
If the schema has a `oneOf` or `anyOf`, then the value is the list of `const` values from the schema and the label is either the `schema.title` or the value.
Gets the list of options from the `schema`. If the schema has an enum list, then those enum values are returned.
The labels for the options will be extracted from the non-standard, RJSF-deprecated `enumNames` if it exists, otherwise
the label will be the same as the `value`. If the schema has a `oneOf` or `anyOf`, then the value is the list of
`const` values from the schema and the label is either the `schema.title` or the value. If a `uiSchema` is provided
and it has the `ui:enumNames` matched with `enum` or it has an associated `oneOf` or `anyOf` with a list of objects
containing `ui:title` then the UI schema values will replace the values from the schema.

NOTE: `enumNames` is deprecated and may be removed in a future major version of RJSF.
NOTE: `enumNames` is deprecated and will be removed in a future major version of RJSF. Use the "ui:enumNames" property in the uiSchema instead.

#### Parameters

- schema: S - The schema from which to extract the options list
- uiSchema: UiSchema<T, S, F> - The optional uiSchema from which to get alternate labels for the options

#### Returns

Expand Down
18 changes: 17 additions & 1 deletion packages/docs/docs/json-schema/single.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ const schema: RJSFSchema = {
render(<Form schema={schema} validator={validator} />, document.getElementById('app'));
```

In your JSON Schema, you may also specify `enumNames`, a non-standard field which RJSF can use to label an enumeration. **This behavior is deprecated and may be removed in a future major release of RJSF.**
In your JSON Schema, you may also specify `enumNames`, a non-standard field which RJSF can use to label an enumeration. **This behavior is deprecated and will be removed in a future major release of RJSF. Use the "ui:enumNames" property in the uiSchema instead.**

```tsx
import { RJSFSchema } from '@rjsf/utils';
Expand All @@ -121,6 +121,22 @@ const schema: RJSFSchema = {
render(<Form schema={schema} validator={validator} />, document.getElementById('app'));
```

Same example using the `uiSchema`'s `ui:enumNames` instead.

```tsx
import { RJSFSchema, UiSchema } from '@rjsf/utils';
import validator from '@rjsf/validator-ajv8';

const schema: RJSFSchema = {
type: 'number',
enum: [1, 2, 3],
};
const uiSchema: UiSchema = {
'ui:enumNames': ['one', 'two', 'three'],
};
render(<Form schema={schema} uiSchema={uiSchema} validator={validator} />, document.getElementById('app'));
```

### Disabled attribute for `enum` fields

To disable an option, use the `ui:enumDisabled` property in the uiSchema.
Expand Down
2 changes: 1 addition & 1 deletion packages/docs/docs/migration-guides/v5.x upgrade guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ The utility function `getMatchingOption()` was deprecated in favor of the more a

`enumNames` is a non-standard JSON Schema field that was deprecated in version 5.
`enumNames` could be included in the schema to apply labels that differed from an enumeration value.
This behavior can still be accomplished with `oneOf` or `anyOf` containing `const` values, so `enumNames` support may be removed from a future major version of RJSF.
This behavior can still be accomplished with `oneOf` or `anyOf` containing `const` values, so `enumNames` support will be removed from a future major version of RJSF.
For more information, see [#532](https://github.com/rjsf-team/react-jsonschema-form/issues/532).

##### uiSchema.classNames
Expand Down
53 changes: 39 additions & 14 deletions packages/utils/src/optionsList.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,61 @@
import toConstant from './toConstant';
import { RJSFSchema, EnumOptionsType, StrictRJSFSchema } from './types';
import { RJSFSchema, EnumOptionsType, StrictRJSFSchema, FormContextType, UiSchema } from './types';
import getUiOptions from './getUiOptions';

/** Gets the list of options from the schema. If the schema has an enum list, then those enum values are returned. The
/** Gets the list of options from the `schema`. If the schema has an enum list, then those enum values are returned. The
* labels for the options will be extracted from the non-standard, RJSF-deprecated `enumNames` if it exists, otherwise
* the label will be the same as the `value`. If the schema has a `oneOf` or `anyOf`, then the value is the list of
* `const` values from the schema and the label is either the `schema.title` or the value.
* `const` values from the schema and the label is either the `schema.title` or the value. If a `uiSchema` is provided
* and it has the `ui:enumNames` matched with `enum` or it has an associated `oneOf` or `anyOf` with a list of objects
* containing `ui:title` then the UI schema values will replace the values from the schema.
*
* @param schema - The schema from which to extract the options list
* @param [uiSchema] - The optional uiSchema from which to get alternate labels for the options
* @returns - The list of options from the schema
*/
export default function optionsList<S extends StrictRJSFSchema = RJSFSchema>(
schema: S
export default function optionsList<S extends StrictRJSFSchema = RJSFSchema, T = any, F extends FormContextType = any>(
schema: S,
uiSchema?: UiSchema<T, S, F>
): EnumOptionsType<S>[] | undefined {
// enumNames was deprecated in v5 and is intentionally omitted from the RJSFSchema type.
// Cast the type to include enumNames so the feature still works.
// TODO flip generics to move T first in v6
const schemaWithEnumNames = schema as S & { enumNames?: string[] };
if (schemaWithEnumNames.enumNames && process.env.NODE_ENV !== 'production') {
console.warn('The enumNames property is deprecated and may be removed in a future major release.');
}
if (schema.enum) {
let enumNames: string[] | undefined;
if (uiSchema) {
const { enumNames: uiEnumNames } = getUiOptions<T, S, F>(uiSchema);
enumNames = uiEnumNames;
}
if (!enumNames && schemaWithEnumNames.enumNames) {
// enumNames was deprecated in v5 and is intentionally omitted from the RJSFSchema type.
// Cast the type to include enumNames so the feature still works.
if (process.env.NODE_ENV !== 'production') {
console.warn(
'The "enumNames" property in the schema is deprecated and will be removed in a future major release. Use the "ui:enumNames" property in the uiSchema instead.'
);
}
enumNames = schemaWithEnumNames.enumNames;
}
return schema.enum.map((value, i) => {
const label = (schemaWithEnumNames.enumNames && schemaWithEnumNames.enumNames[i]) || String(value);
const label = enumNames?.[i] || String(value);
return { label, value };
});
}
const altSchemas = schema.oneOf || schema.anyOf;
let altSchemas: S['anyOf'] | S['oneOf'] = undefined;
let altUiSchemas: UiSchema<T, S, F> | undefined = undefined;
if (schema.anyOf) {
altSchemas = schema.anyOf;
altUiSchemas = uiSchema?.anyOf;
} else if (schema.oneOf) {
altSchemas = schema.oneOf;
altUiSchemas = uiSchema?.oneOf;
}
return (
altSchemas &&
altSchemas.map((aSchemaDef) => {
altSchemas.map((aSchemaDef, index) => {
const { title } = getUiOptions<T, S, F>(altUiSchemas?.[index]);
const aSchema = aSchemaDef as S;
const value = toConstant(aSchema);
const label = aSchema.title || String(value);
const label = title || aSchema.title || String(value);
return {
schema: aSchema,
label,
Expand Down
2 changes: 2 additions & 0 deletions packages/utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,8 @@ type UIOptionsBaseType<T = any, S extends StrictRJSFSchema = RJSFSchema, F exten
* to look up an implementation from the `widgets` list or an actual one-off widget implementation itself
*/
widget?: Widget<T, S, F> | string;
/** Allows a user to provide a list of labels for enum values in the schema */
enumNames?: string[];
};

/** The type that represents the Options potentially provided by `ui:options` */
Expand Down
Loading