Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,23 @@ it according to semantic versioning. For example, if your PR adds a breaking cha
should change the heading of the (upcoming) version to include a major version bump.

-->
# 6.0.0-beta.21

## @rjsf/core

- Added `initialDefaultsGenerated` flag to state, which indicates whether the initial generation of defaults has been completed
- Added `ObjectField` tests for additionalProperties with defaults

## @rjsf/utils

- 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)
- Updated `createSchemaUtils` definition to reflect addition of `initialDefaultsGenerated`
- Updated existing tests where `getDefaultFormState` is used to reflect addition of `initialDefaultsGenerated`

## @rjsf/docs

- Updated docs for `getDefaultFormState` to reflect addition of `initialDefaultsGenerated` prop

# 6.0.0-beta-20

## @rjsf/antd
Expand Down
12 changes: 11 additions & 1 deletion packages/core/src/components/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,8 @@ export interface FormState<T = any, S extends StrictRJSFSchema = RJSFSchema, F e
// Private
/** @description result of schemaUtils.retrieveSchema(schema, formData). This a memoized value to avoid re calculate at internal functions (getStateFromProps, onChange) */
retrievedSchema: S;
/** Flag indicating whether the initial form defaults have been generated */
initialDefaultsGenerated: boolean;
}

/** The event data passed when changes have been made to the form, includes everything from the `FormState` except
Expand Down Expand Up @@ -464,8 +466,14 @@ export default class Form<
experimental_customMergeAllOf,
);
}

const rootSchema = schemaUtils.getRootSchema();
const formData: T = schemaUtils.getDefaultFormState(rootSchema, inputFormData) as T;
const formData: T = schemaUtils.getDefaultFormState(
rootSchema,
inputFormData,
false,
state.initialDefaultsGenerated,
) as T;
const _retrievedSchema = this.updateRetrievedSchema(
retrievedSchema ?? schemaUtils.retrieveSchema(rootSchema, formData),
);
Expand Down Expand Up @@ -546,6 +554,7 @@ export default class Form<
schemaValidationErrors,
schemaValidationErrorSchema,
retrievedSchema: _retrievedSchema,
initialDefaultsGenerated: true,
};
return nextState;
}
Expand Down Expand Up @@ -888,6 +897,7 @@ export default class Form<
errors: [] as unknown,
schemaValidationErrors: [] as unknown,
schemaValidationErrorSchema: {},
initialDefaultsGenerated: false,
} as FormState<T, S, F>;

this.setState(state, () => onChange && onChange({ ...this.state, ...state }));
Expand Down
2 changes: 2 additions & 0 deletions packages/core/test/Form.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ describeRepeated('Form common', (createFormComponent) => {
schemaValidationErrorSchema: undefined,
schemaUtils: sinon.match.object,
retrievedSchema: schema,
initialDefaultsGenerated: true,
});
});
});
Expand Down Expand Up @@ -1979,6 +1980,7 @@ describeRepeated('Form common', (createFormComponent) => {
schemaValidationErrorSchema: undefined,
schemaUtils: sinon.match.object,
retrievedSchema: formProps.schema,
initialDefaultsGenerated: true,
});
});
});
Expand Down
159 changes: 159 additions & 0 deletions packages/core/test/ObjectField.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1144,6 +1144,165 @@ describe('ObjectField', () => {
});
});

it('should generate the specified default key and value inputs if default is provided outside of additionalProperties schema', () => {
const customSchema = {
...schema,
default: {
defaultKey: 'defaultValue',
},
};
const { onChange } = createFormComponent({
schema: customSchema,
formData: {},
});

sinon.assert.calledWithMatch(onChange.lastCall, {
formData: {
defaultKey: 'defaultValue',
},
});
});

it('should generate the specified default key/value input with custom formData provided', () => {
const customSchema = {
...schema,
default: {
defaultKey: 'defaultValue',
},
};
const { onChange } = createFormComponent({
schema: customSchema,
formData: {
someData: 'someValue',
},
});

sinon.assert.calledWithMatch(onChange.lastCall, {
formData: {
defaultKey: 'defaultValue',
someData: 'someValue',
},
});
});

it('should edit the specified default key without duplicating', () => {
const customSchema = {
...schema,
default: {
defaultKey: 'defaultValue',
},
};
const { node, onChange } = createFormComponent({
schema: customSchema,
formData: {},
});

fireEvent.blur(node.querySelector('#root_defaultKey-key'), { target: { value: 'newDefaultKey' } });

sinon.assert.calledWithMatch(onChange.lastCall, {
formData: {
newDefaultKey: 'defaultValue',
},
});
});

it('should remove the specified default key/value input item', () => {
const customSchema = {
...schema,
default: {
defaultKey: 'defaultValue',
},
};
const { node, onChange } = createFormComponent({
schema: customSchema,
formData: {},
});

fireEvent.click(node.querySelector('.rjsf-object-property-remove'));

sinon.assert.calledWithMatch(onChange.lastCall, {
formData: {},
});
});

it('should handle nested additional property default key/value input generation', () => {
const customSchema = {
...schema,
default: {
defaultKey: 'defaultValue',
},
properties: {
nested: {
type: 'object',
properties: {
bar: {
type: 'object',
additionalProperties: {
type: 'string',
},
default: {
baz: 'value',
},
},
},
},
},
};

const { onChange } = createFormComponent({
schema: customSchema,
formData: {},
});

sinon.assert.calledWithMatch(onChange.lastCall, {
formData: {
defaultKey: 'defaultValue',
nested: {
bar: {
baz: 'value',
},
},
},
});
});

it('should remove nested additional property default key/value input', () => {
const customSchema = {
...schema,
properties: {
nested: {
type: 'object',
properties: {
bar: {
type: 'object',
additionalProperties: {
type: 'string',
},
default: {
baz: 'value',
},
},
},
},
},
};

const { node, onChange } = createFormComponent({
schema: customSchema,
formData: {},
});

fireEvent.click(node.querySelector('.rjsf-object-property-remove'));

sinon.assert.calledWithMatch(onChange.lastCall, {
formData: {
nested: {
bar: {},
},
},
});
});

it('should not provide an expand button if length equals maxProperties', () => {
const { node } = createFormComponent({
schema: { maxProperties: 1, ...schema },
Expand Down
1 change: 1 addition & 0 deletions packages/docs/docs/api-reference/utility-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -1065,6 +1065,7 @@ Returns the superset of `formData` that includes the given set updated to includ
- [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.
- [experimental_defaultFormStateBehavior]: Experimental_DefaultFormStateBehavior - See `Form` documentation for the [experimental_defaultFormStateBehavior](./form-props.md#experimental_defaultFormStateBehavior) prop
- [experimental_customMergeAllOf]: Experimental_CustomMergeAllOf&lt;S&gt; - See `Form` documentation for the [experimental_customMergeAllOf](./form-props.md#experimental_custommergeallof) prop
- [initialDefaultsGenerated]: boolean - Optional flag, indicates whether or not initial defaults have been generated

#### Returns

Expand Down
4 changes: 4 additions & 0 deletions packages/docs/docs/migration-guides/v6.x upgrade guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,10 @@ Three new validator-based utility functions are available in `@rjsf/utils`:
- `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
- `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

### Changes to existing utility functions

- `getDefaultFormState`: Added optional `initialDefaultsGenerated` boolean flag that indicates whether or not initial defaults have been generated

### Dynamic UI Schema for Array Items

RJSF 6.x introduces a new feature that allows dynamic UI schema generation for array items.
Expand Down
3 changes: 3 additions & 0 deletions packages/utils/src/createSchemaUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,14 @@ class SchemaUtils<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo
* @param [includeUndefinedValues=false] - Optional flag, if true, cause undefined values to be added as defaults.
* If "excludeObjectChildren", pass `includeUndefinedValues` as false when computing defaults for any nested
* object properties.
* @param initialDefaultsGenerated - Indicates whether or not initial defaults have been generated
* @returns - The resulting `formData` with all the defaults provided
*/
getDefaultFormState(
schema: S,
formData?: T,
includeUndefinedValues: boolean | 'excludeObjectChildren' = false,
initialDefaultsGenerated?: boolean,
): T | T[] | undefined {
return getDefaultFormState<T, S, F>(
this.validator,
Expand All @@ -180,6 +182,7 @@ class SchemaUtils<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo
includeUndefinedValues,
this.experimental_defaultFormStateBehavior,
this.experimental_customMergeAllOf,
initialDefaultsGenerated,
);
}

