Skip to content
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

fix: reimplement #1588 and #2816 #3435

Merged
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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ should change the heading of the (upcoming) version to include a major version b
- Added new feature prop `focusOnFirstError`, that if true, will cause the first field with an error to be focused on when a submit has errors

## @rjsf/utils
- Updated `getDefaultFormState()` to add a new possible value for `includeUndefinedValues` called `allowEmptyObject` which prevents undefined values within an object but allows an empty object itself.
- Updated `getDefaultFormState()` to add a new possible value for `includeUndefinedValues` called `allowEmptyObject` which prevents undefined values within an object but allows an empty object itself.
- Updated `computeDefaults()` to fix additionalProperties defaults not being propagated, fixing [#2593](https://github.com/rjsf-team/react-jsonschema-form/issues/2593)
- Also made sure to properly deal with empty `anyOf`/`oneOf` lists by simply returning undefined

## Dev / docs / playground
- Updated the `utility-functions` documentation to describe the addition of `allowEmptyObject` to `getDefaultFormState()`'s `includeUndefinedValues` parameter.
Expand Down
193 changes: 193 additions & 0 deletions packages/core/test/Form_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,199 @@ describeRepeated("Form common", (createFormComponent) => {
});
});
});
describe("Defaults additionalProperties propagation", () => {
it("should submit string string map defaults", () => {
const schema = {
type: "object",
additionalProperties: {
type: "string",
},
default: {
foo: "bar",
},
};

const { node, onSubmit } = createFormComponent({ schema });
Simulate.submit(node);

sinon.assert.calledWithMatch(onSubmit.lastCall, {
formData: {
foo: "bar",
},
});
});

it("should submit a combination of properties and additional properties defaults", () => {
const schema = {
type: "object",
properties: {
x: {
type: "string",
},
},
additionalProperties: {
type: "string",
},
default: {
x: "x default value",
y: "y default value",
},
};

const { node, onSubmit } = createFormComponent({ schema });
Simulate.submit(node);

sinon.assert.calledWithMatch(onSubmit.lastCall, {
formData: {
x: "x default value",
y: "y default value",
},
});
});

it("should submit a properties and additional properties defaults when properties default is nested", () => {
const schema = {
type: "object",
properties: {
x: {
type: "string",
default: "x default value",
},
},
additionalProperties: {
type: "string",
},
default: {
y: "y default value",
},
};

const { node, onSubmit } = createFormComponent({ schema });
Simulate.submit(node);

sinon.assert.calledWithMatch(onSubmit.lastCall, {
formData: {
x: "x default value",
y: "y default value",
},
});
});

it("should submit defaults when nested map has map values", () => {
const schema = {
type: "object",
properties: {
x: {
additionalProperties: {
$ref: "#/definitions/objectDef",
},
},
},
definitions: {
objectDef: {
type: "object",
additionalProperties: {
type: "string",
},
},
},
default: {
x: {
y: {
z: "x.y.z default value",
},
},
},
};

const { node, onSubmit } = createFormComponent({ schema });
Simulate.submit(node);

sinon.assert.calledWithMatch(onSubmit.lastCall, {
formData: {
x: {
y: {
z: "x.y.z default value",
},
},
},
});
});

it("should submit defaults when they are defined in a nested additionalProperties", () => {
const schema = {
type: "object",
properties: {
x: {
additionalProperties: {
type: "string",
default: "x.y default value",
},
},
},
default: {
x: {
y: {},
},
},
};

const { node, onSubmit } = createFormComponent({ schema });
Simulate.submit(node);

sinon.assert.calledWithMatch(onSubmit.lastCall, {
formData: {
x: {
y: "x.y default value",
},
},
});
});

it("should submit defaults when additionalProperties is a boolean value", () => {
const schema = {
type: "object",
additionalProperties: true,
default: {
foo: "bar",
},
};

const { node, onSubmit } = createFormComponent({ schema });
Simulate.submit(node);

sinon.assert.calledWithMatch(onSubmit.lastCall, {
formData: {
foo: "bar",
},
});
});

it("should NOT submit default values when additionalProperties is false", () => {
const schema = {
type: "object",
properties: {
foo: {
type: "string",
},
},
additionalProperties: false,
default: {
foo: "I'm the only one",
bar: "I don't belong here",
},
};

const { node, onSubmit } = createFormComponent({ schema });
Simulate.submit(node);

sinon.assert.calledWithMatch(onSubmit.lastCall, {
formData: {
foo: "I'm the only one",
},
});
});
});

