From 0ac0da7eb3fb99bb76017572dd10ef7a11ffadcd Mon Sep 17 00:00:00 2001 From: Lucian Buzzo Date: Thu, 13 Dec 2018 16:21:54 +0000 Subject: [PATCH] Infer type from enum if a type is not provided to SelectWidget (#1100) Connects to #1098 Change-type: minor Signed-off-by: Lucian --- src/components/fields/StringField.js | 6 +- src/components/widgets/SelectWidget.js | 17 +++++- src/utils.js | 2 +- test/BooleanField_test.js | 79 ++++++++++++++++++++++++++ test/NumberField_test.js | 21 +++++++ test/utils_test.js | 27 +++++++++ 6 files changed, 148 insertions(+), 4 deletions(-) diff --git a/src/components/fields/StringField.js b/src/components/fields/StringField.js index c2887e28e3..91fcf6911e 100644 --- a/src/components/fields/StringField.js +++ b/src/components/fields/StringField.js @@ -65,7 +65,11 @@ if (process.env.NODE_ENV !== "production") { onChange: PropTypes.func.isRequired, onBlur: PropTypes.func, onFocus: PropTypes.func, - formData: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + formData: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.bool, + ]), registry: PropTypes.shape({ widgets: PropTypes.objectOf( PropTypes.oneOfType([PropTypes.func, PropTypes.object]) diff --git a/src/components/widgets/SelectWidget.js b/src/components/widgets/SelectWidget.js index a3da1fe37b..a5ef160355 100644 --- a/src/components/widgets/SelectWidget.js +++ b/src/components/widgets/SelectWidget.js @@ -1,7 +1,7 @@ import React from "react"; import PropTypes from "prop-types"; -import { asNumber } from "../../utils"; +import { asNumber, guessType } from "../../utils"; const nums = new Set(["number", "integer"]); @@ -9,7 +9,9 @@ const nums = new Set(["number", "integer"]); * This is a silly limitation in the DOM where option change event values are * always retrieved as strings. */ -function processValue({ type, items }, value) { +function processValue(schema, value) { + // "enum" is a reserved word, so only "type" and "items" can be destructured + const { type, items } = schema; if (value === "") { return undefined; } else if (type === "array" && items && nums.has(items.type)) { @@ -19,6 +21,17 @@ function processValue({ type, items }, value) { } else if (type === "number") { return asNumber(value); } + + // If type is undefined, but an enum is present, try and infer the type from + // the enum values + if (schema.enum) { + if (schema.enum.every(x => guessType(x) === "number")) { + return asNumber(value); + } else if (schema.enum.every(x => guessType(x) === "boolean")) { + return value === "true"; + } + } + return value; } diff --git a/src/utils.js b/src/utils.js index 816db131dd..47856324d2 100644 --- a/src/utils.js +++ b/src/utils.js @@ -406,7 +406,7 @@ function findSchemaDefinition($ref, definitions = {}) { // In the case where we have to implicitly create a schema, it is useful to know what type to use // based on the data we are defining -const guessType = function guessType(value) { +export const guessType = function guessType(value) { if (Array.isArray(value)) { return "array"; } else if (typeof value === "string") { diff --git a/test/BooleanField_test.js b/test/BooleanField_test.js index 95bb58853b..eaa8a5a983 100644 --- a/test/BooleanField_test.js +++ b/test/BooleanField_test.js @@ -1,6 +1,7 @@ import React from "react"; import { expect } from "chai"; import { Simulate } from "react-addons-test-utils"; +import sinon from "sinon"; import { createFormComponent, createSandbox } from "./test_utils"; @@ -227,4 +228,82 @@ describe("BooleanField", () => { expect(node.querySelector("#label-")).to.not.be.null; }); }); + + describe("SelectWidget", () => { + it("should render a field that contains an enum of booleans", () => { + const { node } = createFormComponent({ + schema: { + enum: [true, false], + }, + }); + + expect(node.querySelectorAll(".field select")).to.have.length.of(1); + }); + + it("should infer the value from an enum on change", () => { + const spy = sinon.spy(); + const { node } = createFormComponent({ + schema: { + enum: [true, false], + }, + onChange: spy, + }); + + expect(node.querySelectorAll(".field select")).to.have.length.of(1); + const $select = node.querySelector(".field select"); + expect($select.value).eql(""); + + Simulate.change($select, { + target: { value: "true" }, + }); + expect($select.value).eql("true"); + expect(spy.lastCall.args[0].formData).eql(true); + }); + + it("should render a string field with a label", () => { + const { node } = createFormComponent({ + schema: { + enum: [true, false], + title: "foo", + }, + }); + + expect(node.querySelector(".field label").textContent).eql("foo"); + }); + + it("should assign a default value", () => { + const { comp } = createFormComponent({ + schema: { + enum: [true, false], + default: true, + }, + }); + + expect(comp.state.formData).eql(true); + }); + + it("should handle a change event", () => { + const { comp, node } = createFormComponent({ + schema: { + enum: [true, false], + }, + }); + + Simulate.change(node.querySelector("select"), { + target: { value: "false" }, + }); + + expect(comp.state.formData).eql(false); + }); + + it("should render the widget with the expected id", () => { + const { node } = createFormComponent({ + schema: { + enum: [true, false], + }, + }); + + expect(node.querySelector("select").id).eql("root"); + }); + }); }); diff --git a/test/NumberField_test.js b/test/NumberField_test.js index f1f5fd4824..b265e8e642 100644 --- a/test/NumberField_test.js +++ b/test/NumberField_test.js @@ -1,6 +1,7 @@ import React from "react"; import { expect } from "chai"; import { Simulate } from "react-addons-test-utils"; +import sinon from "sinon"; import { createFormComponent, createSandbox } from "./test_utils"; @@ -208,6 +209,26 @@ describe("NumberField", () => { expect(node.querySelectorAll(".field select")).to.have.length.of(1); }); + it("should infer the value from an enum on change", () => { + const spy = sinon.spy(); + const { node } = createFormComponent({ + schema: { + enum: [1, 2], + }, + onChange: spy, + }); + + expect(node.querySelectorAll(".field select")).to.have.length.of(1); + const $select = node.querySelector(".field select"); + expect($select.value).eql(""); + + Simulate.change(node.querySelector(".field select"), { + target: { value: "1" }, + }); + expect($select.value).eql("1"); + expect(spy.lastCall.args[0].formData).eql(1); + }); + it("should render a string field with a label", () => { const { node } = createFormComponent({ schema: { diff --git a/test/utils_test.js b/test/utils_test.js index deb24f5a37..0d97f0551c 100644 --- a/test/utils_test.js +++ b/test/utils_test.js @@ -16,6 +16,7 @@ import { shouldRender, toDateString, toIdSchema, + guessType, } from "../src/utils"; describe("utils", () => { @@ -1322,4 +1323,30 @@ describe("utils", () => { ); }); }); + + describe("guessType()", () => { + it("should guess the type of array values", () => { + expect(guessType([1, 2, 3])).eql("array"); + }); + + it("should guess the type of string values", () => { + expect(guessType("foobar")).eql("string"); + }); + + it("should guess the type of null values", () => { + expect(guessType(null)).eql("null"); + }); + + it("should treat undefined values as null values", () => { + expect(guessType()).eql("null"); + }); + + it("should guess the type of boolean values", () => { + expect(guessType(true)).eql("boolean"); + }); + + it("should guess the type of object values", () => { + expect(guessType({})).eql("object"); + }); + }); });