diff --git a/src/core/components/array-model.jsx b/src/core/components/array-model.jsx index b507b42b..3442d4c9 100644 --- a/src/core/components/array-model.jsx +++ b/src/core/components/array-model.jsx @@ -15,7 +15,7 @@ export default class ArrayModel extends Component { } render(){ - let { getComponent, required, schema, depth, expandDepth } = this.props + let { getComponent, required, schema, depth, expandDepth, name } = this.props let items = schema.get("items") let title = schema.get("title") || name let properties = schema.filter( ( v, key) => ["type", "items", "$$ref"].indexOf(key) === -1 ) @@ -23,7 +23,7 @@ export default class ArrayModel extends Component { const ModelCollapse = getComponent("ModelCollapse") const Model = getComponent("Model") - const titleEl = title && + const titleEl = title && { title } @@ -44,4 +44,4 @@ export default class ArrayModel extends Component { { required && *} } -} \ No newline at end of file +} diff --git a/src/core/components/layout-utils.jsx b/src/core/components/layout-utils.jsx index 00a4a09d..f80e0c90 100644 --- a/src/core/components/layout-utils.jsx +++ b/src/core/components/layout-utils.jsx @@ -129,7 +129,8 @@ export class Select extends React.Component { value: PropTypes.any, onChange: PropTypes.func, multiple: PropTypes.bool, - allowEmptyValue: PropTypes.bool + allowEmptyValue: PropTypes.bool, + className: PropTypes.string } static defaultProps = { @@ -142,7 +143,7 @@ export class Select extends React.Component { let value - if (props.value !== undefined) { + if (props.value) { value = props.value } else { value = props.multiple ? [""] : "" @@ -178,7 +179,7 @@ export class Select extends React.Component { let value = this.state.value.toJS ? this.state.value.toJS() : this.state.value return ( - { allowEmptyValue ? : null } { allowedValues.map(function (item, key) { diff --git a/src/core/components/model.jsx b/src/core/components/model.jsx index 499a0f05..cba34289 100644 --- a/src/core/components/model.jsx +++ b/src/core/components/model.jsx @@ -48,7 +48,7 @@ export default class Model extends Component { switch(type) { case "object": return case "array": return diff --git a/src/core/components/object-model.jsx b/src/core/components/object-model.jsx index 5e0a83a9..cfaab2e6 100644 --- a/src/core/components/object-model.jsx +++ b/src/core/components/object-model.jsx @@ -23,7 +23,7 @@ export default class ObjectModel extends Component { let properties = schema.get("properties") let additionalProperties = schema.get("additionalProperties") let title = schema.get("title") || name - let required = schema.get("required") + let requiredProperties = schema.get("required") const JumpToPath = getComponent("JumpToPath", true) const Markdown = getComponent("Markdown") @@ -63,14 +63,16 @@ export default class ObjectModel extends Component { { !(properties && properties.size) ? null : properties.entrySeq().map( ([key, value]) => { - let isRequired = List.isList(required) && required.contains(key) + let isRequired = List.isList(requiredProperties) && requiredProperties.contains(key) let propertyStyle = { verticalAlign: "top", paddingRight: "0.2em" } if ( isRequired ) { propertyStyle.fontWeight = "bold" } return ( - { key }: + + { key }{ isRequired && * } + ) @@ -121,6 +122,7 @@ export class JsonSchema_array extends PureComponent { render() { let { getComponent, required, schema, fn } = this.props + let errors = schema.errors || [] let itemSchema = fn.inferSchema(schema.items) const JsonSchemaForm = getComponent("JsonSchemaForm") @@ -131,19 +133,17 @@ export class JsonSchema_array extends PureComponent { if ( enumValue ) { const Select = getComponent("Select") - return () } } diff --git a/src/core/utils.js b/src/core/utils.js index 95771fb4..7cc5beda 100644 --- a/src/core/utils.js +++ b/src/core/utils.js @@ -41,7 +41,7 @@ export function fromJSOrdered (js) { return !isObject(js) ? js : Array.isArray(js) ? Im.Seq(js).map(fromJSOrdered).toList() : - Im.Seq(js).map(fromJSOrdered).toOrderedMap() + Im.OrderedMap(js).map(fromJSOrdered) } export function bindToState(obj, state) { @@ -468,6 +468,18 @@ export const validateFile = ( val ) => { } } +export const validateBoolean = ( val ) => { + if ( !(val === "true" || val === "false" || val === true || val === false) ) { + return "Value must be a boolean" + } +} + +export const validateString = ( val ) => { + if ( val && typeof val !== "string" ) { + return "Value must be a string" + } +} + // validation of parameters before execute export const validateParam = (param, isXml) => { let errors = [] @@ -475,52 +487,66 @@ export const validateParam = (param, isXml) => { let required = param.get("required") let type = param.get("type") - let stringCheck = type === "string" && !value - let arrayCheck = type === "array" && Array.isArray(value) && !value.length - let listCheck = type === "array" && Im.List.isList(value) && !value.count() - let fileCheck = type === "file" && !(value instanceof win.File) + // If the parameter is required OR the parameter has a value (meaning optional, but filled in) + // then we should do our validation routine + if ( required || value ) { + // These checks should evaluate to true if the parameter's value is valid + let stringCheck = type === "string" && value && !validateString(value) + let arrayCheck = type === "array" && Array.isArray(value) && value.length + let listCheck = type === "array" && Im.List.isList(value) && value.count() + let fileCheck = type === "file" && value instanceof win.File + let booleanCheck = type === "boolean" && !validateBoolean(value) + let numberCheck = type === "number" && !validateNumber(value) // validateNumber returns undefined if the value is a number + let integerCheck = type === "integer" && !validateInteger(value) // validateInteger returns undefined if the value is an integer - if ( required && (stringCheck || arrayCheck || listCheck || fileCheck) ) { - errors.push("Required field is not provided") - return errors - } + if ( required && !(stringCheck || arrayCheck || listCheck || fileCheck || booleanCheck || numberCheck || integerCheck) ) { + errors.push("Required field is not provided") + return errors + } - if ( value === null || value === undefined ) { - return errors - } + if ( type === "string" ) { + let err = validateString(value) + if (!err) return errors + errors.push(err) + } else if ( type === "boolean" ) { + let err = validateBoolean(value) + if (!err) return errors + errors.push(err) + } else if ( type === "number" ) { + let err = validateNumber(value) + if (!err) return errors + errors.push(err) + } else if ( type === "integer" ) { + let err = validateInteger(value) + if (!err) return errors + errors.push(err) + } else if ( type === "array" ) { + let itemType - if ( type === "number" ) { - let err = validateNumber(value) - if (!err) return errors - errors.push(err) - } else if ( type === "integer" ) { - let err = validateInteger(value) - if (!err) return errors - errors.push(err) - } else if ( type === "array" ) { - let itemType + if ( !value.count() ) { return errors } - if ( !value.count() ) { return errors } + itemType = param.getIn(["items", "type"]) - itemType = param.getIn(["items", "type"]) + value.forEach((item, index) => { + let err - value.forEach((item, index) => { - let err + if (itemType === "number") { + err = validateNumber(item) + } else if (itemType === "integer") { + err = validateInteger(item) + } else if (itemType === "string") { + err = validateString(item) + } - if (itemType === "number") { - err = validateNumber(item) - } else if (itemType === "integer") { - err = validateInteger(item) - } - - if ( err ) { - errors.push({ index: index, error: err}) - } - }) - } else if ( type === "file" ) { - let err = validateFile(value) - if (!err) return errors - errors.push(err) + if ( err ) { + errors.push({ index: index, error: err}) + } + }) + } else if ( type === "file" ) { + let err = validateFile(value) + if (!err) return errors + errors.push(err) + } } return errors diff --git a/src/style/_buttons.scss b/src/style/_buttons.scss index 2470f8b0..bfb85023 100644 --- a/src/style/_buttons.scss +++ b/src/style/_buttons.scss @@ -14,6 +14,11 @@ @include text_headline(); + &.btn-sm { + font-size: 12px; + padding: 4px 23px; + } + &[disabled] { cursor: not-allowed; @@ -165,6 +170,9 @@ button { cursor: pointer; - outline: none; + + &.invalid { + @include invalidFormElement(); + } } diff --git a/src/style/_form.scss b/src/style/_form.scss index 185b836e..e54d5438 100644 --- a/src/style/_form.scss +++ b/src/style/_form.scss @@ -21,6 +21,10 @@ select background: #f7f7f7; } + + &.invalid { + @include invalidFormElement(); + } } .opblock-body select @@ -53,12 +57,8 @@ input[type=file] border-radius: 4px; background: #fff; - &.invalid - { - animation: shake .4s 1; - - border-color: $_color-delete; - background: lighten($_color-delete, 35%); + &.invalid { + @include invalidFormElement(); } } diff --git a/src/style/_mixins.scss b/src/style/_mixins.scss index a2a27d48..8fd01720 100644 --- a/src/style/_mixins.scss +++ b/src/style/_mixins.scss @@ -166,3 +166,9 @@ $browser-context: 16; @warn 'Breakpoint mixin supports: tablet, mobile, desktop'; } } + +@mixin invalidFormElement() { + animation: shake .4s 1; + border-color: $_color-delete; + background: lighten($_color-delete, 35%); +} diff --git a/src/style/_table.scss b/src/style/_table.scss index 3d58f3d8..d1481709 100644 --- a/src/style/_table.scss +++ b/src/style/_table.scss @@ -97,6 +97,10 @@ table width: 100%; max-width: 340px; } + + select { + border-width: 1px; + } } .parameter__name diff --git a/test/components/schemes.js b/test/components/schemes.js new file mode 100644 index 00000000..a21c0628 --- /dev/null +++ b/test/components/schemes.js @@ -0,0 +1,41 @@ + +/* eslint-env mocha */ +import React from "react" +import expect, { createSpy } from "expect" +import { shallow } from "enzyme" +import { fromJS } from "immutable" +import Schemes from "components/schemes" + +describe("", function(){ + it("calls props.specActions.setScheme() when no operationScheme is selected", function(){ + + // Given + let props = { + specActions: { + setScheme: createSpy() + }, + schemes: fromJS([ + "http", + "https" + ]), + operationScheme: undefined, + path: "/test", + method: "get" + } + + // When + let wrapper = shallow() + + // Then operationScheme should default to first scheme in options list + expect(props.specActions.setScheme).toHaveBeenCalledWith("http", "/test" , "get") + + // When the operationScheme is no longer in the list of options + props.schemes = fromJS([ + "https" + ]) + wrapper.setProps(props) + + // Then operationScheme should default to first scheme in options list + expect(props.specActions.setScheme).toHaveBeenCalledWith("https", "/test", "get") + }) +}) diff --git a/test/core/utils.js b/test/core/utils.js index a63da13d..acc5a14e 100644 --- a/test/core/utils.js +++ b/test/core/utils.js @@ -1,10 +1,10 @@ /* eslint-env mocha */ import expect from "expect" import { fromJS } from "immutable" -import { mapToList, validateNumber, validateInteger, validateParam, validateFile } from "core/utils" +import { mapToList, validateNumber, validateInteger, validateParam, validateFile, fromJSOrdered } from "core/utils" import win from "core/window" -describe("utils", function(){ +describe("utils", function() { describe("mapToList", function(){ @@ -176,6 +176,7 @@ describe("utils", function(){ let result = null it("validates required strings", function() { + // invalid string param = fromJS({ required: true, type: "string", @@ -183,9 +184,39 @@ describe("utils", function(){ }) result = validateParam( param, false ) expect( result ).toEqual( ["Required field is not provided"] ) + + // valid string + param = fromJS({ + required: true, + type: "string", + value: "test string" + }) + result = validateParam( param, false ) + expect( result ).toEqual( [] ) + }) + + it("validates optional strings", function() { + // valid (empty) string + param = fromJS({ + required: false, + type: "string", + value: "" + }) + result = validateParam( param, false ) + expect( result ).toEqual( [] ) + + // valid string + param = fromJS({ + required: false, + type: "string", + value: "test" + }) + result = validateParam( param, false ) + expect( result ).toEqual( [] ) }) it("validates required files", function() { + // invalid file param = fromJS({ required: true, type: "file", @@ -193,9 +224,48 @@ describe("utils", function(){ }) result = validateParam( param, false ) expect( result ).toEqual( ["Required field is not provided"] ) + + // valid file + param = fromJS({ + required: true, + type: "file", + value: new win.File() + }) + result = validateParam( param, false ) + expect( result ).toEqual( [] ) + }) + + it("validates optional files", function() { + // invalid file + param = fromJS({ + required: false, + type: "file", + value: "not a file" + }) + result = validateParam( param, false ) + expect( result ).toEqual( ["Value must be a file"] ) + + // valid (empty) file + param = fromJS({ + required: false, + type: "file", + value: undefined + }) + result = validateParam( param, false ) + expect( result ).toEqual( [] ) + + // valid file + param = fromJS({ + required: false, + type: "file", + value: new win.File() + }) + result = validateParam( param, false ) + expect( result ).toEqual( [] ) }) it("validates required arrays", function() { + // invalid (empty) array param = fromJS({ required: true, type: "array", @@ -204,75 +274,51 @@ describe("utils", function(){ result = validateParam( param, false ) expect( result ).toEqual( ["Required field is not provided"] ) + // invalid (not an array) param = fromJS({ required: true, type: "array", - value: [] + value: undefined }) result = validateParam( param, false ) expect( result ).toEqual( ["Required field is not provided"] ) - }) - it("validates numbers", function() { - // string instead of a number + // invalid array, items do not match correct type param = fromJS({ - required: false, - type: "number", - value: "test" + required: true, + type: "array", + value: [1], + items: { + type: "string" + } }) result = validateParam( param, false ) - expect( result ).toEqual( ["Value must be a number"] ) + expect( result ).toEqual( [{index: 0, error: "Value must be a string"}] ) - // undefined value + // valid array, with no 'type' for items param = fromJS({ - required: false, - type: "number", - value: undefined + required: true, + type: "array", + value: ["1"] }) result = validateParam( param, false ) expect( result ).toEqual( [] ) - // null value + // valid array, items match type param = fromJS({ - required: false, - type: "number", - value: null + required: true, + type: "array", + value: ["1"], + items: { + type: "string" + } }) result = validateParam( param, false ) expect( result ).toEqual( [] ) }) - it("validates integers", function() { - // string instead of integer - param = fromJS({ - required: false, - type: "integer", - value: "test" - }) - result = validateParam( param, false ) - expect( result ).toEqual( ["Value must be an integer"] ) - - // undefined value - param = fromJS({ - required: false, - type: "integer", - value: undefined - }) - result = validateParam( param, false ) - expect( result ).toEqual( [] ) - - // null value - param = fromJS({ - required: false, - type: "integer", - value: null - }) - result = validateParam( param, false ) - expect( result ).toEqual( [] ) - }) - - it("validates arrays", function() { - // empty array + it("validates optional arrays", function() { + // valid, empty array param = fromJS({ required: false, type: "array", @@ -281,7 +327,7 @@ describe("utils", function(){ result = validateParam( param, false ) expect( result ).toEqual( [] ) - // numbers + // invalid, items do not match correct type param = fromJS({ required: false, type: "array", @@ -293,17 +339,236 @@ describe("utils", function(){ result = validateParam( param, false ) expect( result ).toEqual( [{index: 0, error: "Value must be a number"}] ) - // integers + // valid param = fromJS({ required: false, type: "array", - value: ["not", "numbers"], + value: ["test"], items: { - type: "integer" + type: "string" } }) result = validateParam( param, false ) - expect( result ).toEqual( [{index: 0, error: "Value must be an integer"}, {index: 1, error: "Value must be an integer"}] ) + expect( result ).toEqual( [] ) + }) + + it("validates required booleans", function() { + // invalid boolean value + param = fromJS({ + required: true, + type: "boolean", + value: undefined + }) + result = validateParam( param, false ) + expect( result ).toEqual( ["Required field is not provided"] ) + + // invalid boolean value (not a boolean) + param = fromJS({ + required: true, + type: "boolean", + value: "test string" + }) + result = validateParam( param, false ) + expect( result ).toEqual( ["Required field is not provided"] ) + + // valid boolean value + param = fromJS({ + required: true, + type: "boolean", + value: "true" + }) + result = validateParam( param, false ) + expect( result ).toEqual( [] ) + + // valid boolean value + param = fromJS({ + required: true, + type: "boolean", + value: false + }) + result = validateParam( param, false ) + expect( result ).toEqual( [] ) + }) + + it("validates optional booleans", function() { + // valid (empty) boolean value + param = fromJS({ + required: false, + type: "boolean", + value: undefined + }) + result = validateParam( param, false ) + expect( result ).toEqual( [] ) + + // invalid boolean value (not a boolean) + param = fromJS({ + required: false, + type: "boolean", + value: "test string" + }) + result = validateParam( param, false ) + expect( result ).toEqual( ["Value must be a boolean"] ) + + // valid boolean value + param = fromJS({ + required: false, + type: "boolean", + value: "true" + }) + result = validateParam( param, false ) + expect( result ).toEqual( [] ) + + // valid boolean value + param = fromJS({ + required: false, + type: "boolean", + value: false + }) + result = validateParam( param, false ) + expect( result ).toEqual( [] ) + }) + + it("validates required numbers", function() { + // invalid number, string instead of a number + param = fromJS({ + required: true, + type: "number", + value: "test" + }) + result = validateParam( param, false ) + expect( result ).toEqual( ["Required field is not provided"] ) + + // invalid number, undefined value + param = fromJS({ + required: true, + type: "number", + value: undefined + }) + result = validateParam( param, false ) + expect( result ).toEqual( ["Required field is not provided"] ) + + // valid number + param = fromJS({ + required: true, + type: "number", + value: 10 + }) + result = validateParam( param, false ) + expect( result ).toEqual( [] ) + }) + + it("validates optional numbers", function() { + // invalid number, string instead of a number + param = fromJS({ + required: false, + type: "number", + value: "test" + }) + result = validateParam( param, false ) + expect( result ).toEqual( ["Value must be a number"] ) + + // valid (empty) number + param = fromJS({ + required: false, + type: "number", + value: undefined + }) + result = validateParam( param, false ) + expect( result ).toEqual( [] ) + + // valid number + param = fromJS({ + required: false, + type: "number", + value: 10 + }) + result = validateParam( param, false ) + expect( result ).toEqual( [] ) + }) + + it("validates required integers", function() { + // invalid integer, string instead of an integer + param = fromJS({ + required: true, + type: "integer", + value: "test" + }) + result = validateParam( param, false ) + expect( result ).toEqual( ["Required field is not provided"] ) + + // invalid integer, undefined value + param = fromJS({ + required: true, + type: "integer", + value: undefined + }) + result = validateParam( param, false ) + expect( result ).toEqual( ["Required field is not provided"] ) + + // valid integer + param = fromJS({ + required: true, + type: "integer", + value: 10 + }) + result = validateParam( param, false ) + expect( result ).toEqual( [] ) + }) + + it("validates optional integers", function() { + // invalid integer, string instead of an integer + param = fromJS({ + required: false, + type: "integer", + value: "test" + }) + result = validateParam( param, false ) + expect( result ).toEqual( ["Value must be an integer"] ) + + // valid (empty) integer + param = fromJS({ + required: false, + type: "integer", + value: undefined + }) + result = validateParam( param, false ) + expect( result ).toEqual( [] ) + + // valid number + param = fromJS({ + required: false, + type: "integer", + value: 10 + }) + result = validateParam( param, false ) + expect( result ).toEqual( [] ) }) }) + + describe("fromJSOrdered", () => { + it("should create an OrderedMap from an object", () => { + const param = { + value: "test" + } + + const result = fromJSOrdered(param).toJS() + expect( result ).toEqual( { value: "test" } ) + }) + + it("should not use an object's length property for Map size", () => { + const param = { + length: 5 + } + + const result = fromJSOrdered(param).toJS() + expect( result ).toEqual( { length: 5 } ) + }) + + it("should create an OrderedMap from an array", () => { + const param = [1, 1, 2, 3, 5, 8] + + const result = fromJSOrdered(param).toJS() + expect( result ).toEqual( [1, 1, 2, 3, 5, 8] ) + }) + }) })