Expand Down
15 changes: 14 additions & 1 deletion packages/utils/src/schema/getDefaultFormState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ interface ComputeDefaultsProps<T = any, S extends StrictRJSFSchema = RJSFSchema>
* 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.
*/
shouldMergeDefaultsIntoFormData?: boolean;
/** Indicates whether initial defaults have been generated */
initialDefaultsGenerated?: boolean;
}

/** Computes the defaults for the current `schema` given the `rawFormData` and `parentDefaults` if any. This drills into
Expand All @@ -229,6 +231,7 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
experimental_customMergeAllOf = undefined,
required,
shouldMergeDefaultsIntoFormData = false,
initialDefaultsGenerated,
} = computeDefaultsProps;
let formData: T = (isObject(rawFormData) ? rawFormData : {}) as T;
const schema: S = isObject(rawSchema) ? rawSchema : ({} as S);
Expand Down Expand Up @@ -363,6 +366,7 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
rawFormData: (rawFormData ?? formData) as T,
required,
shouldMergeDefaultsIntoFormData,
initialDefaultsGenerated,
});
}

Expand Down Expand Up @@ -463,6 +467,7 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
experimental_customMergeAllOf = undefined,
required,
shouldMergeDefaultsIntoFormData,
initialDefaultsGenerated,
}: ComputeDefaultsProps<T, S> = {},
defaults?: T | T[],
): T {
Expand Down Expand Up @@ -498,6 +503,7 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
rawFormData: get(formData, [key]),
required: retrievedSchema.required?.includes(key),
shouldMergeDefaultsIntoFormData,
initialDefaultsGenerated,
});

maybeAddDefaultToObject<T>(
Expand All @@ -515,7 +521,7 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
},
{},
) as T;
if (retrievedSchema.additionalProperties) {
if (retrievedSchema.additionalProperties && !initialDefaultsGenerated) {
// as per spec additionalProperties may be either schema or boolean
const additionalPropertiesSchema = isObject(retrievedSchema.additionalProperties)
? retrievedSchema.additionalProperties
Expand Down Expand Up @@ -545,6 +551,7 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
rawFormData: get(formData, [key]),
required: retrievedSchema.required?.includes(key),
shouldMergeDefaultsIntoFormData,
initialDefaultsGenerated,
});
// Since these are additional properties we don't need to add the `experimental_defaultFormStateBehavior` prop
maybeAddDefaultToObject<T>(
Expand Down Expand Up @@ -580,6 +587,7 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
experimental_customMergeAllOf = undefined,
required,
shouldMergeDefaultsIntoFormData,
initialDefaultsGenerated,
}: ComputeDefaultsProps<T, S> = {},
defaults?: T[],
): T[] | undefined {
Expand Down Expand Up @@ -608,6 +616,7 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
parentDefaults: item,
required,
shouldMergeDefaultsIntoFormData,
initialDefaultsGenerated,
});
}) as T[];
}
Expand All @@ -628,6 +637,7 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
parentDefaults: get(defaults, [idx]),
required,
shouldMergeDefaultsIntoFormData,
initialDefaultsGenerated,
});
}) as T[];

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

if (schema.type !== 'object' && isObject(schema.default)) {
Expand Down
Loading