Skip to content

Commit

Permalink
Make fields with const pre-fiiled and readonly #3843 (#4326)
Browse files Browse the repository at this point in the history
* 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 <abdallah.al-soqatri@aspentech.com>
Co-authored-by: Heath C <51679588+heath-freenome@users.noreply.github.com>
  • Loading branch information
3 people authored Oct 21, 2024
1 parent a347257 commit f6c5bf7
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 15 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ should change the heading of the (upcoming) version to include a major version b
-->

# 5.21.3
# 5.22.0

## @rjsf/utils

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

# 5.21.2
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/components/fields/ObjectField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,15 +202,18 @@ class ObjectField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo
const newFormData = { ...formData } as T;

let type: RJSFSchema['type'] = undefined;
let constValue: RJSFSchema['const'] = undefined;
let defaultValue: RJSFSchema['default'] = undefined;
if (isObject(schema.additionalProperties)) {
type = schema.additionalProperties.type;
constValue = schema.additionalProperties.const;
defaultValue = schema.additionalProperties.default;
let apSchema = schema.additionalProperties;
if (REF_KEY in apSchema) {
const { schemaUtils } = registry;
apSchema = schemaUtils.retrieveSchema({ $ref: apSchema[REF_KEY] } as S, formData);
type = apSchema.type;
constValue = apSchema.const;
defaultValue = apSchema.default;
}
if (!type && (ANY_OF_KEY in apSchema || ONE_OF_KEY in apSchema)) {
Expand All @@ -219,8 +222,9 @@ class ObjectField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo
}

const newKey = this.getAvailableKey('newKey', newFormData);
const newValue = constValue ?? defaultValue ?? this.getDefaultValue(type);
// Cast this to make the `set` work properly
set(newFormData as GenericObjectType, newKey, defaultValue ?? this.getDefaultValue(type));
set(newFormData as GenericObjectType, newKey, newValue);

onChange(newFormData);
};
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/components/fields/SchemaField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,9 @@ function SchemaFieldRender<T = any, S extends StrictRJSFSchema = RJSFSchema, F e

const FieldComponent = getFieldComponent<T, S, F>(schema, uiOptions, idSchema, registry);
const disabled = Boolean(uiOptions.disabled ?? props.disabled);
const readonly = Boolean(uiOptions.readonly ?? (props.readonly || props.schema.readOnly || schema.readOnly));
const readonly = Boolean(
uiOptions.readonly ?? (props.readonly || props.schema.const || props.schema.readOnly || schema.readOnly)
);
const uiSchemaHideError = uiOptions.hideError;
// Set hideError to the value provided in the uiSchema, otherwise stick with the prop to propagate to children
const hideError = uiSchemaHideError === undefined ? props.hideError : Boolean(uiSchemaHideError);
Expand Down
44 changes: 32 additions & 12 deletions packages/utils/src/schema/getDefaultFormState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import isEmpty from 'lodash/isEmpty';

