Skip to content

Commit 63a860e

Browse files
fix 4215 and 4260 by updating optionsList() to take a uiSchema (#4263)
* fix 4215 and 4260 by updating optionsList() to take a uiSchema Fixes #4215 and #4260 by supporting alternate titles for enums and anyOf/oneOf lists via the uiSchema - In `@rjsf/utils` added support for alternate option labels from the `UiSchema` as follows: - Updated `UIOptionsBaseType` to add the new `enumNames` prop to support an alternate way to provide labels for `enum`s in a schema - Updated `optionsList()` to take an optional `uiSchema` that is used to extract alternate labels for `enum`s or `oneOf`/`anyOf` in a schema - NOTE: The generics for `optionsList()` were expanded to add `T = any, F extends FormContextType = any` to support the `UiSchema` - Added unit tests to maintain 100% coverage - In `@rjsf/core` updated `ArrayField`, `BooleanField` and `StringField` to call `optionsList()` with the additional `UiSchema` parameter - In `docs` added documentation about the new `ui:enumNames` property, fixing up the `enumNames` documentation to indicate it WILL be removed - Also updated the `optionsList()` function's documentation to add the new `uiSchema` prop - Updated the `CHANGELOG.md` accordingly * Update packages/utils/test/optionsList.test.ts Fix typo in test name --------- Co-authored-by: Nick Grosenbacher <nickgrosenbacher@gmail.com>
1 parent 4d3094c commit 63a860e

File tree

12 files changed

+402
-129
lines changed

12 files changed

+402
-129
lines changed

CHANGELOG.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,14 @@ should change the heading of the (upcoming) version to include a major version b
2121
## @rjsf/core
2222

2323
- Support allowing raising errors from within a custom Widget [#2718](https://github.com/rjsf-team/react-jsonschema-form/issues/2718)
24+
- 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)
2425

2526
## @rjsf/utils
2627

2728
- Updated the `WidgetProps` type to add `es?: ErrorSchema<T>, id?: string` to the params of the `onChange` handler function
29+
- 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)
30+
- 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)
31+
- 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`.
2832

2933
## Dev / docs / playground
3034

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

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

4650
## @rjsf/validator-ajv8
4751

packages/core/src/components/fields/ArrayField.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,7 @@ class ArrayField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
612612
} = this.props;
613613
const { widgets, schemaUtils, formContext, globalUiOptions } = registry;
614614
const itemsSchema = schemaUtils.retrieveSchema(schema.items as S, items);
615-
const enumOptions = optionsList(itemsSchema);
615+
const enumOptions = optionsList<S, T[], F>(itemsSchema, uiSchema);
616616
const { widget = 'select', title: uiTitle, ...options } = getUiOptions<T[], S, F>(uiSchema, globalUiOptions);
617617
const Widget = getWidget<T[], S, F>(schema, widget, widgets);
618618
const label = uiTitle ?? schema.title ?? name;

packages/core/src/components/fields/BooleanField.tsx

+24-18
Original file line numberDiff line numberDiff line change
@@ -52,19 +52,22 @@ function BooleanField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extend
5252
let enumOptions: EnumOptionsType<S>[] | undefined;
5353
const label = uiTitle ?? schemaTitle ?? title ?? name;
5454
if (Array.isArray(schema.oneOf)) {
55-
enumOptions = optionsList<S>({
56-
oneOf: schema.oneOf
57-
.map((option) => {
58-
if (isObject(option)) {
59-
return {
60-
...option,
61-
title: option.title || (option.const === true ? yes : no),
62-
};
63-
}
64-
return undefined;
65-
})
66-
.filter((o: any) => o) as S[], // cast away the error that typescript can't grok is fixed
67-
} as unknown as S);
55+
enumOptions = optionsList<S, T, F>(
56+
{
57+
oneOf: schema.oneOf
58+
.map((option) => {
59+
if (isObject(option)) {
60+
return {
61+
...option,
62+
title: option.title || (option.const === true ? yes : no),
63+
};
64+
}
65+
return undefined;
66+
})
67+
.filter((o: any) => o) as S[], // cast away the error that typescript can't grok is fixed
68+
} as unknown as S,
69+
uiSchema
70+
);
6871
} else {
6972
// We deprecated enumNames in v5. It's intentionally omitted from RSJFSchema type, so we need to cast here.
7073
const schemaWithEnumNames = schema as S & { enumNames?: string[] };
@@ -81,11 +84,14 @@ function BooleanField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extend
8184
},
8285
];
8386
} else {
84-
enumOptions = optionsList<S>({
85-
enum: enums,
86-
// NOTE: enumNames is deprecated, but still supported for now.
87-
enumNames: schemaWithEnumNames.enumNames,
88-
} as unknown as S);
87+
enumOptions = optionsList<S, T, F>(
88+
{
89+
enum: enums,
90+
// NOTE: enumNames is deprecated, but still supported for now.
91+
enumNames: schemaWithEnumNames.enumNames,
92+
} as unknown as S,
93+
uiSchema
94+
);
8995
}
9096
}
9197

packages/core/src/components/fields/StringField.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ function StringField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends
3535
} = props;
3636
const { title, format } = schema;
3737
const { widgets, formContext, schemaUtils, globalUiOptions } = registry;
38-
const enumOptions = schemaUtils.isSelect(schema) ? optionsList(schema) : undefined;
38+
const enumOptions = schemaUtils.isSelect(schema) ? optionsList<S, T, F>(schema, uiSchema) : undefined;
3939
let defaultWidget = enumOptions ? 'select' : 'text';
4040
if (format && hasWidget<T, S, F>(schema, format, widgets)) {
4141
defaultWidget = format;

packages/core/test/BooleanField.test.jsx

+28-5
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ describe('BooleanField', () => {
387387

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

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

416+
it('should support oneOf titles for radio widgets, overrides in uiSchema', () => {
417+
const { node } = createFormComponent({
418+
schema: {
419+
type: 'boolean',
420+
oneOf: [
421+
{
422+
const: true,
423+
title: 'Yes',
424+
},
425+
{
426+
const: false,
427+
title: 'No',
428+
},
429+
],
430+
},
431+
formData: true,
432+
uiSchema: { 'ui:widget': 'radio', oneOf: [{ 'ui:title': 'Si!' }, { 'ui:title': 'No!' }] },
433+
});
434+
435+
const labels = [].map.call(node.querySelectorAll('.field-radio-group label'), (label) => label.textContent);
436+
expect(labels).eql(['Si!', 'No!']);
437+
});
438+
416439
it('should preserve oneOf option ordering for radio widgets', () => {
417440
const { node } = createFormComponent({
418441
schema: {
@@ -495,19 +518,19 @@ describe('BooleanField', () => {
495518
expect(onBlur.calledWith(element.id, false)).to.be.true;
496519
});
497520

498-
it('should support enumNames for select', () => {
521+
it('should support enumNames for select, with overrides in uiSchema', () => {
499522
const { node } = createFormComponent({
500523
schema: {
501524
type: 'boolean',
502525
enumNames: ['Yes', 'No'],
503526
},
504527
formData: true,
505-
uiSchema: { 'ui:widget': 'select' },
528+
uiSchema: { 'ui:widget': 'select', 'ui:enumNames': ['Si!', 'No!'] },
506529
});
507530

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

513536
it('should handle a focus event with checkbox', () => {

packages/docs/docs/api-reference/uiSchema.md

+16
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,22 @@ const uiSchema: UiSchema = {
228228
};
229229
```
230230

