Skip to content

Commit f4cd0a5

Browse files
Fix 3759 - uneditable & permanent defaults with additional properties (#4490)
* fix additional property defaults not generating * update tests * update descriptions, rm log * fix core form tests * add object additionalProperties defaults test * fix verbiage * allow regeneration of additional property defaults on reset * update verbiage & docs * update CHANGELOG.md * Update CHANGELOG.md Co-authored-by: Heath C <[email protected]> * move new param & update tests * update documentation * update changelog version * rm old descriptions, add change to v6.x upgrade guide.md * Update CHANGELOG.md Co-authored-by: Heath C <[email protected]> --------- Co-authored-by: Heath C <[email protected]>
1 parent 4747213 commit f4cd0a5

File tree

9 files changed

+213
-2
lines changed

9 files changed

+213
-2
lines changed

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,23 @@ it according to semantic versioning. For example, if your PR adds a breaking cha
1515
should change the heading of the (upcoming) version to include a major version bump.
1616
1717
-->
18+
# 6.0.0-beta.21
19+
20+
## @rjsf/core
21+
22+
- Added `initialDefaultsGenerated` flag to state, which indicates whether the initial generation of defaults has been completed
23+
- Added `ObjectField` tests for additionalProperties with defaults
24+
25+
## @rjsf/utils
26+
27+
- Updated `getDefaultFormState` to add a new `initialDefaultsGenerated` prop flag, along with type definitions, fixing uneditable & permanent defaults with additional properties [3759](https://github.com/rjsf-team/react-jsonschema-form/issues/3759)
28+
- Updated `createSchemaUtils` definition to reflect addition of `initialDefaultsGenerated`
29+
- Updated existing tests where `getDefaultFormState` is used to reflect addition of `initialDefaultsGenerated`
30+
31+
## @rjsf/docs
32+
33+
- Updated docs for `getDefaultFormState` to reflect addition of `initialDefaultsGenerated` prop
34+
1835
# 6.0.0-beta-20
1936

2037
## @rjsf/antd

packages/core/src/components/Form.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,8 @@ export interface FormState<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
269269
// Private
270270
/** @description result of schemaUtils.retrieveSchema(schema, formData). This a memoized value to avoid re calculate at internal functions (getStateFromProps, onChange) */
271271
retrievedSchema: S;
272+
/** Flag indicating whether the initial form defaults have been generated */
273+
initialDefaultsGenerated: boolean;
272274
}
273275

274276
/** The event data passed when changes have been made to the form, includes everything from the `FormState` except
@@ -464,8 +466,14 @@ export default class Form<
464466
experimental_customMergeAllOf,
465467
);
466468
}
469+
467470
const rootSchema = schemaUtils.getRootSchema();
468-
const formData: T = schemaUtils.getDefaultFormState(rootSchema, inputFormData) as T;
471+
const formData: T = schemaUtils.getDefaultFormState(
472+
rootSchema,
473+
inputFormData,
474+
false,
475+
state.initialDefaultsGenerated,
476+
) as T;
469477
const _retrievedSchema = this.updateRetrievedSchema(
470478
retrievedSchema ?? schemaUtils.retrieveSchema(rootSchema, formData),
471479
);
@@ -546,6 +554,7 @@ export default class Form<
546554
schemaValidationErrors,
547555
schemaValidationErrorSchema,
548556
retrievedSchema: _retrievedSchema,
557+
initialDefaultsGenerated: true,
549558
};
550559
return nextState;
551560
}
@@ -888,6 +897,7 @@ export default class Form<
888897
errors: [] as unknown,
889898
schemaValidationErrors: [] as unknown,
890899
schemaValidationErrorSchema: {},
900+
initialDefaultsGenerated: false,
891901
} as FormState<T, S, F>;
892902

893903
this.setState(state, () => onChange && onChange({ ...this.state, ...state }));

packages/core/test/Form.test.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ describeRepeated('Form common', (createFormComponent) => {
147147
schemaValidationErrorSchema: undefined,
148148
schemaUtils: sinon.match.object,
149149
retrievedSchema: schema,
150+
initialDefaultsGenerated: true,
150151
});
151152
});
152153
});
@@ -1979,6 +1980,7 @@ describeRepeated('Form common', (createFormComponent) => {
19791980
schemaValidationErrorSchema: undefined,
19801981
schemaUtils: sinon.match.object,
19811982
retrievedSchema: formProps.schema,
1983+
initialDefaultsGenerated: true,
19821984
});
19831985
});
19841986
});

packages/core/test/ObjectField.test.jsx

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,6 +1144,165 @@ describe('ObjectField', () => {
11441144
});
11451145
});
11461146

1147+
it('should generate the specified default key and value inputs if default is provided outside of additionalProperties schema', () => {
1148+
const customSchema = {
1149+
...schema,
1150+
default: {
1151+
defaultKey: 'defaultValue',
1152+
},
1153+
};
1154+
const { onChange } = createFormComponent({
1155+
schema: customSchema,
1156+
formData: {},
1157+
});
1158+
1159+
sinon.assert.calledWithMatch(onChange.lastCall, {
1160+
formData: {
1161+
defaultKey: 'defaultValue',
1162+
},
1163+
});
1164+
});
1165+
1166+
it('should generate the specified default key/value input with custom formData provided', () => {
1167+
const customSchema = {
1168+
...schema,
1169+
default: {
1170+
defaultKey: 'defaultValue',
1171+
},
1172+
};
1173+
const { onChange } = createFormComponent({
1174+
schema: customSchema,
1175+
formData: {
1176+
someData: 'someValue',
1177+
},
1178+
});
1179+
1180+
sinon.assert.calledWithMatch(onChange.lastCall, {
1181+
formData: {
1182+
defaultKey: 'defaultValue',
1183+
someData: 'someValue',
1184+
},
1185+
});
1186+
});
1187+
1188+
it('should edit the specified default key without duplicating', () => {
1189+
const customSchema = {
1190+
...schema,
1191+
default: {
1192+
defaultKey: 'defaultValue',
1193+
},
1194+
};
1195+
const { node, onChange } = createFormComponent({
1196+
schema: customSchema,
1197+
formData: {},
1198+
});
1199+
1200+
fireEvent.blur(node.querySelector('#root_defaultKey-key'), { target: { value: 'newDefaultKey' } });
1201+
1202+
sinon.assert.calledWithMatch(onChange.lastCall, {
1203+
formData: {
1204+
newDefaultKey: 'defaultValue',
1205+
},
1206+
});
1207+
});
1208+
1209+
it('should remove the specified default key/value input item', () => {
1210+
const customSchema = {
1211+
...schema,
1212+
default: {
1213+
defaultKey: 'defaultValue',
1214+
},
1215+
};
1216+
const { node, onChange } = createFormComponent({
1217+
schema: customSchema,
1218+
formData: {},
1219+
});
1220+
1221+
fireEvent.click(node.querySelector('.rjsf-object-property-remove'));
1222+
1223+
sinon.assert.calledWithMatch(onChange.lastCall, {
1224+
formData: {},
1225+
});
1226+
});
1227+
1228+
it('should handle nested additional property default key/value input generation', () => {
1229+
const customSchema = {
1230+
...schema,
1231+
default: {
1232+
defaultKey: 'defaultValue',
1233+
},
1234+
properties: {
1235+
nested: {
1236+
type: 'object',
1237+
properties: {
1238+
bar: {
1239+
type: 'object',
1240+
additionalProperties: {
1241+
type: 'string',
1242+
},
1243+
default: {
1244+
baz: 'value',
1245+
},
1246+
},
1247+
},
1248+
},
1249+
},
1250+
};
1251+
1252+
const { onChange } = createFormComponent({
1253+
schema: customSchema,
1254+
formData: {},
1255+
});
1256+
1257+
sinon.assert.calledWithMatch(onChange.lastCall, {
1258+
formData: {
1259+
defaultKey: 'defaultValue',
1260+
nested: {
1261+
bar: {
1262+
baz: 'value',
1263+
},
1264+
},
1265+
},
1266+
});
1267+
});
1268+
1269+
it('should remove nested additional property default key/value input', () => {
1270+
const customSchema = {
1271+
...schema,
1272+
properties: {
1273+
nested: {
1274+
type: 'object',
1275+
properties: {
1276+
bar: {
1277+
type: 'object',
1278+
additionalProperties: {
1279+
type: 'string',
1280+
},
1281+
default: {
1282+
baz: 'value',
1283+
},
1284+
},
1285+
},
1286+
},
1287+
},
1288+
};
1289+
1290+
const { node, onChange } = createFormComponent({
1291+
schema: customSchema,
1292+
formData: {},
1293+
});
1294+
1295+
fireEvent.click(node.querySelector('.rjsf-object-property-remove'));
1296+
1297+
sinon.assert.calledWithMatch(onChange.lastCall, {
1298+
formData: {
1299+
nested: {
1300+
bar: {},
1301+
},
1302+
},
1303+
});
1304+
});
1305+
11471306
it('should not provide an expand button if length equals maxProperties', () => {
11481307
const { node } = createFormComponent({
11491308
schema: { maxProperties: 1, ...schema },

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,7 @@ Returns the superset of `formData` that includes the given set updated to includ
10651065
- [includeUndefinedValues=false]: boolean | "excludeObjectChildren" - Optional flag, if true, cause undefined values to be added as defaults. If "excludeObjectChildren", cause undefined values for this object and pass `includeUndefinedValues` as false when computing defaults for any nested object properties.
10661066
- [experimental_defaultFormStateBehavior]: Experimental_DefaultFormStateBehavior - See `Form` documentation for the [experimental_defaultFormStateBehavior](./form-props.md#experimental_defaultFormStateBehavior) prop
10671067
- [experimental_customMergeAllOf]: Experimental_CustomMergeAllOf&lt;S&gt; - See `Form` documentation for the [experimental_customMergeAllOf](./form-props.md#experimental_custommergeallof) prop
1068+
- [initialDefaultsGenerated]: boolean - Optional flag, indicates whether or not initial defaults have been generated
10681069

10691070
#### Returns
10701071

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,10 @@ Three new validator-based utility functions are available in `@rjsf/utils`:
699699
- `findSelectedOptionInXxxOf<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(validator: ValidatorType<T, S, F>, rootSchema: S, schema: S, fallbackField: string,xxx: 'anyOf' | 'oneOf', formData?: T, experimental_customMergeAllOf?: Experimental_CustomMergeAllOf<S>): S | undefined`: Finds the option that matches the selector field in the `schema` or undefined if nothing is selected
700700
- `getFromSchema<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(validator: ValidatorType<T, S, F>, rootSchema: S, schema: S, path: string | string[], defaultValue: T | S, experimental_customMergeAllOf?: Experimental_CustomMergeAllOf<S>): T | S`: Helper that acts like lodash's `get` but additionally retrieves `$ref`s as needed to get the path for schemas
701701

702+
### Changes to existing utility functions
703+
704+
- `getDefaultFormState`: Added optional `initialDefaultsGenerated` boolean flag that indicates whether or not initial defaults have been generated
705+
702706
### Dynamic UI Schema for Array Items
703707

704708
RJSF 6.x introduces a new feature that allows dynamic UI schema generation for array items.

packages/utils/src/createSchemaUtils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,12 +165,14 @@ class SchemaUtils<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo
165165
* @param [includeUndefinedValues=false] - Optional flag, if true, cause undefined values to be added as defaults.
166166
* If "excludeObjectChildren", pass `includeUndefinedValues` as false when computing defaults for any nested
167167
* object properties.
168+
* @param initialDefaultsGenerated - Indicates whether or not initial defaults have been generated
168169
* @returns - The resulting `formData` with all the defaults provided
169170
*/
170171
getDefaultFormState(
171172
schema: S,
172173
formData?: T,
173174
includeUndefinedValues: boolean | 'excludeObjectChildren' = false,
175+
initialDefaultsGenerated?: boolean,
174176
): T | T[] | undefined {
175177
return getDefaultFormState<T, S, F>(
176178
this.validator,
@@ -180,6 +182,7 @@ class SchemaUtils<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo
180182
includeUndefinedValues,
181183
this.experimental_defaultFormStateBehavior,
182184
this.experimental_customMergeAllOf,
185+
initialDefaultsGenerated,
183186
);
184187
}
185188

packages/utils/src/schema/getDefaultFormState.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,8 @@ interface ComputeDefaultsProps<T = any, S extends StrictRJSFSchema = RJSFSchema>
204204
* The formData should take precedence unless it's not valid. This is useful when for example the value from formData does not exist in the schema 'enum' property, in such cases we take the value from the defaults because the value from the formData is not valid.
205205
*/
206206
shouldMergeDefaultsIntoFormData?: boolean;
207+
/** Indicates whether initial defaults have been generated */
208+
initialDefaultsGenerated?: boolean;
207209
}
208210

