From f9d4c63cac24146e522528d33eb04f465464e052 Mon Sep 17 00:00:00 2001 From: Lucian Buzzo Date: Thu, 21 Feb 2019 00:48:04 +0000 Subject: [PATCH] Fix multiple bugs related to switching between anyOf/oneOf options (#1169) * Fix multiple bugs related to switching between anyOf/oneOf options Fixes #1168 - Fixed a bug that would prevent input fields from rendering when switching between a non-object type option and an object type option - Fixed a bug where options would incorrectly change when entering values if a subschema with multiple required fields is used - Fixed a bug where switching from an object tpye option to a non-object type option would result in an input field containing the value [Object object] Change-type: patch Signed-off-by: Lucian * Update src/utils.js * Update src/utils.js --- src/components/fields/MultiSchemaField.js | 13 ++- src/components/fields/ObjectField.js | 2 +- src/utils.js | 4 +- test/anyOf_test.js | 46 ++++++++ test/oneOf_test.js | 135 ++++++++++++++++++++++ test/utils_test.js | 76 ++++++++++++ 6 files changed, 273 insertions(+), 3 deletions(-) diff --git a/src/components/fields/MultiSchemaField.js b/src/components/fields/MultiSchemaField.js index 593590e6b8..325a1b9851 100644 --- a/src/components/fields/MultiSchemaField.js +++ b/src/components/fields/MultiSchemaField.js @@ -69,6 +69,10 @@ class AnyOfField extends Component { augmentedSchema = Object.assign({}, option, requiresAnyOf); } + // Remove the "required" field as it's likely that not all fields have + // been filled in yet, which will mean that the schema is not valid + delete augmentedSchema.required; + if (isValid(augmentedSchema, formData)) { return i; } @@ -85,7 +89,14 @@ class AnyOfField extends Component { const selectedOption = parseInt(event.target.value, 10); const { formData, onChange, options } = this.props; - if (guessType(formData) === "object") { + const newOption = options[selectedOption]; + + // If the new option is of type object and the current data is an object, + // discard properties added using the old option. + if ( + guessType(formData) === "object" && + (newOption.type === "object" || newOption.properties) + ) { const newFormData = Object.assign({}, formData); const optionsToDiscard = options.slice(); diff --git a/src/components/fields/ObjectField.js b/src/components/fields/ObjectField.js index b6514d82e0..8189536516 100644 --- a/src/components/fields/ObjectField.js +++ b/src/components/fields/ObjectField.js @@ -237,7 +237,7 @@ class ObjectField extends Component { errorSchema={errorSchema[name]} idSchema={idSchema[name]} idPrefix={idPrefix} - formData={formData[name]} + formData={(formData || {})[name]} onKeyChange={this.onKeyChange(name)} onChange={this.onPropertyChange( name, diff --git a/src/utils.js b/src/utils.js index ad92ee494b..d55386cf91 100644 --- a/src/utils.js +++ b/src/utils.js @@ -732,7 +732,9 @@ export function toIdSchema( field, fieldId, definitions, - formData[name], + // It's possible that formData is not an object -- this can happen if an + // array item has just been added, but not populated with data yet + (formData || {})[name], idPrefix ); } diff --git a/test/anyOf_test.js b/test/anyOf_test.js index 51566c3bf1..a2bd14b200 100644 --- a/test/anyOf_test.js +++ b/test/anyOf_test.js @@ -571,5 +571,51 @@ describe("anyOf", () => { expect(node.querySelectorAll("input#root_foo")).to.have.length.of(1); }); + + it("should correctly render mixed types for anyOf inside array items", () => { + const schema = { + type: "object", + properties: { + items: { + type: "array", + items: { + anyOf: [ + { + type: "string", + }, + { + type: "object", + properties: { + foo: { + type: "integer", + }, + bar: { + type: "string", + }, + }, + }, + ], + }, + }, + }, + }; + + const { node } = createFormComponent({ + schema, + }); + + expect(node.querySelector(".array-item-add button")).not.eql(null); + + Simulate.click(node.querySelector(".array-item-add button")); + + const $select = node.querySelector("select"); + expect($select).not.eql(null); + Simulate.change($select, { + target: { value: $select.options[1].value }, + }); + + expect(node.querySelectorAll("input#root_foo")).to.have.length.of(1); + expect(node.querySelectorAll("input#root_bar")).to.have.length.of(1); + }); }); }); diff --git a/test/oneOf_test.js b/test/oneOf_test.js index 25fe39f97d..a7589dae68 100644 --- a/test/oneOf_test.js +++ b/test/oneOf_test.js @@ -299,4 +299,139 @@ describe("oneOf", () => { expect(node.querySelector("select").value).eql("1"); }); + + it("should not change the selected option when entering values on a subschema with multiple required options", () => { + const schema = { + type: "object", + properties: { + items: { + oneOf: [ + { + type: "string", + }, + { + type: "object", + properties: { + foo: { + type: "integer", + }, + bar: { + type: "string", + }, + }, + required: ["foo", "bar"], + }, + ], + }, + }, + }; + + const { node } = createFormComponent({ + schema, + }); + + const $select = node.querySelector("select"); + + expect($select.value).eql("0"); + + Simulate.change($select, { + target: { value: $select.options[1].value }, + }); + + expect($select.value).eql("1"); + + Simulate.change(node.querySelector("input#root_bar"), { + target: { value: "Lorem ipsum dolor sit amet" }, + }); + + expect($select.value).eql("1"); + }); + + it("should empty the form data when switching from an option of type 'object'", () => { + const schema = { + oneOf: [ + { + type: "object", + properties: { + foo: { + type: "integer", + }, + bar: { + type: "string", + }, + }, + required: ["foo", "bar"], + }, + { + type: "string", + }, + ], + }; + + const { node } = createFormComponent({ + schema, + formData: { + foo: 1, + bar: "abc", + }, + }); + + const $select = node.querySelector("select"); + + Simulate.change($select, { + target: { value: $select.options[1].value }, + }); + + expect($select.value).eql("1"); + + expect(node.querySelector("input#root").value).eql(""); + }); + + describe("Arrays", () => { + it("should correctly render mixed types for oneOf inside array items", () => { + const schema = { + type: "object", + properties: { + items: { + type: "array", + items: { + oneOf: [ + { + type: "string", + }, + { + type: "object", + properties: { + foo: { + type: "integer", + }, + bar: { + type: "string", + }, + }, + }, + ], + }, + }, + }, + }; + + const { node } = createFormComponent({ + schema, + }); + + expect(node.querySelector(".array-item-add button")).not.eql(null); + + Simulate.click(node.querySelector(".array-item-add button")); + + const $select = node.querySelector("select"); + expect($select).not.eql(null); + Simulate.change($select, { + target: { value: $select.options[1].value }, + }); + + expect(node.querySelectorAll("input#root_foo")).to.have.length.of(1); + expect(node.querySelectorAll("input#root_bar")).to.have.length.of(1); + }); + }); }); diff --git a/test/utils_test.js b/test/utils_test.js index cb32727c06..5b84d0108b 100644 --- a/test/utils_test.js +++ b/test/utils_test.js @@ -1183,6 +1183,64 @@ describe("utils", () => { }); }); + it("should return an idSchema for nested property dependencies", () => { + const schema = { + type: "object", + properties: { + obj: { + type: "object", + properties: { + foo: { type: "string" }, + }, + dependencies: { + foo: { + properties: { + bar: { type: "string" }, + }, + }, + }, + }, + }, + }; + const formData = { + obj: { + foo: "test", + }, + }; + + expect(toIdSchema(schema, undefined, schema.definitions, formData)).eql({ + $id: "root", + obj: { + $id: "root_obj", + foo: { $id: "root_obj_foo" }, + bar: { $id: "root_obj_bar" }, + }, + }); + }); + + it("should return an idSchema for unmet property dependencies", () => { + const schema = { + type: "object", + properties: { + foo: { type: "string" }, + }, + dependencies: { + foo: { + properties: { + bar: { type: "string" }, + }, + }, + }, + }; + + const formData = {}; + + expect(toIdSchema(schema, undefined, schema.definitions, formData)).eql({ + $id: "root", + foo: { $id: "root_foo" }, + }); + }); + it("should handle idPrefix parameter", () => { const schema = { definitions: { @@ -1205,6 +1263,24 @@ describe("utils", () => { } ); }); + + it("should handle null form data for object schemas", () => { + const schema = { + type: "object", + properties: { + foo: { type: "string" }, + bar: { type: "string" }, + }, + }; + const formData = null; + const result = toIdSchema(schema, null, {}, formData, "rjsf"); + + expect(result).eql({ + $id: "rjsf", + foo: { $id: "rjsf_foo" }, + bar: { $id: "rjsf_bar" }, + }); + }); }); describe("parseDateString()", () => {