231+
### enumNames
232+
233+
Allows a user to provide a list of labels for enum values in the schema.
234+
235+
```tsx
236+
import { RJSFSchema, UiSchema } from '@rjsf/utils';
237+
238+
const schema: RJSFSchema = {
239+
type: 'number',
240+
enum: [1, 2, 3],
241+
};
242+
const uiSchema: UiSchema = {
243+
'ui:enumNames': ['one', 'two', 'three'],
244+
};
245+
```
246+
231247
### filePreview
232248

233249
The `FileWidget` can be configured to show a preview of an image or a download link for non-images using this flag.

packages/docs/docs/api-reference/utility-functions.md

+9-5
Original file line numberDiff line numberDiff line change
@@ -621,17 +621,21 @@ Return a consistent `id` for the `optionIndex`s of a `Radio` or `Checkboxes` wid
621621

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

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

626-
Gets the list of options from the schema. If the schema has an enum list, then those enum values are returned.
627-
The labels for the options will be extracted from the non-standard `enumNames` if it exists otherwise will be the same as the `value`.
628-
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.
626+
Gets the list of options from the `schema`. If the schema has an enum list, then those enum values are returned.
627+
The labels for the options will be extracted from the non-standard, RJSF-deprecated `enumNames` if it exists, otherwise
628+
the label will be the same as the `value`. If the schema has a `oneOf` or `anyOf`, then the value is the list of
629+
`const` values from the schema and the label is either the `schema.title` or the value. If a `uiSchema` is provided
630+
and it has the `ui:enumNames` matched with `enum` or it has an associated `oneOf` or `anyOf` with a list of objects
631+
containing `ui:title` then the UI schema values will replace the values from the schema.
629632

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

