Skip to content

Fix 3759 - uneditable & permanent defaults with additional properties #4490

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,30 @@ 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.

-->
# 5.24.4

## @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

## @rjsf/validator-ajv6

- Updated `getDefaultFormState` calls to reflect addition of `initialDefaultsGenerated`

## @rjsf/validator-ajv8

- Updated `getDefaultFormState` calls to reflect addition of `initialDefaultsGenerated`

# 5.24.3

## @rjsf/utils
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/components/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,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 @@ -423,7 +425,8 @@ export default class Form<
experimental_customMergeAllOf
);
}
const formData: T = schemaUtils.getDefaultFormState(schema, inputFormData) as T;

const formData: T = schemaUtils.getDefaultFormState(schema, inputFormData, state.initialDefaultsGenerated) as T;
const _retrievedSchema = this.updateRetrievedSchema(
retrievedSchema ?? schemaUtils.retrieveSchema(schema, formData)
);
Expand Down Expand Up @@ -505,6 +508,7 @@ export default class Form<
schemaValidationErrors,
schemaValidationErrorSchema,
retrievedSchema: _retrievedSchema,
initialDefaultsGenerated: true,
};
return nextState;
}
Expand Down Expand Up @@ -757,6 +761,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: 1 addition & 1 deletion packages/core/src/components/fields/MultiSchemaField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ class AnyOfField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
if (newOption) {
// Call getDefaultFormState to make sure defaults are populated on change. Pass "excludeObjectChildren"
// so that only the root objects themselves are created without adding undefined children properties
newFormData = schemaUtils.getDefaultFormState(newOption, newFormData, 'excludeObjectChildren') as T;
newFormData = schemaUtils.getDefaultFormState(newOption, newFormData, false, 'excludeObjectChildren') as T;
}

this.setState({ selectedOption: intOption }, () => {
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 @@ -1512,6 +1513,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 @@ -1142,6 +1142,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('.array-item-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('.array-item-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 @@ -921,6 +921,7 @@ Returns the superset of `formData` that includes the given set updated to includ
- theSchema: S - The schema for which the default state is desired
- [formData]: T | undefined - The current formData, if any, onto which to provide any missing defaults
- [rootSchema]: S | undefined - The root schema, used to primarily to look up `$ref`s
- [initialDefaultsGenerated]: boolean - Flag indicating whether the initial form defaults have been generated
- [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
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 @@ -103,6 +103,7 @@ class SchemaUtils<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo
*
* @param schema - The schema for which the default state is desired
* @param [formData] - The current formData, if any, onto which to provide any missing defaults
* @param initialDefaultsGenerated - Indicates whether or not initial defaults have been generated
* @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.
Expand All @@ -111,13 +112,15 @@ class SchemaUtils<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo
getDefaultFormState(
schema: S,
formData?: T,
initialDefaultsGenerated?: boolean,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Piotr-Debicki Unfortunately, unless you add it to the end of the function call, you are introducing a breaking change. This will need to be the LAST, optional, parameter to the function.

includeUndefinedValues: boolean | 'excludeObjectChildren' = false
): T | T[] | undefined {
return getDefaultFormState<T, S, F>(
this.validator,
schema,
formData,
this.rootSchema,
initialDefaultsGenerated,
includeUndefinedValues,
this.experimental_defaultFormStateBehavior,
this.experimental_customMergeAllOf
Expand Down
Loading
Loading