describe("Submit handler", () => {
it("should call provided submit handler with form state", () => {
Expand Down
102 changes: 80 additions & 22 deletions packages/utils/src/schema/getDefaultFormState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,47 @@ export function getInnerSchemaForArrayItem<
return {} as S;
}

/** Either add `computedDefault` at `key` into `obj` or not add it based on its value and the value of
* `includeUndefinedValues`. Generally undefined `computedDefault` values are added only when `includeUndefinedValues`
* is either true or "excludeObjectChildren". If `includeUndefinedValues` is false, then non-undefined and
* non-empty-object values will be added.
*
* @param obj - The object into which the computed default may be added
* @param key - The key into the object at which the computed default may be added
* @param computedDefault - The computed default value that maybe should be added to the obj
* @param includeUndefinedValues - 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. If "allowEmptyObject", prevents undefined
* values in this object while allow the object itself to be empty and passing `includeUndefinedValues` as
* false when computing defaults for any nested object properties.
*/
function maybeAddDefaultToObject<T = any>(
obj: GenericObjectType,
key: string,
computedDefault: T | T[] | undefined,
includeUndefinedValues: boolean | "excludeObjectChildren" | "allowEmptyObject"
) {
if (includeUndefinedValues) {
// Check to make sure an undefined value is allowed, otherwise don't save it
if (
allowUndefinedForIncludeUndefinedValues.includes(
includeUndefinedValues
) ||
computedDefault !== undefined
) {
obj[key] = computedDefault;
}
} else if (isObject(computedDefault)) {
// Store computedDefault if it's a non-empty object (e.g. not {})
if (!isEmpty(computedDefault)) {
obj[key] = computedDefault;
}
} else if (computedDefault !== undefined) {
// Store computedDefault if it's a defined primitive (e.g. true)
obj[key] = computedDefault;
}
}

/** Computes the defaults for the current `schema` given the `rawFormData` and `parentDefaults` if any. This drills into
* each level of the schema, recursively, to fill out every level of defaults provided by the schema.
*
Expand Down Expand Up @@ -167,6 +208,9 @@ export function computeDefaults<
)
) as T[];
} else if (ONE_OF_KEY in schema) {
if (schema.oneOf!.length === 0) {
return undefined;
}
schema = schema.oneOf![
getClosestMatchingOption<T, S, F>(
validator,
Expand All @@ -177,6 +221,9 @@ export function computeDefaults<
)
] as S;
} else if (ANY_OF_KEY in schema) {
if (schema.anyOf!.length === 0) {
return undefined;
}
schema = schema.anyOf![
getClosestMatchingOption<T, S, F>(
validator,
Expand All @@ -195,8 +242,8 @@ export function computeDefaults<

switch (getSchemaType<S>(schema)) {
// We need to recur for object schema inner default values.
case "object":
return Object.keys(schema.properties || {}).reduce(
case "object": {
const objectDefaults = Object.keys(schema.properties || {}).reduce(
(acc: GenericObjectType, key: string) => {
// Compute the defaults for this node, with the parent defaults we might
// have from a previous run: defaults[key].
Expand All @@ -208,30 +255,41 @@ export function computeDefaults<
get(formData, [key]),
includeUndefinedValues === true
);
if (includeUndefinedValues) {
// Check to make sure an undefined value is allowed, otherwise don't save it
if (
allowUndefinedForIncludeUndefinedValues.includes(
includeUndefinedValues
) ||
computedDefault !== undefined
) {
acc[key] = computedDefault;
}
} else if (isObject(computedDefault)) {
// Store computedDefault if it's a non-empty object (e.g. not {})
if (!isEmpty(computedDefault)) {
acc[key] = computedDefault;
}
} else if (computedDefault !== undefined) {
// Store computedDefault if it's a defined primitive (e.g. true)
acc[key] = computedDefault;
}
maybeAddDefaultToObject<T>(
acc,
key,
computedDefault,
includeUndefinedValues
);
return acc;
},
{}
) as T;

if (schema.additionalProperties && isObject(defaults)) {
const additionalPropertiesSchema = isObject(schema.additionalProperties)
? schema.additionalProperties
: {}; // as per spec additionalProperties may be either schema or boolean
Object.keys(defaults as GenericObjectType)
.filter((key) => !schema.properties || !schema.properties[key])
.forEach((key) => {
const computedDefault = computeDefaults(
validator,
additionalPropertiesSchema as S,
get(defaults, [key]),
rootSchema,
get(formData, [key]),
includeUndefinedValues === true
);
maybeAddDefaultToObject<T>(
objectDefaults as GenericObjectType,
key,
computedDefault,
includeUndefinedValues
);
});
}
return objectDefaults;
}
case "array":
// Inject defaults into existing array defaults
if (Array.isArray(defaults)) {
Expand Down
Loading