Skip to content

Commit f6c5bf7

Browse files
abdalla-rkoAbdallah Al-Soqatriheath-freenome
authored
Make fields with const pre-fiiled and readonly #3843 (#4326)
* Make fields with const pre-fiiled and readonl * fixed issue with default on root level. * fixed array const populate values * fixed array issue and written tests to cover the behavior * updated changeLog * fixed issue with core failing tests. * changed changeLog to rerun tests * improvement based on feedback * Update packages/utils/test/schema/getDefaultFormStateTest.ts Adding default that should not be used * Update packages/utils/test/schema/getDefaultFormStateTest.ts Fix linter --------- Co-authored-by: Abdallah Al-Soqatri <[email protected]> Co-authored-by: Heath C <[email protected]>
1 parent a347257 commit f6c5bf7

File tree

5 files changed

+188
-15
lines changed

5 files changed

+188
-15
lines changed

CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ should change the heading of the (upcoming) version to include a major version b
1616
1717
-->
1818

19-
# 5.21.3
19+
# 5.22.0
2020

2121
## @rjsf/utils
2222

23+
- Made fields with const property pre-filled and readonly, fixing [#2600](https://github.com/rjsf-team/react-jsonschema-form/issues/2600)
2324
- Added `experimental_customMergeAllOf` option to `retrieveSchema` to allow custom merging of `allOf` schemas
2425

2526
# 5.21.2

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -202,15 +202,18 @@ class ObjectField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo
202202
const newFormData = { ...formData } as T;
203203

204204
let type: RJSFSchema['type'] = undefined;
205+
let constValue: RJSFSchema['const'] = undefined;
205206
let defaultValue: RJSFSchema['default'] = undefined;
206207
if (isObject(schema.additionalProperties)) {
207208
type = schema.additionalProperties.type;
209+
constValue = schema.additionalProperties.const;
208210
defaultValue = schema.additionalProperties.default;
209211
let apSchema = schema.additionalProperties;
210212
if (REF_KEY in apSchema) {
211213
const { schemaUtils } = registry;
212214
apSchema = schemaUtils.retrieveSchema({ $ref: apSchema[REF_KEY] } as S, formData);
213215
type = apSchema.type;
216+
constValue = apSchema.const;
214217
defaultValue = apSchema.default;
215218
}
216219
if (!type && (ANY_OF_KEY in apSchema || ONE_OF_KEY in apSchema)) {
@@ -219,8 +222,9 @@ class ObjectField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo
219222
}
220223

221224
const newKey = this.getAvailableKey('newKey', newFormData);
225+
const newValue = constValue ?? defaultValue ?? this.getDefaultValue(type);
222226
// Cast this to make the `set` work properly
223-
set(newFormData as GenericObjectType, newKey, defaultValue ?? this.getDefaultValue(type));
227+
set(newFormData as GenericObjectType, newKey, newValue);
224228

225229
onChange(newFormData);
226230
};

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,9 @@ function SchemaFieldRender<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
151151

152152
const FieldComponent = getFieldComponent<T, S, F>(schema, uiOptions, idSchema, registry);
153153
const disabled = Boolean(uiOptions.disabled ?? props.disabled);
154-
const readonly = Boolean(uiOptions.readonly ?? (props.readonly || props.schema.readOnly || schema.readOnly));
154+
const readonly = Boolean(
155+
uiOptions.readonly ?? (props.readonly || props.schema.const || props.schema.readOnly || schema.readOnly)
156+
);
155157
const uiSchemaHideError = uiOptions.hideError;
156158
// Set hideError to the value provided in the uiSchema, otherwise stick with the prop to propagate to children
157159
const hideError = uiSchemaHideError === undefined ? props.hideError : Boolean(uiSchemaHideError);

packages/utils/src/schema/getDefaultFormState.ts

+32-12
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import isEmpty from 'lodash/isEmpty';
33

44
import {
55
ANY_OF_KEY,
6+
CONST_KEY,
67
DEFAULT_KEY,
78
DEPENDENCIES_KEY,
89
PROPERTIES_KEY,
@@ -30,6 +31,8 @@ import {
3031
} from '../types';
3132
import isMultiSelect from './isMultiSelect';
3233
import retrieveSchema, { resolveDependencies } from './retrieveSchema';
34+
import isConstant from '../isConstant';
35+
import { JSONSchema7Object } from 'json-schema';
3336

3437
/** Enum that indicates how `schema.additionalItems` should be handled by the `getInnerSchemaForArrayItem()` function.
3538
*/
@@ -93,6 +96,7 @@ export function getInnerSchemaForArrayItem<S extends StrictRJSFSchema = RJSFSche
9396
* @param requiredFields - The list of fields that are required
9497
* @param experimental_defaultFormStateBehavior - Optional configuration object, if provided, allows users to override
9598
* default form state behavior
99+
* @param isConst - Optional flag, if true, indicates that the schema has a const property defined, thus we should always return the computedDefault since it's coming from the const.
96100
*/
97101
function maybeAddDefaultToObject<T = any>(
98102
obj: GenericObjectType,
@@ -101,10 +105,13 @@ function maybeAddDefaultToObject<T = any>(
101105
includeUndefinedValues: boolean | 'excludeObjectChildren',
102106
isParentRequired?: boolean,
103107
requiredFields: string[] = [],
104-
experimental_defaultFormStateBehavior: Experimental_DefaultFormStateBehavior = {}
108+
experimental_defaultFormStateBehavior: Experimental_DefaultFormStateBehavior = {},
109+
isConst = false
105110
) {
106111
const { emptyObjectFields = 'populateAllDefaults' } = experimental_defaultFormStateBehavior;
107-
if (includeUndefinedValues) {
112+
if (includeUndefinedValues || isConst) {
113+
// If includeUndefinedValues
114+
// Or if the schema has a const property defined, then we should always return the computedDefault since it's coming from the const.
108115
obj[key] = computedDefault;
109116
} else if (emptyObjectFields !== 'skipDefaults') {
110117
if (isObject(computedDefault)) {
@@ -194,7 +201,9 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
194201
let schemaToCompute: S | null = null;
195202
let updatedRecurseList = _recurseList;
196203

197-
if (isObject(defaults) && isObject(schema.default)) {
204+
if (isConstant(schema)) {
205+
defaults = schema.const as unknown as T;
206+
} else if (isObject(defaults) && isObject(schema.default)) {
198207
// For object defaults, only override parent defaults that are defined in
199208
// schema.default.
200209
defaults = mergeObjects(defaults!, schema.default as GenericObjectType) as T;
@@ -324,11 +333,16 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
324333
experimental_defaultFormStateBehavior?.allOf === 'populateDefaults' && ALL_OF_KEY in schema
325334
? retrieveSchema<T, S, F>(validator, schema, rootSchema, formData, experimental_customMergeAllOf)
326335
: schema;
336+
const parentConst = retrievedSchema[CONST_KEY];
327337
const objectDefaults = Object.keys(retrievedSchema.properties || {}).reduce(
328338
(acc: GenericObjectType, key: string) => {
339+
const propertySchema = get(retrievedSchema, [PROPERTIES_KEY, key]);
340+
// Check if the parent schema has a const property defined, then we should always return the computedDefault since it's coming from the const.
341+
const hasParentConst = isObject(parentConst) && (parentConst as JSONSchema7Object)[key] !== undefined;
342+
const hasConst = (isObject(propertySchema) && CONST_KEY in propertySchema) || hasParentConst;
329343
// Compute the defaults for this node, with the parent defaults we might
330344
// have from a previous run: defaults[key].
331-
const computedDefault = computeDefaults<T, S, F>(validator, get(retrievedSchema, [PROPERTIES_KEY, key]), {
345+
const computedDefault = computeDefaults<T, S, F>(validator, propertySchema, {
332346
rootSchema,
333347
_recurseList,
334348
experimental_defaultFormStateBehavior,
@@ -345,7 +359,8 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
345359
includeUndefinedValues,
346360
required,
347361
retrievedSchema.required,
348-
experimental_defaultFormStateBehavior
362+
experimental_defaultFormStateBehavior,
363+
hasConst
349364
);
350365
return acc;
351366
},
@@ -458,13 +473,17 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
458473
}
459474
}
460475

461-
if (neverPopulate) {
462-
return defaults ?? emptyDefault;
463-
}
464-
if (ignoreMinItemsFlagSet && !required) {
465-
// If no form data exists or defaults are set leave the field empty/non-existent, otherwise
466-
// return form data/defaults
467-
return defaults ? defaults : undefined;
476+
// Check if the schema has a const property defined, then we should always return the computedDefault since it's coming from the const.
477+
const hasConst = isObject(schema) && CONST_KEY in schema;
478+
if (hasConst === false) {
479+
if (neverPopulate) {
480+
return defaults ?? emptyDefault;
481+
}
482+
if (ignoreMinItemsFlagSet && !required) {
483+
// If no form data exists or defaults are set leave the field empty/non-existent, otherwise
484+
// return form data/defaults
485+
return defaults ? defaults : undefined;
486+
}
468487
}
469488

470489
const defaultsLength = Array.isArray(defaults) ? defaults.length : 0;
@@ -562,6 +581,7 @@ export default function getDefaultFormState<
562581
experimental_customMergeAllOf,
563582
rawFormData: formData,
564583
});
584+
565585
if (formData === undefined || formData === null || (typeof formData === 'number' && isNaN(formData))) {
566586
// No form data? Use schema defaults.
567587
return defaults;

packages/utils/test/schema/getDefaultFormStateTest.ts

+146
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,62 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
2222
it('throws error when schema is not an object', () => {
2323
expect(() => getDefaultFormState(testValidator, null as unknown as RJSFSchema)).toThrowError('Invalid schema:');
2424
});
25+
it('test an object const value merge with formData', () => {
26+
const schema: RJSFSchema = {
27+
type: 'object',
28+
properties: {
29+
localConst: {
30+
type: 'string',
31+
const: 'local',
32+
},
33+
RootConst: {
34+
type: 'object',
35+
properties: {
36+
attr1: {
37+
type: 'number',
38+
},
39+
attr2: {
40+
type: 'boolean',
41+
},
42+
},
43+
const: {
44+
attr1: 1,
45+
attr2: true,
46+
},
47+
},
48+
RootAndLocalConst: {
49+
type: 'string',
50+
const: 'FromLocal',
51+
},
52+
fromFormData: {
53+
type: 'string',
54+
},
55+
},
56+
const: {
57+
RootAndLocalConst: 'FromRoot',
58+
},
59+
};
60+
expect(
61+
getDefaultFormState(
62+
testValidator,
63+
schema,
64+
{
65+
fromFormData: 'fromFormData',
66+
},
67+
schema,
68+
false,
69+
{ emptyObjectFields: 'skipDefaults' }
70+
)
71+
).toEqual({
72+
localConst: 'local',
73+
RootConst: {
74+
attr1: 1,
75+
attr2: true,
76+
},
77+
RootAndLocalConst: 'FromLocal',
78+
fromFormData: 'fromFormData',
79+
});
80+
});
2581
it('getInnerSchemaForArrayItem() item of type boolean returns empty schema', () => {
2682
expect(getInnerSchemaForArrayItem({ items: [true] }, AdditionalItemsHandling.Ignore, 0)).toEqual({});
2783
});
@@ -48,6 +104,20 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
48104
foo: 42,
49105
});
50106
});
107+
it('test computeDefaults that is passed a schema with a const property', () => {
108+
const schema: RJSFSchema = {
109+
type: 'object',
110+
properties: {
111+
test: {
112+
type: 'string',
113+
const: 'test',
114+
},
115+
},
116+
};
117+
expect(computeDefaults(testValidator, schema, { rootSchema: schema })).toEqual({
118+
test: 'test',
119+
});
120+
});
51121
it('test an object with an optional property that has a nested required property', () => {
52122
const schema: RJSFSchema = {
53123
type: 'object',
@@ -848,6 +918,59 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
848918
requiredProperty: 'foo',
849919
});
850920
});
921+
it('test an object const value populate as field defaults', () => {
922+
const schema: RJSFSchema = {
923+
type: 'object',
924+
properties: {
925+
localConst: {
926+
type: 'string',
927+
const: 'local',
928+
},
929+
RootConst: {
930+
type: 'object',
931+
properties: {
932+
attr1: {
933+
type: 'number',
934+
},
935+
attr2: {
936+
type: 'boolean',
937+
},
938+
},
939+
const: {
940+
attr1: 1,
941+
attr2: true,
942+
},
943+
},
944+
fromFormData: {
945+
type: 'string',
946+
default: 'notUsed',
947+
},
948+
RootAndLocalConst: {
949+
type: 'string',
950+
const: 'FromLocal',
951+
},
952+
},
953+
const: {
954+
RootAndLocalConst: 'FromRoot',
955+
},
956+
};
957+
expect(
958+
getObjectDefaults(testValidator, schema, {
959+
rootSchema: schema,
960+
experimental_defaultFormStateBehavior: { emptyObjectFields: 'skipDefaults' },
961+
rawFormData: {
962+
fromFormData: 'fromFormData',
963+
},
964+
})
965+
).toEqual({
966+
localConst: 'local',
967+
RootConst: {
968+
attr1: 1,
969+
attr2: true,
970+
},
971+
RootAndLocalConst: 'FromLocal',
972+
});
973+
});
851974
it('test an object with an additionalProperties', () => {
852975
const schema: RJSFSchema = {
853976
type: 'object',
@@ -1065,6 +1188,29 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
10651188
)
10661189
).toEqual(['Raphael', 'Michaelangelo', 'Unknown', 'Unknown']);
10671190
});
1191+
it('test an array const value populate as defaults', () => {
1192+
const schema: RJSFSchema = {
1193+
type: 'array',
1194+
minItems: 4,
1195+
const: ['ConstFromRoot', 'ConstFromRoot'],
1196+
items: {
1197+
type: 'string',
1198+
const: 'Constant',
1199+
},
1200+
};
1201+
1202+
expect(
1203+
getArrayDefaults(
1204+
testValidator,
1205+
schema,
1206+
{
1207+
rootSchema: schema,
1208+
includeUndefinedValues: 'excludeObjectChildren',
1209+
},
1210+
['ConstFromRoot', 'ConstFromRoot']
1211+
)
1212+
).toEqual(['ConstFromRoot', 'ConstFromRoot', 'Constant', 'Constant']);
1213+
});
10681214
it('test an array with no defaults', () => {
10691215
const schema: RJSFSchema = {
10701216
type: 'array',

0 commit comments

Comments
 (0)