import {
ANY_OF_KEY,
CONST_KEY,
DEFAULT_KEY,
DEPENDENCIES_KEY,
PROPERTIES_KEY,
Expand Down Expand Up @@ -30,6 +31,8 @@ import {
} from '../types';
import isMultiSelect from './isMultiSelect';
import retrieveSchema, { resolveDependencies } from './retrieveSchema';
import isConstant from '../isConstant';
import { JSONSchema7Object } from 'json-schema';

/** Enum that indicates how `schema.additionalItems` should be handled by the `getInnerSchemaForArrayItem()` function.
*/
Expand Down Expand Up @@ -93,6 +96,7 @@ export function getInnerSchemaForArrayItem<S extends StrictRJSFSchema = RJSFSche
* @param requiredFields - The list of fields that are required
* @param experimental_defaultFormStateBehavior - Optional configuration object, if provided, allows users to override
* default form state behavior
* @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.
*/
function maybeAddDefaultToObject<T = any>(
obj: GenericObjectType,
Expand All @@ -101,10 +105,13 @@ function maybeAddDefaultToObject<T = any>(
includeUndefinedValues: boolean | 'excludeObjectChildren',
isParentRequired?: boolean,
requiredFields: string[] = [],
experimental_defaultFormStateBehavior: Experimental_DefaultFormStateBehavior = {}
experimental_defaultFormStateBehavior: Experimental_DefaultFormStateBehavior = {},
isConst = false
) {
const { emptyObjectFields = 'populateAllDefaults' } = experimental_defaultFormStateBehavior;
if (includeUndefinedValues) {
if (includeUndefinedValues || isConst) {
// If includeUndefinedValues
// Or if the schema has a const property defined, then we should always return the computedDefault since it's coming from the const.
obj[key] = computedDefault;
} else if (emptyObjectFields !== 'skipDefaults') {
if (isObject(computedDefault)) {
Expand Down Expand Up @@ -194,7 +201,9 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
let schemaToCompute: S | null = null;
let updatedRecurseList = _recurseList;

if (isObject(defaults) && isObject(schema.default)) {
if (isConstant(schema)) {
defaults = schema.const as unknown as T;
} else if (isObject(defaults) && isObject(schema.default)) {
// For object defaults, only override parent defaults that are defined in
// schema.default.
defaults = mergeObjects(defaults!, schema.default as GenericObjectType) as T;
Expand Down Expand Up @@ -324,11 +333,16 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
experimental_defaultFormStateBehavior?.allOf === 'populateDefaults' && ALL_OF_KEY in schema
? retrieveSchema<T, S, F>(validator, schema, rootSchema, formData, experimental_customMergeAllOf)
: schema;
const parentConst = retrievedSchema[CONST_KEY];
const objectDefaults = Object.keys(retrievedSchema.properties || {}).reduce(
(acc: GenericObjectType, key: string) => {
const propertySchema = get(retrievedSchema, [PROPERTIES_KEY, key]);
// Check if the parent schema has a const property defined, then we should always return the computedDefault since it's coming from the const.
const hasParentConst = isObject(parentConst) && (parentConst as JSONSchema7Object)[key] !== undefined;
const hasConst = (isObject(propertySchema) && CONST_KEY in propertySchema) || hasParentConst;
// Compute the defaults for this node, with the parent defaults we might
// have from a previous run: defaults[key].
const computedDefault = computeDefaults<T, S, F>(validator, get(retrievedSchema, [PROPERTIES_KEY, key]), {
const computedDefault = computeDefaults<T, S, F>(validator, propertySchema, {
rootSchema,
_recurseList,
experimental_defaultFormStateBehavior,
Expand All @@ -345,7 +359,8 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
includeUndefinedValues,
required,
retrievedSchema.required,
experimental_defaultFormStateBehavior
experimental_defaultFormStateBehavior,
hasConst
);
return acc;
},
Expand Down Expand Up @@ -458,13 +473,17 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
}
}

if (neverPopulate) {
return defaults ?? emptyDefault;
}
if (ignoreMinItemsFlagSet && !required) {
// If no form data exists or defaults are set leave the field empty/non-existent, otherwise
// return form data/defaults
return defaults ? defaults : undefined;
// Check if the schema has a const property defined, then we should always return the computedDefault since it's coming from the const.
const hasConst = isObject(schema) && CONST_KEY in schema;
if (hasConst === false) {
if (neverPopulate) {
return defaults ?? emptyDefault;
}
if (ignoreMinItemsFlagSet && !required) {
// If no form data exists or defaults are set leave the field empty/non-existent, otherwise
// return form data/defaults
return defaults ? defaults : undefined;
}
}

const defaultsLength = Array.isArray(defaults) ? defaults.length : 0;
Expand Down Expand Up @@ -562,6 +581,7 @@ export default function getDefaultFormState<
experimental_customMergeAllOf,
rawFormData: formData,
});

if (formData === undefined || formData === null || (typeof formData === 'number' && isNaN(formData))) {
// No form data? Use schema defaults.
return defaults;
Expand Down
146 changes: 146 additions & 0 deletions packages/utils/test/schema/getDefaultFormStateTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,62 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
it('throws error when schema is not an object', () => {
expect(() => getDefaultFormState(testValidator, null as unknown as RJSFSchema)).toThrowError('Invalid schema:');
});
it('test an object const value merge with formData', () => {
const schema: RJSFSchema = {
type: 'object',
properties: {
localConst: {
type: 'string',
const: 'local',
},
RootConst: {
type: 'object',
properties: {
attr1: {
type: 'number',
},
attr2: {
type: 'boolean',
},
},
const: {
attr1: 1,
attr2: true,
},
},
RootAndLocalConst: {
type: 'string',
const: 'FromLocal',
},
fromFormData: {
type: 'string',
},
},
const: {
RootAndLocalConst: 'FromRoot',
},
};
expect(
getDefaultFormState(
testValidator,
schema,
{
fromFormData: 'fromFormData',
},
schema,
false,
{ emptyObjectFields: 'skipDefaults' }
)
).toEqual({
localConst: 'local',
RootConst: {
attr1: 1,
attr2: true,
},
RootAndLocalConst: 'FromLocal',
fromFormData: 'fromFormData',
});
});
it('getInnerSchemaForArrayItem() item of type boolean returns empty schema', () => {
expect(getInnerSchemaForArrayItem({ items: [true] }, AdditionalItemsHandling.Ignore, 0)).toEqual({});
});
Expand All @@ -48,6 +104,20 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
foo: 42,
});
});
it('test computeDefaults that is passed a schema with a const property', () => {
const schema: RJSFSchema = {
type: 'object',
properties: {
test: {
type: 'string',
const: 'test',
},
},
};
expect(computeDefaults(testValidator, schema, { rootSchema: schema })).toEqual({
test: 'test',
});
});
it('test an object with an optional property that has a nested required property', () => {
const schema: RJSFSchema = {
type: 'object',
Expand Down Expand Up @@ -848,6 +918,59 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
requiredProperty: 'foo',
});
});
it('test an object const value populate as field defaults', () => {
const schema: RJSFSchema = {
type: 'object',
properties: {
localConst: {
type: 'string',
const: 'local',
},
RootConst: {
type: 'object',
properties: {
attr1: {
type: 'number',
},
attr2: {
type: 'boolean',
},
},
const: {
attr1: 1,
attr2: true,
},
},
fromFormData: {
type: 'string',
default: 'notUsed',
},
RootAndLocalConst: {
type: 'string',
const: 'FromLocal',
},
},
const: {
RootAndLocalConst: 'FromRoot',
},
};
expect(
getObjectDefaults(testValidator, schema, {
rootSchema: schema,
experimental_defaultFormStateBehavior: { emptyObjectFields: 'skipDefaults' },
rawFormData: {
fromFormData: 'fromFormData',
},
})
).toEqual({
localConst: 'local',
RootConst: {
attr1: 1,
attr2: true,
},
RootAndLocalConst: 'FromLocal',
});
});
it('test an object with an additionalProperties', () => {
const schema: RJSFSchema = {
type: 'object',
Expand Down Expand Up @@ -1065,6 +1188,29 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
)
).toEqual(['Raphael', 'Michaelangelo', 'Unknown', 'Unknown']);
});
it('test an array const value populate as defaults', () => {
const schema: RJSFSchema = {
type: 'array',
minItems: 4,
const: ['ConstFromRoot', 'ConstFromRoot'],
items: {
type: 'string',
const: 'Constant',
},
};

expect(
getArrayDefaults(
testValidator,
schema,
{
rootSchema: schema,
includeUndefinedValues: 'excludeObjectChildren',
},
['ConstFromRoot', 'ConstFromRoot']
)
).toEqual(['ConstFromRoot', 'ConstFromRoot', 'Constant', 'Constant']);
});
it('test an array with no defaults', () => {
const schema: RJSFSchema = {
type: 'array',
Expand Down

0 comments on commit f6c5bf7

Please sign in to comment.