@rjsf/utils 6.4.1 → 6.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +198 -84
- package/dist/index.cjs.map +4 -4
- package/dist/utils.esm.js +198 -84
- package/dist/utils.esm.js.map +4 -4
- package/dist/utils.umd.js +185 -80
- package/lib/enumOptionSelectedValue.d.ts +15 -0
- package/lib/enumOptionSelectedValue.js +28 -0
- package/lib/enumOptionSelectedValue.js.map +1 -0
- package/lib/enumOptionValueDecoder.d.ts +17 -0
- package/lib/enumOptionValueDecoder.js +53 -0
- package/lib/enumOptionValueDecoder.js.map +1 -0
- package/lib/enumOptionValueEncoder.d.ts +15 -0
- package/lib/enumOptionValueEncoder.js +27 -0
- package/lib/enumOptionValueEncoder.js.map +1 -0
- package/lib/getInputProps.js +9 -0
- package/lib/getInputProps.js.map +1 -1
- package/lib/getOptionValueFormat.d.ts +16 -0
- package/lib/getOptionValueFormat.js +17 -0
- package/lib/getOptionValueFormat.js.map +1 -0
- package/lib/index.d.ts +7 -2
- package/lib/index.js +7 -2
- package/lib/index.js.map +1 -1
- package/lib/removeOptionalEmptyObjects.d.ts +17 -0
- package/lib/removeOptionalEmptyObjects.js +108 -0
- package/lib/removeOptionalEmptyObjects.js.map +1 -0
- package/lib/resolveUiSchema.d.ts +5 -19
- package/lib/resolveUiSchema.js +49 -95
- package/lib/resolveUiSchema.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/types.d.ts +27 -1
- package/package.json +4 -4
- package/src/enumOptionSelectedValue.ts +39 -0
- package/src/enumOptionValueDecoder.ts +64 -0
- package/src/enumOptionValueEncoder.ts +33 -0
- package/src/getInputProps.ts +10 -0
- package/src/getOptionValueFormat.ts +17 -0
- package/src/index.ts +11 -2
- package/src/removeOptionalEmptyObjects.ts +127 -0
- package/src/resolveUiSchema.ts +55 -122
- package/src/types.ts +28 -1
package/lib/types.d.ts
CHANGED
|
@@ -20,6 +20,13 @@ export type FormContextType = GenericObjectType;
|
|
|
20
20
|
/** The interface for the test ID proxy objects that are returned by the `getTestId` utility function.
|
|
21
21
|
*/
|
|
22
22
|
export type TestIdShape = Record<string, string>;
|
|
23
|
+
/** Controls how enum-backed widgets encode option values in their DOM `value` attributes.
|
|
24
|
+
*
|
|
25
|
+
* - `'indexed'`: options are encoded as their array index (default, historical behavior).
|
|
26
|
+
* - `'realValue'`: primitive option values are stringified directly, enabling native form
|
|
27
|
+
* submission. Object/array values still fall back to their index.
|
|
28
|
+
*/
|
|
29
|
+
export type OptionValueFormat = 'indexed' | 'realValue';
|
|
23
30
|
/** Function to generate HTML name attributes from path segments */
|
|
24
31
|
export type NameGeneratorFunction = (path: FieldPathList, idPrefix: string, isMultiValue?: boolean) => string;
|
|
25
32
|
/** Experimental feature that specifies the Array `minItems` default form state behavior
|
|
@@ -125,11 +132,15 @@ export type RangeSpecType = {
|
|
|
125
132
|
max?: number;
|
|
126
133
|
};
|
|
127
134
|
/** Properties describing a Range specification in terms of attribute that can be added to the `HTML` `<input>` */
|
|
128
|
-
export type InputPropsType =
|
|
135
|
+
export type InputPropsType = {
|
|
129
136
|
/** Specifies the type of the <input> element */
|
|
130
137
|
type: string;
|
|
131
138
|
/** Specifies the interval between legal numbers in an input field or "any" */
|
|
132
139
|
step?: number | 'any';
|
|
140
|
+
/** Specifies a minimum value for an <input> element; accepts a number for numeric inputs or a string for date/time inputs */
|
|
141
|
+
min?: number | string;
|
|
142
|
+
/** Specifies the maximum value for an <input> element; accepts a number for numeric inputs or a string for date/time inputs */
|
|
143
|
+
max?: number | string;
|
|
133
144
|
/** Specifies the `autoComplete` value for an <input> element */
|
|
134
145
|
autoComplete?: HTMLInputElement['autocomplete'];
|
|
135
146
|
/** Specifies a filter for what file types the user can upload. */
|
|
@@ -357,6 +368,21 @@ export type GlobalUISchemaOptions = GenericObjectType & {
|
|
|
357
368
|
* both. To disable the Optional Data Field UI for a specific field, provide an empty array within the UI schema.
|
|
358
369
|
*/
|
|
359
370
|
enableOptionalDataFieldForType?: ('object' | 'array')[];
|
|
371
|
+
/** Controls how enum-backed widgets (select, radio, checkboxes) encode option values
|
|
372
|
+
* in their DOM `value` attributes.
|
|
373
|
+
*
|
|
374
|
+
* - `'indexed'` (default): options are encoded as their array index. This is the
|
|
375
|
+
* historical behavior and keeps object/array enum values addressable without
|
|
376
|
+
* stringifying them.
|
|
377
|
+
* - `'realValue'`: primitive option values are stringified directly (e.g. `"foo"`,
|
|
378
|
+
* `"42"`, `"true"`). This enables native form submission and browser autocomplete
|
|
379
|
+
* since the submitted value matches the enum value. Object/array values still
|
|
380
|
+
* fall back to their index since `String(obj)` would produce `"[object Object]"`.
|
|
381
|
+
*
|
|
382
|
+
* The form data passed to `onChange` is always the typed enum value; this option
|
|
383
|
+
* only affects the DOM-level encoding.
|
|
384
|
+
*/
|
|
385
|
+
optionValueFormat?: OptionValueFormat;
|
|
360
386
|
};
|
|
361
387
|
/** The set of options from the `Form` that will be available on the `Registry` for use in everywhere the `registry` is
|
|
362
388
|
* available.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rjsf/utils",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.5.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
@@ -65,11 +65,11 @@
|
|
|
65
65
|
"react": ">=18"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@x0k/json-schema-merge": "^1.0.
|
|
68
|
+
"@x0k/json-schema-merge": "^1.0.3",
|
|
69
69
|
"fast-uri": "^3.1.0",
|
|
70
70
|
"jsonpointer": "^5.0.1",
|
|
71
|
-
"lodash": "^4.
|
|
72
|
-
"lodash-es": "^4.
|
|
71
|
+
"lodash": "^4.18.1",
|
|
72
|
+
"lodash-es": "^4.18.1",
|
|
73
73
|
"react-is": "^18.3.1"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { EnumOptionsType, OptionValueFormat, StrictRJSFSchema, RJSFSchema } from './types';
|
|
2
|
+
import enumOptionsIndexForValue from './enumOptionsIndexForValue';
|
|
3
|
+
|
|
4
|
+
/** Computes the value to pass to a select element's `value` attribute.
|
|
5
|
+
*
|
|
6
|
+
* When `format` is `'realValue'`, converts form data values to strings.
|
|
7
|
+
* When `format` is `'indexed'` (the default), resolves to index-based values via
|
|
8
|
+
* `enumOptionsIndexForValue`. Returns `emptyValue` when the current value is empty.
|
|
9
|
+
*
|
|
10
|
+
* @param value - The current form data value
|
|
11
|
+
* @param enumOptions - The available enum options
|
|
12
|
+
* @param multiple - Whether the select allows multiple selections
|
|
13
|
+
* @param [format='indexed'] - How option values are encoded on the DOM
|
|
14
|
+
* @param emptyValue - The value to return when the selection is empty
|
|
15
|
+
* @returns The value to use for the select element's `value` attribute
|
|
16
|
+
*/
|
|
17
|
+
export default function enumOptionSelectedValue<S extends StrictRJSFSchema = RJSFSchema>(
|
|
18
|
+
value: any,
|
|
19
|
+
enumOptions: EnumOptionsType<S>[] | undefined,
|
|
20
|
+
multiple: boolean,
|
|
21
|
+
format: OptionValueFormat = 'indexed',
|
|
22
|
+
emptyValue?: any,
|
|
23
|
+
): any {
|
|
24
|
+
const isEmpty =
|
|
25
|
+
typeof value === 'undefined' ||
|
|
26
|
+
(multiple && Array.isArray(value) && value.length < 1) ||
|
|
27
|
+
(!multiple && value === emptyValue);
|
|
28
|
+
|
|
29
|
+
if (isEmpty) {
|
|
30
|
+
return emptyValue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (format === 'realValue') {
|
|
34
|
+
return multiple ? value.map(String) : String(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const indexes = enumOptionsIndexForValue<S>(value, enumOptions, multiple);
|
|
38
|
+
return typeof indexes === 'undefined' ? emptyValue : indexes;
|
|
39
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { EnumOptionsType, OptionValueFormat, StrictRJSFSchema, RJSFSchema } from './types';
|
|
2
|
+
import enumOptionsValueForIndex from './enumOptionsValueForIndex';
|
|
3
|
+
|
|
4
|
+
/** Resolves a single DOM value string back to its typed enum value in `'realValue'` mode.
|
|
5
|
+
*
|
|
6
|
+
* First attempts a reverse lookup by matching `String(opt.value)` against the input.
|
|
7
|
+
* If no option matches and the input parses as a valid index, falls back to the
|
|
8
|
+
* option at that index — this is how object/array enum values round-trip, since
|
|
9
|
+
* they are encoded as indices by the encoder.
|
|
10
|
+
*
|
|
11
|
+
* @param value - A single string value from a DOM attribute
|
|
12
|
+
* @param enumOptions - The available enum options
|
|
13
|
+
* @param emptyValue - The value to return when the input is empty, options are missing, or no match is found
|
|
14
|
+
* @returns The original typed enum value, or `emptyValue`
|
|
15
|
+
*/
|
|
16
|
+
function decodeSingle<S extends StrictRJSFSchema = RJSFSchema>(
|
|
17
|
+
value: string,
|
|
18
|
+
enumOptions: EnumOptionsType<S>[] | undefined,
|
|
19
|
+
emptyValue?: unknown,
|
|
20
|
+
): unknown {
|
|
21
|
+
if (value === '' || !Array.isArray(enumOptions)) {
|
|
22
|
+
return emptyValue;
|
|
23
|
+
}
|
|
24
|
+
const match = enumOptions.find((opt) => String(opt.value) === value);
|
|
25
|
+
if (match) {
|
|
26
|
+
return match.value;
|
|
27
|
+
}
|
|
28
|
+
// Fallback: value might be an index (for object/array enum values)
|
|
29
|
+
const index = Number(value);
|
|
30
|
+
if (!isNaN(index) && index >= 0 && index < enumOptions.length) {
|
|
31
|
+
return enumOptions[index].value;
|
|
32
|
+
}
|
|
33
|
+
return emptyValue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Decodes a string from a DOM value attribute back to a typed enum value.
|
|
37
|
+
*
|
|
38
|
+
* When `format` is `'realValue'`, does a reverse lookup: finds the enum option
|
|
39
|
+
* whose `String(value)` matches the input string and returns the original typed value.
|
|
40
|
+
* For object/array values that were encoded as indices, falls back to index resolution.
|
|
41
|
+
*
|
|
42
|
+
* When `format` is `'indexed'` (the default), uses index-based resolution via
|
|
43
|
+
* `enumOptionsValueForIndex`.
|
|
44
|
+
*
|
|
45
|
+
* @param value - The string value(s) from the DOM
|
|
46
|
+
* @param enumOptions - The available enum options
|
|
47
|
+
* @param [format='indexed'] - How the values were encoded on the DOM
|
|
48
|
+
* @param emptyValue - The value to return for empty/missing selections
|
|
49
|
+
* @returns The original typed enum value(s)
|
|
50
|
+
*/
|
|
51
|
+
export default function enumOptionValueDecoder<S extends StrictRJSFSchema = RJSFSchema>(
|
|
52
|
+
value: string | string[],
|
|
53
|
+
enumOptions: EnumOptionsType<S>[] | undefined,
|
|
54
|
+
format: OptionValueFormat = 'indexed',
|
|
55
|
+
emptyValue?: unknown,
|
|
56
|
+
): unknown {
|
|
57
|
+
if (format !== 'realValue') {
|
|
58
|
+
return enumOptionsValueForIndex<S>(value, enumOptions, emptyValue);
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(value)) {
|
|
61
|
+
return value.map((v) => decodeSingle(v, enumOptions, emptyValue));
|
|
62
|
+
}
|
|
63
|
+
return decodeSingle(value, enumOptions, emptyValue);
|
|
64
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import isNil from 'lodash/isNil';
|
|
2
|
+
|
|
3
|
+
import { OptionValueFormat } from './types';
|
|
4
|
+
|
|
5
|
+
/** Encodes an enum option value into a string for a DOM value attribute.
|
|
6
|
+
*
|
|
7
|
+
* When `format` is `'realValue'`, primitive values are converted via `String()`.
|
|
8
|
+
* Non-primitive values (objects, arrays) fall back to the index since
|
|
9
|
+
* `String()` would produce `"[object Object]"`.
|
|
10
|
+
*
|
|
11
|
+
* When `format` is `'indexed'` (the default), returns the index as a string.
|
|
12
|
+
*
|
|
13
|
+
* @param value - The typed enum value
|
|
14
|
+
* @param index - The option's position in the enumOptions array
|
|
15
|
+
* @param [format='indexed'] - How to encode the value for the DOM attribute
|
|
16
|
+
* @returns The string to use as the DOM value attribute
|
|
17
|
+
*/
|
|
18
|
+
export default function enumOptionValueEncoder(
|
|
19
|
+
value: unknown,
|
|
20
|
+
index: number,
|
|
21
|
+
format: OptionValueFormat = 'indexed',
|
|
22
|
+
): string {
|
|
23
|
+
if (format !== 'realValue') {
|
|
24
|
+
return String(index);
|
|
25
|
+
}
|
|
26
|
+
if (isNil(value)) {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
if (typeof value === 'object') {
|
|
30
|
+
return String(index);
|
|
31
|
+
}
|
|
32
|
+
return String(value);
|
|
33
|
+
}
|
package/src/getInputProps.ts
CHANGED
|
@@ -47,6 +47,16 @@ export default function getInputProps<
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// For date/time input types, propagate formatMinimum/formatMaximum to min/max
|
|
51
|
+
if (['date', 'datetime-local', 'time', 'week', 'month'].includes(inputProps.type)) {
|
|
52
|
+
if (schema.formatMinimum !== undefined) {
|
|
53
|
+
inputProps.min = schema.formatMinimum as string;
|
|
54
|
+
}
|
|
55
|
+
if (schema.formatMaximum !== undefined) {
|
|
56
|
+
inputProps.max = schema.formatMaximum as string;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
50
60
|
if (options.autocomplete) {
|
|
51
61
|
inputProps.autoComplete = options.autocomplete;
|
|
52
62
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { OptionValueFormat } from './types';
|
|
2
|
+
|
|
3
|
+
/** Resolves the effective `optionValueFormat` for enum-backed widgets.
|
|
4
|
+
*
|
|
5
|
+
* Provides a single source of truth for the default DOM encoding format
|
|
6
|
+
* (`'indexed'`) used by `SelectWidget`, `RadioWidget`, and `CheckboxesWidget`.
|
|
7
|
+
* Widgets should call this helper once and pass the result to
|
|
8
|
+
* `enumOptionValueEncoder`, `enumOptionValueDecoder`, and `enumOptionSelectedValue`
|
|
9
|
+
* rather than reading `options.optionValueFormat` directly.
|
|
10
|
+
*
|
|
11
|
+
* @param options - The widget options (typically from the `options` prop, already
|
|
12
|
+
* resolved from `ui:options` and `ui:globalOptions`)
|
|
13
|
+
* @returns The resolved `OptionValueFormat`, defaulting to `'indexed'` when not set
|
|
14
|
+
*/
|
|
15
|
+
export default function getOptionValueFormat(options?: { optionValueFormat?: OptionValueFormat }): OptionValueFormat {
|
|
16
|
+
return options?.optionValueFormat ?? 'indexed';
|
|
17
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,9 @@ import dateRangeOptions from './dateRangeOptions';
|
|
|
8
8
|
import deepEquals from './deepEquals';
|
|
9
9
|
import shallowEquals from './shallowEquals';
|
|
10
10
|
import englishStringTranslator from './englishStringTranslator';
|
|
11
|
+
import enumOptionSelectedValue from './enumOptionSelectedValue';
|
|
12
|
+
import enumOptionValueDecoder from './enumOptionValueDecoder';
|
|
13
|
+
import enumOptionValueEncoder from './enumOptionValueEncoder';
|
|
11
14
|
import enumOptionsDeselectValue from './enumOptionsDeselectValue';
|
|
12
15
|
import enumOptionsIndexForValue from './enumOptionsIndexForValue';
|
|
13
16
|
import enumOptionsIsSelected from './enumOptionsIsSelected';
|
|
@@ -20,6 +23,7 @@ import getDateElementProps, { DateElementFormat, DateElementProp } from './getDa
|
|
|
20
23
|
import getDiscriminatorFieldFromSchema from './getDiscriminatorFieldFromSchema';
|
|
21
24
|
import getInputProps from './getInputProps';
|
|
22
25
|
import getOptionMatchingSimpleDiscriminator from './getOptionMatchingSimpleDiscriminator';
|
|
26
|
+
import getOptionValueFormat from './getOptionValueFormat';
|
|
23
27
|
import getSchemaType from './getSchemaType';
|
|
24
28
|
import getSubmitButtonOptions from './getSubmitButtonOptions';
|
|
25
29
|
import getTemplate from './getTemplate';
|
|
@@ -53,12 +57,13 @@ import mergeDefaultsWithFormData from './mergeDefaultsWithFormData';
|
|
|
53
57
|
import mergeObjects from './mergeObjects';
|
|
54
58
|
import mergeSchemas from './mergeSchemas';
|
|
55
59
|
import optionsList from './optionsList';
|
|
60
|
+
import removeOptionalEmptyObjects from './removeOptionalEmptyObjects';
|
|
56
61
|
import orderProperties from './orderProperties';
|
|
57
62
|
import pad from './pad';
|
|
58
63
|
import parseDateString from './parseDateString';
|
|
59
64
|
import rangeSpec from './rangeSpec';
|
|
60
65
|
import replaceStringParameters from './replaceStringParameters';
|
|
61
|
-
import resolveUiSchema
|
|
66
|
+
import resolveUiSchema from './resolveUiSchema';
|
|
62
67
|
import schemaRequiresTrueValue from './schemaRequiresTrueValue';
|
|
63
68
|
import shouldRender, { ComponentUpdateStrategy } from './shouldRender';
|
|
64
69
|
import shouldRenderOptionalField from './shouldRenderOptionalField';
|
|
@@ -107,6 +112,9 @@ export {
|
|
|
107
112
|
deepEquals,
|
|
108
113
|
descriptionId,
|
|
109
114
|
englishStringTranslator,
|
|
115
|
+
enumOptionSelectedValue,
|
|
116
|
+
enumOptionValueDecoder,
|
|
117
|
+
enumOptionValueEncoder,
|
|
110
118
|
enumOptionsDeselectValue,
|
|
111
119
|
enumOptionsIndexForValue,
|
|
112
120
|
enumOptionsIsSelected,
|
|
@@ -121,6 +129,7 @@ export {
|
|
|
121
129
|
getDiscriminatorFieldFromSchema,
|
|
122
130
|
getInputProps,
|
|
123
131
|
getOptionMatchingSimpleDiscriminator,
|
|
132
|
+
getOptionValueFormat,
|
|
124
133
|
getSchemaType,
|
|
125
134
|
getSubmitButtonOptions,
|
|
126
135
|
getTemplate,
|
|
@@ -152,9 +161,9 @@ export {
|
|
|
152
161
|
pad,
|
|
153
162
|
parseDateString,
|
|
154
163
|
rangeSpec,
|
|
164
|
+
removeOptionalEmptyObjects,
|
|
155
165
|
replaceStringParameters,
|
|
156
166
|
resolveUiSchema,
|
|
157
|
-
expandUiSchemaDefinitions,
|
|
158
167
|
schemaRequiresTrueValue,
|
|
159
168
|
shallowEquals,
|
|
160
169
|
shouldRender,
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import isNil from 'lodash/isNil';
|
|
2
|
+
|
|
3
|
+
import isObject from './isObject';
|
|
4
|
+
import { FormContextType, GenericObjectType, RJSFSchema, StrictRJSFSchema, ValidatorType } from './types';
|
|
5
|
+
import retrieveSchema from './schema/retrieveSchema';
|
|
6
|
+
|
|
7
|
+
/** Determines whether a value is considered "empty" for the purposes of optional object pruning.
|
|
8
|
+
* A value is empty if it is `undefined`, `null`, an empty string, or an object where all own
|
|
9
|
+
* properties are themselves empty.
|
|
10
|
+
*
|
|
11
|
+
* @param value - The value to check
|
|
12
|
+
* @returns True if the value is considered empty
|
|
13
|
+
*/
|
|
14
|
+
function isValueEmpty(value: unknown): boolean {
|
|
15
|
+
if (isNil(value) || value === '') {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
if (Array.isArray(value)) {
|
|
19
|
+
// An empty array is considered empty; a non-empty array is not
|
|
20
|
+
return value.length === 0;
|
|
21
|
+
}
|
|
22
|
+
if (isObject(value)) {
|
|
23
|
+
const obj = value as GenericObjectType;
|
|
24
|
+
const keys = Object.keys(obj);
|
|
25
|
+
return keys.every((key) => isValueEmpty(obj[key]));
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Recursively removes optional objects from the `formData` that are empty (i.e., all their fields
|
|
31
|
+
* are undefined, null, empty strings, or themselves empty optional objects). This solves the problem
|
|
32
|
+
* where interacting with fields inside an optional object "activates" it permanently, making the
|
|
33
|
+
* form unsubmittable when the optional object has required inner fields.
|
|
34
|
+
*
|
|
35
|
+
* An object property is considered "optional" when it is NOT listed in its parent schema's `required`
|
|
36
|
+
* array.
|
|
37
|
+
*
|
|
38
|
+
* @param validator - An implementation of the `ValidatorType` interface that will be used when necessary
|
|
39
|
+
* @param schema - The JSON schema describing the `formData`
|
|
40
|
+
* @param [rootSchema] - The root schema, used primarily to look up `$ref`s
|
|
41
|
+
* @param [formData] - The current form data to prune
|
|
42
|
+
* @returns - A new copy of `formData` with empty optional objects removed, or `undefined` if the
|
|
43
|
+
* entire formData was pruned
|
|
44
|
+
*/
|
|
45
|
+
export default function removeOptionalEmptyObjects<
|
|
46
|
+
T = any,
|
|
47
|
+
S extends StrictRJSFSchema = RJSFSchema,
|
|
48
|
+
F extends FormContextType = any,
|
|
49
|
+
>(validator: ValidatorType<T, S, F>, schema: S, rootSchema?: S, formData?: T): T | undefined {
|
|
50
|
+
if (!isObject(schema)) {
|
|
51
|
+
return formData;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const resolvedSchema = retrieveSchema<T, S, F>(validator, schema, rootSchema, formData);
|
|
55
|
+
|
|
56
|
+
if (Array.isArray(formData)) {
|
|
57
|
+
const itemsSchema = resolvedSchema.items as S | S[];
|
|
58
|
+
if (!itemsSchema) {
|
|
59
|
+
return formData;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let hasChanges = false;
|
|
63
|
+
const mapped = formData.map((item, index) => {
|
|
64
|
+
let itemSchema = itemsSchema as S;
|
|
65
|
+
if (Array.isArray(itemsSchema)) {
|
|
66
|
+
itemSchema = itemsSchema[index] || (resolvedSchema.additionalItems as S) || ({} as S);
|
|
67
|
+
}
|
|
68
|
+
const cleaned = removeOptionalEmptyObjects<T, S, F>(validator, itemSchema, rootSchema, item);
|
|
69
|
+
if (cleaned !== item) {
|
|
70
|
+
hasChanges = true;
|
|
71
|
+
}
|
|
72
|
+
return cleaned === undefined ? ({} as T) : cleaned;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Although T is an array type here, we still need to cast it back to T since TS
|
|
76
|
+
// doesn't narrow the generic T automatically
|
|
77
|
+
return hasChanges ? (mapped as T) : formData;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const { properties, required: requiredFields = [] } = resolvedSchema;
|
|
81
|
+
|
|
82
|
+
if (!isObject(formData) || !properties) {
|
|
83
|
+
return formData;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const result: GenericObjectType = {};
|
|
87
|
+
const data = formData as GenericObjectType;
|
|
88
|
+
let hasAnyValue = false;
|
|
89
|
+
|
|
90
|
+
for (const key of Object.keys(data)) {
|
|
91
|
+
const value = data[key];
|
|
92
|
+
const propertySchema = (properties[key] || {}) as S;
|
|
93
|
+
const isRequired = (requiredFields as string[]).includes(key);
|
|
94
|
+
|
|
95
|
+
const isObj = isObject(value);
|
|
96
|
+
const isArr = Array.isArray(value);
|
|
97
|
+
|
|
98
|
+
if ((isObj || isArr) && properties[key]) {
|
|
99
|
+
// Recursively process nested objects and arrays
|
|
100
|
+
const cleaned = removeOptionalEmptyObjects<T, S, F>(validator, propertySchema, rootSchema, value as T);
|
|
101
|
+
|
|
102
|
+
if (!isRequired && isValueEmpty(cleaned)) {
|
|
103
|
+
// This is an optional property and the cleaned result is empty — omit it
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
result[key] = cleaned;
|
|
108
|
+
hasAnyValue = true;
|
|
109
|
+
} else if (!isRequired && isValueEmpty(value) && properties[key]) {
|
|
110
|
+
// Optional scalar property that is empty — omit it
|
|
111
|
+
continue;
|
|
112
|
+
} else {
|
|
113
|
+
result[key] = value;
|
|
114
|
+
if (!isValueEmpty(value)) {
|
|
115
|
+
hasAnyValue = true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// If the entire object ended up empty after pruning, return undefined so that the
|
|
121
|
+
// caller (which may itself be a recursive call) can decide whether to keep or drop it
|
|
122
|
+
if (!hasAnyValue && Object.keys(result).length === 0) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return result as T;
|
|
127
|
+
}
|
package/src/resolveUiSchema.ts
CHANGED
|
@@ -1,112 +1,81 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
ANY_OF_KEY,
|
|
5
|
-
ITEMS_KEY,
|
|
6
|
-
ONE_OF_KEY,
|
|
7
|
-
PROPERTIES_KEY,
|
|
8
|
-
REF_KEY,
|
|
9
|
-
RJSF_REF_KEY,
|
|
10
|
-
} from './constants';
|
|
1
|
+
import isEmpty from 'lodash/isEmpty';
|
|
2
|
+
|
|
3
|
+
import { ANY_OF_KEY, ONE_OF_KEY, REF_KEY, RJSF_REF_KEY } from './constants';
|
|
11
4
|
import findSchemaDefinition from './findSchemaDefinition';
|
|
12
|
-
import isObject from './isObject';
|
|
13
5
|
import mergeObjects from './mergeObjects';
|
|
14
6
|
import { FormContextType, GenericObjectType, Registry, RJSFSchema, StrictRJSFSchema, UiSchema } from './types';
|
|
15
7
|
|
|
16
|
-
|
|
17
|
-
const SAME_KEY_KEYWORDS = [ITEMS_KEY, ADDITIONAL_PROPERTIES_KEY] as const;
|
|
18
|
-
|
|
19
|
-
// Keywords where child schemas are in an array, each mapping to uiSchema[keyword][i]
|
|
20
|
-
const ARRAY_KEYWORDS = [ONE_OF_KEY, ANY_OF_KEY, ALL_OF_KEY] as const;
|
|
21
|
-
|
|
22
|
-
/** Expands `ui:definitions` into the uiSchema by walking the schema tree and finding all `$ref`s.
|
|
23
|
-
* Called once at form initialization to pre-expand definitions into the uiSchema structure.
|
|
8
|
+
/** Resolves the uiSchema for a given schema, considering `ui:definitions` stored in the registry.
|
|
24
9
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
10
|
+
* Called at runtime for each field. When the schema contains a `$ref`, looks up the corresponding
|
|
11
|
+
* uiSchema definition from `registry.uiSchemaDefinitions` and merges it with local overrides.
|
|
12
|
+
* For schemas with `oneOf`/`anyOf` branches, also populates `uiSchema[keyword][i]` for branches
|
|
13
|
+
* whose `$ref` matches a definition, so `MultiSchemaField` can read dropdown option titles.
|
|
27
14
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* @
|
|
15
|
+
* Resolution order (later sources override earlier):
|
|
16
|
+
* 1. `ui:definitions[$ref]` - base definition from registry
|
|
17
|
+
* 2. `localUiSchema` - local overrides at current path
|
|
18
|
+
*
|
|
19
|
+
* @param schema - The JSON schema (may contain `$ref` or `RJSF_REF_KEY`)
|
|
20
|
+
* @param localUiSchema - The uiSchema at the current path (local overrides)
|
|
21
|
+
* @param registry - The registry containing `uiSchemaDefinitions`
|
|
22
|
+
* @returns - The resolved uiSchema with definitions merged in
|
|
33
23
|
*/
|
|
34
|
-
export function
|
|
24
|
+
export default function resolveUiSchema<
|
|
35
25
|
T = any,
|
|
36
26
|
S extends StrictRJSFSchema = RJSFSchema,
|
|
37
27
|
F extends FormContextType = any,
|
|
38
|
-
>(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
visited: Set<string> = new Set(),
|
|
43
|
-
): UiSchema<T, S, F> {
|
|
44
|
-
const { rootSchema, uiSchemaDefinitions: definitions } = registry;
|
|
45
|
-
let result = { ...uiSchema };
|
|
46
|
-
let resolvedSchema = currentSchema;
|
|
47
|
-
|
|
48
|
-
const ref = currentSchema[REF_KEY] as string | undefined;
|
|
49
|
-
const isRecursive = ref && visited.has(ref);
|
|
50
|
-
|
|
51
|
-
if (ref) {
|
|
52
|
-
visited.add(ref);
|
|
53
|
-
|
|
54
|
-
if (definitions && ref in definitions) {
|
|
55
|
-
result = mergeObjects(definitions[ref] as GenericObjectType, result as GenericObjectType) as UiSchema<T, S, F>;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (isRecursive) {
|
|
59
|
-
return result;
|
|
60
|
-
}
|
|
28
|
+
>(schema: S, localUiSchema: UiSchema<T, S, F> | undefined, registry: Registry<T, S, F>): UiSchema<T, S, F> {
|
|
29
|
+
const ref = ((schema as GenericObjectType)[RJSF_REF_KEY] ?? schema[REF_KEY]) as string | undefined;
|
|
30
|
+
const definitions = registry.uiSchemaDefinitions;
|
|
31
|
+
const definitionUiSchema = ref && definitions ? definitions[ref] : undefined;
|
|
61
32
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
33
|
+
let result: UiSchema<T, S, F>;
|
|
34
|
+
if (!definitionUiSchema) {
|
|
35
|
+
result = localUiSchema || {};
|
|
36
|
+
} else if (!localUiSchema || isEmpty(localUiSchema)) {
|
|
37
|
+
result = { ...definitionUiSchema };
|
|
38
|
+
} else {
|
|
39
|
+
result = mergeObjects(definitionUiSchema as GenericObjectType, localUiSchema as GenericObjectType) as UiSchema<
|
|
40
|
+
T,
|
|
41
|
+
S,
|
|
42
|
+
F
|
|
43
|
+
>;
|
|
67
44
|
}
|
|
68
45
|
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
46
|
+
// Walk oneOf/anyOf branches to populate uiSchema[keyword][i] so MultiSchemaField
|
|
47
|
+
// can read dropdown option titles at the parent level.
|
|
48
|
+
if (definitions) {
|
|
49
|
+
let resolvedSchema: S = schema;
|
|
50
|
+
if (ref && schema[REF_KEY] && !(schema as GenericObjectType)[RJSF_REF_KEY]) {
|
|
51
|
+
try {
|
|
52
|
+
resolvedSchema = findSchemaDefinition<S>(ref, registry.rootSchema as S);
|
|
53
|
+
} catch (e) {
|
|
54
|
+
console.warn('could not resolve $ref in resolveUiSchema:\n', e);
|
|
55
|
+
return result;
|
|
77
56
|
}
|
|
78
57
|
}
|
|
79
|
-
}
|
|
80
58
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const currentUiSchema = result[keyword];
|
|
86
|
-
if (typeof currentUiSchema !== 'function') {
|
|
87
|
-
const subUiSchema = ((currentUiSchema as GenericObjectType) || {}) as UiSchema<T, S, F>;
|
|
88
|
-
const expanded = expandUiSchemaDefinitions(subSchema as S, subUiSchema, registry, new Set(visited));
|
|
89
|
-
if (Object.keys(expanded).length > 0) {
|
|
90
|
-
(result as GenericObjectType)[keyword] = expanded;
|
|
91
|
-
}
|
|
59
|
+
for (const keyword of [ONE_OF_KEY, ANY_OF_KEY] as const) {
|
|
60
|
+
const schemaOptions = resolvedSchema[keyword];
|
|
61
|
+
if (!Array.isArray(schemaOptions) || schemaOptions.length === 0) {
|
|
62
|
+
continue;
|
|
92
63
|
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
64
|
|
|
96
|
-
// Process array keywords (oneOf, anyOf, allOf) - each option maps to uiSchema[keyword][i]
|
|
97
|
-
for (const keyword of ARRAY_KEYWORDS) {
|
|
98
|
-
const schemaOptions = resolvedSchema[keyword];
|
|
99
|
-
if (Array.isArray(schemaOptions) && schemaOptions.length > 0) {
|
|
100
65
|
const currentUiSchemaArray = (result as GenericObjectType)[keyword];
|
|
101
66
|
const uiSchemaArray: UiSchema<T, S, F>[] = Array.isArray(currentUiSchemaArray) ? [...currentUiSchemaArray] : [];
|
|
102
67
|
|
|
103
68
|
let hasExpanded = false;
|
|
104
69
|
for (let i = 0; i < schemaOptions.length; i++) {
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
uiSchemaArray[i] =
|
|
70
|
+
const option = schemaOptions[i] as GenericObjectType | undefined;
|
|
71
|
+
const optionRef = (option?.[RJSF_REF_KEY] ?? option?.[REF_KEY]) as string | undefined;
|
|
72
|
+
if (optionRef && optionRef in definitions) {
|
|
73
|
+
const optionUiSchema = (uiSchemaArray[i] || {}) as GenericObjectType;
|
|
74
|
+
uiSchemaArray[i] = mergeObjects(definitions[optionRef] as GenericObjectType, optionUiSchema) as UiSchema<
|
|
75
|
+
T,
|
|
76
|
+
S,
|
|
77
|
+
F
|
|
78
|
+
>;
|
|
110
79
|
hasExpanded = true;
|
|
111
80
|
}
|
|
112
81
|
}
|
|
@@ -119,39 +88,3 @@ export function expandUiSchemaDefinitions<
|
|
|
119
88
|
|
|
120
89
|
return result;
|
|
121
90
|
}
|
|
122
|
-
|
|
123
|
-
/** Resolves the uiSchema for a given schema, considering `ui:definitions` stored in the registry.
|
|
124
|
-
*
|
|
125
|
-
* This function is called at runtime for each field. It handles recursive schemas where the
|
|
126
|
-
* pre-expansion in `expandUiSchemaDefinitions` couldn't go deeper.
|
|
127
|
-
*
|
|
128
|
-
* When the schema contains a `$ref`, this function looks up the corresponding uiSchema definition
|
|
129
|
-
* from `registry.uiSchemaDefinitions` and merges it with any local uiSchema overrides.
|
|
130
|
-
*
|
|
131
|
-
* Resolution order (later sources override earlier):
|
|
132
|
-
* 1. `ui:definitions[$ref]` - base definition from registry
|
|
133
|
-
* 2. `localUiSchema` - local overrides at current path
|
|
134
|
-
*
|
|
135
|
-
* @param schema - The JSON schema (may still contain `$ref` for recursive schemas)
|
|
136
|
-
* @param localUiSchema - The uiSchema at the current path (local overrides)
|
|
137
|
-
* @param registry - The registry containing `uiSchemaDefinitions`
|
|
138
|
-
* @returns - The resolved uiSchema with definitions merged in
|
|
139
|
-
*/
|
|
140
|
-
export default function resolveUiSchema<
|
|
141
|
-
T = any,
|
|
142
|
-
S extends StrictRJSFSchema = RJSFSchema,
|
|
143
|
-
F extends FormContextType = any,
|
|
144
|
-
>(schema: S, localUiSchema: UiSchema<T, S, F> | undefined, registry: Registry<T, S, F>): UiSchema<T, S, F> {
|
|
145
|
-
const ref = ((schema as GenericObjectType)[RJSF_REF_KEY] ?? schema[REF_KEY]) as string | undefined;
|
|
146
|
-
const definitionUiSchema = ref ? registry.uiSchemaDefinitions?.[ref] : undefined;
|
|
147
|
-
|
|
148
|
-
if (!definitionUiSchema) {
|
|
149
|
-
return localUiSchema || {};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (!localUiSchema || Object.keys(localUiSchema).length === 0) {
|
|
153
|
-
return { ...definitionUiSchema };
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return mergeObjects(definitionUiSchema as GenericObjectType, localUiSchema as GenericObjectType) as UiSchema<T, S, F>;
|
|
157
|
-
}
|