209211
/** Computes the defaults for the current `schema` given the `rawFormData` and `parentDefaults` if any. This drills into
@@ -229,6 +231,7 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
229231
experimental_customMergeAllOf = undefined,
230232
required,
231233
shouldMergeDefaultsIntoFormData = false,
234+
initialDefaultsGenerated,
232235
} = computeDefaultsProps;
233236
let formData: T = (isObject(rawFormData) ? rawFormData : {}) as T;
234237
const schema: S = isObject(rawSchema) ? rawSchema : ({} as S);
@@ -363,6 +366,7 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
363366
rawFormData: (rawFormData ?? formData) as T,
364367
required,
365368
shouldMergeDefaultsIntoFormData,
369+
initialDefaultsGenerated,
366370
});
367371
}
368372

@@ -463,6 +467,7 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
463467
experimental_customMergeAllOf = undefined,
464468
required,
465469
shouldMergeDefaultsIntoFormData,
470+
initialDefaultsGenerated,
466471
}: ComputeDefaultsProps<T, S> = {},
467472
defaults?: T | T[],
468473
): T {
@@ -498,6 +503,7 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
498503
rawFormData: get(formData, [key]),
499504
required: retrievedSchema.required?.includes(key),
500505
shouldMergeDefaultsIntoFormData,
506+
initialDefaultsGenerated,
501507
});
502508

503509
maybeAddDefaultToObject<T>(
@@ -515,7 +521,7 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
515521
},
516522
{},
517523
) as T;
518-
if (retrievedSchema.additionalProperties) {
524+
if (retrievedSchema.additionalProperties && !initialDefaultsGenerated) {
519525
// as per spec additionalProperties may be either schema or boolean
520526
const additionalPropertiesSchema = isObject(retrievedSchema.additionalProperties)
521527
? retrievedSchema.additionalProperties
@@ -545,6 +551,7 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
545551
rawFormData: get(formData, [key]),
546552
required: retrievedSchema.required?.includes(key),
547553
shouldMergeDefaultsIntoFormData,
554+
initialDefaultsGenerated,
548555
});
549556
// Since these are additional properties we don't need to add the `experimental_defaultFormStateBehavior` prop
550557
maybeAddDefaultToObject<T>(
@@ -580,6 +587,7 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
580587
experimental_customMergeAllOf = undefined,
581588
required,
582589
shouldMergeDefaultsIntoFormData,
590+
initialDefaultsGenerated,
583591
}: ComputeDefaultsProps<T, S> = {},
584592
defaults?: T[],
585593
): T[] | undefined {
@@ -608,6 +616,7 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
608616
parentDefaults: item,
609617
required,
610618
shouldMergeDefaultsIntoFormData,
619+
initialDefaultsGenerated,
611620
});
612621
}) as T[];
613622
}
@@ -628,6 +637,7 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
628637
parentDefaults: get(defaults, [idx]),
629638
required,
630639
shouldMergeDefaultsIntoFormData,
640+
initialDefaultsGenerated,
631641
});
632642
}) as T[];
633643

@@ -727,6 +737,7 @@ export function getDefaultBasedOnSchemaType<
727737
* false when computing defaults for any nested object properties.
728738
* @param [experimental_defaultFormStateBehavior] Optional configuration object, if provided, allows users to override default form state behavior
729739
* @param [experimental_customMergeAllOf] - Optional function that allows for custom merging of `allOf` schemas
740+
* @param initialDefaultsGenerated - Optional flag, indicates whether or not initial defaults have been generated
730741
* @returns - The resulting `formData` with all the defaults provided
731742
*/
732743
export default function getDefaultFormState<
@@ -741,6 +752,7 @@ export default function getDefaultFormState<
741752
includeUndefinedValues: boolean | 'excludeObjectChildren' = false,
742753
experimental_defaultFormStateBehavior?: Experimental_DefaultFormStateBehavior,
743754
experimental_customMergeAllOf?: Experimental_CustomMergeAllOf<S>,
755+
initialDefaultsGenerated?: boolean,
744756
) {
745757
if (!isObject(theSchema)) {
746758
throw new Error('Invalid schema: ' + theSchema);
@@ -757,6 +769,7 @@ export default function getDefaultFormState<
757769
experimental_customMergeAllOf,
758770
rawFormData: formData,
759771
shouldMergeDefaultsIntoFormData: true,
772+
initialDefaultsGenerated,
760773
});
761774

762775
if (schema.type !== 'object' && isObject(schema.default)) {

0 commit comments

Comments
 (0)