632635
#### Parameters
633636

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

636640
#### Returns
637641

packages/docs/docs/json-schema/single.md

+17-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ const schema: RJSFSchema = {
107107
render(<Form schema={schema} validator={validator} />, document.getElementById('app'));
108108
```
109109

110-
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.**
110+
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.**
111111

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

124+
Same example using the `uiSchema`'s `ui:enumNames` instead.
125+
126+
```tsx
127+
import { RJSFSchema, UiSchema } from '@rjsf/utils';
128+
import validator from '@rjsf/validator-ajv8';
129+
130+
const schema: RJSFSchema = {
131+
type: 'number',
132+
enum: [1, 2, 3],
133+
};
134+
const uiSchema: UiSchema = {
135+
'ui:enumNames': ['one', 'two', 'three'],
136+
};
137+
render(<Form schema={schema} uiSchema={uiSchema} validator={validator} />, document.getElementById('app'));
138+
```
139+
124140
### Disabled attribute for `enum` fields
125141

126142
To disable an option, use the `ui:enumDisabled` property in the uiSchema.

packages/docs/docs/migration-guides/v5.x upgrade guide.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,7 @@ The utility function `getMatchingOption()` was deprecated in favor of the more a
437437

438438
`enumNames` is a non-standard JSON Schema field that was deprecated in version 5.
439439
`enumNames` could be included in the schema to apply labels that differed from an enumeration value.
440-
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.
440+
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.
441441
For more information, see [#532](https://github.com/rjsf-team/react-jsonschema-form/issues/532).
442442

443443
##### uiSchema.classNames

packages/utils/src/optionsList.ts

+39-14
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,61 @@
11
import toConstant from './toConstant';
2-
import { RJSFSchema, EnumOptionsType, StrictRJSFSchema } from './types';
2+
import { RJSFSchema, EnumOptionsType, StrictRJSFSchema, FormContextType, UiSchema } from './types';
3+
import getUiOptions from './getUiOptions';
34

4-
/** Gets the list of options from the schema. If the schema has an enum list, then those enum values are returned. The
5+
/** Gets the list of options from the `schema`. If the schema has an enum list, then those enum values are returned. The
56
* labels for the options will be extracted from the non-standard, RJSF-deprecated `enumNames` if it exists, otherwise
67
* the label will be the same as the `value`. If the schema has a `oneOf` or `anyOf`, then the value is the list of
7-
* `const` values from the schema and the label is either the `schema.title` or the value.
8+
* `const` values from the schema and the label is either the `schema.title` or the value. If a `uiSchema` is provided
9+
* and it has the `ui:enumNames` matched with `enum` or it has an associated `oneOf` or `anyOf` with a list of objects
10+
* containing `ui:title` then the UI schema values will replace the values from the schema.
811
*
912
* @param schema - The schema from which to extract the options list
13+
* @param [uiSchema] - The optional uiSchema from which to get alternate labels for the options
1014
* @returns - The list of options from the schema
1115
*/
12-
export default function optionsList<S extends StrictRJSFSchema = RJSFSchema>(
13-
schema: S
16+
export default function optionsList<S extends StrictRJSFSchema = RJSFSchema, T = any, F extends FormContextType = any>(
17+
schema: S,
18+
uiSchema?: UiSchema<T, S, F>
1419
): EnumOptionsType<S>[] | undefined {
15-
// enumNames was deprecated in v5 and is intentionally omitted from the RJSFSchema type.
16-
// Cast the type to include enumNames so the feature still works.
20+
// TODO flip generics to move T first in v6
1721
const schemaWithEnumNames = schema as S & { enumNames?: string[] };
18-
if (schemaWithEnumNames.enumNames && process.env.NODE_ENV !== 'production') {
19-
console.warn('The enumNames property is deprecated and may be removed in a future major release.');
20-
}
2122
if (schema.enum) {
23+
let enumNames: string[] | undefined;
24+
if (uiSchema) {
25+
const { enumNames: uiEnumNames } = getUiOptions<T, S, F>(uiSchema);
26+
enumNames = uiEnumNames;
27+
}
28+
if (!enumNames && schemaWithEnumNames.enumNames) {
29+
// enumNames was deprecated in v5 and is intentionally omitted from the RJSFSchema type.
30+
// Cast the type to include enumNames so the feature still works.
31+
if (process.env.NODE_ENV !== 'production') {
32+
console.warn(
33+
'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.'
34+
);
35+
}
36+
enumNames = schemaWithEnumNames.enumNames;
37+
}
2238
return schema.enum.map((value, i) => {
23-
const label = (schemaWithEnumNames.enumNames && schemaWithEnumNames.enumNames[i]) || String(value);
39+
const label = enumNames?.[i] || String(value);
2440
return { label, value };
2541
});
2642
}
27-
const altSchemas = schema.oneOf || schema.anyOf;
43+
let altSchemas: S['anyOf'] | S['oneOf'] = undefined;
44+
let altUiSchemas: UiSchema<T, S, F> | undefined = undefined;
45+
if (schema.anyOf) {
46+
altSchemas = schema.anyOf;
47+
altUiSchemas = uiSchema?.anyOf;
48+
} else if (schema.oneOf) {
49+
altSchemas = schema.oneOf;
50+
altUiSchemas = uiSchema?.oneOf;
51+
}
2852
return (
2953
altSchemas &&
30-
altSchemas.map((aSchemaDef) => {
54+
altSchemas.map((aSchemaDef, index) => {
55+
const { title } = getUiOptions<T, S, F>(altUiSchemas?.[index]);
3156
const aSchema = aSchemaDef as S;
3257
const value = toConstant(aSchema);
33-
const label = aSchema.title || String(value);
58+
const label = title || aSchema.title || String(value);
3459
return {
3560
schema: aSchema,
3661
label,

packages/utils/src/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,8 @@ type UIOptionsBaseType<T = any, S extends StrictRJSFSchema = RJSFSchema, F exten
890890
* to look up an implementation from the `widgets` list or an actual one-off widget implementation itself
891891
*/
892892
widget?: Widget<T, S, F> | string;
893+
/** Allows a user to provide a list of labels for enum values in the schema */
894+
enumNames?: string[];
893895
};
894896

895897
/** The type that represents the Options potentially provided by `ui:options` */

0 commit comments

Comments
 (0)