From 9013edc89eefad4c7eca88bf65696574f4892980 Mon Sep 17 00:00:00 2001 From: Jean-Michel FRANCOIS Date: Thu, 27 Apr 2017 09:25:36 +0200 Subject: [PATCH 01/73] chore: start trying a refactor --- packages/forms/package.json | 6 +- packages/forms/src/UIForm/UIForm.component.js | 48 +++++++++++ packages/forms/src/UIForm/UIForm.test.js | 13 +++ packages/forms/src/UIForm/index.js | 3 + packages/forms/stories/index.js | 36 +++++--- packages/forms/yarn.lock | 84 +++++++++++++++---- 6 files changed, 163 insertions(+), 27 deletions(-) create mode 100644 packages/forms/src/UIForm/UIForm.component.js create mode 100644 packages/forms/src/UIForm/UIForm.test.js create mode 100644 packages/forms/src/UIForm/index.js diff --git a/packages/forms/package.json b/packages/forms/package.json index e4d5466386d..9ae244537b0 100644 --- a/packages/forms/package.json +++ b/packages/forms/package.json @@ -57,9 +57,13 @@ }, "dependencies": { "classnames": "^2.2.5", + "json-schema-form-core": "^1.0.0-alpha.2", "keycode": "^2.1.8", "react-autowhatever": "^7.0.0", - "react-jsonschema-form": "^0.42.0" + "react-jsonschema-form": "^0.42.0", + "react-redux": "^5.0.4", + "redux-form": "^6.6.3", + "tv4": "^1.3.0" }, "peerDependencies": { "react": "^15.4.0", diff --git a/packages/forms/src/UIForm/UIForm.component.js b/packages/forms/src/UIForm/UIForm.component.js new file mode 100644 index 00000000000..c72294074d9 --- /dev/null +++ b/packages/forms/src/UIForm/UIForm.component.js @@ -0,0 +1,48 @@ +import React, { PropTypes } from 'react'; +import { Field, reduxForm } from 'redux-form'; + +import { + schemaDefaults, + jsonref, + merge, + traverseSchema, + traverseForm, +} from 'json-schema-form-core'; + +class UIForm extends React.Component { + + render() { + const { jsonSchema, uiSchema, properties } = this.props.data || {}; + debugger; + schemaDefaults; + jsonref; + merge; + traverseSchema; + traverseForm; + return ( +
+
+ + +
+
+ + +
+
+ + +
+ +
+ ); + } +} + +UIForm.propTypes = { + handleSubmit: PropTypes.func.isRequired, +}; + +export default reduxForm({ + form: 'form' // a unique name for this form +})(UIForm); diff --git a/packages/forms/src/UIForm/UIForm.test.js b/packages/forms/src/UIForm/UIForm.test.js new file mode 100644 index 00000000000..ceb205da709 --- /dev/null +++ b/packages/forms/src/UIForm/UIForm.test.js @@ -0,0 +1,13 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import UIForm from './UIForm.component'; + +describe('UIForm', () => { + it('should render', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/packages/forms/src/UIForm/index.js b/packages/forms/src/UIForm/index.js new file mode 100644 index 00000000000..370245b7ae7 --- /dev/null +++ b/packages/forms/src/UIForm/index.js @@ -0,0 +1,3 @@ +import UIForm from './UIForm.component'; + +export default UIForm; diff --git a/packages/forms/stories/index.js b/packages/forms/stories/index.js index 2f2e790177c..7e0fa2d09c4 100644 --- a/packages/forms/stories/index.js +++ b/packages/forms/stories/index.js @@ -1,30 +1,44 @@ import React from 'react'; import ReactDOM from 'react-dom'; import a11y from 'react-a11y'; - +import { Provider } from 'react-redux'; import { storiesOf, action } from '@kadira/storybook'; import { withKnobs, object } from '@kadira/storybook-addon-knobs'; import Well from 'react-bootstrap/lib/Well'; import IconsProvider from 'react-talend-components/lib/IconsProvider'; -import Form from '../src/Form'; + +import { createStore, combineReducers } from 'redux'; +import { reducer as formReducer } from 'redux-form'; + +const reducers = { + // ... your other reducers here ... + form: formReducer, // <---- Mounted at 'form' +}; + +const reducer = combineReducers(reducers); +const store = createStore(reducer); + +import Form from '../src/UIForm'; a11y(ReactDOM); const decoratedStories = storiesOf('Form', module) .addDecorator(withKnobs) .addDecorator(story => ( -
-
- - {story()} - + +
+
+ + {story()} + +
-
+ )); const capitalizeFirstLetter = diff --git a/packages/forms/yarn.lock b/packages/forms/yarn.lock index cd6edb4052e..98a9021a5e5 100644 --- a/packages/forms/yarn.lock +++ b/packages/forms/yarn.lock @@ -1256,9 +1256,9 @@ bootstrap-sass@^3.3.7: version "3.3.7" resolved "https://registry.yarnpkg.com/bootstrap-sass/-/bootstrap-sass-3.3.7.tgz#6596c7ab40f6637393323ab0bc80d064fc630498" -bootstrap-talend-theme@^0.71.0: - version "0.71.0" - resolved "https://registry.yarnpkg.com/bootstrap-talend-theme/-/bootstrap-talend-theme-0.71.0.tgz#b6946f3d5f9742cbefd7f71d3542c341c887ec7f" +bootstrap-talend-theme@^0.72.2: + version "0.72.2" + resolved "https://registry.yarnpkg.com/bootstrap-talend-theme/-/bootstrap-talend-theme-0.72.2.tgz#d164a2d5cc39538b40bf3918b9e0738184ae9a0b" dependencies: bootstrap-sass "^3.3.7" @@ -1692,6 +1692,13 @@ cpx@^1.5.0: shell-quote "^1.6.1" subarg "^1.0.0" +create-react-class@^15.5.1: + version "15.5.2" + resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.5.2.tgz#6a8758348df660b88326a0e764d569f274aad681" + dependencies: + fbjs "^0.8.9" + object-assign "^4.1.1" + cross-spawn@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" @@ -2101,6 +2108,10 @@ es5-shim@^4.5.9: version "4.5.9" resolved "https://registry.yarnpkg.com/es5-shim/-/es5-shim-4.5.9.tgz#2a1e2b9e583ff5fed0c20a3ee2cbf3f75230a5c0" +es6-error@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.0.2.tgz#eec5c726eacef51b7f6b73c20db6e1b13b069c98" + es6-iterator@2, es6-iterator@^2.0.1, es6-iterator@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.1.tgz#8e319c9f0453bf575d374940a655920e59ca5512" @@ -2469,7 +2480,7 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" -fbjs@^0.8.1, fbjs@^0.8.4: +fbjs@^0.8.1, fbjs@^0.8.4, fbjs@^0.8.9: version "0.8.12" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.12.tgz#10b5d92f76d45575fd63a217d4ea02bea2f8ed04" dependencies: @@ -2861,7 +2872,7 @@ hoek@2.x.x: version "2.16.3" resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" -hoist-non-react-statics@1.x.x, hoist-non-react-statics@^1.2.0: +hoist-non-react-statics@1.x.x, hoist-non-react-statics@^1.0.3, hoist-non-react-statics@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb" @@ -3013,7 +3024,7 @@ interpret@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.2.tgz#f4f623f0bb7122f15f5717c8e254b8161b5c5b2d" -invariant@2.x.x, invariant@^2.1.0, invariant@^2.2.0, invariant@^2.2.1: +invariant@2.x.x, invariant@^2.0.0, invariant@^2.1.0, invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" dependencies: @@ -3156,6 +3167,10 @@ is-primitive@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" +is-promise@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + is-property@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" @@ -3749,6 +3764,10 @@ json-loader@^0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.4.tgz#8baa1365a632f58a3c46d20175fc6002c96e37de" +json-schema-form-core@^1.0.0-alpha.2: + version "1.0.0-alpha.2" + resolved "https://registry.yarnpkg.com/json-schema-form-core/-/json-schema-form-core-1.0.0-alpha.2.tgz#60566287e5f3fb8d2ccb1337dca5d3b80c3391c9" + json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" @@ -3871,7 +3890,7 @@ locate-path@^2.0.0: p-locate "^2.0.0" path-exists "^3.0.0" -lodash-es@^4.2.1: +lodash-es@^4.17.3, lodash-es@^4.2.0, lodash-es@^4.2.1: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7" @@ -4068,7 +4087,7 @@ lodash.uniq@^4.3.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" -lodash@4.x.x, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0: +lodash@4.x.x, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -4500,7 +4519,7 @@ object-assign@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" -object-assign@^4.0.1, object-assign@^4.1.0: +object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -5057,6 +5076,12 @@ promise@^7.1.1: dependencies: asap "~2.0.3" +prop-types@^15.0.0, prop-types@^15.5.6: + version "15.5.8" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.8.tgz#6b7b2e141083be38c8595aa51fc55775c7199394" + dependencies: + fbjs "^0.8.9" + proxy-addr@~1.1.3: version "1.1.4" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.4.tgz#27e545f6960a44a627d9b44467e35c1b6b4ce2f3" @@ -5283,6 +5308,18 @@ react-prop-types@^0.4.0: dependencies: warning "^3.0.0" +react-redux@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.4.tgz#1563babadcfb2672f57f9ceaa439fb16bf85d55b" + dependencies: + create-react-class "^15.5.1" + hoist-non-react-statics "^1.0.3" + invariant "^2.0.0" + lodash "^4.2.0" + lodash-es "^4.2.0" + loose-envify "^1.1.0" + prop-types "^15.0.0" + react-simple-di@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/react-simple-di/-/react-simple-di-1.2.0.tgz#dde0e5bf689f391ef2ab02c9043b213fe239c6d0" @@ -5296,9 +5333,9 @@ react-stubber@^1.0.0: dependencies: babel-runtime "^6.5.0" -react-talend-components@^0.71.0: - version "0.71.0" - resolved "https://registry.yarnpkg.com/react-talend-components/-/react-talend-components-0.71.0.tgz#dec2f1d5c14669e89460a1dbf56821124d340f9b" +react-talend-components@^0.72.2: + version "0.72.2" + resolved "https://registry.yarnpkg.com/react-talend-components/-/react-talend-components-0.72.2.tgz#9426e165996359ecc82697ce1083157804e98cb1" dependencies: lodash "^4.17.4" react-autowhatever "^7.0.0" @@ -5428,6 +5465,19 @@ reduce-function-call@^1.0.1: dependencies: balanced-match "^0.4.2" +redux-form@^6.6.3: + version "6.6.3" + resolved "https://registry.yarnpkg.com/redux-form/-/redux-form-6.6.3.tgz#62362654f2214c83a8f9fcb8313702bb46f92205" + dependencies: + deep-equal "^1.0.1" + es6-error "^4.0.0" + hoist-non-react-statics "^1.2.0" + invariant "^2.2.2" + is-promise "^2.1.0" + lodash "^4.17.3" + lodash-es "^4.17.3" + prop-types "^15.5.6" + redux@^3.5.2: version "3.6.0" resolved "https://registry.yarnpkg.com/redux/-/redux-3.6.0.tgz#887c2b3d0b9bd86eca2be70571c27654c19e188d" @@ -6011,9 +6061,9 @@ table@^3.7.8: slice-ansi "0.0.4" string-width "^2.0.0" -talend-icons@^0.71.0: - version "0.71.0" - resolved "https://registry.yarnpkg.com/talend-icons/-/talend-icons-0.71.0.tgz#d52c632180d35385a6ae382dfb59e7f56ccb9ac2" +talend-icons@^0.72.2: + version "0.72.2" + resolved "https://registry.yarnpkg.com/talend-icons/-/talend-icons-0.72.2.tgz#4bdabf0091695c6ede2ecdb218c4fb0cb66b8da9" tapable@^0.1.8, tapable@~0.1.8: version "0.1.10" @@ -6126,6 +6176,10 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tv4@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/tv4/-/tv4-1.3.0.tgz#d020c846fadd50c855abb25ebaecc68fc10f7963" + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" From 6f0f362a4f82cf2cf8d60f3ab8ec9eb7f7bc3076 Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Tue, 9 May 2017 17:51:13 +0200 Subject: [PATCH 02/73] Dependency : talend-json-schema-form-core --- packages/forms/package.json | 5 +- packages/forms/src/UIForm/UIForm.component.js | 17 ++++--- packages/forms/stories/json/core-simple.json | 48 +++++++++++++++++++ packages/forms/yarn.lock | 30 +++++------- 4 files changed, 73 insertions(+), 27 deletions(-) create mode 100644 packages/forms/stories/json/core-simple.json diff --git a/packages/forms/package.json b/packages/forms/package.json index 9ae244537b0..d5a8696c16a 100644 --- a/packages/forms/package.json +++ b/packages/forms/package.json @@ -57,13 +57,12 @@ }, "dependencies": { "classnames": "^2.2.5", - "json-schema-form-core": "^1.0.0-alpha.2", + "talend-json-schema-form-core": "^1.0.0-alpha.2", "keycode": "^2.1.8", "react-autowhatever": "^7.0.0", "react-jsonschema-form": "^0.42.0", "react-redux": "^5.0.4", - "redux-form": "^6.6.3", - "tv4": "^1.3.0" + "redux-form": "^6.6.3" }, "peerDependencies": { "react": "^15.4.0", diff --git a/packages/forms/src/UIForm/UIForm.component.js b/packages/forms/src/UIForm/UIForm.component.js index c72294074d9..a1c2cde96e3 100644 --- a/packages/forms/src/UIForm/UIForm.component.js +++ b/packages/forms/src/UIForm/UIForm.component.js @@ -7,18 +7,21 @@ import { merge, traverseSchema, traverseForm, -} from 'json-schema-form-core'; +} from 'talend-json-schema-form-core'; class UIForm extends React.Component { render() { const { jsonSchema, uiSchema, properties } = this.props.data || {}; - debugger; - schemaDefaults; - jsonref; - merge; - traverseSchema; - traverseForm; + try { + console.log(merge(jsonSchema, uiSchema)); + } + catch (error) {} + // schemaDefaults; + // jsonref; + // merge; + // traverseSchema; + // traverseForm; return (
diff --git a/packages/forms/stories/json/core-simple.json b/packages/forms/stories/json/core-simple.json new file mode 100644 index 00000000000..48ff1bf70f1 --- /dev/null +++ b/packages/forms/stories/json/core-simple.json @@ -0,0 +1,48 @@ +{ + "jsonSchema": { + "type": "object", + "title": "Comment", + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "email": { + "title": "Email", + "type": "string", + "pattern": "^\\S+@\\S+$", + "description": "Email will be used for evil." + }, + "comment": { + "title": "Comment", + "type": "string", + "maxLength": 20, + "validationMessage": "Don't be greedy!" + } + }, + "required": [ + "name", + "email", + "comment" + ] + }, + "uiSchema": [ + "name", + "email", + { + "key": "comment", + "type": "textarea", + "placeholder": "Make a comment" + }, + { + "type": "submit", + "style": "btn-info", + "title": "OK" + } + ], + "properties": { + "name": "Chuck Norris", + "email": "ChuckyFTW@gmail.com", + "comment": "lol" + } +} diff --git a/packages/forms/yarn.lock b/packages/forms/yarn.lock index 98a9021a5e5..6a02639b6b6 100644 --- a/packages/forms/yarn.lock +++ b/packages/forms/yarn.lock @@ -1256,9 +1256,9 @@ bootstrap-sass@^3.3.7: version "3.3.7" resolved "https://registry.yarnpkg.com/bootstrap-sass/-/bootstrap-sass-3.3.7.tgz#6596c7ab40f6637393323ab0bc80d064fc630498" -bootstrap-talend-theme@^0.72.2: - version "0.72.2" - resolved "https://registry.yarnpkg.com/bootstrap-talend-theme/-/bootstrap-talend-theme-0.72.2.tgz#d164a2d5cc39538b40bf3918b9e0738184ae9a0b" +bootstrap-talend-theme@^0.73.0: + version "0.73.0" + resolved "https://registry.yarnpkg.com/bootstrap-talend-theme/-/bootstrap-talend-theme-0.73.0.tgz#e722203c043e5a8fe6ca6fea5068d10d66b0dea3" dependencies: bootstrap-sass "^3.3.7" @@ -3764,10 +3764,6 @@ json-loader@^0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.4.tgz#8baa1365a632f58a3c46d20175fc6002c96e37de" -json-schema-form-core@^1.0.0-alpha.2: - version "1.0.0-alpha.2" - resolved "https://registry.yarnpkg.com/json-schema-form-core/-/json-schema-form-core-1.0.0-alpha.2.tgz#60566287e5f3fb8d2ccb1337dca5d3b80c3391c9" - json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" @@ -5333,9 +5329,9 @@ react-stubber@^1.0.0: dependencies: babel-runtime "^6.5.0" -react-talend-components@^0.72.2: - version "0.72.2" - resolved "https://registry.yarnpkg.com/react-talend-components/-/react-talend-components-0.72.2.tgz#9426e165996359ecc82697ce1083157804e98cb1" +react-talend-components@^0.73.0: + version "0.73.0" + resolved "https://registry.yarnpkg.com/react-talend-components/-/react-talend-components-0.73.0.tgz#270a194fb3874b54f73f2f15eeabdc9c6c8d8089" dependencies: lodash "^4.17.4" react-autowhatever "^7.0.0" @@ -6061,9 +6057,13 @@ table@^3.7.8: slice-ansi "0.0.4" string-width "^2.0.0" -talend-icons@^0.72.2: - version "0.72.2" - resolved "https://registry.yarnpkg.com/talend-icons/-/talend-icons-0.72.2.tgz#4bdabf0091695c6ede2ecdb218c4fb0cb66b8da9" +talend-icons@^0.73.0: + version "0.73.0" + resolved "https://registry.yarnpkg.com/talend-icons/-/talend-icons-0.73.0.tgz#5e75efbce39925883e43125345a8524f4418bbf7" + +talend-json-schema-form-core@^1.0.0-alpha.2: + version "1.0.0-alpha.2" + resolved "https://registry.yarnpkg.com/talend-json-schema-form-core/-/talend-json-schema-form-core-1.0.0-alpha.2.tgz#d7f9da3f3ec88d38dc5d5643f5dd7c573feca863" tapable@^0.1.8, tapable@~0.1.8: version "0.1.10" @@ -6176,10 +6176,6 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" -tv4@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/tv4/-/tv4-1.3.0.tgz#d020c846fadd50c855abb25ebaecc68fc10f7963" - tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" From cf7ba60a2db8afbc20a03b17aabc1fc896cbb9fc Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Wed, 10 May 2017 10:06:34 +0200 Subject: [PATCH 03/73] Bump schema form core version --- packages/forms/package.json | 2 +- packages/forms/yarn.lock | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/forms/package.json b/packages/forms/package.json index d5a8696c16a..e6068298acb 100644 --- a/packages/forms/package.json +++ b/packages/forms/package.json @@ -57,7 +57,7 @@ }, "dependencies": { "classnames": "^2.2.5", - "talend-json-schema-form-core": "^1.0.0-alpha.2", + "talend-json-schema-form-core": "1.0.2-alpha.2", "keycode": "^2.1.8", "react-autowhatever": "^7.0.0", "react-jsonschema-form": "^0.42.0", diff --git a/packages/forms/yarn.lock b/packages/forms/yarn.lock index 6a02639b6b6..56b285a50bd 100644 --- a/packages/forms/yarn.lock +++ b/packages/forms/yarn.lock @@ -4571,6 +4571,10 @@ object.values@^1.0.3: function-bind "^1.1.0" has "^1.0.1" +objectpath@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/objectpath/-/objectpath-1.2.1.tgz#c87433bdd352aea014e4f9c0b52d70a42652052e" + on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -6061,9 +6065,12 @@ talend-icons@^0.73.0: version "0.73.0" resolved "https://registry.yarnpkg.com/talend-icons/-/talend-icons-0.73.0.tgz#5e75efbce39925883e43125345a8524f4418bbf7" -talend-json-schema-form-core@^1.0.0-alpha.2: - version "1.0.0-alpha.2" - resolved "https://registry.yarnpkg.com/talend-json-schema-form-core/-/talend-json-schema-form-core-1.0.0-alpha.2.tgz#d7f9da3f3ec88d38dc5d5643f5dd7c573feca863" +talend-json-schema-form-core@1.0.2-alpha.2: + version "1.0.2-alpha.2" + resolved "https://registry.yarnpkg.com/talend-json-schema-form-core/-/talend-json-schema-form-core-1.0.2-alpha.2.tgz#5e78155e25aa99f50cfb7b054b263a1367a9201d" + dependencies: + objectpath "^1.2.1" + tv4 "^1.3.0" tapable@^0.1.8, tapable@~0.1.8: version "0.1.10" @@ -6176,6 +6183,10 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tv4@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/tv4/-/tv4-1.3.0.tgz#d020c846fadd50c855abb25ebaecc68fc10f7963" + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" From 5356a010ab66fe9766a03a661ba84006276545e7 Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Wed, 10 May 2017 16:22:10 +0200 Subject: [PATCH 04/73] Integrate simple text field with validation, description, placeholder, readonly support --- .../FieldMessage/FieldMessage.component.js | 24 +++ packages/forms/src/FieldMessage/index.js | 3 + packages/forms/src/UIForm/UIForm.component.js | 144 ++++++++++++++---- packages/forms/src/fields/TextField.js | 60 ++++++++ .../forms/src/utils/propertiesNavigator.js | 31 ++++ packages/forms/src/utils/validation.js | 20 +++ packages/forms/src/utils/widgets.js | 8 + packages/forms/stories/json/core-simple.json | 41 ++++- 8 files changed, 293 insertions(+), 38 deletions(-) create mode 100644 packages/forms/src/FieldMessage/FieldMessage.component.js create mode 100644 packages/forms/src/FieldMessage/index.js create mode 100644 packages/forms/src/fields/TextField.js create mode 100644 packages/forms/src/utils/propertiesNavigator.js create mode 100644 packages/forms/src/utils/validation.js create mode 100644 packages/forms/src/utils/widgets.js diff --git a/packages/forms/src/FieldMessage/FieldMessage.component.js b/packages/forms/src/FieldMessage/FieldMessage.component.js new file mode 100644 index 00000000000..e4cd3d17ed5 --- /dev/null +++ b/packages/forms/src/FieldMessage/FieldMessage.component.js @@ -0,0 +1,24 @@ +import React, { PropTypes } from 'react'; + +export default function FieldMessage(props) { + const { + errorMessage, + description, + isValid, + } = props; + + const message = isValid ? description : errorMessage; + return message ? + ( +
+ { message } +
+ ) : + null; +} + +FieldMessage.propTypes = { + errorMessage: PropTypes.string, + description: PropTypes.string, + isValid: PropTypes.bool, +}; diff --git a/packages/forms/src/FieldMessage/index.js b/packages/forms/src/FieldMessage/index.js new file mode 100644 index 00000000000..e4ccd6b9510 --- /dev/null +++ b/packages/forms/src/FieldMessage/index.js @@ -0,0 +1,3 @@ +import FieldMessage from './FieldMessage.component'; + +export default FieldMessage; diff --git a/packages/forms/src/UIForm/UIForm.component.js b/packages/forms/src/UIForm/UIForm.component.js index a1c2cde96e3..a75db2c131f 100644 --- a/packages/forms/src/UIForm/UIForm.component.js +++ b/packages/forms/src/UIForm/UIForm.component.js @@ -1,51 +1,133 @@ import React, { PropTypes } from 'react'; -import { Field, reduxForm } from 'redux-form'; +import { reduxForm } from 'redux-form'; import { - schemaDefaults, - jsonref, merge, - traverseSchema, - traverseForm, + sfPath, + validate, } from 'talend-json-schema-form-core'; +import validateAll from '../utils/validation'; +import widgets from '../utils/widgets'; +import { getValue, mutateValue } from '../utils/propertiesNavigator'; + class UIForm extends React.Component { + constructor(props) { + super(props); + const { jsonSchema, uiSchema, properties } = props.data; + this.state = { + mergedSchema: merge(jsonSchema, uiSchema), + properties: { ...properties }, + validations: {}, + }; - render() { - const { jsonSchema, uiSchema, properties } = this.props.data || {}; - try { - console.log(merge(jsonSchema, uiSchema)); + this.consolidate = this.consolidate.bind(this); + this.submit = this.submit.bind(this); + } + + /** + * Update the state with the new schema. + * @param jsonSchema + * @param uiSchema + * @param properties + */ + componentWillReceiveProps({ jsonSchema, uiSchema, properties }) { + if (!jsonSchema || !uiSchema || !properties) { + return; + } + this.setState(() => ({ + mergedSchema: merge(jsonSchema, uiSchema), + properties: { ...properties }, + validations: {}, + })); + } + + /** + * Consolidate form with the new value. + * This updates the validation on the modified field. + * @param event The change event + * @param schema The schema of the changed field + * @param value The new field value + */ + consolidate(event, schema, value) { + this.setState(prevState => ({ + properties: mutateValue(prevState.properties, schema.key, value), + validations: { + ...prevState.validations, + [schema.key]: validate(schema, value), + }, + })); + } + + /** + * Triggers a validation and update state. + * @returns {boolean} true if the form is valid, false otherwise + */ + isValid() { + const validations = validateAll(this.state.mergedSchema, this.state.properties); + const keys = Object.keys(validations); + for (const key of keys) { + if (!validations[key].valid) { + this.setState(() => ({ validations })); + return false; + } + } + return true; + } + + /** + * Triggers submit callback if form is valid + * @param event the submit event + */ + submit(event) { + event.preventDefault(); + if (this.isValid()) { + this.props.onSubmit(event, this.state.properties); } - catch (error) {} - // schemaDefaults; - // jsonref; - // merge; - // traverseSchema; - // traverseForm; + } + + render() { + const { formName } = this.props; + const { properties, validations } = this.state; + return ( - -
- - -
-
- - -
-
- - -
- + + { + this.state.mergedSchema.map((nextSchema) => { + const { key, type, validationMessage } = nextSchema; + const id = sfPath.name(key, '-', formName); + const { error, valid } = validations[nextSchema.key] || {}; + const errorMessage = validationMessage || (error && error.message); + const Widget = widgets[type]; + return Widget && ( + + ); + }) + } + ); } } UIForm.propTypes = { - handleSubmit: PropTypes.func.isRequired, + data: PropTypes.shape({ + jsonSchema: PropTypes.object, + uiSchema: PropTypes.array, + properties: PropTypes.object, + }), + formName: PropTypes.string, + onSubmit: PropTypes.func.isRequired, }; export default reduxForm({ - form: 'form' // a unique name for this form + form: 'form', // a unique name for this form })(UIForm); diff --git a/packages/forms/src/fields/TextField.js b/packages/forms/src/fields/TextField.js new file mode 100644 index 00000000000..4f03ff43177 --- /dev/null +++ b/packages/forms/src/fields/TextField.js @@ -0,0 +1,60 @@ +import React, { PropTypes } from 'react'; +import classNames from 'classnames'; + +import FieldMessage from '../FieldMessage'; + +function convertValue(type, value) { + if (type === 'number') { + return parseFloat(value); + } + return value; +} + +export default function TextField(props) { + const { id, isValid, errorMessage, onChange, schema, value } = props; + const { description, placeholder, readOnly, title, type } = schema; + + const groupsClassNames = classNames( + 'form-group', + { 'has-error': !isValid }, + ); + return ( +
+ onChange(event, schema, convertValue(type, event.target.value))} + readOnly={readOnly} + type={type} + value={value} + /> + + +
+ ); +} + +TextField.propTypes = { + id: PropTypes.string, + isValid: PropTypes.bool, + errorMessage: PropTypes.string, + onChange: PropTypes.func, + schema: PropTypes.shape({ + description: PropTypes.string, + placeholder: PropTypes.string, + readOnly: PropTypes.bool, + title: PropTypes.string, + type: PropTypes.string, + }), + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), +}; +TextField.defaultProps = { + isValid: true, + value: '', +}; diff --git a/packages/forms/src/utils/propertiesNavigator.js b/packages/forms/src/utils/propertiesNavigator.js new file mode 100644 index 00000000000..add849a6bbd --- /dev/null +++ b/packages/forms/src/utils/propertiesNavigator.js @@ -0,0 +1,31 @@ +/** + * Get a value stored in properties, identified by key + * @param {object} properties The properties store + * @param {array} key The key chain (array of strings) to access to the value + */ +export function getValue(properties, key) { + return key.reduce( + (accu, nextKey) => accu[nextKey], + properties + ); +} + +/** + * Mutate the properties, setting the value in the path identified by key + * @param {object} properties The original properties store + * @param {array} key The key chain (array of strings) to identify the path + * @param {any} value The value to set + * @returns {object} The new mutated properties store. + */ +export function mutateValue(properties, key, value) { + if (!key.length) { + return value; + } + + const nextKey = key[0]; + const restKeys = key.slice(1); + return { + ...properties, + [nextKey]: mutateValue(properties[nextKey], restKeys, value), + }; +} diff --git a/packages/forms/src/utils/validation.js b/packages/forms/src/utils/validation.js new file mode 100644 index 00000000000..07a77f68522 --- /dev/null +++ b/packages/forms/src/utils/validation.js @@ -0,0 +1,20 @@ +import { validate } from 'talend-json-schema-form-core'; + +import { getValue } from './propertiesNavigator'; + +/** + * Validate values. This supports only 1 level of fields for now + * @param mergedSchema The merged schema. + * @param properties The values. + * @returns {object} The validation result by field. + */ +export default function validateAll(mergedSchema, properties) { + const validations = {}; + mergedSchema.forEach((schema) => { + validations[schema.key] = validate( + schema, + getValue(properties, schema.key) + ); + }); + return validations; +} diff --git a/packages/forms/src/utils/widgets.js b/packages/forms/src/utils/widgets.js new file mode 100644 index 00000000000..69b58159ae2 --- /dev/null +++ b/packages/forms/src/utils/widgets.js @@ -0,0 +1,8 @@ +import TextField from '../fields/TextField'; + +const widgets = { + text: TextField, + number: TextField, +}; + +export default widgets; diff --git a/packages/forms/stories/json/core-simple.json b/packages/forms/stories/json/core-simple.json index 48ff1bf70f1..f69d548c21e 100644 --- a/packages/forms/stories/json/core-simple.json +++ b/packages/forms/stories/json/core-simple.json @@ -7,11 +7,29 @@ "title": "Name", "type": "string" }, + "lastname": { + "title": "Last Name (with description)", + "type": "string", + "description": "Hint: this is the last name" + }, + "firstname": { + "title": "First Name (with placeholder)", + "type": "string" + }, + "nochange": { + "title": "Field (read only mode)", + "type": "string" + }, + "age": { + "title": "Age", + "type": "number" + }, "email": { - "title": "Email", + "title": "Email (with pattern validation and custom validation message)", "type": "string", "pattern": "^\\S+@\\S+$", - "description": "Email will be used for evil." + "description": "Email will be used for evil.", + "validationMessage": "Please enter a valid email address, e.g. user@email.com" }, "comment": { "title": "Comment", @@ -22,26 +40,35 @@ }, "required": [ "name", + "firstname", "email", "comment" ] }, "uiSchema": [ "name", + "lastname", + { + "key": "firstname", + "type": "text", + "placeholder": "Enter your firstname here" + }, + "age", "email", + { + "key": "nochange", + "type": "text", + "readOnly": true + }, { "key": "comment", "type": "textarea", "placeholder": "Make a comment" - }, - { - "type": "submit", - "style": "btn-info", - "title": "OK" } ], "properties": { "name": "Chuck Norris", + "nochange": "You can't change that", "email": "ChuckyFTW@gmail.com", "comment": "lol" } From dac6b4e456b912b86716ca80497c141d32cb27e0 Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Thu, 11 May 2017 10:08:27 +0200 Subject: [PATCH 05/73] Rename file --- packages/forms/src/UIForm/UIForm.component.js | 2 +- .../forms/src/utils/{propertiesNavigator.js => properties.js} | 0 packages/forms/src/utils/validation.js | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/forms/src/utils/{propertiesNavigator.js => properties.js} (100%) diff --git a/packages/forms/src/UIForm/UIForm.component.js b/packages/forms/src/UIForm/UIForm.component.js index a75db2c131f..a4d4c34805b 100644 --- a/packages/forms/src/UIForm/UIForm.component.js +++ b/packages/forms/src/UIForm/UIForm.component.js @@ -9,7 +9,7 @@ import { import validateAll from '../utils/validation'; import widgets from '../utils/widgets'; -import { getValue, mutateValue } from '../utils/propertiesNavigator'; +import { getValue, mutateValue } from '../utils/properties'; class UIForm extends React.Component { constructor(props) { diff --git a/packages/forms/src/utils/propertiesNavigator.js b/packages/forms/src/utils/properties.js similarity index 100% rename from packages/forms/src/utils/propertiesNavigator.js rename to packages/forms/src/utils/properties.js diff --git a/packages/forms/src/utils/validation.js b/packages/forms/src/utils/validation.js index 07a77f68522..12fbe547eb0 100644 --- a/packages/forms/src/utils/validation.js +++ b/packages/forms/src/utils/validation.js @@ -1,6 +1,6 @@ import { validate } from 'talend-json-schema-form-core'; -import { getValue } from './propertiesNavigator'; +import { getValue } from './properties'; /** * Validate values. This supports only 1 level of fields for now From dd4b1bad4431d228de7b7763e996e19253182d94 Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Thu, 11 May 2017 10:27:12 +0200 Subject: [PATCH 06/73] Add structured model story --- .../stories/json/core-structured-model.json | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 packages/forms/stories/json/core-structured-model.json diff --git a/packages/forms/stories/json/core-structured-model.json b/packages/forms/stories/json/core-structured-model.json new file mode 100644 index 00000000000..098416aa5bb --- /dev/null +++ b/packages/forms/stories/json/core-structured-model.json @@ -0,0 +1,82 @@ +{ + "jsonSchema": { + "type": "object", + "title": "Comment", + "properties": { + "user": { + "type": "object", + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "lastname": { + "title": "Last Name (with description)", + "type": "string", + "description": "Hint: this is the last name" + }, + "firstname": { + "title": "First Name (with placeholder)", + "type": "string" + }, + "age": { + "title": "Age", + "type": "number" + } + } + }, + "nochange": { + "title": "Field (read only mode)", + "type": "string" + }, + "email": { + "title": "Email (with pattern validation and custom validation message)", + "type": "string", + "pattern": "^\\S+@\\S+$", + "description": "Email will be used for evil.", + "validationMessage": "Please enter a valid email address, e.g. user@email.com" + }, + "comment": { + "title": "Comment", + "type": "string", + "maxLength": 20, + "validationMessage": "Don't be greedy!" + } + }, + "required": [ + "name", + "firstname", + "email", + "comment" + ] + }, + "uiSchema": [ + "user.name", + "user.lastname", + { + "key": "user.firstname", + "type": "text", + "placeholder": "Enter your firstname here" + }, + "user.age", + "email", + { + "key": "nochange", + "type": "text", + "readOnly": true + }, + { + "key": "comment", + "type": "textarea", + "placeholder": "Make a comment" + } + ], + "properties": { + "user": { + "name": "Chuck Norris" + }, + "nochange": "You can't change that", + "email": "ChuckyFTW@gmail.com", + "comment": "lol" + } +} From 7e399842b9eaa2a0adbe0f77bb976407a1298bed Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Thu, 11 May 2017 10:56:47 +0200 Subject: [PATCH 07/73] Move info in schemas --- packages/forms/stories/json/core-simple.json | 54 ++++++++++-------- .../stories/json/core-structured-model.json | 56 +++++++++++-------- 2 files changed, 62 insertions(+), 48 deletions(-) diff --git a/packages/forms/stories/json/core-simple.json b/packages/forms/stories/json/core-simple.json index f69d548c21e..90d2095e8c1 100644 --- a/packages/forms/stories/json/core-simple.json +++ b/packages/forms/stories/json/core-simple.json @@ -4,38 +4,27 @@ "title": "Comment", "properties": { "name": { - "title": "Name", "type": "string" }, "lastname": { - "title": "Last Name (with description)", - "type": "string", - "description": "Hint: this is the last name" - }, - "firstname": { - "title": "First Name (with placeholder)", "type": "string" }, - "nochange": { - "title": "Field (read only mode)", + "firstname": { "type": "string" }, "age": { - "title": "Age", "type": "number" }, + "nochange": { + "type": "string" + }, "email": { - "title": "Email (with pattern validation and custom validation message)", "type": "string", - "pattern": "^\\S+@\\S+$", - "description": "Email will be used for evil.", - "validationMessage": "Please enter a valid email address, e.g. user@email.com" + "pattern": "^\\S+@\\S+$" }, "comment": { - "title": "Comment", "type": "string", - "maxLength": 20, - "validationMessage": "Don't be greedy!" + "maxLength": 20 } }, "required": [ @@ -46,24 +35,41 @@ ] }, "uiSchema": [ - "name", - "lastname", + { + "key": "name", + "title": "Name" + }, + { + "key": "lastname", + "title": "Last Name (with description)", + "description": "Hint: this is the last name" + }, { "key": "firstname", - "type": "text", + "title": "First Name (with placeholder)", "placeholder": "Enter your firstname here" }, - "age", - "email", + { + "key": "age", + "title": "Age" + }, + { + "key": "email", + "title": "Email (with pattern validation and custom validation message)", + "description": "Email will be used for evil.", + "validationMessage": "Please enter a valid email address, e.g. user@email.com" + }, { "key": "nochange", - "type": "text", + "title": "Field (read only mode)", "readOnly": true }, { "key": "comment", "type": "textarea", - "placeholder": "Make a comment" + "title": "Comment", + "placeholder": "Make a comment", + "validationMessage": "Don't be greedy!" } ], "properties": { diff --git a/packages/forms/stories/json/core-structured-model.json b/packages/forms/stories/json/core-structured-model.json index 098416aa5bb..7da37c72051 100644 --- a/packages/forms/stories/json/core-structured-model.json +++ b/packages/forms/stories/json/core-structured-model.json @@ -7,68 +7,76 @@ "type": "object", "properties": { "name": { - "title": "Name", "type": "string" }, "lastname": { - "title": "Last Name (with description)", - "type": "string", - "description": "Hint: this is the last name" + "type": "string" }, "firstname": { - "title": "First Name (with placeholder)", "type": "string" }, "age": { - "title": "Age", "type": "number" } - } + }, + "required": [ + "name", + "firstname" + ] }, "nochange": { - "title": "Field (read only mode)", "type": "string" }, "email": { - "title": "Email (with pattern validation and custom validation message)", "type": "string", - "pattern": "^\\S+@\\S+$", - "description": "Email will be used for evil.", - "validationMessage": "Please enter a valid email address, e.g. user@email.com" + "pattern": "^\\S+@\\S+$" }, "comment": { - "title": "Comment", "type": "string", - "maxLength": 20, - "validationMessage": "Don't be greedy!" + "maxLength": 20 } }, "required": [ - "name", - "firstname", "email", "comment" ] }, "uiSchema": [ - "user.name", - "user.lastname", + { + "key": "user.name", + "title": "Name" + }, + { + "key": "user.lastname", + "title": "Last Name (with description)", + "description": "Hint: this is the last name" + }, { "key": "user.firstname", - "type": "text", + "title": "First Name (with placeholder)", "placeholder": "Enter your firstname here" }, - "user.age", - "email", + { + "key": "user.age", + "title": "Age" + }, + { + "key": "email", + "title": "Email (with pattern validation and custom validation message)", + "description": "Email will be used for evil.", + "validationMessage": "Please enter a valid email address, e.g. user@email.com" + }, { "key": "nochange", - "type": "text", + "title": "Field (read only mode)", "readOnly": true }, { "key": "comment", "type": "textarea", - "placeholder": "Make a comment" + "title": "Comment", + "placeholder": "Make a comment", + "validationMessage": "Don't be greedy!" } ], "properties": { From f7fc2d05a61ac82fea30d587c599aa232daf9279 Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Thu, 11 May 2017 15:10:41 +0200 Subject: [PATCH 08/73] Fieldsets --- packages/forms/src/FieldMessage/index.js | 3 - .../Message/Message.component.js} | 4 +- packages/forms/src/UIForm/Message/index.js | 3 + packages/forms/src/UIForm/UIForm.component.js | 41 +++----- .../src/UIForm/Widget/Widget.component.js | 40 ++++++++ packages/forms/src/UIForm/Widget/index.js | 3 + .../TextField.js => UIForm/fields/Text.js} | 10 +- .../forms/src/UIForm/fieldsets/Fieldset.js | 27 ++++++ .../src/{ => UIForm}/utils/properties.js | 4 + .../src/{ => UIForm}/utils/validation.js | 17 +++- packages/forms/src/UIForm/utils/widgets.js | 10 ++ packages/forms/src/utils/widgets.js | 8 -- .../forms/stories/json/core-fieldset.json | 93 +++++++++++++++++++ 13 files changed, 213 insertions(+), 50 deletions(-) delete mode 100644 packages/forms/src/FieldMessage/index.js rename packages/forms/src/{FieldMessage/FieldMessage.component.js => UIForm/Message/Message.component.js} (82%) create mode 100644 packages/forms/src/UIForm/Message/index.js create mode 100644 packages/forms/src/UIForm/Widget/Widget.component.js create mode 100644 packages/forms/src/UIForm/Widget/index.js rename packages/forms/src/{fields/TextField.js => UIForm/fields/Text.js} (89%) create mode 100644 packages/forms/src/UIForm/fieldsets/Fieldset.js rename packages/forms/src/{ => UIForm}/utils/properties.js (95%) rename packages/forms/src/{ => UIForm}/utils/validation.js (58%) create mode 100644 packages/forms/src/UIForm/utils/widgets.js delete mode 100644 packages/forms/src/utils/widgets.js create mode 100644 packages/forms/stories/json/core-fieldset.json diff --git a/packages/forms/src/FieldMessage/index.js b/packages/forms/src/FieldMessage/index.js deleted file mode 100644 index e4ccd6b9510..00000000000 --- a/packages/forms/src/FieldMessage/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import FieldMessage from './FieldMessage.component'; - -export default FieldMessage; diff --git a/packages/forms/src/FieldMessage/FieldMessage.component.js b/packages/forms/src/UIForm/Message/Message.component.js similarity index 82% rename from packages/forms/src/FieldMessage/FieldMessage.component.js rename to packages/forms/src/UIForm/Message/Message.component.js index e4cd3d17ed5..5e2452fcce6 100644 --- a/packages/forms/src/FieldMessage/FieldMessage.component.js +++ b/packages/forms/src/UIForm/Message/Message.component.js @@ -1,6 +1,6 @@ import React, { PropTypes } from 'react'; -export default function FieldMessage(props) { +export default function Message(props) { const { errorMessage, description, @@ -17,7 +17,7 @@ export default function FieldMessage(props) { null; } -FieldMessage.propTypes = { +Message.propTypes = { errorMessage: PropTypes.string, description: PropTypes.string, isValid: PropTypes.bool, diff --git a/packages/forms/src/UIForm/Message/index.js b/packages/forms/src/UIForm/Message/index.js new file mode 100644 index 00000000000..14b3a36cdf9 --- /dev/null +++ b/packages/forms/src/UIForm/Message/index.js @@ -0,0 +1,3 @@ +import Message from './Message.component'; + +export default Message; diff --git a/packages/forms/src/UIForm/UIForm.component.js b/packages/forms/src/UIForm/UIForm.component.js index a4d4c34805b..bde67b67c75 100644 --- a/packages/forms/src/UIForm/UIForm.component.js +++ b/packages/forms/src/UIForm/UIForm.component.js @@ -1,15 +1,10 @@ import React, { PropTypes } from 'react'; import { reduxForm } from 'redux-form'; +import { merge, validate } from 'talend-json-schema-form-core'; -import { - merge, - sfPath, - validate, -} from 'talend-json-schema-form-core'; - -import validateAll from '../utils/validation'; -import widgets from '../utils/widgets'; -import { getValue, mutateValue } from '../utils/properties'; +import Widget from './Widget'; +import validateAll from './utils/validation'; +import { mutateValue } from './utils/properties'; class UIForm extends React.Component { constructor(props) { @@ -93,24 +88,16 @@ class UIForm extends React.Component { return (
{ - this.state.mergedSchema.map((nextSchema) => { - const { key, type, validationMessage } = nextSchema; - const id = sfPath.name(key, '-', formName); - const { error, valid } = validations[nextSchema.key] || {}; - const errorMessage = validationMessage || (error && error.message); - const Widget = widgets[type]; - return Widget && ( - - ); - }) + this.state.mergedSchema.map((nextSchema, index) => ( + + )) } diff --git a/packages/forms/src/UIForm/Widget/Widget.component.js b/packages/forms/src/UIForm/Widget/Widget.component.js new file mode 100644 index 00000000000..f690f278299 --- /dev/null +++ b/packages/forms/src/UIForm/Widget/Widget.component.js @@ -0,0 +1,40 @@ +import React, { PropTypes } from 'react'; +import { sfPath } from 'talend-json-schema-form-core'; + +import widgets from '../utils/widgets'; +import { getValue } from '../utils/properties'; + +export default function Widget({ formName, onChange, properties, schema, validations }) { + const { key, type, validationMessage } = schema; + const id = sfPath.name(key, '-', formName); + const { error, valid } = validations[key] || {}; + const errorMessage = validationMessage || (error && error.message); + const WidgetImpl = widgets[type]; + return WidgetImpl ? + ( + + ) : null; +} + +Widget.propTypes = { + formName: PropTypes.string, + onChange: PropTypes.func, + schema: PropTypes.shape({ + key: PropTypes.array, + type: PropTypes.string.isRequired, + validationMessage: PropTypes.string, + }).isRequired, + properties: PropTypes.object, // eslint-disable-line react/forbid-prop-types + validations: PropTypes.object, // eslint-disable-line react/forbid-prop-types +}; diff --git a/packages/forms/src/UIForm/Widget/index.js b/packages/forms/src/UIForm/Widget/index.js new file mode 100644 index 00000000000..3317235a36b --- /dev/null +++ b/packages/forms/src/UIForm/Widget/index.js @@ -0,0 +1,3 @@ +import Widget from './Widget.component'; + +export default Widget; diff --git a/packages/forms/src/fields/TextField.js b/packages/forms/src/UIForm/fields/Text.js similarity index 89% rename from packages/forms/src/fields/TextField.js rename to packages/forms/src/UIForm/fields/Text.js index 4f03ff43177..12a885dec73 100644 --- a/packages/forms/src/fields/TextField.js +++ b/packages/forms/src/UIForm/fields/Text.js @@ -1,7 +1,7 @@ import React, { PropTypes } from 'react'; import classNames from 'classnames'; -import FieldMessage from '../FieldMessage'; +import Message from '../Message'; function convertValue(type, value) { if (type === 'number') { @@ -10,7 +10,7 @@ function convertValue(type, value) { return value; } -export default function TextField(props) { +export default function Text(props) { const { id, isValid, errorMessage, onChange, schema, value } = props; const { description, placeholder, readOnly, title, type } = schema; @@ -31,7 +31,7 @@ export default function TextField(props) { value={value} /> - + {title && ({title})} + {items.map((itemSchema, index) => ( + + ))} + + ); +} + +Fieldset.propTypes = { + schema: PropTypes.shape({ + items: PropTypes.array.isRequired, + title: PropTypes.string, + }).isRequired, +}; diff --git a/packages/forms/src/utils/properties.js b/packages/forms/src/UIForm/utils/properties.js similarity index 95% rename from packages/forms/src/utils/properties.js rename to packages/forms/src/UIForm/utils/properties.js index add849a6bbd..0dfd46622f0 100644 --- a/packages/forms/src/utils/properties.js +++ b/packages/forms/src/UIForm/utils/properties.js @@ -4,6 +4,10 @@ * @param {array} key The key chain (array of strings) to access to the value */ export function getValue(properties, key) { + if (!key) { + return undefined; + } + return key.reduce( (accu, nextKey) => accu[nextKey], properties diff --git a/packages/forms/src/utils/validation.js b/packages/forms/src/UIForm/utils/validation.js similarity index 58% rename from packages/forms/src/utils/validation.js rename to packages/forms/src/UIForm/utils/validation.js index 12fbe547eb0..1dabecd2ef7 100644 --- a/packages/forms/src/utils/validation.js +++ b/packages/forms/src/UIForm/utils/validation.js @@ -3,7 +3,7 @@ import { validate } from 'talend-json-schema-form-core'; import { getValue } from './properties'; /** - * Validate values. This supports only 1 level of fields for now + * Validate values. * @param mergedSchema The merged schema. * @param properties The values. * @returns {object} The validation result by field. @@ -11,10 +11,17 @@ import { getValue } from './properties'; export default function validateAll(mergedSchema, properties) { const validations = {}; mergedSchema.forEach((schema) => { - validations[schema.key] = validate( - schema, - getValue(properties, schema.key) - ); + const { key, items } = schema; + if (key) { + validations[key] = validate( + schema, + getValue(properties, key) + ); + } + if (items) { + const subValidations = validateAll(items, properties); + Object.assign(validations, subValidations); + } }); return validations; } diff --git a/packages/forms/src/UIForm/utils/widgets.js b/packages/forms/src/UIForm/utils/widgets.js new file mode 100644 index 00000000000..b7e22b5c36f --- /dev/null +++ b/packages/forms/src/UIForm/utils/widgets.js @@ -0,0 +1,10 @@ +import Fieldset from '../fieldsets/Fieldset'; +import Text from '../fields/Text'; + +const widgets = { + fieldset: Fieldset, + number: Text, + text: Text, +}; + +export default widgets; diff --git a/packages/forms/src/utils/widgets.js b/packages/forms/src/utils/widgets.js deleted file mode 100644 index 69b58159ae2..00000000000 --- a/packages/forms/src/utils/widgets.js +++ /dev/null @@ -1,8 +0,0 @@ -import TextField from '../fields/TextField'; - -const widgets = { - text: TextField, - number: TextField, -}; - -export default widgets; diff --git a/packages/forms/stories/json/core-fieldset.json b/packages/forms/stories/json/core-fieldset.json new file mode 100644 index 00000000000..68c5b22dc29 --- /dev/null +++ b/packages/forms/stories/json/core-fieldset.json @@ -0,0 +1,93 @@ +{ + "jsonSchema": { + "type": "object", + "title": "Comment", + "properties": { + "name": { + "type": "string" + }, + "lastname": { + "type": "string" + }, + "firstname": { + "type": "string" + }, + "age": { + "type": "number" + }, + "nochange": { + "type": "string" + }, + "email": { + "type": "string", + "pattern": "^\\S+@\\S+$" + }, + "comment": { + "type": "string", + "maxLength": 20 + } + }, + "required": [ + "name", + "firstname", + "email", + "comment" + ] + }, + "uiSchema": [ + { + "type": "fieldset", + "title": "My awesome USER form", + "items": [ + { + "key": "name", + "title": "Name" + }, + { + "key": "lastname", + "title": "Last Name (with description)", + "description": "Hint: this is the last name" + }, + { + "key": "firstname", + "title": "First Name (with placeholder)", + "placeholder": "Enter your firstname here" + }, + { + "key": "age", + "title": "Age" + } + ] + }, + { + "type": "fieldset", + "title": "My awesome OTHER form", + "items": [ + { + "key": "email", + "title": "Email (with pattern validation and custom validation message)", + "description": "Email will be used for evil.", + "validationMessage": "Please enter a valid email address, e.g. user@email.com" + }, + { + "key": "nochange", + "title": "Field (read only mode)", + "readOnly": true + }, + { + "key": "comment", + "type": "textarea", + "title": "Comment", + "placeholder": "Make a comment", + "validationMessage": "Don't be greedy!" + } + ] + } + ], + "properties": { + "name": "Chuck Norris", + "nochange": "You can't change that", + "email": "ChuckyFTW@gmail.com", + "comment": "lol" + } +} From 400040f7a8b423b8ed10f66a4171c64ba03e34be Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Thu, 11 May 2017 16:35:23 +0200 Subject: [PATCH 09/73] Tabs --- .../src/UIForm/Message/Message.component.js | 12 ++- packages/forms/src/UIForm/UIForm.component.js | 23 +++-- .../src/UIForm/Widget/Widget.component.js | 24 ++--- packages/forms/src/UIForm/fields/Text.js | 30 +++--- .../forms/src/UIForm/fieldsets/Fieldset.js | 14 +-- packages/forms/src/UIForm/fieldsets/Tabs.js | 44 +++++++++ packages/forms/src/UIForm/fieldsets/Tabs.scss | 12 +++ packages/forms/src/UIForm/utils/validation.js | 28 +++++- packages/forms/src/UIForm/utils/widgets.js | 6 ++ packages/forms/stories/json/core-tabs.json | 96 +++++++++++++++++++ 10 files changed, 242 insertions(+), 47 deletions(-) create mode 100644 packages/forms/src/UIForm/fieldsets/Tabs.js create mode 100644 packages/forms/src/UIForm/fieldsets/Tabs.scss create mode 100644 packages/forms/stories/json/core-tabs.json diff --git a/packages/forms/src/UIForm/Message/Message.component.js b/packages/forms/src/UIForm/Message/Message.component.js index 5e2452fcce6..90e500a93a4 100644 --- a/packages/forms/src/UIForm/Message/Message.component.js +++ b/packages/forms/src/UIForm/Message/Message.component.js @@ -17,8 +17,10 @@ export default function Message(props) { null; } -Message.propTypes = { - errorMessage: PropTypes.string, - description: PropTypes.string, - isValid: PropTypes.bool, -}; +if (process.env.NODE_ENV !== 'production') { + Message.propTypes = { + errorMessage: PropTypes.string, + description: PropTypes.string, + isValid: PropTypes.bool, + }; +} diff --git a/packages/forms/src/UIForm/UIForm.component.js b/packages/forms/src/UIForm/UIForm.component.js index bde67b67c75..c8db77dee2d 100644 --- a/packages/forms/src/UIForm/UIForm.component.js +++ b/packages/forms/src/UIForm/UIForm.component.js @@ -3,7 +3,7 @@ import { reduxForm } from 'redux-form'; import { merge, validate } from 'talend-json-schema-form-core'; import Widget from './Widget'; -import validateAll from './utils/validation'; +import { validateAll } from './utils/validation'; import { mutateValue } from './utils/properties'; class UIForm extends React.Component { @@ -15,6 +15,7 @@ class UIForm extends React.Component { properties: { ...properties }, validations: {}, }; + console.log(this.state.mergedSchema) this.consolidate = this.consolidate.bind(this); this.submit = this.submit.bind(this); @@ -105,15 +106,17 @@ class UIForm extends React.Component { } } -UIForm.propTypes = { - data: PropTypes.shape({ - jsonSchema: PropTypes.object, - uiSchema: PropTypes.array, - properties: PropTypes.object, - }), - formName: PropTypes.string, - onSubmit: PropTypes.func.isRequired, -}; +if (process.env.NODE_ENV !== 'production') { + UIForm.propTypes = { + data: PropTypes.shape({ + jsonSchema: PropTypes.object, + uiSchema: PropTypes.array, + properties: PropTypes.object, + }), + formName: PropTypes.string, + onSubmit: PropTypes.func.isRequired, + }; +} export default reduxForm({ form: 'form', // a unique name for this form diff --git a/packages/forms/src/UIForm/Widget/Widget.component.js b/packages/forms/src/UIForm/Widget/Widget.component.js index f690f278299..15cdda6409c 100644 --- a/packages/forms/src/UIForm/Widget/Widget.component.js +++ b/packages/forms/src/UIForm/Widget/Widget.component.js @@ -27,14 +27,16 @@ export default function Widget({ formName, onChange, properties, schema, validat ) : null; } -Widget.propTypes = { - formName: PropTypes.string, - onChange: PropTypes.func, - schema: PropTypes.shape({ - key: PropTypes.array, - type: PropTypes.string.isRequired, - validationMessage: PropTypes.string, - }).isRequired, - properties: PropTypes.object, // eslint-disable-line react/forbid-prop-types - validations: PropTypes.object, // eslint-disable-line react/forbid-prop-types -}; +if (process.env.NODE_ENV !== 'production') { + Widget.propTypes = { + formName: PropTypes.string, + onChange: PropTypes.func, + schema: PropTypes.shape({ + key: PropTypes.array, + type: PropTypes.string.isRequired, + validationMessage: PropTypes.string, + }).isRequired, + properties: PropTypes.object, // eslint-disable-line react/forbid-prop-types + validations: PropTypes.object, // eslint-disable-line react/forbid-prop-types + }; +} diff --git a/packages/forms/src/UIForm/fields/Text.js b/packages/forms/src/UIForm/fields/Text.js index 12a885dec73..09b50e1d715 100644 --- a/packages/forms/src/UIForm/fields/Text.js +++ b/packages/forms/src/UIForm/fields/Text.js @@ -40,20 +40,22 @@ export default function Text(props) { ); } -Text.propTypes = { - id: PropTypes.string, - isValid: PropTypes.bool, - errorMessage: PropTypes.string, - onChange: PropTypes.func, - schema: PropTypes.shape({ - description: PropTypes.string, - placeholder: PropTypes.string, - readOnly: PropTypes.bool, - title: PropTypes.string, - type: PropTypes.string, - }), - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), -}; +if (process.env.NODE_ENV !== 'production') { + Text.propTypes = { + id: PropTypes.string, + isValid: PropTypes.bool, + errorMessage: PropTypes.string, + onChange: PropTypes.func, + schema: PropTypes.shape({ + description: PropTypes.string, + placeholder: PropTypes.string, + readOnly: PropTypes.bool, + title: PropTypes.string, + type: PropTypes.string, + }), + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + }; +} Text.defaultProps = { isValid: true, value: '', diff --git a/packages/forms/src/UIForm/fieldsets/Fieldset.js b/packages/forms/src/UIForm/fieldsets/Fieldset.js index a36b142e1b6..956405e2705 100644 --- a/packages/forms/src/UIForm/fieldsets/Fieldset.js +++ b/packages/forms/src/UIForm/fieldsets/Fieldset.js @@ -19,9 +19,11 @@ export default function Fieldset(props) { ); } -Fieldset.propTypes = { - schema: PropTypes.shape({ - items: PropTypes.array.isRequired, - title: PropTypes.string, - }).isRequired, -}; +if (process.env.NODE_ENV !== 'production') { + Fieldset.propTypes = { + schema: PropTypes.shape({ + title: PropTypes.string, + items: PropTypes.array.isRequired, + }).isRequired, + }; +} diff --git a/packages/forms/src/UIForm/fieldsets/Tabs.js b/packages/forms/src/UIForm/fieldsets/Tabs.js new file mode 100644 index 00000000000..ed5aeba565c --- /dev/null +++ b/packages/forms/src/UIForm/fieldsets/Tabs.js @@ -0,0 +1,44 @@ +import React, { PropTypes } from 'react'; +import { Tabs as RBTabs, Tab as RBTab } from 'react-bootstrap'; + +import Fieldset from './Fieldset'; +import { isValid } from '../utils/validation'; +import theme from './Tabs.scss'; + +export default function Tabs(props) { + const { schema, ...restProps } = props; + const tabs = schema.items; + + return ( + + {tabs.map((tabSchema, index) => { + const tabClassName = isValid(tabSchema, restProps.validations) ? + null : + theme['has-error']; + return ( + +
+ + ); + })} + + ); +} + +if (process.env.NODE_ENV !== 'production') { + Tabs.propTypes = { + schema: PropTypes.shape({ + items: PropTypes.arrayOf( + React.PropTypes.shape({ + title: PropTypes.string.isRequired, + items: PropTypes.array.isRequired, + }) + ).isRequired, + }).isRequired, + }; +} diff --git a/packages/forms/src/UIForm/fieldsets/Tabs.scss b/packages/forms/src/UIForm/fieldsets/Tabs.scss new file mode 100644 index 00000000000..534995bde06 --- /dev/null +++ b/packages/forms/src/UIForm/fieldsets/Tabs.scss @@ -0,0 +1,12 @@ +.tf-tabs { + li.has-error { + > a, + > a:focus { + color: $brand-danger; + } + + &:global(.active) > a { + @include box-shadow(inset 0 -2px 0 $brand-danger); + } + } +} diff --git a/packages/forms/src/UIForm/utils/validation.js b/packages/forms/src/UIForm/utils/validation.js index 1dabecd2ef7..6959ccbedc0 100644 --- a/packages/forms/src/UIForm/utils/validation.js +++ b/packages/forms/src/UIForm/utils/validation.js @@ -8,7 +8,7 @@ import { getValue } from './properties'; * @param properties The values. * @returns {object} The validation result by field. */ -export default function validateAll(mergedSchema, properties) { +export function validateAll(mergedSchema, properties) { const validations = {}; mergedSchema.forEach((schema) => { const { key, items } = schema; @@ -25,3 +25,29 @@ export default function validateAll(mergedSchema, properties) { }); return validations; } + +/** + * Check if a schema value is invalid. + * It is invalid if : + * - the schema is an invalid field (validations[key] = { valid: false }) + * - the schema has items (ex: fieldset, tabs, ...), and at least one of them is invalid + * @param schema The schema + * @param validations The validations results + * @returns {boolean} true if it is invalid, false otherwise + */ +export function isValid(schema, validations) { + const { key, items } = schema; + if (key && validations[key] && !validations[key].valid) { + return false; + } + + if (items) { + for (const itemSchema of items) { + if (!isValid(itemSchema, validations)) { + return false; + } + } + } + + return true; +} diff --git a/packages/forms/src/UIForm/utils/widgets.js b/packages/forms/src/UIForm/utils/widgets.js index b7e22b5c36f..bfed6e112f8 100644 --- a/packages/forms/src/UIForm/utils/widgets.js +++ b/packages/forms/src/UIForm/utils/widgets.js @@ -1,8 +1,14 @@ import Fieldset from '../fieldsets/Fieldset'; +import Tabs from '../fieldsets/Tabs'; + import Text from '../fields/Text'; const widgets = { + // fieldsets fieldset: Fieldset, + tabs: Tabs, + + // fields number: Text, text: Text, }; diff --git a/packages/forms/stories/json/core-tabs.json b/packages/forms/stories/json/core-tabs.json new file mode 100644 index 00000000000..4aad6fcbb0b --- /dev/null +++ b/packages/forms/stories/json/core-tabs.json @@ -0,0 +1,96 @@ +{ + "jsonSchema": { + "type": "object", + "title": "Comment", + "properties": { + "name": { + "type": "string" + }, + "lastname": { + "type": "string" + }, + "firstname": { + "type": "string" + }, + "age": { + "type": "number" + }, + "nochange": { + "type": "string" + }, + "email": { + "type": "string", + "pattern": "^\\S+@\\S+$" + }, + "comment": { + "type": "string", + "maxLength": 20 + } + }, + "required": [ + "name", + "firstname", + "email", + "comment" + ] + }, + "uiSchema": [ + { + "type": "tabs", + "items": [ + { + "title": "User", + "items": [ + { + "key": "name", + "title": "Name" + }, + { + "key": "lastname", + "title": "Last Name (with description)", + "description": "Hint: this is the last name" + }, + { + "key": "firstname", + "title": "First Name (with placeholder)", + "placeholder": "Enter your firstname here" + }, + { + "key": "age", + "title": "Age" + } + ] + }, + { + "title": "Other", + "items": [ + { + "key": "email", + "title": "Email (with pattern validation and custom validation message)", + "description": "Email will be used for evil.", + "validationMessage": "Please enter a valid email address, e.g. user@email.com" + }, + { + "key": "nochange", + "title": "Field (read only mode)", + "readOnly": true + }, + { + "key": "comment", + "type": "textarea", + "title": "Comment", + "placeholder": "Make a comment", + "validationMessage": "Don't be greedy!" + } + ] + } + ] + } + ], + "properties": { + "name": "Chuck Norris", + "nochange": "You can't change that", + "email": "ChuckyFTW@gmail.com", + "comment": "lol" + } +} From 2512674b0c4262e2e573bd5e949f22f85b3bd7b5 Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Thu, 11 May 2017 17:22:08 +0200 Subject: [PATCH 10/73] Remove unnecessery arrow functions --- packages/forms/src/UIForm/UIForm.component.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/forms/src/UIForm/UIForm.component.js b/packages/forms/src/UIForm/UIForm.component.js index c8db77dee2d..eea7e7ed255 100644 --- a/packages/forms/src/UIForm/UIForm.component.js +++ b/packages/forms/src/UIForm/UIForm.component.js @@ -31,11 +31,11 @@ class UIForm extends React.Component { if (!jsonSchema || !uiSchema || !properties) { return; } - this.setState(() => ({ + this.setState({ mergedSchema: merge(jsonSchema, uiSchema), properties: { ...properties }, validations: {}, - })); + }); } /** @@ -64,7 +64,7 @@ class UIForm extends React.Component { const keys = Object.keys(validations); for (const key of keys) { if (!validations[key].valid) { - this.setState(() => ({ validations })); + this.setState({ validations }); return false; } } From 0dda42129542efba77d435bdf82c1618602c2f68 Mon Sep 17 00:00:00 2001 From: Jimmy Somsanith Date: Fri, 12 May 2017 11:52:06 +0200 Subject: [PATCH 11/73] Trigger after --- packages/forms/BREAKING_CHANGES_LOG.md | 9 +++ packages/forms/src/UIForm/UIForm.component.js | 56 ++++++++++++++++--- packages/forms/stories/index.js | 1 + .../stories/json/core-trigger-after.json | 31 ++++++++++ 4 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 packages/forms/stories/json/core-trigger-after.json diff --git a/packages/forms/BREAKING_CHANGES_LOG.md b/packages/forms/BREAKING_CHANGES_LOG.md index 252a8f48e75..36ff4c27a03 100644 --- a/packages/forms/BREAKING_CHANGES_LOG.md +++ b/packages/forms/BREAKING_CHANGES_LOG.md @@ -3,6 +3,15 @@ Before 1.0, `react-talend-forms` do NOT follow semver version in releases. This document aims to ease the WIP migration from a version to another by providing intels about what to do to migrate. +## Next version + +Schemas format changes +TODO + +On change callback api change +On trigger callback api change +TODO + ## v0.71.0 * PR #364 [feat: onTrigger !== onChange] diff --git a/packages/forms/src/UIForm/UIForm.component.js b/packages/forms/src/UIForm/UIForm.component.js index eea7e7ed255..26609a13278 100644 --- a/packages/forms/src/UIForm/UIForm.component.js +++ b/packages/forms/src/UIForm/UIForm.component.js @@ -6,6 +6,8 @@ import Widget from './Widget'; import { validateAll } from './utils/validation'; import { mutateValue } from './utils/properties'; +const TRIGGER_AFTER = 'after'; + class UIForm extends React.Component { constructor(props) { super(props); @@ -35,24 +37,49 @@ class UIForm extends React.Component { mergedSchema: merge(jsonSchema, uiSchema), properties: { ...properties }, validations: {}, + // TODO consolidate validation + // or each state.validations, revalidate it if key is still in form, remove otherwise }); } /** * Consolidate form with the new value. - * This updates the validation on the modified field. + * - it updates the validation on the modified field. + * - it triggers onChange / onTrigger callbacks * @param event The change event * @param schema The schema of the changed field * @param value The new field value */ consolidate(event, schema, value) { - this.setState(prevState => ({ - properties: mutateValue(prevState.properties, schema.key, value), - validations: { - ...prevState.validations, - [schema.key]: validate(schema, value), - }, - })); + this.setState( + prevState => ({ + properties: mutateValue(prevState.properties, schema.key, value), + validations: { + ...prevState.validations, + [schema.key]: validate(schema, value), + }, + }), + () => { + const { onChange, onTrigger } = this.props; + + if (onChange) { + onChange({ + jsonSchema: this.props.data.jsonSchema, // original jsonSchema + uiSchema: this.props.data.uiSchema, // original uiSchema + properties: this.state.properties, // current properties values + }); + } + + const { key, triggers } = schema; + if (onTrigger && triggers && triggers.indexOf(TRIGGER_AFTER) !== -1) { + onTrigger( + this.state.properties, // current properties values + key[key.length - 1], // field name + value // field value + ); + } + } + ); } /** @@ -108,13 +135,26 @@ class UIForm extends React.Component { if (process.env.NODE_ENV !== 'production') { UIForm.propTypes = { + /** Form schema configuration */ data: PropTypes.shape({ + /** Json schema that specify the data model */ jsonSchema: PropTypes.object, + /** UI schema that specify how to render the fields */ uiSchema: PropTypes.array, + /** Form fields values. Note that it should contains @definitionName for triggers. */ properties: PropTypes.object, }), + /** The form name that will be used to create ids */ formName: PropTypes.string, + /** The change callback. It takes */ + onChange: PropTypes.func, + /** Form submit callback */ onSubmit: PropTypes.func.isRequired, + /** + * Tigger > after callback. + * This is executed on changes on fields with uiSchema > triggers : ['after'] + */ + onTrigger: PropTypes.func, }; } diff --git a/packages/forms/stories/index.js b/packages/forms/stories/index.js index 7e0fa2d09c4..8c72fe88252 100644 --- a/packages/forms/stories/index.js +++ b/packages/forms/stories/index.js @@ -62,6 +62,7 @@ sampleFilenames autocomplete="off" data={object(capitalizedSampleName, sampleFilenames(filename))} onChange={action('Change')} + onTrigger={action('Trigger')} onBlur={action('Blur')} onSubmit={action('Submit')} /> diff --git a/packages/forms/stories/json/core-trigger-after.json b/packages/forms/stories/json/core-trigger-after.json new file mode 100644 index 00000000000..4d79f93dd30 --- /dev/null +++ b/packages/forms/stories/json/core-trigger-after.json @@ -0,0 +1,31 @@ +{ + "jsonSchema": { + "type": "object", + "properties": { + "lastname": { + "type": "string" + }, + "firstname": { + "type": "string" + } + }, + "required": [ + "name", + "firstname", + "email", + "comment" + ] + }, + "uiSchema": [ + { + "key": "lastname", + "title": "Last Name (with trigger)", + "triggers": ["after"] + }, + { + "key": "firstname", + "title": "First Name" + } + ], + "properties": {} +} From 1ded597c090b039215e06e6af3710e520de3210c Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Mon, 15 May 2017 09:34:57 +0200 Subject: [PATCH 12/73] Add role on message --- packages/forms/src/UIForm/Message/Message.component.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/forms/src/UIForm/Message/Message.component.js b/packages/forms/src/UIForm/Message/Message.component.js index 90e500a93a4..7c6bf47c1e7 100644 --- a/packages/forms/src/UIForm/Message/Message.component.js +++ b/packages/forms/src/UIForm/Message/Message.component.js @@ -10,7 +10,10 @@ export default function Message(props) { const message = isValid ? description : errorMessage; return message ? ( -
+
{ message }
) : From 51f54ea144ea574ce7cfcab9f35fee6256218304 Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Mon, 15 May 2017 15:04:37 +0200 Subject: [PATCH 13/73] Custom validation --- packages/forms/src/UIForm/UIForm.component.js | 92 +++++++++++-------- packages/forms/src/UIForm/utils/validation.js | 33 +++++-- packages/forms/stories/index.js | 9 ++ .../stories/json/core-custom-validation.json | 32 +++++++ 4 files changed, 122 insertions(+), 44 deletions(-) create mode 100644 packages/forms/stories/json/core-custom-validation.json diff --git a/packages/forms/src/UIForm/UIForm.component.js b/packages/forms/src/UIForm/UIForm.component.js index 26609a13278..61fbcc2b265 100644 --- a/packages/forms/src/UIForm/UIForm.component.js +++ b/packages/forms/src/UIForm/UIForm.component.js @@ -1,9 +1,9 @@ import React, { PropTypes } from 'react'; import { reduxForm } from 'redux-form'; -import { merge, validate } from 'talend-json-schema-form-core'; +import { merge } from 'talend-json-schema-form-core'; import Widget from './Widget'; -import { validateAll } from './utils/validation'; +import { validate, validateAll } from './utils/validation'; import { mutateValue } from './utils/properties'; const TRIGGER_AFTER = 'after'; @@ -52,50 +52,62 @@ class UIForm extends React.Component { */ consolidate(event, schema, value) { this.setState( - prevState => ({ - properties: mutateValue(prevState.properties, schema.key, value), - validations: { + (prevState) => { + const properties = mutateValue(prevState.properties, schema.key, value); + const validations = { ...prevState.validations, - [schema.key]: validate(schema, value), - }, - }), - () => { - const { onChange, onTrigger } = this.props; - - if (onChange) { - onChange({ - jsonSchema: this.props.data.jsonSchema, // original jsonSchema - uiSchema: this.props.data.uiSchema, // original uiSchema - properties: this.state.properties, // current properties values - }); - } - - const { key, triggers } = schema; - if (onTrigger && triggers && triggers.indexOf(TRIGGER_AFTER) !== -1) { - onTrigger( - this.state.properties, // current properties values - key[key.length - 1], // field name - value // field value - ); - } - } + [schema.key]: validate(schema, value, properties, this.props.validation), + }; + return { properties, validations }; + }, + () => this.handleChangesCallbacks(schema, value) ); } + /** + * Triggers the onTrigger and onChange if needed + * - onChange : at each field change + * - onTrigger : when schema.trigger : ['after'] + * @param schema The field schema + * @param value The new value + */ + handleChangesCallbacks(schema, value) { + const { onChange, onTrigger } = this.props; + + if (onChange) { + onChange({ + jsonSchema: this.props.data.jsonSchema, // original jsonSchema + uiSchema: this.props.data.uiSchema, // original uiSchema + properties: this.state.properties, // current properties values + }); + } + + const { key, triggers } = schema; + if (onTrigger && triggers && triggers.indexOf(TRIGGER_AFTER) !== -1) { + onTrigger( + this.state.properties, // current properties values + key[key.length - 1], // field name + value // field value + ); + } + } + /** * Triggers a validation and update state. * @returns {boolean} true if the form is valid, false otherwise */ isValid() { - const validations = validateAll(this.state.mergedSchema, this.state.properties); - const keys = Object.keys(validations); - for (const key of keys) { - if (!validations[key].valid) { - this.setState({ validations }); - return false; - } + const validations = validateAll( + this.state.mergedSchema, + this.state.properties, + this.props.validation + ); + + const isValid = Object.keys(validations).every(key => validations[key].valid); + if (!isValid) { + this.setState({ validations }); } - return true; + return isValid; } /** @@ -152,9 +164,17 @@ if (process.env.NODE_ENV !== 'production') { onSubmit: PropTypes.func.isRequired, /** * Tigger > after callback. + * Prototype: function onTrigger(properties, fieldName, value) * This is executed on changes on fields with uiSchema > triggers : ['after'] */ onTrigger: PropTypes.func, + /** + * Custom validation function. + * Prototype: function validation(properties, fieldName, value) + * Return format : { valid: true|false, error: { message: 'my validation message' } } + * This is triggered on fields that has their uiSchema > customValidation : true + */ + validation: PropTypes.func, }; } diff --git a/packages/forms/src/UIForm/utils/validation.js b/packages/forms/src/UIForm/utils/validation.js index 6959ccbedc0..443d075463f 100644 --- a/packages/forms/src/UIForm/utils/validation.js +++ b/packages/forms/src/UIForm/utils/validation.js @@ -1,22 +1,39 @@ -import { validate } from 'talend-json-schema-form-core'; +import { validate as staticValidate } from 'talend-json-schema-form-core'; import { getValue } from './properties'; /** * Validate values. - * @param mergedSchema The merged schema. + * @param schema The merged schema. + * @param value The value. * @param properties The values. + * @param customValidationFn A custom validation function + * that is applied on schema.customValidation = true + * @returns {object} The validation result. + */ +export function validate(schema, value, properties, customValidationFn) { + const staticResult = staticValidate(schema, value); + if (staticResult.valid && schema.customValidation && customValidationFn) { + return customValidationFn(properties, schema, value); + } + return staticResult; +} + +/** + * Validate values. + * @param mergedSchema The merged schema array. + * @param properties The values. + * @param customValidationFn A custom validation function + * that is applied on schema.customValidation = true * @returns {object} The validation result by field. */ -export function validateAll(mergedSchema, properties) { +export function validateAll(mergedSchema, properties, customValidationFn) { const validations = {}; mergedSchema.forEach((schema) => { const { key, items } = schema; if (key) { - validations[key] = validate( - schema, - getValue(properties, key) - ); + const value = getValue(properties, key); + validations[key] = validate(schema, value, properties, customValidationFn); } if (items) { const subValidations = validateAll(items, properties); @@ -27,7 +44,7 @@ export function validateAll(mergedSchema, properties) { } /** - * Check if a schema value is invalid. + * Check if a schema value is valid. * It is invalid if : * - the schema is an invalid field (validations[key] = { valid: false }) * - the schema has items (ex: fieldset, tabs, ...), and at least one of them is invalid diff --git a/packages/forms/stories/index.js b/packages/forms/stories/index.js index 8c72fe88252..d1e2a456509 100644 --- a/packages/forms/stories/index.js +++ b/packages/forms/stories/index.js @@ -65,6 +65,15 @@ sampleFilenames onTrigger={action('Trigger')} onBlur={action('Blur')} onSubmit={action('Submit')} + validation={(properties, schema, value) => { + action('customValidation')(properties, schema, value); + return { + valid: value.length < 5, + error: { + message: 'Custom validation : The value should be less than 5 chars', + }, + }; + }} /> )); diff --git a/packages/forms/stories/json/core-custom-validation.json b/packages/forms/stories/json/core-custom-validation.json new file mode 100644 index 00000000000..4559c937793 --- /dev/null +++ b/packages/forms/stories/json/core-custom-validation.json @@ -0,0 +1,32 @@ +{ + "jsonSchema": { + "type": "object", + "title": "Comment", + "properties": { + "lastname": { + "type": "string" + }, + "firstname": { + "type": "string" + } + }, + "required": [ + "lastname", + "firstname" + ] + }, + "uiSchema": [ + { + "key": "lastname", + "title": "Last Name", + "description": "This field has custom validation (less than 5 chars)", + "customValidation": true + }, + { + "key": "firstname", + "title": "First Name", + "description": "This field has no custom validation" + } + ], + "properties": {} +} From 1830168c2729b9bacf8a3a9acc2d0699ba55042b Mon Sep 17 00:00:00 2001 From: travis Date: Mon, 15 May 2017 13:17:36 +0000 Subject: [PATCH 14/73] test(ci): update code style outputs --- output/forms.eslint.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/output/forms.eslint.txt b/output/forms.eslint.txt index 4b47016c273..0dfcdc065c3 100755 --- a/output/forms.eslint.txt +++ b/output/forms.eslint.txt @@ -118,6 +118,10 @@ The react/require-extension rule is deprecated. Please use the import/extensions 21:2 error 'schema' is missing in props validation react/prop-types 42:1 error Line 42 exceeds the maximum line length of 100 max-len +/home/travis/build/Talend/ui/packages/forms/src/UIForm/UIForm.component.js + 20:3 warning Unexpected console statement no-console + 20:39 error Missing semicolon semi + /home/travis/build/Talend/ui/packages/forms/src/widgets/DatalistWidget/DatalistWidget.test.js 30:10 error Missing trailing comma comma-dangle 72:10 error Missing trailing comma comma-dangle @@ -130,7 +134,7 @@ The react/require-extension rule is deprecated. Please use the import/extensions 151:5 error Value must be omitted for boolean attributes react/jsx-boolean-value 172:5 error Value must be omitted for boolean attributes react/jsx-boolean-value -✖ 112 problems (112 errors, 0 warnings) +✖ 114 problems (113 errors, 1 warning) Errored while running command 'npm' with arguments 'run lint:es' in 'react-talend-forms' Errored while running ExecCommand.execute From 7ec903295248b2e2c6ef9ae9babeed2c2fad4197 Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Mon, 15 May 2017 15:18:37 +0200 Subject: [PATCH 15/73] Clean --- packages/forms/src/UIForm/UIForm.component.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/forms/src/UIForm/UIForm.component.js b/packages/forms/src/UIForm/UIForm.component.js index 61fbcc2b265..da91edad702 100644 --- a/packages/forms/src/UIForm/UIForm.component.js +++ b/packages/forms/src/UIForm/UIForm.component.js @@ -36,9 +36,6 @@ class UIForm extends React.Component { this.setState({ mergedSchema: merge(jsonSchema, uiSchema), properties: { ...properties }, - validations: {}, - // TODO consolidate validation - // or each state.validations, revalidate it if key is still in form, remove otherwise }); } From 04635059f76e2b5f9e89137e2b4200abd58a3197 Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Mon, 15 May 2017 18:31:10 +0200 Subject: [PATCH 16/73] Split UIForm component and base state change on reducers --- packages/forms/src/UIForm/UIForm.component.js | 116 +++------------ packages/forms/src/UIForm/UIForm.container.js | 135 ++++++++++++++++++ .../src/UIForm/Widget/Widget.component.js | 12 +- .../forms/src/UIForm/actions/constants.js | 2 + packages/forms/src/UIForm/actions/index.js | 3 + .../forms/src/UIForm/actions/model.actions.js | 11 ++ .../src/UIForm/actions/validation.actions.js | 10 ++ packages/forms/src/UIForm/fieldsets/Tabs.js | 5 +- packages/forms/src/UIForm/index.js | 7 +- packages/forms/src/UIForm/reducers/index.js | 4 + .../src/UIForm/reducers/model.reducer.js | 35 +++++ .../UIForm/reducers/validations.reducer.js | 92 ++++++++++++ packages/forms/src/UIForm/utils/properties.js | 20 --- packages/forms/src/UIForm/utils/validation.js | 55 +------ packages/forms/stories/index.js | 8 +- 15 files changed, 337 insertions(+), 178 deletions(-) create mode 100644 packages/forms/src/UIForm/UIForm.container.js create mode 100644 packages/forms/src/UIForm/actions/constants.js create mode 100644 packages/forms/src/UIForm/actions/index.js create mode 100644 packages/forms/src/UIForm/actions/model.actions.js create mode 100644 packages/forms/src/UIForm/actions/validation.actions.js create mode 100644 packages/forms/src/UIForm/reducers/index.js create mode 100644 packages/forms/src/UIForm/reducers/model.reducer.js create mode 100644 packages/forms/src/UIForm/reducers/validations.reducer.js diff --git a/packages/forms/src/UIForm/UIForm.component.js b/packages/forms/src/UIForm/UIForm.component.js index da91edad702..fff5defa5d2 100644 --- a/packages/forms/src/UIForm/UIForm.component.js +++ b/packages/forms/src/UIForm/UIForm.component.js @@ -1,25 +1,20 @@ import React, { PropTypes } from 'react'; -import { reduxForm } from 'redux-form'; import { merge } from 'talend-json-schema-form-core'; import Widget from './Widget'; -import { validate, validateAll } from './utils/validation'; -import { mutateValue } from './utils/properties'; const TRIGGER_AFTER = 'after'; -class UIForm extends React.Component { +export default class UIForm extends React.Component { constructor(props) { super(props); - const { jsonSchema, uiSchema, properties } = props.data; + const { jsonSchema, uiSchema } = props; this.state = { mergedSchema: merge(jsonSchema, uiSchema), - properties: { ...properties }, - validations: {}, }; console.log(this.state.mergedSchema) - this.consolidate = this.consolidate.bind(this); + this.onChange = this.onChange.bind(this); this.submit = this.submit.bind(this); } @@ -27,40 +22,16 @@ class UIForm extends React.Component { * Update the state with the new schema. * @param jsonSchema * @param uiSchema - * @param properties */ - componentWillReceiveProps({ jsonSchema, uiSchema, properties }) { - if (!jsonSchema || !uiSchema || !properties) { + componentWillReceiveProps({ jsonSchema, uiSchema }) { + if (!jsonSchema || !uiSchema) { return; } this.setState({ mergedSchema: merge(jsonSchema, uiSchema), - properties: { ...properties }, }); } - /** - * Consolidate form with the new value. - * - it updates the validation on the modified field. - * - it triggers onChange / onTrigger callbacks - * @param event The change event - * @param schema The schema of the changed field - * @param value The new field value - */ - consolidate(event, schema, value) { - this.setState( - (prevState) => { - const properties = mutateValue(prevState.properties, schema.key, value); - const validations = { - ...prevState.validations, - [schema.key]: validate(schema, value, properties, this.props.validation), - }; - return { properties, validations }; - }, - () => this.handleChangesCallbacks(schema, value) - ); - } - /** * Triggers the onTrigger and onChange if needed * - onChange : at each field change @@ -68,60 +39,31 @@ class UIForm extends React.Component { * @param schema The field schema * @param value The new value */ - handleChangesCallbacks(schema, value) { - const { onChange, onTrigger } = this.props; - - if (onChange) { - onChange({ - jsonSchema: this.props.data.jsonSchema, // original jsonSchema - uiSchema: this.props.data.uiSchema, // original uiSchema - properties: this.state.properties, // current properties values - }); - } + onChange(event, schema, value) { + const { onChange, onTrigger, properties } = this.props; + onChange(schema, value, properties); const { key, triggers } = schema; if (onTrigger && triggers && triggers.indexOf(TRIGGER_AFTER) !== -1) { onTrigger( - this.state.properties, // current properties values + this.props.properties, // current properties values key[key.length - 1], // field name value // field value ); } } - /** - * Triggers a validation and update state. - * @returns {boolean} true if the form is valid, false otherwise - */ - isValid() { - const validations = validateAll( - this.state.mergedSchema, - this.state.properties, - this.props.validation - ); - - const isValid = Object.keys(validations).every(key => validations[key].valid); - if (!isValid) { - this.setState({ validations }); - } - return isValid; - } - /** * Triggers submit callback if form is valid * @param event the submit event */ submit(event) { event.preventDefault(); - if (this.isValid()) { - this.props.onSubmit(event, this.state.properties); - } + this.props.onSubmit(event, this.state.mergedSchema, this.props.properties); } render() { - const { formName } = this.props; - const { properties, validations } = this.state; - + const { errors, formName, properties } = this.props; return (
{ @@ -129,10 +71,10 @@ class UIForm extends React.Component { )) } @@ -144,19 +86,14 @@ class UIForm extends React.Component { if (process.env.NODE_ENV !== 'production') { UIForm.propTypes = { - /** Form schema configuration */ - data: PropTypes.shape({ - /** Json schema that specify the data model */ - jsonSchema: PropTypes.object, - /** UI schema that specify how to render the fields */ - uiSchema: PropTypes.array, - /** Form fields values. Note that it should contains @definitionName for triggers. */ - properties: PropTypes.object, - }), + /** The forms errors { [fieldKey]: errorMessage } */ + errors: PropTypes.object, // eslint-disable-line react/forbid-prop-types /** The form name that will be used to create ids */ formName: PropTypes.string, - /** The change callback. It takes */ - onChange: PropTypes.func, + /** Json schema that specify the data model */ + jsonSchema: PropTypes.object, // eslint-disable-line react/forbid-prop-types + /** The change callback */ + onChange: PropTypes.func.isRequired, /** Form submit callback */ onSubmit: PropTypes.func.isRequired, /** @@ -165,16 +102,9 @@ if (process.env.NODE_ENV !== 'production') { * This is executed on changes on fields with uiSchema > triggers : ['after'] */ onTrigger: PropTypes.func, - /** - * Custom validation function. - * Prototype: function validation(properties, fieldName, value) - * Return format : { valid: true|false, error: { message: 'my validation message' } } - * This is triggered on fields that has their uiSchema > customValidation : true - */ - validation: PropTypes.func, + /** Form fields values. Note that it should contains @definitionName for triggers. */ + properties: PropTypes.object, // eslint-disable-line react/forbid-prop-types + /** UI schema that specify how to render the fields */ + uiSchema: PropTypes.array, // eslint-disable-line react/forbid-prop-types }; } - -export default reduxForm({ - form: 'form', // a unique name for this form -})(UIForm); diff --git a/packages/forms/src/UIForm/UIForm.container.js b/packages/forms/src/UIForm/UIForm.container.js new file mode 100644 index 00000000000..c715f4338bb --- /dev/null +++ b/packages/forms/src/UIForm/UIForm.container.js @@ -0,0 +1,135 @@ +import React, { PropTypes } from 'react'; +import UIFormComponent from './UIForm.component'; + +import { modelReducer, validationReducer } from './reducers'; +import { mutateValue, validateAll } from './actions'; + +export default class UIForm extends React.Component { + constructor(props) { + super(props); + this.state = { + properties: { ...props.data.properties }, + errors: {}, + }; + + this.onChange = this.onChange.bind(this); + this.onSubmit = this.onSubmit.bind(this); + } + + /** + * Update the properties. + */ + componentWillReceiveProps({ properties }) { + if (!properties) { + return; + } + + this.setState({ + properties: { ...properties }, + }); + } + + /** + * Update the model and validation + * If onChange is provided, it is triggered + * @param schema The schema + * @param value The new value + * @param properties The values + */ + onChange(schema, value, properties) { + const action = mutateValue(schema, value, properties, this.props.validation); + this.setState( + { + properties: modelReducer(this.state.properties, action), + errors: validationReducer(this.state.errors, action), + }, + () => { + if (this.props.onChange) { + this.props.onChange({ + jsonSchema: this.props.data.jsonSchema, + uiSchema: this.props.data.uiSchema, + properties: this.state.properties, + }); + } + } + ); + } + + /** + * Triggers submit callback if form is valid + * @param event the submit event + * @param schema the schema + * @param properties the properties values + */ + onSubmit(event, schema, properties) { + event.preventDefault(); + if (this.isValid(schema, properties)) { + this.props.onSubmit(event, properties); + } + } + + /** + * Triggers a validation and update state. + * @returns {boolean} true if the form is valid, false otherwise + */ + isValid(schema, properties) { + const action = validateAll(schema, properties, this.props.validation); + const errors = validationReducer(this.state.errors, action); + const isValid = !Object.keys(errors).length; + + if (!isValid) { + this.setState({ errors }); + } + return isValid; + } + + render() { + const { data, ...restProps } = this.props; + const { properties, errors } = this.state; + + return ( + + ); + } +} + +if (process.env.NODE_ENV !== 'production') { + UIForm.propTypes = { + /** Form schema configuration */ + data: PropTypes.shape({ + /** Json schema that specify the data model */ + jsonSchema: PropTypes.object, + /** UI schema that specify how to render the fields */ + uiSchema: PropTypes.array, + /** Form fields values. Note that it should contains @definitionName for triggers. */ + properties: PropTypes.object, + }), + /** The form name that will be used to create ids */ + formName: PropTypes.string, + /** The change callback. It takes */ + onChange: PropTypes.func, + /** Form submit callback */ + onSubmit: PropTypes.func.isRequired, + /** + * Tigger > after callback. + * Prototype: function onTrigger(properties, fieldName, value) + * This is executed on changes on fields with uiSchema > triggers : ['after'] + */ + onTrigger: PropTypes.func, + /** + * Custom validation function. + * Prototype: function validation(properties, fieldName, value) + * Return format : { valid: true|false, error: { message: 'my validation message' } } + * This is triggered on fields that has their uiSchema > customValidation : true + */ + validation: PropTypes.func, + }; +} diff --git a/packages/forms/src/UIForm/Widget/Widget.component.js b/packages/forms/src/UIForm/Widget/Widget.component.js index 15cdda6409c..f6751b21629 100644 --- a/packages/forms/src/UIForm/Widget/Widget.component.js +++ b/packages/forms/src/UIForm/Widget/Widget.component.js @@ -4,11 +4,11 @@ import { sfPath } from 'talend-json-schema-form-core'; import widgets from '../utils/widgets'; import { getValue } from '../utils/properties'; -export default function Widget({ formName, onChange, properties, schema, validations }) { +export default function Widget({ errors, formName, onChange, properties, schema }) { const { key, type, validationMessage } = schema; const id = sfPath.name(key, '-', formName); - const { error, valid } = validations[key] || {}; - const errorMessage = validationMessage || (error && error.message); + const error = errors[key]; + const errorMessage = validationMessage || error; const WidgetImpl = widgets[type]; return WidgetImpl ? ( @@ -17,11 +17,11 @@ export default function Widget({ formName, onChange, properties, schema, validat key={id} errorMessage={errorMessage} formName={formName} - isValid={valid} + isValid={!error} onChange={onChange} properties={properties} schema={schema} - validations={validations} + errors={errors} value={getValue(properties, key)} /> ) : null; @@ -29,6 +29,7 @@ export default function Widget({ formName, onChange, properties, schema, validat if (process.env.NODE_ENV !== 'production') { Widget.propTypes = { + errors: PropTypes.object, // eslint-disable-line react/forbid-prop-types formName: PropTypes.string, onChange: PropTypes.func, schema: PropTypes.shape({ @@ -37,6 +38,5 @@ if (process.env.NODE_ENV !== 'production') { validationMessage: PropTypes.string, }).isRequired, properties: PropTypes.object, // eslint-disable-line react/forbid-prop-types - validations: PropTypes.object, // eslint-disable-line react/forbid-prop-types }; } diff --git a/packages/forms/src/UIForm/actions/constants.js b/packages/forms/src/UIForm/actions/constants.js new file mode 100644 index 00000000000..fbcfb784cb5 --- /dev/null +++ b/packages/forms/src/UIForm/actions/constants.js @@ -0,0 +1,2 @@ +export const MUTATE_VALUE = 'MUTATE_VALUE'; +export const VALIDATE_ALL = 'VALIDATE_ALL'; diff --git a/packages/forms/src/UIForm/actions/index.js b/packages/forms/src/UIForm/actions/index.js new file mode 100644 index 00000000000..b1760ba1ed8 --- /dev/null +++ b/packages/forms/src/UIForm/actions/index.js @@ -0,0 +1,3 @@ +export { mutateValue } from './model.actions'; +export { validateAll } from './validation.actions'; +export * from './constants'; diff --git a/packages/forms/src/UIForm/actions/model.actions.js b/packages/forms/src/UIForm/actions/model.actions.js new file mode 100644 index 00000000000..c1d895687eb --- /dev/null +++ b/packages/forms/src/UIForm/actions/model.actions.js @@ -0,0 +1,11 @@ +import { MUTATE_VALUE } from './constants'; + +export function mutateValue(schema, value, properties, customValidationFn) { + return { + type: MUTATE_VALUE, + customValidationFn, + properties, + schema, + value, + }; +} diff --git a/packages/forms/src/UIForm/actions/validation.actions.js b/packages/forms/src/UIForm/actions/validation.actions.js new file mode 100644 index 00000000000..03f2f226b15 --- /dev/null +++ b/packages/forms/src/UIForm/actions/validation.actions.js @@ -0,0 +1,10 @@ +import { VALIDATE_ALL } from './constants'; + +export function validateAll(schema, properties, customValidationFn) { + return { + type: VALIDATE_ALL, + schema, + properties, + customValidationFn, + }; +} diff --git a/packages/forms/src/UIForm/fieldsets/Tabs.js b/packages/forms/src/UIForm/fieldsets/Tabs.js index ed5aeba565c..6a35bc5e1df 100644 --- a/packages/forms/src/UIForm/fieldsets/Tabs.js +++ b/packages/forms/src/UIForm/fieldsets/Tabs.js @@ -2,7 +2,7 @@ import React, { PropTypes } from 'react'; import { Tabs as RBTabs, Tab as RBTab } from 'react-bootstrap'; import Fieldset from './Fieldset'; -import { isValid } from '../utils/validation'; +import isValid from '../utils/validation'; import theme from './Tabs.scss'; export default function Tabs(props) { @@ -12,7 +12,7 @@ export default function Tabs(props) { return ( {tabs.map((tabSchema, index) => { - const tabClassName = isValid(tabSchema, restProps.validations) ? + const tabClassName = isValid(tabSchema, restProps.errors) ? null : theme['has-error']; return ( @@ -32,6 +32,7 @@ export default function Tabs(props) { if (process.env.NODE_ENV !== 'production') { Tabs.propTypes = { + errors: PropTypes.object, // eslint-disable-line react/forbid-prop-types schema: PropTypes.shape({ items: PropTypes.arrayOf( React.PropTypes.shape({ diff --git a/packages/forms/src/UIForm/index.js b/packages/forms/src/UIForm/index.js index 370245b7ae7..38ee3ef5fbd 100644 --- a/packages/forms/src/UIForm/index.js +++ b/packages/forms/src/UIForm/index.js @@ -1,3 +1,8 @@ -import UIForm from './UIForm.component'; +import UIForm from './UIForm.container'; +import { modelReducer, validationReducer } from './reducers'; +export const formReducers = { + model: modelReducer, + errors: validationReducer, +}; export default UIForm; diff --git a/packages/forms/src/UIForm/reducers/index.js b/packages/forms/src/UIForm/reducers/index.js new file mode 100644 index 00000000000..e71c9a3a814 --- /dev/null +++ b/packages/forms/src/UIForm/reducers/index.js @@ -0,0 +1,4 @@ +import modelReducer from './model.reducer'; +import validationReducer from './validations.reducer'; + +export { modelReducer, validationReducer }; diff --git a/packages/forms/src/UIForm/reducers/model.reducer.js b/packages/forms/src/UIForm/reducers/model.reducer.js new file mode 100644 index 00000000000..3f6a5577d21 --- /dev/null +++ b/packages/forms/src/UIForm/reducers/model.reducer.js @@ -0,0 +1,35 @@ +import { MUTATE_VALUE } from '../actions'; + +/** + * Mutate the properties, setting the value in the path identified by key + * @param {object} properties The original properties store + * @param {array} key The key chain (array of strings) to identify the path + * @param {any} value The value to set + * @returns {object} The new mutated properties store. + */ +function mutateValue(properties, key, value) { + if (!key.length) { + return value; + } + + const nextKey = key[0]; + const restKeys = key.slice(1); + return { + ...properties, + [nextKey]: mutateValue(properties[nextKey], restKeys, value), + }; +} + +/** + * Form model change reducer + * @param state The model + * @param action The action to perform + */ +export default function modelConsolidation(state = {}, action) { + switch (action.type) { + case MUTATE_VALUE: + return mutateValue(state, action.schema.key, action.value); + default: + return state; + } +} diff --git a/packages/forms/src/UIForm/reducers/validations.reducer.js b/packages/forms/src/UIForm/reducers/validations.reducer.js new file mode 100644 index 00000000000..d780cfab039 --- /dev/null +++ b/packages/forms/src/UIForm/reducers/validations.reducer.js @@ -0,0 +1,92 @@ +import { validate } from 'talend-json-schema-form-core'; +import { MUTATE_VALUE, VALIDATE_ALL } from '../actions'; +import { getValue } from '../utils/properties'; + +/** + * Validate values. + * @param schema The merged schema. + * @param value The value. + * @param properties The values. + * @param customValidationFn A custom validation function + * that is applied on schema.customValidation = true + * @returns {object} The validation result. + */ +function validateValue(schema, value, properties, customValidationFn) { + const staticResult = validate(schema, value); + if (staticResult.valid && schema.customValidation && customValidationFn) { + return customValidationFn(properties, schema, value); + } + return staticResult.valid ? null : staticResult.error.message; +} + +/** + * Validate values. + * @param mergedSchema The merged schema array. + * @param properties The values. + * @param customValidationFn A custom validation function + * that is applied on schema.customValidation = true + * @returns {object} The validation result by field. + */ +function validateAll(mergedSchema, properties, customValidationFn) { + const results = {}; + mergedSchema.forEach((schema) => { + const { key, items } = schema; + if (key) { + const value = getValue(properties, key); + const error = validateValue(schema, value, properties, customValidationFn); + if (error) { + results[key] = error; + } + } + if (items) { + const subResults = validateAll(items, properties); + Object.assign(results, subResults); + } + }); + return results; +} + +/** + * Omit a property from an object + * @param errors The object + * @param key The key to omit + */ +function omit(errors, key) { + if (!key) { + return errors; + } + const result = {}; + Object.keys(errors) + .filter(nextKey => nextKey !== key) + .forEach((nextKey) => { + result[nextKey] = errors[nextKey]; + }); + return result; +} + +/** + * Form validations reducer + * @param state The errors { propertyKey: errorMessage } + * @param action The action to perform + */ +export default function validations(state = {}, action) { + switch (action.type) { + case MUTATE_VALUE: { + const { schema, value, properties, customValidationFn } = action; + const error = validateValue(schema, value, properties, customValidationFn); + if (error) { + return { + ...state, + [action.schema.key]: error, + }; + } + return omit(state, action.schema.key.toString()); + } + case VALIDATE_ALL: { + const { schema, properties, customValidationFn } = action; + return validateAll(schema, properties, customValidationFn); + } + default: + return state; + } +} diff --git a/packages/forms/src/UIForm/utils/properties.js b/packages/forms/src/UIForm/utils/properties.js index 0dfd46622f0..64ebc758345 100644 --- a/packages/forms/src/UIForm/utils/properties.js +++ b/packages/forms/src/UIForm/utils/properties.js @@ -13,23 +13,3 @@ export function getValue(properties, key) { properties ); } - -/** - * Mutate the properties, setting the value in the path identified by key - * @param {object} properties The original properties store - * @param {array} key The key chain (array of strings) to identify the path - * @param {any} value The value to set - * @returns {object} The new mutated properties store. - */ -export function mutateValue(properties, key, value) { - if (!key.length) { - return value; - } - - const nextKey = key[0]; - const restKeys = key.slice(1); - return { - ...properties, - [nextKey]: mutateValue(properties[nextKey], restKeys, value), - }; -} diff --git a/packages/forms/src/UIForm/utils/validation.js b/packages/forms/src/UIForm/utils/validation.js index 443d075463f..0dfb6f97c04 100644 --- a/packages/forms/src/UIForm/utils/validation.js +++ b/packages/forms/src/UIForm/utils/validation.js @@ -1,66 +1,21 @@ -import { validate as staticValidate } from 'talend-json-schema-form-core'; - -import { getValue } from './properties'; - -/** - * Validate values. - * @param schema The merged schema. - * @param value The value. - * @param properties The values. - * @param customValidationFn A custom validation function - * that is applied on schema.customValidation = true - * @returns {object} The validation result. - */ -export function validate(schema, value, properties, customValidationFn) { - const staticResult = staticValidate(schema, value); - if (staticResult.valid && schema.customValidation && customValidationFn) { - return customValidationFn(properties, schema, value); - } - return staticResult; -} - -/** - * Validate values. - * @param mergedSchema The merged schema array. - * @param properties The values. - * @param customValidationFn A custom validation function - * that is applied on schema.customValidation = true - * @returns {object} The validation result by field. - */ -export function validateAll(mergedSchema, properties, customValidationFn) { - const validations = {}; - mergedSchema.forEach((schema) => { - const { key, items } = schema; - if (key) { - const value = getValue(properties, key); - validations[key] = validate(schema, value, properties, customValidationFn); - } - if (items) { - const subValidations = validateAll(items, properties); - Object.assign(validations, subValidations); - } - }); - return validations; -} - /** * Check if a schema value is valid. * It is invalid if : - * - the schema is an invalid field (validations[key] = { valid: false }) + * - the schema is an invalid field (errors[key] is falsy) * - the schema has items (ex: fieldset, tabs, ...), and at least one of them is invalid * @param schema The schema - * @param validations The validations results + * @param errors The errors * @returns {boolean} true if it is invalid, false otherwise */ -export function isValid(schema, validations) { +export default function isValid(schema, errors) { const { key, items } = schema; - if (key && validations[key] && !validations[key].valid) { + if (key && errors[key]) { return false; } if (items) { for (const itemSchema of items) { - if (!isValid(itemSchema, validations)) { + if (!isValid(itemSchema, errors)) { return false; } } diff --git a/packages/forms/stories/index.js b/packages/forms/stories/index.js index d1e2a456509..e9abc810f6c 100644 --- a/packages/forms/stories/index.js +++ b/packages/forms/stories/index.js @@ -67,12 +67,8 @@ sampleFilenames onSubmit={action('Submit')} validation={(properties, schema, value) => { action('customValidation')(properties, schema, value); - return { - valid: value.length < 5, - error: { - message: 'Custom validation : The value should be less than 5 chars', - }, - }; + return value.length >= 5 && + 'Custom validation : The value should be less than 5 chars'; }} /> From 2766c30eff71d4353276e8a19dc94e1cbb68d01c Mon Sep 17 00:00:00 2001 From: travis Date: Tue, 16 May 2017 12:48:54 +0000 Subject: [PATCH 17/73] test(ci): update code style outputs --- output/forms.eslint.txt | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/output/forms.eslint.txt b/output/forms.eslint.txt index 0dfcdc065c3..7b56832cd87 100755 --- a/output/forms.eslint.txt +++ b/output/forms.eslint.txt @@ -118,9 +118,18 @@ The react/require-extension rule is deprecated. Please use the import/extensions 21:2 error 'schema' is missing in props validation react/prop-types 42:1 error Line 42 exceeds the maximum line length of 100 max-len +/home/travis/build/Talend/ui/packages/forms/src/UIForm/actions/model.actions.js + 3:1 error Prefer default export import/prefer-default-export + +/home/travis/build/Talend/ui/packages/forms/src/UIForm/actions/validation.actions.js + 3:1 error Prefer default export import/prefer-default-export + /home/travis/build/Talend/ui/packages/forms/src/UIForm/UIForm.component.js - 20:3 warning Unexpected console statement no-console - 20:39 error Missing semicolon semi + 15:3 warning Unexpected console statement no-console + 15:39 error Missing semicolon semi + +/home/travis/build/Talend/ui/packages/forms/src/UIForm/utils/properties.js + 6:1 error Prefer default export import/prefer-default-export /home/travis/build/Talend/ui/packages/forms/src/widgets/DatalistWidget/DatalistWidget.test.js 30:10 error Missing trailing comma comma-dangle @@ -134,7 +143,7 @@ The react/require-extension rule is deprecated. Please use the import/extensions 151:5 error Value must be omitted for boolean attributes react/jsx-boolean-value 172:5 error Value must be omitted for boolean attributes react/jsx-boolean-value -✖ 114 problems (113 errors, 1 warning) +✖ 117 problems (116 errors, 1 warning) Errored while running command 'npm' with arguments 'run lint:es' in 'react-talend-forms' Errored while running ExecCommand.execute From bc9b56ad84e6342dc510803f33a852f576a553f0 Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Tue, 16 May 2017 17:00:28 +0200 Subject: [PATCH 18/73] Redux connection implem --- packages/forms/package.json | 10 +- packages/forms/src/UIForm/UIForm.component.js | 18 +- packages/forms/src/UIForm/UIForm.connect.js | 188 ++++++++++++++++++ packages/forms/src/UIForm/UIForm.container.js | 34 ++-- .../forms/src/UIForm/actions/constants.js | 2 + .../forms/src/UIForm/actions/form.actions.js | 16 ++ packages/forms/src/UIForm/actions/index.js | 1 + .../forms/src/UIForm/actions/model.actions.js | 6 +- .../src/UIForm/actions/validation.actions.js | 7 +- packages/forms/src/UIForm/index.js | 11 +- .../forms/src/UIForm/reducers/form.reducer.js | 50 +++++ packages/forms/src/UIForm/reducers/index.js | 3 +- .../src/UIForm/reducers/model.reducer.js | 2 +- .../UIForm/reducers/validations.reducer.js | 75 +------ packages/forms/src/UIForm/utils/properties.js | 20 +- packages/forms/src/UIForm/utils/validation.js | 47 +++++ packages/forms/stories/index.js | 32 ++- packages/forms/yarn.lock | 31 +-- 18 files changed, 404 insertions(+), 149 deletions(-) create mode 100644 packages/forms/src/UIForm/UIForm.connect.js create mode 100644 packages/forms/src/UIForm/actions/form.actions.js create mode 100644 packages/forms/src/UIForm/reducers/form.reducer.js diff --git a/packages/forms/package.json b/packages/forms/package.json index e6068298acb..e6ce090bbed 100644 --- a/packages/forms/package.json +++ b/packages/forms/package.json @@ -45,9 +45,11 @@ "react-addons-test-utils": "^15.4.0", "react-bootstrap": "^0.30.3", "react-dom": "^15.4.0", + "react-redux": "^5.0.4", "react-talend-components": "^0.73.0", "react-test-renderer": "15.4.0", "react-virtualized": "^9.1.0", + "redux": "^3.6.0", "rimraf": "^2.5.4", "sass-lint": "^1.10.2", "sass-loader": "^4.1.1", @@ -57,18 +59,18 @@ }, "dependencies": { "classnames": "^2.2.5", - "talend-json-schema-form-core": "1.0.2-alpha.2", "keycode": "^2.1.8", "react-autowhatever": "^7.0.0", "react-jsonschema-form": "^0.42.0", - "react-redux": "^5.0.4", - "redux-form": "^6.6.3" + "talend-json-schema-form-core": "1.0.2-alpha.2" }, "peerDependencies": { "react": "^15.4.0", "react-bootstrap": "^0.30.3", "react-dom": "^15.4.0", - "react-talend-components": "^0.73.0" + "react-redux": "^5.0.4", + "react-talend-components": "^0.73.0", + "redux": "^3.6.0" }, "scripts": { "prepublish": "rimraf lib && babel -d lib ./src/ && cpx \"./src/**/*.scss\" lib", diff --git a/packages/forms/src/UIForm/UIForm.component.js b/packages/forms/src/UIForm/UIForm.component.js index fff5defa5d2..a54b3516e49 100644 --- a/packages/forms/src/UIForm/UIForm.component.js +++ b/packages/forms/src/UIForm/UIForm.component.js @@ -1,6 +1,7 @@ import React, { PropTypes } from 'react'; import { merge } from 'talend-json-schema-form-core'; +import { validateValue, validateAll } from './utils/validation'; import Widget from './Widget'; const TRIGGER_AFTER = 'after'; @@ -40,8 +41,9 @@ export default class UIForm extends React.Component { * @param value The new value */ onChange(event, schema, value) { - const { onChange, onTrigger, properties } = this.props; - onChange(schema, value, properties); + const { onChange, onTrigger, properties, validation } = this.props; + const error = validateValue(schema, value, properties, validation); + onChange(schema, value, error); const { key, triggers } = schema; if (onTrigger && triggers && triggers.indexOf(TRIGGER_AFTER) !== -1) { @@ -59,7 +61,10 @@ export default class UIForm extends React.Component { */ submit(event) { event.preventDefault(); - this.props.onSubmit(event, this.state.mergedSchema, this.props.properties); + const { mergedSchema } = this.state; + const { properties, validation } = this.props; + const errors = validateAll(mergedSchema, properties, validation); + this.props.onSubmit(event, properties, errors); } render() { @@ -106,5 +111,12 @@ if (process.env.NODE_ENV !== 'production') { properties: PropTypes.object, // eslint-disable-line react/forbid-prop-types /** UI schema that specify how to render the fields */ uiSchema: PropTypes.array, // eslint-disable-line react/forbid-prop-types + /** + * Custom validation function. + * Prototype: function validation(properties, fieldName, value) + * Return format : { valid: true|false, error: { message: 'my validation message' } } + * This is triggered on fields that has their uiSchema > customValidation : true + */ + validation: PropTypes.func, }; } diff --git a/packages/forms/src/UIForm/UIForm.connect.js b/packages/forms/src/UIForm/UIForm.connect.js new file mode 100644 index 00000000000..9a895cda629 --- /dev/null +++ b/packages/forms/src/UIForm/UIForm.connect.js @@ -0,0 +1,188 @@ +import React, { PropTypes } from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import UIFormComponent from './UIForm.component'; + +import { + createForm, + removeForm, + mutateValue, + validateAll, +} from './actions'; + +class UIForm extends React.Component { + constructor(props) { + super(props); + this.state = { + properties: { ...props.data.properties }, + errors: {}, + }; + + this.onChange = this.onChange.bind(this); + this.onSubmit = this.onSubmit.bind(this); + } + + /** + * Create form on mount + */ + componentWillMount() { + this.props.createForm( + this.props.formName, + this.props.data.properties, + ); + } + + /** + * Remove form on unmount + */ + componentWillUnmount() { + this.props.removeForm(this.props.formName); + } + + /** + * Update the model and validation + * If onChange is provided, it is triggered + * @param schema The schema + * @param value The new value + * @param error The validation error + */ + onChange(schema, value, error) { + this.props.mutateValue( + this.props.formName, + schema, + value, + error + ); + if (this.props.onChange) { + this.props.onChange({ + jsonSchema: this.props.data.jsonSchema, + uiSchema: this.props.data.uiSchema, + properties: this.props.form.properties, // TODO fix that, old props + }); + } + } + + /** + * Triggers submit callback if form is valid + * @param event the submit event + * @param properties the properties values + * @param errors the validations errors + */ + onSubmit(event, properties, errors) { + event.preventDefault(); + const isValid = !Object.keys(errors).length; + if (isValid) { + this.props.onSubmit(event, properties); + } else { + this.props.validateAll( + this.props.formName, + errors + ); + } + } + + render() { + const { data, form, ...restProps } = this.props; + + return ( + + ); + } +} + +if (process.env.NODE_ENV !== 'production') { + UIForm.propTypes = { + /** Form schema configuration */ + data: PropTypes.shape({ + /** Json schema that specify the data model */ + jsonSchema: PropTypes.object, + /** UI schema that specify how to render the fields */ + uiSchema: PropTypes.array, + /** + * Form fields initial values. + * Note that it should contains @definitionName for triggers. + */ + properties: PropTypes.object, + }), + /** The form name that will be used to create ids */ + formName: PropTypes.string.isRequired, + /** The change callback. */ + onChange: PropTypes.func, + /** Form submit callback */ + onSubmit: PropTypes.func.isRequired, + /** + * Tigger > after callback. + * Prototype: function onTrigger(properties, fieldName, value) + * This is executed on changes on fields with uiSchema > triggers : ['after'] + */ + onTrigger: PropTypes.func, + /** + * Custom validation function. + * Prototype: function validation(properties, fieldName, value) + * Return format : string + * This is triggered on fields that has their uiSchema > customValidation : true + */ + validation: PropTypes.func, + + /** + * Form data from store. + * This is injected by react-redux. See mapStateToProps + */ + form: PropTypes.shape({ + /** Form properties values */ + properties: PropTypes.object, + /** Form validations errors */ + errors: PropTypes.object, + }), + /** + * Form creation action. + * This is injected by react-redux. See mapDispatchToProps + */ + createForm: PropTypes.func, + /** + * Form removal action. + * This is injected by react-redux. See mapDispatchToProps + */ + removeForm: PropTypes.func, + /** + * Value mutation action. + * This is injected by react-redux. See mapDispatchToProps + */ + mutateValue: PropTypes.func, + /** + * Form validation action. + * This is injected by react-redux. See mapDispatchToProps + */ + validateAll: PropTypes.func, + }; +} + +UIForm.defaultProps = { + form: { + properties: {}, + errors: {}, + }, +}; + +function mapStateToProps(state, ownProps) { + return { form: state.forms[ownProps.formName] }; +} + +function mapDispatchToProps(dispatch) { + return { + createForm: bindActionCreators(createForm, dispatch), + removeForm: bindActionCreators(removeForm, dispatch), + mutateValue: bindActionCreators(mutateValue, dispatch), + validateAll: bindActionCreators(validateAll, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(UIForm); diff --git a/packages/forms/src/UIForm/UIForm.container.js b/packages/forms/src/UIForm/UIForm.container.js index c715f4338bb..8465c0d5178 100644 --- a/packages/forms/src/UIForm/UIForm.container.js +++ b/packages/forms/src/UIForm/UIForm.container.js @@ -34,10 +34,10 @@ export default class UIForm extends React.Component { * If onChange is provided, it is triggered * @param schema The schema * @param value The new value - * @param properties The values + * @param error The validation error */ - onChange(schema, value, properties) { - const action = mutateValue(schema, value, properties, this.props.validation); + onChange(schema, value, error) { + const action = mutateValue(this.props.formName, schema, value, error); this.setState( { properties: modelReducer(this.state.properties, action), @@ -58,29 +58,16 @@ export default class UIForm extends React.Component { /** * Triggers submit callback if form is valid * @param event the submit event - * @param schema the schema * @param properties the properties values + * @param errors the validation errors */ - onSubmit(event, schema, properties) { - event.preventDefault(); - if (this.isValid(schema, properties)) { - this.props.onSubmit(event, properties); - } - } - - /** - * Triggers a validation and update state. - * @returns {boolean} true if the form is valid, false otherwise - */ - isValid(schema, properties) { - const action = validateAll(schema, properties, this.props.validation); - const errors = validationReducer(this.state.errors, action); + onSubmit(event, properties, errors) { const isValid = !Object.keys(errors).length; - - if (!isValid) { + if (isValid) { + this.props.onSubmit(event, properties); + } else { this.setState({ errors }); } - return isValid; } render() { @@ -109,7 +96,10 @@ if (process.env.NODE_ENV !== 'production') { jsonSchema: PropTypes.object, /** UI schema that specify how to render the fields */ uiSchema: PropTypes.array, - /** Form fields values. Note that it should contains @definitionName for triggers. */ + /** + * Form fields initial values. + * Note that it should contains @definitionName for triggers. + */ properties: PropTypes.object, }), /** The form name that will be used to create ids */ diff --git a/packages/forms/src/UIForm/actions/constants.js b/packages/forms/src/UIForm/actions/constants.js index fbcfb784cb5..702870a99be 100644 --- a/packages/forms/src/UIForm/actions/constants.js +++ b/packages/forms/src/UIForm/actions/constants.js @@ -1,2 +1,4 @@ export const MUTATE_VALUE = 'MUTATE_VALUE'; export const VALIDATE_ALL = 'VALIDATE_ALL'; +export const CREATE_FORM = 'CREATE_FORM'; +export const REMOVE_FORM = 'REMOVE_FORM'; diff --git a/packages/forms/src/UIForm/actions/form.actions.js b/packages/forms/src/UIForm/actions/form.actions.js new file mode 100644 index 00000000000..14a0f3286da --- /dev/null +++ b/packages/forms/src/UIForm/actions/form.actions.js @@ -0,0 +1,16 @@ +import { CREATE_FORM, REMOVE_FORM } from './constants'; + +export function createForm(formName, properties) { + return { + type: CREATE_FORM, + formName, + properties, + }; +} + +export function removeForm(formName) { + return { + type: REMOVE_FORM, + formName, + }; +} diff --git a/packages/forms/src/UIForm/actions/index.js b/packages/forms/src/UIForm/actions/index.js index b1760ba1ed8..cd44eb204c2 100644 --- a/packages/forms/src/UIForm/actions/index.js +++ b/packages/forms/src/UIForm/actions/index.js @@ -1,3 +1,4 @@ +export { createForm, removeForm } from './form.actions'; export { mutateValue } from './model.actions'; export { validateAll } from './validation.actions'; export * from './constants'; diff --git a/packages/forms/src/UIForm/actions/model.actions.js b/packages/forms/src/UIForm/actions/model.actions.js index c1d895687eb..c41e58547ce 100644 --- a/packages/forms/src/UIForm/actions/model.actions.js +++ b/packages/forms/src/UIForm/actions/model.actions.js @@ -1,10 +1,10 @@ import { MUTATE_VALUE } from './constants'; -export function mutateValue(schema, value, properties, customValidationFn) { +export function mutateValue(formName, schema, value, error) { return { type: MUTATE_VALUE, - customValidationFn, - properties, + error, + formName, schema, value, }; diff --git a/packages/forms/src/UIForm/actions/validation.actions.js b/packages/forms/src/UIForm/actions/validation.actions.js index 03f2f226b15..dada4957a4d 100644 --- a/packages/forms/src/UIForm/actions/validation.actions.js +++ b/packages/forms/src/UIForm/actions/validation.actions.js @@ -1,10 +1,9 @@ import { VALIDATE_ALL } from './constants'; -export function validateAll(schema, properties, customValidationFn) { +export function validateAll(formName, errors) { return { type: VALIDATE_ALL, - schema, - properties, - customValidationFn, + formName, + errors, }; } diff --git a/packages/forms/src/UIForm/index.js b/packages/forms/src/UIForm/index.js index 38ee3ef5fbd..04d2fecb389 100644 --- a/packages/forms/src/UIForm/index.js +++ b/packages/forms/src/UIForm/index.js @@ -1,8 +1,9 @@ import UIForm from './UIForm.container'; -import { modelReducer, validationReducer } from './reducers'; +import ConnectedUIForm from './UIForm.connect'; +import { formReducer } from './reducers'; -export const formReducers = { - model: modelReducer, - errors: validationReducer, +export { + ConnectedUIForm, + UIForm, + formReducer, }; -export default UIForm; diff --git a/packages/forms/src/UIForm/reducers/form.reducer.js b/packages/forms/src/UIForm/reducers/form.reducer.js new file mode 100644 index 00000000000..d3962df395c --- /dev/null +++ b/packages/forms/src/UIForm/reducers/form.reducer.js @@ -0,0 +1,50 @@ +import { CREATE_FORM, REMOVE_FORM, MUTATE_VALUE, VALIDATE_ALL } from '../actions'; +import { omit } from '../utils/properties'; +import modelReducer from './model.reducer'; +import validationsReducer from './validations.reducer'; + +/** + * Form reducer, that manage multiple form state. + * Format : { + * [form_name]: { + * properties: {}, + * errors: {}, + * }, + * ... + * } + */ +export default function formReducer(state = {}, action) { + switch (action.type) { + case CREATE_FORM: { + const form = state[action.formName]; + if (form) { + return state; + } + return { + ...state, + [action.formName]: { + properties: action.properties || {}, + errors: action.errors || {}, + }, + }; + } + case REMOVE_FORM: + return omit(state, action.formName); + case MUTATE_VALUE: + case VALIDATE_ALL: { + const form = state[action.formName]; + if (!form) { + return state; + } + return { + ...state, + [action.formName]: { + properties: modelReducer(form.properties, action), + errors: validationsReducer(form.errors, action), + }, + }; + } + default: + return state; + } +} diff --git a/packages/forms/src/UIForm/reducers/index.js b/packages/forms/src/UIForm/reducers/index.js index e71c9a3a814..af18c19bbd8 100644 --- a/packages/forms/src/UIForm/reducers/index.js +++ b/packages/forms/src/UIForm/reducers/index.js @@ -1,4 +1,5 @@ +import formReducer from './form.reducer'; import modelReducer from './model.reducer'; import validationReducer from './validations.reducer'; -export { modelReducer, validationReducer }; +export { formReducer, modelReducer, validationReducer }; diff --git a/packages/forms/src/UIForm/reducers/model.reducer.js b/packages/forms/src/UIForm/reducers/model.reducer.js index 3f6a5577d21..17079cf3af4 100644 --- a/packages/forms/src/UIForm/reducers/model.reducer.js +++ b/packages/forms/src/UIForm/reducers/model.reducer.js @@ -25,7 +25,7 @@ function mutateValue(properties, key, value) { * @param state The model * @param action The action to perform */ -export default function modelConsolidation(state = {}, action) { +export default function modelReducer(state = {}, action) { switch (action.type) { case MUTATE_VALUE: return mutateValue(state, action.schema.key, action.value); diff --git a/packages/forms/src/UIForm/reducers/validations.reducer.js b/packages/forms/src/UIForm/reducers/validations.reducer.js index d780cfab039..50bb99e0574 100644 --- a/packages/forms/src/UIForm/reducers/validations.reducer.js +++ b/packages/forms/src/UIForm/reducers/validations.reducer.js @@ -1,68 +1,5 @@ -import { validate } from 'talend-json-schema-form-core'; import { MUTATE_VALUE, VALIDATE_ALL } from '../actions'; -import { getValue } from '../utils/properties'; - -/** - * Validate values. - * @param schema The merged schema. - * @param value The value. - * @param properties The values. - * @param customValidationFn A custom validation function - * that is applied on schema.customValidation = true - * @returns {object} The validation result. - */ -function validateValue(schema, value, properties, customValidationFn) { - const staticResult = validate(schema, value); - if (staticResult.valid && schema.customValidation && customValidationFn) { - return customValidationFn(properties, schema, value); - } - return staticResult.valid ? null : staticResult.error.message; -} - -/** - * Validate values. - * @param mergedSchema The merged schema array. - * @param properties The values. - * @param customValidationFn A custom validation function - * that is applied on schema.customValidation = true - * @returns {object} The validation result by field. - */ -function validateAll(mergedSchema, properties, customValidationFn) { - const results = {}; - mergedSchema.forEach((schema) => { - const { key, items } = schema; - if (key) { - const value = getValue(properties, key); - const error = validateValue(schema, value, properties, customValidationFn); - if (error) { - results[key] = error; - } - } - if (items) { - const subResults = validateAll(items, properties); - Object.assign(results, subResults); - } - }); - return results; -} - -/** - * Omit a property from an object - * @param errors The object - * @param key The key to omit - */ -function omit(errors, key) { - if (!key) { - return errors; - } - const result = {}; - Object.keys(errors) - .filter(nextKey => nextKey !== key) - .forEach((nextKey) => { - result[nextKey] = errors[nextKey]; - }); - return result; -} +import { omit } from '../utils/properties'; /** * Form validations reducer @@ -72,19 +9,17 @@ function omit(errors, key) { export default function validations(state = {}, action) { switch (action.type) { case MUTATE_VALUE: { - const { schema, value, properties, customValidationFn } = action; - const error = validateValue(schema, value, properties, customValidationFn); + const { schema, error } = action; if (error) { return { ...state, - [action.schema.key]: error, + [schema.key]: error, }; } - return omit(state, action.schema.key.toString()); + return omit(state, schema.key.toString()); } case VALIDATE_ALL: { - const { schema, properties, customValidationFn } = action; - return validateAll(schema, properties, customValidationFn); + return action.errors; } default: return state; diff --git a/packages/forms/src/UIForm/utils/properties.js b/packages/forms/src/UIForm/utils/properties.js index 64ebc758345..224a6d43db1 100644 --- a/packages/forms/src/UIForm/utils/properties.js +++ b/packages/forms/src/UIForm/utils/properties.js @@ -9,7 +9,25 @@ export function getValue(properties, key) { } return key.reduce( - (accu, nextKey) => accu[nextKey], + (accu, nextKey) => accu && accu[nextKey], properties ); } + +/** + * Omit a properties from an object + * @param properties The object + * @param key The key to omit + */ +export function omit(properties, key) { + if (!key) { + return properties; + } + const result = {}; + Object.keys(properties) + .filter(nextKey => nextKey !== key) + .forEach((nextKey) => { + result[nextKey] = properties[nextKey]; + }); + return result; +} diff --git a/packages/forms/src/UIForm/utils/validation.js b/packages/forms/src/UIForm/utils/validation.js index 0dfb6f97c04..ac700d99061 100644 --- a/packages/forms/src/UIForm/utils/validation.js +++ b/packages/forms/src/UIForm/utils/validation.js @@ -1,3 +1,50 @@ +import { validate } from 'talend-json-schema-form-core'; +import { getValue } from '../utils/properties'; + +/** + * Validate values. + * @param schema The merged schema. + * @param value The value. + * @param properties The values. + * @param customValidationFn A custom validation function + * that is applied on schema.customValidation = true + * @returns {object} The validation result. + */ +export function validateValue(schema, value, properties, customValidationFn) { + const staticResult = validate(schema, value); + if (staticResult.valid && schema.customValidation && customValidationFn) { + return customValidationFn(properties, schema, value); + } + return staticResult.valid ? null : staticResult.error.message; +} + +/** + * Validate values. + * @param mergedSchema The merged schema array. + * @param properties The values. + * @param customValidationFn A custom validation function + * that is applied on schema.customValidation = true + * @returns {object} The validation result by field. + */ +export function validateAll(mergedSchema, properties, customValidationFn) { + const results = {}; + mergedSchema.forEach((schema) => { + const { key, items } = schema; + if (key) { + const value = getValue(properties, key); + const error = validateValue(schema, value, properties, customValidationFn); + if (error) { + results[key] = error; + } + } + if (items) { + const subResults = validateAll(items, properties); + Object.assign(results, subResults); + } + }); + return results; +} + /** * Check if a schema value is valid. * It is invalid if : diff --git a/packages/forms/stories/index.js b/packages/forms/stories/index.js index e9abc810f6c..583d46fb4ab 100644 --- a/packages/forms/stories/index.js +++ b/packages/forms/stories/index.js @@ -8,20 +8,14 @@ import { withKnobs, object } from '@kadira/storybook-addon-knobs'; import Well from 'react-bootstrap/lib/Well'; import IconsProvider from 'react-talend-components/lib/IconsProvider'; - import { createStore, combineReducers } from 'redux'; -import { reducer as formReducer } from 'redux-form'; +import { UIForm, ConnectedUIForm, formReducer } from '../src/UIForm'; -const reducers = { - // ... your other reducers here ... - form: formReducer, // <---- Mounted at 'form' -}; +const reducers = { forms: formReducer }; const reducer = combineReducers(reducers); const store = createStore(reducer); -import Form from '../src/UIForm'; - a11y(ReactDOM); const decoratedStories = storiesOf('Form', module) @@ -58,9 +52,29 @@ sampleFilenames decoratedStories.add(capitalizedSampleName, () => (
- { + action('customValidation')(properties, schema, value); + return value.length >= 5 && + 'Custom validation : The value should be less than 5 chars'; + }} + /> +
+ )); + decoratedStories.add(`Redux-${capitalizedSampleName}`, () => ( +
+ + Date: Tue, 16 May 2017 15:11:04 +0000 Subject: [PATCH 19/73] test(ci): update code style outputs --- output/forms.eslint.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/output/forms.eslint.txt b/output/forms.eslint.txt index 7b56832cd87..7f2ddb8148f 100755 --- a/output/forms.eslint.txt +++ b/output/forms.eslint.txt @@ -125,11 +125,11 @@ The react/require-extension rule is deprecated. Please use the import/extensions 3:1 error Prefer default export import/prefer-default-export /home/travis/build/Talend/ui/packages/forms/src/UIForm/UIForm.component.js - 15:3 warning Unexpected console statement no-console - 15:39 error Missing semicolon semi + 16:3 warning Unexpected console statement no-console + 16:39 error Missing semicolon semi -/home/travis/build/Talend/ui/packages/forms/src/UIForm/utils/properties.js - 6:1 error Prefer default export import/prefer-default-export +/home/travis/build/Talend/ui/packages/forms/src/UIForm/UIForm.container.js + 5:23 error 'validateAll' is defined but never used no-unused-vars /home/travis/build/Talend/ui/packages/forms/src/widgets/DatalistWidget/DatalistWidget.test.js 30:10 error Missing trailing comma comma-dangle From cc546acecd34d2751619b9db707af27f6172dfbf Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Tue, 16 May 2017 17:21:55 +0200 Subject: [PATCH 20/73] Use reducer in container --- packages/forms/src/UIForm/UIForm.container.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/forms/src/UIForm/UIForm.container.js b/packages/forms/src/UIForm/UIForm.container.js index 8465c0d5178..dd9f9b03959 100644 --- a/packages/forms/src/UIForm/UIForm.container.js +++ b/packages/forms/src/UIForm/UIForm.container.js @@ -66,7 +66,8 @@ export default class UIForm extends React.Component { if (isValid) { this.props.onSubmit(event, properties); } else { - this.setState({ errors }); + const action = validateAll(this.props.formName, errors); + this.setState({ errors: validationReducer(this.state.errors, action) }); } } From ab5ff8af1cabe1716cdbf27df6de3398b967ccd7 Mon Sep 17 00:00:00 2001 From: travis Date: Tue, 16 May 2017 15:31:13 +0000 Subject: [PATCH 21/73] test(ci): update code style outputs --- output/forms.eslint.txt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/output/forms.eslint.txt b/output/forms.eslint.txt index 7f2ddb8148f..4cdfa6a69da 100755 --- a/output/forms.eslint.txt +++ b/output/forms.eslint.txt @@ -128,9 +128,6 @@ The react/require-extension rule is deprecated. Please use the import/extensions 16:3 warning Unexpected console statement no-console 16:39 error Missing semicolon semi -/home/travis/build/Talend/ui/packages/forms/src/UIForm/UIForm.container.js - 5:23 error 'validateAll' is defined but never used no-unused-vars - /home/travis/build/Talend/ui/packages/forms/src/widgets/DatalistWidget/DatalistWidget.test.js 30:10 error Missing trailing comma comma-dangle 72:10 error Missing trailing comma comma-dangle @@ -143,7 +140,7 @@ The react/require-extension rule is deprecated. Please use the import/extensions 151:5 error Value must be omitted for boolean attributes react/jsx-boolean-value 172:5 error Value must be omitted for boolean attributes react/jsx-boolean-value -✖ 117 problems (116 errors, 1 warning) +✖ 116 problems (115 errors, 1 warning) Errored while running command 'npm' with arguments 'run lint:es' in 'react-talend-forms' Errored while running ExecCommand.execute From 9f3f7f0c855fb247b0eb2f2ba4c9dccfedecb172 Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Thu, 18 May 2017 10:37:08 +0200 Subject: [PATCH 22/73] Add autoFocus feature --- packages/forms/src/UIForm/fields/Text.js | 4 +++- packages/forms/stories/json/core-simple.json | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/forms/src/UIForm/fields/Text.js b/packages/forms/src/UIForm/fields/Text.js index 09b50e1d715..4198ed623b6 100644 --- a/packages/forms/src/UIForm/fields/Text.js +++ b/packages/forms/src/UIForm/fields/Text.js @@ -12,7 +12,7 @@ function convertValue(type, value) { export default function Text(props) { const { id, isValid, errorMessage, onChange, schema, value } = props; - const { description, placeholder, readOnly, title, type } = schema; + const { autoFocus, description, placeholder, readOnly, title, type } = schema; const groupsClassNames = classNames( 'form-group', @@ -22,6 +22,7 @@ export default function Text(props) {
Date: Thu, 18 May 2017 11:06:35 +0200 Subject: [PATCH 23/73] Set submit intelligence in component --- packages/forms/src/UIForm/UIForm.component.js | 12 ++++++-- packages/forms/src/UIForm/UIForm.connect.js | 30 ++----------------- packages/forms/src/UIForm/UIForm.container.js | 20 +++++-------- 3 files changed, 19 insertions(+), 43 deletions(-) diff --git a/packages/forms/src/UIForm/UIForm.component.js b/packages/forms/src/UIForm/UIForm.component.js index a54b3516e49..648d406087d 100644 --- a/packages/forms/src/UIForm/UIForm.component.js +++ b/packages/forms/src/UIForm/UIForm.component.js @@ -62,9 +62,15 @@ export default class UIForm extends React.Component { submit(event) { event.preventDefault(); const { mergedSchema } = this.state; - const { properties, validation } = this.props; + const { formName, properties, validation } = this.props; const errors = validateAll(mergedSchema, properties, validation); - this.props.onSubmit(event, properties, errors); + + const isValid = !Object.keys(errors).length; + if (isValid) { + this.props.onSubmit(event, properties); + } else { + this.props.onValidateAll(formName, errors); + } } render() { @@ -107,6 +113,8 @@ if (process.env.NODE_ENV !== 'production') { * This is executed on changes on fields with uiSchema > triggers : ['after'] */ onTrigger: PropTypes.func, + /** All fields validation callback */ + onValidateAll: PropTypes.func, /** Form fields values. Note that it should contains @definitionName for triggers. */ properties: PropTypes.object, // eslint-disable-line react/forbid-prop-types /** UI schema that specify how to render the fields */ diff --git a/packages/forms/src/UIForm/UIForm.connect.js b/packages/forms/src/UIForm/UIForm.connect.js index 9a895cda629..d90f7d1d0f8 100644 --- a/packages/forms/src/UIForm/UIForm.connect.js +++ b/packages/forms/src/UIForm/UIForm.connect.js @@ -13,13 +13,7 @@ import { class UIForm extends React.Component { constructor(props) { super(props); - this.state = { - properties: { ...props.data.properties }, - errors: {}, - }; - this.onChange = this.onChange.bind(this); - this.onSubmit = this.onSubmit.bind(this); } /** @@ -62,25 +56,6 @@ class UIForm extends React.Component { } } - /** - * Triggers submit callback if form is valid - * @param event the submit event - * @param properties the properties values - * @param errors the validations errors - */ - onSubmit(event, properties, errors) { - event.preventDefault(); - const isValid = !Object.keys(errors).length; - if (isValid) { - this.props.onSubmit(event, properties); - } else { - this.props.validateAll( - this.props.formName, - errors - ); - } - } - render() { const { data, form, ...restProps } = this.props; @@ -92,7 +67,6 @@ class UIForm extends React.Component { properties={form.properties} errors={form.errors} onChange={this.onChange} - onSubmit={this.onSubmit} /> ); } @@ -161,7 +135,7 @@ if (process.env.NODE_ENV !== 'production') { * Form validation action. * This is injected by react-redux. See mapDispatchToProps */ - validateAll: PropTypes.func, + onValidateAll: PropTypes.func, }; } @@ -181,7 +155,7 @@ function mapDispatchToProps(dispatch) { createForm: bindActionCreators(createForm, dispatch), removeForm: bindActionCreators(removeForm, dispatch), mutateValue: bindActionCreators(mutateValue, dispatch), - validateAll: bindActionCreators(validateAll, dispatch), + onValidateAll: bindActionCreators(validateAll, dispatch), }; } diff --git a/packages/forms/src/UIForm/UIForm.container.js b/packages/forms/src/UIForm/UIForm.container.js index dd9f9b03959..082c875b4aa 100644 --- a/packages/forms/src/UIForm/UIForm.container.js +++ b/packages/forms/src/UIForm/UIForm.container.js @@ -13,7 +13,7 @@ export default class UIForm extends React.Component { }; this.onChange = this.onChange.bind(this); - this.onSubmit = this.onSubmit.bind(this); + this.onValidateAll = this.onValidateAll.bind(this); } /** @@ -56,19 +56,13 @@ export default class UIForm extends React.Component { } /** - * Triggers submit callback if form is valid - * @param event the submit event - * @param properties the properties values + * Set all fields validation in state + * @param formName the form name * @param errors the validation errors */ - onSubmit(event, properties, errors) { - const isValid = !Object.keys(errors).length; - if (isValid) { - this.props.onSubmit(event, properties); - } else { - const action = validateAll(this.props.formName, errors); - this.setState({ errors: validationReducer(this.state.errors, action) }); - } + onValidateAll(formName, errors) { + const action = validateAll(formName, errors); + this.setState({ errors: validationReducer(this.state.errors, action) }); } render() { @@ -83,7 +77,7 @@ export default class UIForm extends React.Component { properties={properties} errors={errors} onChange={this.onChange} - onSubmit={this.onSubmit} + onValidateAll={this.onValidateAll} /> ); } From fd81b529ae796f981196aa34eb5fe759149ec355 Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Thu, 18 May 2017 16:43:12 +0200 Subject: [PATCH 24/73] Trigger on buttons --- packages/forms/src/UIForm/UIForm.component.js | 67 +++++++++++-------- packages/forms/src/UIForm/UIForm.connect.js | 18 ++--- packages/forms/src/UIForm/UIForm.container.js | 43 ++++++------ .../forms/src/UIForm/actions/constants.js | 1 + packages/forms/src/UIForm/actions/index.js | 2 +- .../src/UIForm/actions/validation.actions.js | 10 ++- packages/forms/src/UIForm/fields/Button.js | 40 +++++++++++ .../forms/src/UIForm/reducers/form.reducer.js | 3 +- .../UIForm/reducers/validations.reducer.js | 11 ++- packages/forms/src/UIForm/utils/widgets.js | 2 + packages/forms/stories/index.js | 43 ++++++------ packages/forms/stories/json/core-buttons.json | 27 ++++++++ 12 files changed, 182 insertions(+), 85 deletions(-) create mode 100644 packages/forms/src/UIForm/fields/Button.js create mode 100644 packages/forms/stories/json/core-buttons.json diff --git a/packages/forms/src/UIForm/UIForm.component.js b/packages/forms/src/UIForm/UIForm.component.js index 648d406087d..74e8b164998 100644 --- a/packages/forms/src/UIForm/UIForm.component.js +++ b/packages/forms/src/UIForm/UIForm.component.js @@ -41,17 +41,19 @@ export default class UIForm extends React.Component { * @param value The new value */ onChange(event, schema, value) { - const { onChange, onTrigger, properties, validation } = this.props; - const error = validateValue(schema, value, properties, validation); + const { formName, onChange, onTrigger, onValidate, properties, customValidation } = this.props; + const error = validateValue(schema, value, properties, customValidation); onChange(schema, value, error); - const { key, triggers } = schema; + const { triggers } = schema; if (onTrigger && triggers && triggers.indexOf(TRIGGER_AFTER) !== -1) { onTrigger( - this.props.properties, // current properties values - key[key.length - 1], // field name - value // field value - ); + properties, // current properties values + schema, // field schema + value // field value + ) + .then(() => {}) + .catch(({ errors }) => onValidate(formName, errors)); } } @@ -62,8 +64,8 @@ export default class UIForm extends React.Component { submit(event) { event.preventDefault(); const { mergedSchema } = this.state; - const { formName, properties, validation } = this.props; - const errors = validateAll(mergedSchema, properties, validation); + const { formName, properties, customValidation } = this.props; + const errors = validateAll(mergedSchema, properties, customValidation); const isValid = !Object.keys(errors).length; if (isValid) { @@ -97,34 +99,41 @@ export default class UIForm extends React.Component { if (process.env.NODE_ENV !== 'production') { UIForm.propTypes = { - /** The forms errors { [fieldKey]: errorMessage } */ - errors: PropTypes.object, // eslint-disable-line react/forbid-prop-types - /** The form name that will be used to create ids */ + /** Form definition: The form name that will be used to create ids */ formName: PropTypes.string, - /** Json schema that specify the data model */ + /** Form definition: Json schema that specify the data model */ jsonSchema: PropTypes.object, // eslint-disable-line react/forbid-prop-types - /** The change callback */ - onChange: PropTypes.func.isRequired, - /** Form submit callback */ - onSubmit: PropTypes.func.isRequired, /** - * Tigger > after callback. - * Prototype: function onTrigger(properties, fieldName, value) - * This is executed on changes on fields with uiSchema > triggers : ['after'] + * Form definition: Form fields values. + * Note that it should contains @definitionName for triggers. */ - onTrigger: PropTypes.func, - /** All fields validation callback */ - onValidateAll: PropTypes.func, - /** Form fields values. Note that it should contains @definitionName for triggers. */ properties: PropTypes.object, // eslint-disable-line react/forbid-prop-types - /** UI schema that specify how to render the fields */ + /** Form definition: UI schema that specify how to render the fields */ uiSchema: PropTypes.array, // eslint-disable-line react/forbid-prop-types + /** Form definition: The forms errors { [fieldKey]: errorMessage } */ + errors: PropTypes.object, // eslint-disable-line react/forbid-prop-types + /** - * Custom validation function. - * Prototype: function validation(properties, fieldName, value) - * Return format : { valid: true|false, error: { message: 'my validation message' } } + * User callback: Custom validation function. + * Prototype: function customValidation(properties, fieldName, value) + * Return format : errorMessage String | falsy * This is triggered on fields that has their uiSchema > customValidation : true */ - validation: PropTypes.func, + customValidation: PropTypes.func, + /** User callback: Form submit callback */ + onSubmit: PropTypes.func.isRequired, + /** + * User callback: Trigger > after callback. + * Prototype: function onTrigger(properties, schema, value) + * This is executed on changes on fields with uiSchema > triggers : ['after'] + */ + onTrigger: PropTypes.func, + + /** State management impl: The change callback */ + onChange: PropTypes.func.isRequired, + /** State management impl: Partial fields validation callback */ + onValidate: PropTypes.func, + /** State management impl: All fields validation callback */ + onValidateAll: PropTypes.func, }; } diff --git a/packages/forms/src/UIForm/UIForm.connect.js b/packages/forms/src/UIForm/UIForm.connect.js index d90f7d1d0f8..48e277bcfc5 100644 --- a/packages/forms/src/UIForm/UIForm.connect.js +++ b/packages/forms/src/UIForm/UIForm.connect.js @@ -7,6 +7,7 @@ import { createForm, removeForm, mutateValue, + validate, validateAll, } from './actions'; @@ -86,6 +87,13 @@ if (process.env.NODE_ENV !== 'production') { */ properties: PropTypes.object, }), + /** + * Custom validation function. + * Prototype: function customValidation(properties, fieldName, value) + * Return format : errorMessage String | falsy + * This is triggered on fields that has their uiSchema > customValidation : true + */ + customValidation: PropTypes.func, /** The form name that will be used to create ids */ formName: PropTypes.string.isRequired, /** The change callback. */ @@ -94,17 +102,10 @@ if (process.env.NODE_ENV !== 'production') { onSubmit: PropTypes.func.isRequired, /** * Tigger > after callback. - * Prototype: function onTrigger(properties, fieldName, value) + * Prototype: function onTrigger(properties, schema, value) * This is executed on changes on fields with uiSchema > triggers : ['after'] */ onTrigger: PropTypes.func, - /** - * Custom validation function. - * Prototype: function validation(properties, fieldName, value) - * Return format : string - * This is triggered on fields that has their uiSchema > customValidation : true - */ - validation: PropTypes.func, /** * Form data from store. @@ -155,6 +156,7 @@ function mapDispatchToProps(dispatch) { createForm: bindActionCreators(createForm, dispatch), removeForm: bindActionCreators(removeForm, dispatch), mutateValue: bindActionCreators(mutateValue, dispatch), + onValidate: bindActionCreators(validate, dispatch), onValidateAll: bindActionCreators(validateAll, dispatch), }; } diff --git a/packages/forms/src/UIForm/UIForm.container.js b/packages/forms/src/UIForm/UIForm.container.js index 082c875b4aa..aa6de636b16 100644 --- a/packages/forms/src/UIForm/UIForm.container.js +++ b/packages/forms/src/UIForm/UIForm.container.js @@ -2,7 +2,7 @@ import React, { PropTypes } from 'react'; import UIFormComponent from './UIForm.component'; import { modelReducer, validationReducer } from './reducers'; -import { mutateValue, validateAll } from './actions'; +import { mutateValue, validate, validateAll } from './actions'; export default class UIForm extends React.Component { constructor(props) { @@ -13,22 +13,10 @@ export default class UIForm extends React.Component { }; this.onChange = this.onChange.bind(this); + this.onValidate = this.onValidate.bind(this); this.onValidateAll = this.onValidateAll.bind(this); } - /** - * Update the properties. - */ - componentWillReceiveProps({ properties }) { - if (!properties) { - return; - } - - this.setState({ - properties: { ...properties }, - }); - } - /** * Update the model and validation * If onChange is provided, it is triggered @@ -55,6 +43,16 @@ export default class UIForm extends React.Component { ); } + /** + * Set partial fields validation in state + * @param formName the form name + * @param errors the validation errors + */ + onValidate(formName, errors) { + const action = validate(formName, errors); + this.setState({ errors: validationReducer(this.state.errors, action) }); + } + /** * Set all fields validation in state * @param formName the form name @@ -77,6 +75,7 @@ export default class UIForm extends React.Component { properties={properties} errors={errors} onChange={this.onChange} + onValidate={this.onValidate} onValidateAll={this.onValidateAll} /> ); @@ -97,6 +96,13 @@ if (process.env.NODE_ENV !== 'production') { */ properties: PropTypes.object, }), + /** + * Custom validation function. + * Prototype: function customValidation(properties, fieldName, value) + * Return format : errorMessage String | falsy + * This is triggered on fields that has their uiSchema > customValidation : true + */ + customValidation: PropTypes.func, /** The form name that will be used to create ids */ formName: PropTypes.string, /** The change callback. It takes */ @@ -105,16 +111,9 @@ if (process.env.NODE_ENV !== 'production') { onSubmit: PropTypes.func.isRequired, /** * Tigger > after callback. - * Prototype: function onTrigger(properties, fieldName, value) + * Prototype: function onTrigger(properties, schema, value) * This is executed on changes on fields with uiSchema > triggers : ['after'] */ onTrigger: PropTypes.func, - /** - * Custom validation function. - * Prototype: function validation(properties, fieldName, value) - * Return format : { valid: true|false, error: { message: 'my validation message' } } - * This is triggered on fields that has their uiSchema > customValidation : true - */ - validation: PropTypes.func, }; } diff --git a/packages/forms/src/UIForm/actions/constants.js b/packages/forms/src/UIForm/actions/constants.js index 702870a99be..9bb8510ad15 100644 --- a/packages/forms/src/UIForm/actions/constants.js +++ b/packages/forms/src/UIForm/actions/constants.js @@ -1,4 +1,5 @@ export const MUTATE_VALUE = 'MUTATE_VALUE'; export const VALIDATE_ALL = 'VALIDATE_ALL'; +export const VALIDATE_PARTIAL = 'VALIDATE_PARTIAL'; export const CREATE_FORM = 'CREATE_FORM'; export const REMOVE_FORM = 'REMOVE_FORM'; diff --git a/packages/forms/src/UIForm/actions/index.js b/packages/forms/src/UIForm/actions/index.js index cd44eb204c2..accb229a9c4 100644 --- a/packages/forms/src/UIForm/actions/index.js +++ b/packages/forms/src/UIForm/actions/index.js @@ -1,4 +1,4 @@ export { createForm, removeForm } from './form.actions'; export { mutateValue } from './model.actions'; -export { validateAll } from './validation.actions'; +export { validate, validateAll } from './validation.actions'; export * from './constants'; diff --git a/packages/forms/src/UIForm/actions/validation.actions.js b/packages/forms/src/UIForm/actions/validation.actions.js index dada4957a4d..35ac735947d 100644 --- a/packages/forms/src/UIForm/actions/validation.actions.js +++ b/packages/forms/src/UIForm/actions/validation.actions.js @@ -1,4 +1,12 @@ -import { VALIDATE_ALL } from './constants'; +import { VALIDATE_ALL, VALIDATE_PARTIAL } from './constants'; + +export function validate(formName, errors) { + return { + type: VALIDATE_PARTIAL, + formName, + errors, + }; +} export function validateAll(formName, errors) { return { diff --git a/packages/forms/src/UIForm/fields/Button.js b/packages/forms/src/UIForm/fields/Button.js new file mode 100644 index 00000000000..59da46b20b1 --- /dev/null +++ b/packages/forms/src/UIForm/fields/Button.js @@ -0,0 +1,40 @@ +import React, { PropTypes } from 'react'; + +import Message from '../Message'; + +export default function Button(props) { + const { id, errorMessage, isValid, onChange, schema } = props; + const { description, title, type } = schema; + + return ( +
+ + +
+ ); +} + +if (process.env.NODE_ENV !== 'production') { + Button.propTypes = { + id: PropTypes.string, + isValid: PropTypes.bool, + errorMessage: PropTypes.string, + onChange: PropTypes.func, + schema: PropTypes.shape({ + description: PropTypes.string, + title: PropTypes.string, + type: PropTypes.string, + }), + }; +} diff --git a/packages/forms/src/UIForm/reducers/form.reducer.js b/packages/forms/src/UIForm/reducers/form.reducer.js index d3962df395c..4a3f57d82cc 100644 --- a/packages/forms/src/UIForm/reducers/form.reducer.js +++ b/packages/forms/src/UIForm/reducers/form.reducer.js @@ -1,4 +1,4 @@ -import { CREATE_FORM, REMOVE_FORM, MUTATE_VALUE, VALIDATE_ALL } from '../actions'; +import { CREATE_FORM, REMOVE_FORM, MUTATE_VALUE, VALIDATE_ALL, VALIDATE_PARTIAL } from '../actions'; import { omit } from '../utils/properties'; import modelReducer from './model.reducer'; import validationsReducer from './validations.reducer'; @@ -31,6 +31,7 @@ export default function formReducer(state = {}, action) { case REMOVE_FORM: return omit(state, action.formName); case MUTATE_VALUE: + case VALIDATE_PARTIAL: case VALIDATE_ALL: { const form = state[action.formName]; if (!form) { diff --git a/packages/forms/src/UIForm/reducers/validations.reducer.js b/packages/forms/src/UIForm/reducers/validations.reducer.js index 50bb99e0574..70f24854af2 100644 --- a/packages/forms/src/UIForm/reducers/validations.reducer.js +++ b/packages/forms/src/UIForm/reducers/validations.reducer.js @@ -1,4 +1,4 @@ -import { MUTATE_VALUE, VALIDATE_ALL } from '../actions'; +import { MUTATE_VALUE, VALIDATE_ALL, VALIDATE_PARTIAL } from '../actions'; import { omit } from '../utils/properties'; /** @@ -18,6 +18,15 @@ export default function validations(state = {}, action) { } return omit(state, schema.key.toString()); } + case VALIDATE_PARTIAL: { + if (Object.keys(action.errors).length === 0) { + return state; + } + return { + ...state, + ...action.errors, + }; + } case VALIDATE_ALL: { return action.errors; } diff --git a/packages/forms/src/UIForm/utils/widgets.js b/packages/forms/src/UIForm/utils/widgets.js index bfed6e112f8..b767e3ccef8 100644 --- a/packages/forms/src/UIForm/utils/widgets.js +++ b/packages/forms/src/UIForm/utils/widgets.js @@ -1,6 +1,7 @@ import Fieldset from '../fieldsets/Fieldset'; import Tabs from '../fieldsets/Tabs'; +import Button from '../fields/Button'; import Text from '../fields/Text'; const widgets = { @@ -9,6 +10,7 @@ const widgets = { tabs: Tabs, // fields + button: Button, number: Text, text: Text, }; diff --git a/packages/forms/stories/index.js b/packages/forms/stories/index.js index 583d46fb4ab..b8d4e269792 100644 --- a/packages/forms/stories/index.js +++ b/packages/forms/stories/index.js @@ -49,22 +49,31 @@ sampleFilenames const sampleNameMatches = filename.match(sampleFilenameRegex); const sampleName = sampleNameMatches[sampleNameMatches.length - 1]; const capitalizedSampleName = capitalizeFirstLetter(sampleName); + const props = { + customValidation(properties, schema, value) { + action('customValidation')(properties, schema, value); + return value.length >= 5 && + 'Custom validation : The value should be less than 5 chars'; + }, + formName: 'my-form', + onChange: action('Change'), + onTrigger(properties, schema, value) { + action('Trigger')(properties, schema, value); + const key = schema.key[schema.key.length - 1]; + return key.includes('fail') ? + Promise.reject({ errors: { [schema.key]: 'This trigger has failed' } }) : + Promise.resolve({}); + }, + onSubmit: action('Submit'), + // autocomplete: 'off', + // onBlur: action('Blur'), + }; decoratedStories.add(capitalizedSampleName, () => (
{ - action('customValidation')(properties, schema, value); - return value.length >= 5 && - 'Custom validation : The value should be less than 5 chars'; - }} />
)); @@ -72,18 +81,8 @@ sampleFilenames
{ - action('customValidation')(properties, schema, value); - return value.length >= 5 && - 'Custom validation : The value should be less than 5 chars'; - }} />
)); diff --git a/packages/forms/stories/json/core-buttons.json b/packages/forms/stories/json/core-buttons.json new file mode 100644 index 00000000000..8b79bd3df7b --- /dev/null +++ b/packages/forms/stories/json/core-buttons.json @@ -0,0 +1,27 @@ +{ + "jsonSchema": { + "type": "object", + "title": "Comment", + "properties": { + "check": {}, + "checkfail": {} + } + }, + "uiSchema": [ + { + "key": "check", + "title": "Check me", + "type": "button", + "triggers": ["after"], + "description": "This should trigger a successful check" + }, + { + "key": "checkfail", + "title": "Check me", + "type": "button", + "triggers": ["after"], + "description": "This should trigger a failing check" + } + ], + "properties": {} +} From d86502fa7ce4cad3a2a061d93e5ed6e147ce0d3d Mon Sep 17 00:00:00 2001 From: travis Date: Thu, 18 May 2017 14:55:09 +0000 Subject: [PATCH 25/73] test(ci): update code style outputs --- output/forms.eslint.txt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/output/forms.eslint.txt b/output/forms.eslint.txt index 4cdfa6a69da..ca11d3c2323 100755 --- a/output/forms.eslint.txt +++ b/output/forms.eslint.txt @@ -121,9 +121,6 @@ The react/require-extension rule is deprecated. Please use the import/extensions /home/travis/build/Talend/ui/packages/forms/src/UIForm/actions/model.actions.js 3:1 error Prefer default export import/prefer-default-export -/home/travis/build/Talend/ui/packages/forms/src/UIForm/actions/validation.actions.js - 3:1 error Prefer default export import/prefer-default-export - /home/travis/build/Talend/ui/packages/forms/src/UIForm/UIForm.component.js 16:3 warning Unexpected console statement no-console 16:39 error Missing semicolon semi @@ -140,7 +137,7 @@ The react/require-extension rule is deprecated. Please use the import/extensions 151:5 error Value must be omitted for boolean attributes react/jsx-boolean-value 172:5 error Value must be omitted for boolean attributes react/jsx-boolean-value -✖ 116 problems (115 errors, 1 warning) +✖ 115 problems (114 errors, 1 warning) Errored while running command 'npm' with arguments 'run lint:es' in 'react-talend-forms' Errored while running ExecCommand.execute From 324c3128484e62c978c9edc79a2d429321035f25 Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Thu, 18 May 2017 16:58:40 +0200 Subject: [PATCH 26/73] Align onChange api to onTrigger --- packages/forms/src/UIForm/UIForm.connect.js | 10 +++++----- packages/forms/src/UIForm/UIForm.container.js | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/forms/src/UIForm/UIForm.connect.js b/packages/forms/src/UIForm/UIForm.connect.js index 48e277bcfc5..75e2e6b2c5b 100644 --- a/packages/forms/src/UIForm/UIForm.connect.js +++ b/packages/forms/src/UIForm/UIForm.connect.js @@ -49,11 +49,11 @@ class UIForm extends React.Component { error ); if (this.props.onChange) { - this.props.onChange({ - jsonSchema: this.props.data.jsonSchema, - uiSchema: this.props.data.uiSchema, - properties: this.props.form.properties, // TODO fix that, old props - }); + this.props.onChange( + this.props.form.properties, // TODO fix that, old props + schema, + value + ); } } diff --git a/packages/forms/src/UIForm/UIForm.container.js b/packages/forms/src/UIForm/UIForm.container.js index aa6de636b16..6c62bd06f72 100644 --- a/packages/forms/src/UIForm/UIForm.container.js +++ b/packages/forms/src/UIForm/UIForm.container.js @@ -33,11 +33,11 @@ export default class UIForm extends React.Component { }, () => { if (this.props.onChange) { - this.props.onChange({ - jsonSchema: this.props.data.jsonSchema, - uiSchema: this.props.data.uiSchema, - properties: this.state.properties, - }); + this.props.onChange( + this.state.properties, + schema, + value + ); } } ); From db352cfee6879fc4463f372aa97ce499874f999f Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Thu, 18 May 2017 18:07:06 +0200 Subject: [PATCH 27/73] Trigger form change --- packages/forms/src/UIForm/UIForm.component.js | 25 +++++++-- packages/forms/src/UIForm/UIForm.connect.js | 43 ++++++++++++--- packages/forms/src/UIForm/UIForm.container.js | 52 ++++++++++++++----- .../forms/src/UIForm/actions/constants.js | 1 + .../forms/src/UIForm/actions/form.actions.js | 18 ++++++- packages/forms/src/UIForm/actions/index.js | 2 +- .../forms/src/UIForm/reducers/form.reducer.js | 26 +++++++++- 7 files changed, 140 insertions(+), 27 deletions(-) diff --git a/packages/forms/src/UIForm/UIForm.component.js b/packages/forms/src/UIForm/UIForm.component.js index 74e8b164998..03f9cbe82d1 100644 --- a/packages/forms/src/UIForm/UIForm.component.js +++ b/packages/forms/src/UIForm/UIForm.component.js @@ -41,9 +41,17 @@ export default class UIForm extends React.Component { * @param value The new value */ onChange(event, schema, value) { - const { formName, onChange, onTrigger, onValidate, properties, customValidation } = this.props; + const { + formName, + onChange, + onTrigger, + onFormChange, + onValidate, + properties, + customValidation, + } = this.props; const error = validateValue(schema, value, properties, customValidation); - onChange(schema, value, error); + onChange(formName, schema, value, error); const { triggers } = schema; if (onTrigger && triggers && triggers.indexOf(TRIGGER_AFTER) !== -1) { @@ -52,8 +60,14 @@ export default class UIForm extends React.Component { schema, // field schema value // field value ) - .then(() => {}) - .catch(({ errors }) => onValidate(formName, errors)); + .then(newForm => onFormChange( + formName, + newForm.jsonSchema, + newForm.uiSchema, + newForm.properties, + newForm.errors) + ) + .catch(({ errors }) => { console.log(errors); onValidate(formName, errors); }); } } @@ -129,8 +143,11 @@ if (process.env.NODE_ENV !== 'production') { */ onTrigger: PropTypes.func, + /** State management impl: The change callback */ onChange: PropTypes.func.isRequired, + /** State management impl: The form change callback */ + onFormChange: PropTypes.func.isRequired, /** State management impl: Partial fields validation callback */ onValidate: PropTypes.func, /** State management impl: All fields validation callback */ diff --git a/packages/forms/src/UIForm/UIForm.connect.js b/packages/forms/src/UIForm/UIForm.connect.js index 75e2e6b2c5b..c248f91eb1f 100644 --- a/packages/forms/src/UIForm/UIForm.connect.js +++ b/packages/forms/src/UIForm/UIForm.connect.js @@ -5,6 +5,7 @@ import UIFormComponent from './UIForm.component'; import { createForm, + changeForm, removeForm, mutateValue, validate, @@ -23,6 +24,8 @@ class UIForm extends React.Component { componentWillMount() { this.props.createForm( this.props.formName, + this.props.data.jsonSchema, + this.props.data.uiSchema, this.props.data.properties, ); } @@ -37,13 +40,14 @@ class UIForm extends React.Component { /** * Update the model and validation * If onChange is provided, it is triggered + * @param formName The form name * @param schema The schema * @param value The new value * @param error The validation error */ - onChange(schema, value, error) { + onChange(formName, schema, value, error) { this.props.mutateValue( - this.props.formName, + formName, schema, value, error @@ -58,16 +62,24 @@ class UIForm extends React.Component { } render() { - const { data, form, ...restProps } = this.props; + const { form } = this.props; return ( ); } @@ -75,7 +87,7 @@ class UIForm extends React.Component { if (process.env.NODE_ENV !== 'production') { UIForm.propTypes = { - /** Form schema configuration */ + /** Form schema initial configuration */ data: PropTypes.shape({ /** Json schema that specify the data model */ jsonSchema: PropTypes.object, @@ -112,6 +124,10 @@ if (process.env.NODE_ENV !== 'production') { * This is injected by react-redux. See mapStateToProps */ form: PropTypes.shape({ + /** Json schema that specify the data model */ + jsonSchema: PropTypes.object, + /** UI schema that specify how to render the fields */ + uiSchema: PropTypes.array, /** Form properties values */ properties: PropTypes.object, /** Form validations errors */ @@ -132,6 +148,16 @@ if (process.env.NODE_ENV !== 'production') { * This is injected by react-redux. See mapDispatchToProps */ mutateValue: PropTypes.func, + /** + * Form change action. + * This is injected by react-redux. See mapDispatchToProps + */ + onFormChange: PropTypes.func, + /** + * Partial form validation action. + * This is injected by react-redux. See mapDispatchToProps + */ + onValidate: PropTypes.func, /** * Form validation action. * This is injected by react-redux. See mapDispatchToProps @@ -142,6 +168,8 @@ if (process.env.NODE_ENV !== 'production') { UIForm.defaultProps = { form: { + jsonSchema: {}, + uiSchema: [], properties: {}, errors: {}, }, @@ -156,6 +184,7 @@ function mapDispatchToProps(dispatch) { createForm: bindActionCreators(createForm, dispatch), removeForm: bindActionCreators(removeForm, dispatch), mutateValue: bindActionCreators(mutateValue, dispatch), + onFormChange: bindActionCreators(changeForm, dispatch), onValidate: bindActionCreators(validate, dispatch), onValidateAll: bindActionCreators(validateAll, dispatch), }; diff --git a/packages/forms/src/UIForm/UIForm.container.js b/packages/forms/src/UIForm/UIForm.container.js index 6c62bd06f72..e4f76a18c0b 100644 --- a/packages/forms/src/UIForm/UIForm.container.js +++ b/packages/forms/src/UIForm/UIForm.container.js @@ -1,18 +1,23 @@ import React, { PropTypes } from 'react'; import UIFormComponent from './UIForm.component'; -import { modelReducer, validationReducer } from './reducers'; -import { mutateValue, validate, validateAll } from './actions'; +import { formReducer, modelReducer, validationReducer } from './reducers'; +import { createForm, changeForm, mutateValue, validate, validateAll } from './actions'; export default class UIForm extends React.Component { constructor(props) { super(props); - this.state = { - properties: { ...props.data.properties }, - errors: {}, - }; + + const action = createForm( + this.props.formName, + this.props.data.jsonSchema, + this.props.data.uiSchema, + this.props.data.properties, + ); + this.state = formReducer(undefined, action)[this.props.formName]; this.onChange = this.onChange.bind(this); + this.onFormChange = this.onFormChange.bind(this); this.onValidate = this.onValidate.bind(this); this.onValidateAll = this.onValidateAll.bind(this); } @@ -20,11 +25,12 @@ export default class UIForm extends React.Component { /** * Update the model and validation * If onChange is provided, it is triggered + * @param formName The form name * @param schema The schema * @param value The new value * @param error The validation error */ - onChange(schema, value, error) { + onChange(formName, schema, value, error) { const action = mutateValue(this.props.formName, schema, value, error); this.setState( { @@ -43,6 +49,23 @@ export default class UIForm extends React.Component { ); } + /** + * Update the form, the model and errors + * @param formName The form name + * @param schema The schema + * @param values The values + * @param errors The validation errors + */ + onFormChange(formName, schema, values, errors) { + const action = changeForm(formName, schema, values, errors); + const nextState = formReducer( + { [formName]: this.state }, + action + )[formName]; + + this.setState(nextState); + } + /** * Set partial fields validation in state * @param formName the form name @@ -64,17 +87,22 @@ export default class UIForm extends React.Component { } render() { - const { data, ...restProps } = this.props; - const { properties, errors } = this.state; + const { jsonSchema, uiSchema, properties, errors } = this.state; return ( diff --git a/packages/forms/src/UIForm/actions/constants.js b/packages/forms/src/UIForm/actions/constants.js index 9bb8510ad15..64222746ac7 100644 --- a/packages/forms/src/UIForm/actions/constants.js +++ b/packages/forms/src/UIForm/actions/constants.js @@ -2,4 +2,5 @@ export const MUTATE_VALUE = 'MUTATE_VALUE'; export const VALIDATE_ALL = 'VALIDATE_ALL'; export const VALIDATE_PARTIAL = 'VALIDATE_PARTIAL'; export const CREATE_FORM = 'CREATE_FORM'; +export const CHANGE_FORM = 'CHANGE_FORM'; export const REMOVE_FORM = 'REMOVE_FORM'; diff --git a/packages/forms/src/UIForm/actions/form.actions.js b/packages/forms/src/UIForm/actions/form.actions.js index 14a0f3286da..8c032862344 100644 --- a/packages/forms/src/UIForm/actions/form.actions.js +++ b/packages/forms/src/UIForm/actions/form.actions.js @@ -1,10 +1,24 @@ -import { CREATE_FORM, REMOVE_FORM } from './constants'; +import { CREATE_FORM, CHANGE_FORM, REMOVE_FORM } from './constants'; -export function createForm(formName, properties) { +export function createForm(formName, jsonSchema, uiSchema, properties, errors) { return { type: CREATE_FORM, formName, + jsonSchema, + uiSchema, properties, + errors, + }; +} + +export function changeForm(formName, jsonSchema, uiSchema, properties, errors) { + return { + type: CHANGE_FORM, + formName, + jsonSchema, + uiSchema, + properties, + errors, }; } diff --git a/packages/forms/src/UIForm/actions/index.js b/packages/forms/src/UIForm/actions/index.js index accb229a9c4..b66622bddd7 100644 --- a/packages/forms/src/UIForm/actions/index.js +++ b/packages/forms/src/UIForm/actions/index.js @@ -1,4 +1,4 @@ -export { createForm, removeForm } from './form.actions'; +export { createForm, changeForm, removeForm } from './form.actions'; export { mutateValue } from './model.actions'; export { validate, validateAll } from './validation.actions'; export * from './constants'; diff --git a/packages/forms/src/UIForm/reducers/form.reducer.js b/packages/forms/src/UIForm/reducers/form.reducer.js index 4a3f57d82cc..fb81e71e461 100644 --- a/packages/forms/src/UIForm/reducers/form.reducer.js +++ b/packages/forms/src/UIForm/reducers/form.reducer.js @@ -1,4 +1,11 @@ -import { CREATE_FORM, REMOVE_FORM, MUTATE_VALUE, VALIDATE_ALL, VALIDATE_PARTIAL } from '../actions'; +import { + CREATE_FORM, + CHANGE_FORM, + REMOVE_FORM, + MUTATE_VALUE, + VALIDATE_ALL, + VALIDATE_PARTIAL, +} from '../actions'; import { omit } from '../utils/properties'; import modelReducer from './model.reducer'; import validationsReducer from './validations.reducer'; @@ -23,11 +30,28 @@ export default function formReducer(state = {}, action) { return { ...state, [action.formName]: { + jsonSchema: action.jsonSchema, + uiSchema: action.uiSchema, properties: action.properties || {}, errors: action.errors || {}, }, }; } + case CHANGE_FORM: { + const form = state[action.formName]; + if (!form) { + return state; + } + return { + ...state, + [action.formName]: { + jsonSchema: action.jsonSchema || form.jsonSchema, + uiSchema: action.uiSchema || form.uiSchema, + properties: action.properties || form.properties, + errors: action.errors || form.errors, + }, + }; + } case REMOVE_FORM: return omit(state, action.formName); case MUTATE_VALUE: From ffea877478e11e157177e814e8ace5be29f9dac9 Mon Sep 17 00:00:00 2001 From: travis Date: Thu, 18 May 2017 16:16:57 +0000 Subject: [PATCH 28/73] test(ci): update code style outputs --- output/forms.eslint.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/output/forms.eslint.txt b/output/forms.eslint.txt index ca11d3c2323..2747315db77 100755 --- a/output/forms.eslint.txt +++ b/output/forms.eslint.txt @@ -124,6 +124,7 @@ The react/require-extension rule is deprecated. Please use the import/extensions /home/travis/build/Talend/ui/packages/forms/src/UIForm/UIForm.component.js 16:3 warning Unexpected console statement no-console 16:39 error Missing semicolon semi + 70:30 warning Unexpected console statement no-console /home/travis/build/Talend/ui/packages/forms/src/widgets/DatalistWidget/DatalistWidget.test.js 30:10 error Missing trailing comma comma-dangle @@ -137,7 +138,7 @@ The react/require-extension rule is deprecated. Please use the import/extensions 151:5 error Value must be omitted for boolean attributes react/jsx-boolean-value 172:5 error Value must be omitted for boolean attributes react/jsx-boolean-value -✖ 115 problems (114 errors, 1 warning) +✖ 116 problems (114 errors, 2 warnings) Errored while running command 'npm' with arguments 'run lint:es' in 'react-talend-forms' Errored while running ExecCommand.execute From 8a5e1498c45da369fcd7a7cdac3bfc985f56b199 Mon Sep 17 00:00:00 2001 From: Jimmy Somsanith Date: Thu, 18 May 2017 23:07:11 +0200 Subject: [PATCH 29/73] Uniformize callbacks args, proper trigger management --- packages/forms/BREAKING_CHANGES_LOG.md | 10 +++ packages/forms/src/UIForm/UIForm.component.js | 61 ++++++++++++------- .../src/UIForm/Widget/Widget.component.js | 4 +- packages/forms/src/UIForm/fields/Button.js | 8 +-- packages/forms/src/UIForm/utils/validation.js | 2 +- packages/forms/stories/index.js | 12 ++-- 6 files changed, 64 insertions(+), 33 deletions(-) diff --git a/packages/forms/BREAKING_CHANGES_LOG.md b/packages/forms/BREAKING_CHANGES_LOG.md index 36ff4c27a03..5be509f1f96 100644 --- a/packages/forms/BREAKING_CHANGES_LOG.md +++ b/packages/forms/BREAKING_CHANGES_LOG.md @@ -9,6 +9,16 @@ Schemas format changes TODO On change callback api change +TODO +status --> - +schema --> jsonSchema +uiSchema --> uiSchema +idSchema --> - +formData --> properties +edit --> - +errors --> - +errorSchema --> - + On trigger callback api change TODO diff --git a/packages/forms/src/UIForm/UIForm.component.js b/packages/forms/src/UIForm/UIForm.component.js index 03f9cbe82d1..2ed3a2a66b1 100644 --- a/packages/forms/src/UIForm/UIForm.component.js +++ b/packages/forms/src/UIForm/UIForm.component.js @@ -16,6 +16,7 @@ export default class UIForm extends React.Component { console.log(this.state.mergedSchema) this.onChange = this.onChange.bind(this); + this.onTrigger = this.onTrigger.bind(this); this.submit = this.submit.bind(this); } @@ -37,6 +38,7 @@ export default class UIForm extends React.Component { * Triggers the onTrigger and onChange if needed * - onChange : at each field change * - onTrigger : when schema.trigger : ['after'] + * @param event The event that triggered the callback * @param schema The field schema * @param value The new value */ @@ -44,9 +46,6 @@ export default class UIForm extends React.Component { const { formName, onChange, - onTrigger, - onFormChange, - onValidate, properties, customValidation, } = this.props; @@ -54,21 +53,38 @@ export default class UIForm extends React.Component { onChange(formName, schema, value, error); const { triggers } = schema; - if (onTrigger && triggers && triggers.indexOf(TRIGGER_AFTER) !== -1) { - onTrigger( - properties, // current properties values - schema, // field schema - value // field value - ) - .then(newForm => onFormChange( - formName, - newForm.jsonSchema, - newForm.uiSchema, - newForm.properties, - newForm.errors) - ) - .catch(({ errors }) => { console.log(errors); onValidate(formName, errors); }); + if (triggers && triggers.indexOf(TRIGGER_AFTER) !== -1) { + this.onTrigger(event, TRIGGER_AFTER, schema, value, properties); + } + } + + /** + * Triggers an onTrigger callback that is allowed to modify the form + * @param event The event that triggered the callback + * @param type The type of trigger + * @param schema The field schema + * @param value The field value + */ + onTrigger(event, type, schema, value) { + const { formName, onFormChange, onTrigger, onValidate, properties } = this.props; + if (!onTrigger) { + return; } + + onTrigger( + type, // type of trigger + schema, // field schema + value, // field value + properties, // current properties values + ) + .then(newForm => onFormChange( + formName, + newForm.jsonSchema, + newForm.uiSchema, + newForm.properties, + newForm.errors) + ) + .catch(({ errors }) => onValidate(formName, errors)); } /** @@ -90,15 +106,16 @@ export default class UIForm extends React.Component { } render() { - const { errors, formName, properties } = this.props; + const { autoComplete, errors, formName, properties } = this.props; return ( - + { this.state.mergedSchema.map((nextSchema, index) => ( customValidation : true */ @@ -138,7 +157,7 @@ if (process.env.NODE_ENV !== 'production') { onSubmit: PropTypes.func.isRequired, /** * User callback: Trigger > after callback. - * Prototype: function onTrigger(properties, schema, value) + * Prototype: function onTrigger(type, schema, value, properties) * This is executed on changes on fields with uiSchema > triggers : ['after'] */ onTrigger: PropTypes.func, diff --git a/packages/forms/src/UIForm/Widget/Widget.component.js b/packages/forms/src/UIForm/Widget/Widget.component.js index f6751b21629..475cb5f159a 100644 --- a/packages/forms/src/UIForm/Widget/Widget.component.js +++ b/packages/forms/src/UIForm/Widget/Widget.component.js @@ -4,7 +4,7 @@ import { sfPath } from 'talend-json-schema-form-core'; import widgets from '../utils/widgets'; import { getValue } from '../utils/properties'; -export default function Widget({ errors, formName, onChange, properties, schema }) { +export default function Widget({ errors, formName, onChange, onTrigger, properties, schema }) { const { key, type, validationMessage } = schema; const id = sfPath.name(key, '-', formName); const error = errors[key]; @@ -19,6 +19,7 @@ export default function Widget({ errors, formName, onChange, properties, schema formName={formName} isValid={!error} onChange={onChange} + onTrigger={onTrigger} properties={properties} schema={schema} errors={errors} @@ -32,6 +33,7 @@ if (process.env.NODE_ENV !== 'production') { errors: PropTypes.object, // eslint-disable-line react/forbid-prop-types formName: PropTypes.string, onChange: PropTypes.func, + onTrigger: PropTypes.func, schema: PropTypes.shape({ key: PropTypes.array, type: PropTypes.string.isRequired, diff --git a/packages/forms/src/UIForm/fields/Button.js b/packages/forms/src/UIForm/fields/Button.js index 59da46b20b1..be5013ca646 100644 --- a/packages/forms/src/UIForm/fields/Button.js +++ b/packages/forms/src/UIForm/fields/Button.js @@ -3,15 +3,15 @@ import React, { PropTypes } from 'react'; import Message from '../Message'; export default function Button(props) { - const { id, errorMessage, isValid, onChange, schema } = props; - const { description, title, type } = schema; + const { id, errorMessage, isValid, onTrigger, schema } = props; + const { description, title, triggers, type } = schema; return (
+, + "nodes": Array [ +
+ +
, + ], + "options": Object {}, + "renderer": ReactShallowRenderer { + "_instance": ShallowComponentWrapper { + "_calledComponentWillUnmount": false, + "_compositeType": 0, + "_context": Object {}, + "_currentElement": , + "_debugID": 1, + "_hostContainerInfo": null, + "_hostParent": null, + "_instance": UIForm { + "_reactInternalInstance": [Circular], + "context": Object {}, + "onChange": [Function], + "onTrigger": [Function], + "props": Object {}, + "refs": Object {}, + "state": Object { + "mergedSchema": Array [], + }, + "submit": [Function], + "updater": Object { + "enqueueCallback": [Function], + "enqueueCallbackInternal": [Function], + "enqueueElementInternal": [Function], + "enqueueForceUpdate": [Function], + "enqueueReplaceState": [Function], + "enqueueSetState": [Function], + "isMounted": [Function], + "validateCallback": [Function], + }, + }, + "_mountOrder": 1, + "_pendingCallbacks": null, + "_pendingElement": null, + "_pendingForceUpdate": false, + "_pendingReplaceState": false, + "_pendingStateQueue": null, + "_renderedComponent": NoopInternalComponent { + "_currentElement":
+ +
, + "_debugID": 2, + "_renderedOutput":
+ +
, + }, + "_renderedNodeType": 0, + "_rootNodeID": 0, + "_topLevelWrapper": null, + "_updateBatchNumber": null, + "_warnedAboutRefsInRender": false, + }, + "getRenderOutput": [Function], + "render": [Function], + }, + "root": [Circular], + "unrendered": , +} +`; diff --git a/packages/forms/src/UIForm/actions/form.actions.test.js b/packages/forms/src/UIForm/actions/form.actions.test.js index 6ccd2f4e568..8837f96a83a 100644 --- a/packages/forms/src/UIForm/actions/form.actions.test.js +++ b/packages/forms/src/UIForm/actions/form.actions.test.js @@ -14,7 +14,6 @@ const errors = ['errors']; describe('Form actions', () => { describe('#createForm action', () => { it('should test the action', () => { - // given // when const resultAction = createForm(formName, jsonSchema, uiSchema, properties, errors); diff --git a/packages/forms/src/UIForm/actions/validation.actions.test.js b/packages/forms/src/UIForm/actions/validation.actions.test.js index 4629a2dbfc2..5b68a631d02 100644 --- a/packages/forms/src/UIForm/actions/validation.actions.test.js +++ b/packages/forms/src/UIForm/actions/validation.actions.test.js @@ -1,7 +1,7 @@ -import { TF_VALIDATE_ALL, TF_VALIDATE_PARTIAL } from './constants'; +import { TF_SET_ALL_ERRORS, TF_SET_PARTIAL_ERROR } from './constants'; import { - validate, - validateAll, + setError, + setErrors, } from './validation.actions'; const formName = 'formName'; @@ -14,12 +14,12 @@ describe('Validation actions', () => { // given // when - const resultAction = validate(formName, error); + const resultAction = setError(formName, error); // then expect(resultAction).toEqual( { - type: TF_VALIDATE_PARTIAL, + type: TF_SET_PARTIAL_ERROR, errors: 'error', formName: 'formName', } @@ -32,12 +32,12 @@ describe('Validation actions', () => { // given // when - const resultAction = validateAll(formName, errors); + const resultAction = setErrors(formName, errors); // then expect(resultAction).toEqual( { - type: TF_VALIDATE_ALL, + type: TF_SET_ALL_ERRORS, errors: ['errors'], formName: 'formName', } diff --git a/packages/forms/src/widgets/ColumnsWidget/ColumnsWidget.test.js b/packages/forms/src/widgets/ColumnsWidget/ColumnsWidget.test.js index 72cad7c3e52..91394b0bac3 100644 --- a/packages/forms/src/widgets/ColumnsWidget/ColumnsWidget.test.js +++ b/packages/forms/src/widgets/ColumnsWidget/ColumnsWidget.test.js @@ -3,7 +3,7 @@ import { shallow } from 'enzyme'; import ColumnsWidget from './ColumnsWidget'; -import testSchema from '../../../stories/json/columns.json'; +import testSchema from '../../../stories/old-json/columns.json'; describe('ColumnsWidget', () => { it('should render', () => { From c0030ba829c998dce88f269563b7b424ccba86d0 Mon Sep 17 00:00:00 2001 From: travis Date: Fri, 2 Jun 2017 17:01:59 +0000 Subject: [PATCH 42/73] test(ci): update screenshots --- packages/theme/screenshots/form-inputs.png | Bin 20803 -> 20554 bytes packages/theme/screenshots/forms.png | Bin 31758 -> 31870 bytes packages/theme/screenshots/navbar-default.png | Bin 6867 -> 6821 bytes packages/theme/screenshots/navbar-inverse.png | Bin 8830 -> 8778 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/theme/screenshots/form-inputs.png b/packages/theme/screenshots/form-inputs.png index cdf0d10d7ee2e69dec28df935b7db5091e99a357..de9acdf6d0a851792fbf72c6c0cf34609999c695 100644 GIT binary patch literal 20554 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 WeXR%2uE8?`5$rXUt66flpZqVfGTFZX literal 20803 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 diff --git a/packages/theme/screenshots/forms.png b/packages/theme/screenshots/forms.png index 9f4b72e96fc66133cd1819d36ef5577dee686c11..17192029926a76691bf4d4b14310ffc1d75ecaa4 100644 GIT binary patch literal 31870 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 ZOheUc`l>DCFX84GwpQycuUTvl|1W~lQ^^1T literal 31758 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 Q3=zZ1+{P@^bl-`;0w42$wEzGB diff --git a/packages/theme/screenshots/navbar-default.png b/packages/theme/screenshots/navbar-default.png index cebe36da69eab707180fb42565d2646fa1b68f74..5fee83ab49e38149a8d62b1fe63bc3c4a5c34d80 100644 GIT binary patch literal 6821 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 diff --git a/packages/theme/screenshots/navbar-inverse.png b/packages/theme/screenshots/navbar-inverse.png index f1d3abce90e895436fc7473d53c74f4cd5464aa4..49a902313800dc96df20efcfb2d4104e4d77b49d 100644 GIT binary patch literal 8778 zcmd6NcTf}D_H6{DDM%5eNR^^g0ck;6=papcZx)JxK#*PyO*%*s2pCb34oa_5BqA+} z)X)(^=)ISBxc4{n-hJHbKL~jBqY_B9zktQZ4&YzyLFJx-c zs1f{qF|!?%GrTRoR>#D{Gk*M23U`*E)<{I8a5W&u*R3-oe0Qq41%@KMTa3M;Kt-fL zg}@xJ4w1-HA>3>PEY%hdSjd&tY$vQp!`Q$BK}^ORM$cKECddg7xE9Xwq1;sndm#Hn1z02-F3$22)qvrhYTxSpW z@@J3A*@!$3h{4HbCpN%Nv}B>g;&-KRxxdw8 z-?8m7crsI?=r(gIv~aIXG>k1Ol;9r@J42E8sZUwI<7>~8=tSKqAIkNIZ_$lwVAsS1 zoN!XR#BXNxiI`29>#q~r>YJz#5@3y(IyP#K8p`JH|LuL1<3}={QXES6sau{|d4sbO zUZQ~#39J2Q7TTChVyM~M>;KDweDD$zit_y|`e=p*sBOS(^R$We!c$?IJw_NLa#@&6xx{ z>3^oUF|_JBwMg-DJlptu7Hhm>sjfo41e*vuQ_(dG#8kw+0~gZv!dyWP5%w$4rb~ui zaVF)vMB8&*#5ZMVpk9SfSERl3u^vJ?CK1N66dul5!I@vkmlcH+k_{0k`=%6SkcT$v ze_FE1@+RTDsKZI}#KC+Tkun|meW$T^POi$K#l-m>vzwdmnR^o4?g_)6L)rw<-yVz4 z9aNF{crH1mKbgv?XH+fgYy*EAsWK^Bw`pIHhQN(I+pv5;jsO2R~Egv zEGAE++T+=6F2RKM!L6G7GW7i7{m)`97(7L7)()v#VBJv3wj>?(`d04X$IpKR_htOf z#K$%J6pFMHj9jESXuoWfPI2d>Ee(ekUo?Y=9;`nYm_PKZxAOKHEObF%K~_s_#Z=uh z=ci;n*xP-Yn{&4P^gnO~R5S<&sOTeFp`b6Eo9ldY5V2pV zZ*Lry%V?lGoENjO{NeP5zQN~aS&y+ZWh;1fMw~H7c}FHtVSTtmOxDHK&Rk1BickN9O{Rg7WJ z#%Cv@`uYE^cz=Y2YpIrQ{Z#HyXE*+Q&wE=hplX3?Eq`7C>l*UQ0j6Rlb@uf|L%X|< zxi3`({t$!=0&!^w#YRfNp8dV)jB*@66_!$`q^)_Huu$`S|M=OUQOZl~Ox%;O`EW*( z@ed;+Le<}()2L4$CF(K8XfRxqlwldm;HE4f#p{vq}^m|J7KbSv5QT#C~5g&6zwS?&2H5PjH0qaXXPm%LDlqO)`Npor06$dSAt1oYms4=awyO;xeonUROk}JkDsDr*~ua zN7-k|oCIZh)eaS0^p_wX zQ@s*z7=3fNe+d#)|010)JyV|?a;H%D1=djl{zA@}*WdwPv(ySNf5LnmNU&EEqks#v zv{3@abLZ~^mdDqpJ>Z$ILa3vWnrT%e?I>L_kUhh?wX|~&&9LO3(8y~AeJ$SJEF7CO z7PTp{QIZAb$IUERp|On~IrIUx0c=~{&M($#lp?*{I@R@xp3n9|*W)ppk7c}!St?1AR1gKB@<(2o9~ttinh4=Aluj%WrJc8~S3>!=>R;+1Regna3{sxK8hU(D|XpRgI!0 zuy|PYM9A+#zc)sMbG-#c4Hn!q<~Yhm8YHoyC!c|qfTZtj79lN@g%<&QdzRCy=dQnR zFXRd<^@meJ8AH=7})ysHfkUyss-CMp*lkIQmS)C z#eL~@u1|Z#!>6JEUVC;%NS1a7B;;wq@_fxT5P+xK}2b^BZK>{v6_$Q3%n#ks4wgBamF$!h}a3>A1@r_@c_{n9v z_`dp}9si~eziW4Zm3$$K6ja$f6W_yLyh+!n+n=LS8vSb`DBjvXr2Chn#b|KP%lIL&Yj572Qbm^}|JY*>5aUKl{8cN}y|kgdIIix8u*|<* zVK;U8jlqx9%u8eB;Y&q|7=)p82J)zDX4=MOvQsR>;a*pd(9q z^4ZFR*Qhcvn~1ZQ3LG7O7Z%=ESEudT>e3^gnCm%@Nw#iv{u#tn;(gjX8fc;7PfMj^ zNf<6`@6R}y8fZ{zPe!3*a{0e&(x%M}H+0o9ru6aY7wDqojoO(B$&hD?Zlp+_uz9*~ zBUigmQLa~*<9iB1csmb+Ecg}Mk!Tv6smc2s!ovPbRp|#$;2!V9&bx#YF?g>&6^&c* zJOC{&ilf)o*jJ`glXYr)q^&-^PrfOro(36VzG+HX)^gDG=#PZct)*^^EgV|)BaY;4 z?TwUHe-{wxTXg}ftU_b)>8#zy={)?)9&JyohvpN7&*;URX$8mCRq{SjtoMveS2jn+ zZlYtWP#7ktwt5@tS99r!Hoc{jtgT-8cS#9OiPEO8g@4kDEE~GzaV%Mn z?cc}hF41^$K`%!H?`uck3~}zHK6) zV(|u@w_5hT;UgVD^D*wsl7drOzb1yPC9FMM3}B=pfZWUMp_fgPLnoB*#A`Kv-x;U~$F>D>IU00wj^9ADfHNXUN6LYBI5lUw2PAWN#d zVQJC)dG;|OWV&SvN-49w^FYaNKfG`85DWQ+ub~FVxeO_l$Z(km#I4zY?yOsAUR#G8 zr-rVT;#@6PA9!5>9dSIdI2@}z-BUsY@bCc}BEGnuMbvm@Cg{wy+qUD`Uzim(gv3$G z=Wy;&JG>9QJ(P#4U^qW;@@=7mf_Bq0N#A+8(z1OOVBTkg&s4dlQPjC5)2UUKiLvI5 zOrRS8Ml`nUg%`|GDKWY&DoRTeO@zneb&CdB-173()zuZ8os0VSHEmgqR+Tsxt{{JH zSHFL6)U_Oq1&w~EvE;McpAEm7<}F7m&_^53xd69(4)vfT5geb}(}Cp&V~Sv^BIKB# zHg)^Q%{QURdLhkyyq?d?lw(3D#rVmB1HS|}5u~+M$sbzLKm(POOMc^pNB8%F=QeTr zxEa}xg3bK9bcDM{1)pIvJB{E1tZd0t!uzEnw*b>sax`peUtXrPgk4-Lm`d+h)ceefID6{RC_q1R(-kUBFW3ce&}JOPcIsxkY8otNZ80A9-k0NUao_qi6bp zp37nV8dN&e-8Y8)qqO=X)t;w{l#IPxJ1+(xC9OT71lGWj)i0q2rk2Wv!{ZMckPd^@ z_D7kz0gVnM2^l*N@+sauLd_1XYcHO79r>+*47$I)eCN#|tW?+Ca%$^mAv=xqO(@^~ zIr%rJ)QhmnMAf-DTl1OyM{G25)KI{Up(`zhSV{;fxl{rUeULY|nOM@dT{1E`9fE_Q z-A<_&cZk;r%Icew{8O;yEqGR_I0Q%$-2n_@mB-D`;FG7+Gq{zk>!vH@1@zLFd>lJX zgAlxaZ2j2U{K@O4mFTFHSOaZc7<{eGyD{dU={YccZx4x}tH_ERx$8xB7Y*Qt8C37q16}%0aU3W7h`Na#j(J1u@CA}boR+0xyz}Lb&2O4?56=ynY~cI(?8>dUVf{m0CMl7@!eYb zT;ELHx6qLNb@N#zv>*@g6>1>?C9lBSpGqEtWD<*kJEgtOV>&dyP+s&ypv1#$AywxU z)pZXRptypO%`E4qFQp-p%%9Rs+1l4clPIx1wgK;U-9EVBxYO3wB*!Oo00z>Y?Arji zbS*W+Dmuvmdafm4Ht8!c{*oX)Z{Wp-@$yX`O916Y!OaXFyQAcd4{RA=?Rm%`UCX-j z56iA6t^m z1H-+lGrt4@kp63Kn<{$ZCbT+7p^y9H+4E6Z$g~$aCMt#F{y6Vb5F1F@wFqEd9Z`=!LeJ3t_YXrgEVrw4c_IqnOy@IH97I1&`J(8M6= zhZl^us_*8Lwm)?3_=vN^Rq&k#{V>E`p~3b8W|&=j2H`1{n@-)uQbiCsvhQUdmY~st z`VXSbN>t3uGP~#L=;+k6w3Ib8$p3msxxIy<ns)=Ha*9PoMM-IC5pUjHzfMQzj%G{M&@(g)2fuQ1aVg}vuZ%rp$gmwXTJc|O zJUit5FrPNFtR?5!jfMM7)^rUFBRF`u(6-V!GPfdIVQ@ zLU4dye`~S~iLON`S0D!&(yB8kr?0Q;Zb92dv;q*GRLT74d>bv6Jj(pc(oK|7|>E}d>k-zZO!6}a~bh;~3wG^CD%tfv1?sw3oU0EX=j_YzWW5h_> zF2w#1-r^A8g_(xaWzbZzUUb~!DZVb=wq)jK8|h_3D7}G*{e|bPk>5|4LcvBRvn4^9H>f8-AV5h@B0|}pxe+d!Ednh}_-#xvzhSg< zf(|Mad+jf`q1BDqk={OYRGzL_Uc19Acg$}lsLlPizT6@l+OL-@7<|r`1$>@$66cFP zZ_o$YkQOES+yVmXIceB5cKsuv3D5ZDLFa^g z7_Ldm9P@7BS{zWxRP@vM^=6?NJ ze=@Tu_JBW4TEO!zFP#L-V~_3&M+UNSgo_W3S>cd+nO#4+>fec+d*LnD(62NLu!{*M zYfC7gjTu#IYBebw`{_r^o+wdZ0(~kY`(C~o zdVuYr07Ufmf}2#t1^qJpAr*P}d?mcETzmGT?o;HQ0r6X%5n78t*~kOd!<==U5ztnN z^Eej*PUBiWxRPBFo-kzbI=Xatk`!4H<9!LjEigfXohv*S3uc1Ga! zH3kh{_fC=5=iAs@;%hlTIE?C-3Mzl(s433mIkq}^eD|@KM)vC{Z^PQ|moGt}NXqEw=zCec7Iov}7WXA2+8yq52n#1kTvQZJ zJtI5&mVtr6gSw4h%j`*N?}oQoTgV~4zP_Ie3TA?Y1O$|Qee2LT1?s0J)pY(S(EvND z3*Kb2oI)a=-AJ7;nw>kxbUQxb zZUdPV#MI_74v9X>hZarG9~j!+Da#E#moj> z<8lp|n~rtD+ht1osXFO#VIK~7+L{!4)p@-R>$BWlb+F&Y>Ms7MEs)8@4bpJ??~2Q$ zi89mvz?RRA%9`-_5}pK#V>e=?3vLy^{OjEio8VW7bulz5I+R^9Segu&JM&oA z%g{oDv+6DtE*Jr)vK0vCjQUxgjwd(dQBIvdgodES&O%R>X1Su&`(OQfyr;HUYu2Aq z|Ah=G7hFdP7>qWejs}Wco&#VH$9^rAK1#t<^k!}MMiAaf9fs|cjS{LXj&ueFGX>Cm zG~AUbt;v~oVPs-TN=$^rsl3X}WCU$@=2xETEucj(%exK^4)Q80#BLLyeD7S>=}hP0 z;n_y$0K8<84OoP?AbFJf`ubE96<-a5GBq-(qD{&;AK4->jcdOt9Gu-q1JLAsy;U4& zH0g>2jnvR~9^#pkRn9wu0=k8ka@FH!RluGAERqbc$eah&4_MRQ_nwICTN7P34&CFG zHzw1>zfAa`mEQe6(u6A>)%v;!orZ0S$h1DL!ccSM9;|OVtx10UPT(#vz`7#_vn)rpzK0bbi_SHLGv28|m z{g@kotfA2{FC_z4>Pl?!6wE$ezda(=+kx3bFXP4se4*6#m5n`y8XpNIyM9F9w*85_ zsP{lj2=iUXhK7FzHwr~xSrFU=aP@30#!u(&b5KPhHu zCx8aOu|Jw61HG0sB6>kiaKEE9y8gEle$OdwPU&%9M^DDOH$ey*h933H`ID-(2=7eA zxTggRom8lVI5}`S?bp`Yr95G zrd_iw)t)|kmRDLzIXE~teoF$WZ)q7fZgFWCcM}=z zwSFtfXWB}Rf|)7g%aUJqEp_CuoOO7v;P^bnR}VTsiB$!s&_5iVU#yt%1Hg~0(>&(; zx5);ak9ST^t*MLmBae&vL>8*L_hk;U{0u~u&p9nR{Pm(6c@Q10t{Cb0>0 zR5(}ofb@gT7zy49^l*%?GOdzMxOQ;F#mIElRYOx#&%`7uo^rV(HZCqs?cqa&Rhg=$ z=9T1>lsfiDW+iD%K_3lxld`j~rgRadbRlME?Q-5!$PiD+_V)IwX=sGC+Ju1lgS~y; zz?W*2)tgq@i}~ljhhu@JLI5lK11o@E^DV01Xd1+oJ@A1c%<&2HWOY`1iWnGwbuwpt z?f>4V-gWoM*sCY^rfbeffoZzFxw-bqFhBCwLeWVCIbYer1kO z*lk-PYRhHdKb%h{?wKWe@_c$mSJpug#S2w-ikjLB2SQ0_EDSMJpZq|DUE3CyV!NN z|D?@-+qFut7}J;S{&$N+{^}e=ueBds9zA@v(VFg=G0dz$)gk|nVNTQ2$8pSlQ@bN5 zN7%bNMJ=dn;YTcSEwI&yuSy% z0scBh@=QY@%n&6x84Zv3n;9os>h70UQdfxneY8Y|n@MGQrJ&uWO0KEYz>^F&3)&Rj zLe25o56|-}pG&`*E)*!rd#)C_k}Bss(A|hkf*b?4KQpO@Y;#F znmxsa209YzD^rodW5W=$>ecpq|9l4X2?{2bQ6Z~N=4ErCCkbiEb)0+w4P*wd;~RJk zfvIecJO+v@B8>kwAQ2fHn_p>0w>BR2O8h2%R*S1Q%RfJwp<{D_n!Nd5>B54X_qcOY zGGzS_@y{%pg%wv|ZEOKHs|ZBruIe2h{Nc@z>&HL?9pX_e;Yygpze9p!=aH(%H;cvz zWTJzm&d`u}O9>f~EKq3gBRYk;aKA_YxhDie=sm%{#k-EN#`esmD{18oml{XH-0%jjqa;muiqv@b zi6>cN94TIglz#)sz*3E)A8C+|{{p9T(2r~ssc;nrWWv|p=~9>lT9i$I$IvN@+_|g& zjViDNw%w&ar*8nXu z&eKZWW4YY<&Fng!WM7+Adyi>F*(S}X{9iBK{9|uJ^=J}MCuTun7QEv@Pajm9PAv^k zUcEB?jUWHc(0WjEnDNl!g|-sF@=EjnV=6xWL|!i&xiIa`W%37f#5?^8DVyZ7V|z%+ z$Vs>9UdA8g&A5DzIynAQ!+X3l&YIe=m8WD-FyT%!8OaD{=InRbwqW#U3FY}JR#5V? zk0Hy}QFt{59akyUy!x8Tf_qGI!%uOAP*s~(zD^~8>ZiV}ZD@E+hvZ|auXMVO6Wf$I zKiw<8L?iVb^{d*^B^4Kwi)W;1@56}GFeNyJ$ebAGGK!CMT^TM*(Qr+b4|*qkBA+j%OQd{Q9X6Tgr}cXnEnzBfcW+vg5p^_m5_vJ}hs+Nf zC8_VACdhzC+yoG_rq$)2k77uDFinv@aS@LmH?44WkW15jthH{K-a!gF zpkJC%YTi3Mi;#z%ZAFI3 zPtuns<|}GnFd*G8gq#VzgZr*mowFk2>nuiTCt+D8@jy0sUFm4|@b~2LcsJJZ76(&> z+m9KV1Y30)Sy8=^zFu>06zX?C9SNgx`-`VOTXO+EVgW@jy^GQCH>qP{=57VA=W9)HzA?+n2syk+W+#`IZ(x*32+`dJyq4cGY z6ka9X|E_jzU_ZT{WkZ}QRPu3?4w5`W_xrg7VRVH-Y?6xSB@>sL`gAl?tbWzk`f4PO zp@Uc#0TMk&Zy8QE9(wN2pHuba=jY6t;Qvkv|Dp;(%7f+=DW^|L;~TX?QyU%IGj zgJ#Rn82nBhvKKL{oLbww({VNFPmPDZ2Em{QG2f zUdGHfxlBa|ryn=t-Q2GC1mI&!(jyok81i9ob?0D983yGc$S@bU>W2=1*!vD&Zf#hzYZxSnR4;ZChKJZMSJwJ0CW`WtujAVF+ z|AgtdZlqRV-h4Y$(l(=vHc-kBE3&5B3Iqlc+Y77)0; zo6_!h%x6ULTGCoo84V4Fcm{h>SaB5GH+0EytA@QWBRi!XSW(4)t_nN|4c2UA?FVj) z6Bw*OJVm23f(peIYw0_c1kB2$$d{p&o=PidE z3jiSkPFvoe;8?pDaF;rfp8C|nM74c?bm+F$y9*}T`j$buD0f8B218j&Usm!)%YKOc z@xU8{_lC}HYPV1CU#vVok{KI~NnZ^)Hu_1LDA2Gb>B8m3tabRYq80Z7zhKGh-`5CkLD%q03Wcw5eE<5CXe&eB-&1lBcmG zs12U7MWt1G*tAt5%qZz4urO^~|0hc}nJ25%O6;=n^qwZuzDaQ8tLUp=VRTNFBb(xv zwLVfd({Wk%u^=G5F~i9mg3MUE(4voPb2|stT#h@(vY27S&vM(xD>@fnK$1o-W>FJk#gpX_`5@YYyoV~_LCqh@tA zBp3OI1_AN|4(c?@i=ktZu0;e3-7l&=gJX~3*ogIzK5!;gzntw{e4ru@a3pZcYzpPA z`TaR}5bsLw&K>gknWQ^QuWW*2dHTz9io^rwe~pZFh35@Z*(m^8!*sBG3`Lp}_>jY^ z?oHV%9;ddo$-PZUarULXN!((tw;F$!x_NKt_hkHCFYVU0Fmm_}&8@oV{XnPk&XNkA zyWgn&gavo^EcJG~1MRAl9Z7_rp>@nN6+eSyEU+w%uspvuA7(eJdpqo-v}&o{_&+PSC5)d;gfT*##k@C z>-_%JZ2=sh24c0cy3e$zZ0Vd@%oTpVbSG_HTmMrS~GazFXNXE^^qUU!Z0AZ_r;5AoNd_DU)$n9i7GjZ3dsrBu^i6A}zRZXYoPTaNu{e?;dJKrqG0(V)?waoa>bLQ~;BpCc1P0_1-LQ%h z)GL-{u6VK1`@)|P((6Z8>bR~0G4cwc$@J!3Aimz#s#tpqdk%n=%Kq}Iq9>=*^16OB zf^0VP{DvTjApjYYBUZxcbW5B%X_l5jl99KN!CQN$n~9e*kia>q2R(kBVE`=(-vvso*^*(R{Jz$A^VEJ1_T6HB z!e-|xIbZVjZd-2p zlM=3|Zm8?IU|$ zV&xCF<+{aBg*p2gwcD6r;p77pA7o~UG?Hpr~2w;F1f-qcpnu z&}6rztclmVVV7T=4N6dhSTTheB3tetj|TOmneH5Vra#4cJ&G4^*FYSA@K}^ZM;u zj>t9452=0Hde6h^qlCz_6}Bmg*}JStzS7`~tSc9)!Y{85-nK;UoM^TRx1kV_Kj(4}N>8xrqy1DyS-*3N<& z3^6nO`bH};#zCcfHiI%`@cHmxF!)L@mf6GC@;3~2<2E({LL)UX?YeT72VPOt;^vRG z+&y`C;aC!##^%xiX+2L*HnCdwKe5PC(s>vcIpCZHxXr*Rd(?aTTk=*1ND4{}LKPLQ z6Fw<6#Fv;gY49;`e$2Fw`1dO+Oh@|&03f8@)UrqW6ZMA{jf?5-eEBM@w^Z3bdA)&* zNwn7>xH`EMfuu6zXlnCztbP|s^aVBH^DE(JhXUlvB<_qujJXCm-!Yd3Qw2KH(rg9+ zj1+9*K7x}}TNKmjb6rYA4euMiC%k=)I#G)sgH&QWfXQ~E6(5`-D3SzFl^%DH=&bEH z!KzxITr7-n>@6wziEQp5ok!MhKrT?6trOxw5nk{i`#VThZ!aI=IMw<}7h&i3u3-(2 zyT`a(0_V#T)I=Nys50ZD-cK`@cT=-(JwN=4Tpj+XkR56%Ct!1%&8C~Wd-Rrzq^|c( z$j)jGIcwuT&@x@2F|&WR##fbr1<`b`A0Z?aXWcik0HIllzk3gq&2Ke?3AtPfd^0Ou z7fI>kzNKu*yUG&e{B#beE+qm-Qu`*nl;#%T#OUUC6my}i35d=ev@=t|>!Sl~55uQM zNd~1C_XYNp9rvvNR5LjV9f!8T_M|gUQ1_lMCA@fX1scC3ve)RW; z6&LfF3HWL)&dr$whlR1Sv0;M`Ag@nPz2p=W=4V^OphQFuRaIf{(nlwI?ve}ZS5eF} z!X5l-2t02O9v`Ckdffnm4(#^!Br+Adc^-_$6(THuHXEio5XRIP>bIZX`^BO@lY}!~ zGMmR|@*BUzb{PjCHdt*FnRv>HN+Zay3Q0hzNv8<>R9x}FtC*sxL6P#{jy>yzW_Y70 zL`(bmR&iDs3ozK!P>XzQNWpIE{sB>u_PBL^UnCtULLa&%ta57mkimUw{ns`$-(A>p zz}A-g=-)RRCer}|>?uHzLJyuX7k{P&q8XMnY_i=Rt|of)tDuMJay!O{zisimbQJ}C*O^4$fIk-a_do5V!qFRN6i zN7;>*W&oc>26drZTlQ9!x}QIPW@fiAYAiiJ_tCF&BQhyXUG-!oi-0jO{M=|oapmRZ zK`}5eU{URm*Zcb}6Aj+OIqM&Gt`TaAWQEx`16?$9CM~Oso6mJHB*>yt{KC|h0?t$* z8aPCJ7@xmNa3EV0-LxqLWe8C9k1GQv5MIWsmOT87U`u4aIP}kZrjaQ<5`)8nfX#L( z%ot-RqZ_>Ew2bDOU$N$lnw!;2#g{CzD5?q>H}_FF|%qzee*Atw$ zS~s)Hp?>)5dWH6A^2`E6Ze9Vc;F6u3Y1GZAZ`Dsh68KO??7%Y_(=qgfd)(9b&-yP) z+n7h$h)0j`F|n|Ak9SQW(pp+n;63sep^8;jYAQ5B4zp4>=JjicV)io&JvBAbvT|H@ zkLb8K1!H3tbe%qLSW}bKP5h9q-NZLn0>DL;YVIVVi9YX^ZcTnU|+ z7%hL9Au%}zt{r|WuU7O$q- ze6~Dd)`v)KKTkVmwTMPP1(YE3-xOR6vd3e3>c7oFfS1bcD2WxI z+64JU9`WaMZufOoHq39geA+Y0>2f10tg=(5v6K8`q^?{p<-M#r&5Z*&AlHGxa_u*| zsr>=B`I3KPZE3+@BwySKD2v~3=^j&p`|S5Y(7wiZ7EN-^k(0f!E0Kg7E0naxcA zA47Da5<6e-3I{D6qPO-gtTy9WfK)ZytPKw-Vg%bGGnicwJI7 zw4BZ5#&BoYG$T!w<)~vc2hucEc)1RXO7;0LcC(b$Q);9s_0dH`9n;q}J$Zcx*5;`{ zgM-bTowz(aJa>3_uJi8P+#Ge0VH0r(B4BOJT25ZREHgbm{&i#|{?gIRjL{HV=i=gG zweMNg=zM5sXj%7Ett)|$kdT`pZiwOR*9>UzhIvmW_~Jc-78z{h>3Xjjz})^@jU2;AkvdP67eAb~<-*M6OU zpp?$??e^1O%Ji=JjLJR9bP}N*HuJhGI|=>o`%}5~>>pRa8G>6?{O=8^{5i|r_S*wl z=ckH14{S|q{9KWnx3pLt<-|N1tik zKnhYTBRcgjD|ee%t;0UCxZJS3>Th<#W0*JZ*^caXoU>+IMp!x$8`NL~pbqKC=FNqj z{k(fs`Z{oiIwsiKzS&QH8S?F$u+0?- zlce0EN0D8P8k|X8!9V=`{8qNMs!jp(5o>J*Gt0MFg~xPtYY+cSH{3$(nqFAPfGst? zu(hwgkrK?YKzveb#jAhRM+)WGx)=Zrh&+3%fbE}z^(TwG=k&?nX)Vm}G6>)l$>kU= zl}S|7X54sXn%*^$J)gAk@zMUvQ{=OMjJLc6@x*?uAoE^e(|t6^F;dEZyybT%|D{}3 z4igYPydfN=3-5&xmeuHzB@u88fgLHm?{Z0xg_A8*;2A*JNq;_iPxM=Ga4dhzV-D`V zX_MH|^*9y*Q4*bx_~$^_0rhrIw*0#J!Llp*;|{k1s1kwc)eMY4kVgY8<;co?Mm`oU z!xB=XfD!8<&n8%Av8GQYl7)}ZAHp(=;EeYurUr^!<%k;)X4`x4i}jGs*9wl};;e5{ zFYtm-i;Tv_5%EANf=BAdJZU|95>{fhE((U4{d{HD$Mud`p3|j6Q2)xVlWT~bg9CSU zb#-NPuy{_(&@c|9ryq+>6;)NY*w}(;ZVir(b`ds_MYTim@B(xT<$;zeD=X7}mebhC z2>gozQzbSsY#bb!FB*v40eO^=tS`-$ID)N{3m zJQhBsSPtA%c)td|kbTX(KX33!RJ>+s5NABqS@5V(t+({jY`~SklRG#}&Ydw2~{9%}B4Fvim3C z)Bffal^nfyt^tc(0Octx>5bIF4p+vLu;ok;p2nIaaPqF( zf!h}BOXnVM(CBpA_rw(!J-6eSw^Ckku#*s{-%MkM+Ry%bPuqJ&H*$xjXg!v>z#JsU zw^4LqzD!(S^UNRj$iQO%-t#@(cI1;krf+YAKrltG?*bevUNL}%d6Nfd3Jx(G$^$nN zNUrP72Jo;!vl5hEGZk0(4(!tG?390P52F~+VttEHrq$RJhGka;(Ca5v?OYFTx) zr-5x2E2*(xn*Nkt^3;pyKdm)IM|t?xVT+35t*jvKCHG()w&ASSLIT0vSuHka&}o+^ z_&+Zx4CUnHgfe#!g8yA#iGZ16{pA9|zivzXf4JQ6&y9%bzir_^HzFE$uP~@@+K%^9 TpvAzw5{S~nCvrsyqrm?Hed#7x From 64c6b9a0ae6ba23670aba32360748232e0175d95 Mon Sep 17 00:00:00 2001 From: travis Date: Fri, 2 Jun 2017 17:01:59 +0000 Subject: [PATCH 43/73] test(ci): update code style outputs --- output/cmf.eslint.txt | 2 +- output/components.eslint.txt | 2 +- output/components.sasslint.txt | 2 +- output/containers.eslint.txt | 2 +- output/forms.eslint.txt | 11 +++++------ output/forms.sasslint.txt | 2 +- output/logging.eslint.txt | 2 +- output/theme.sasslint.txt | 14 +++++++------- 8 files changed, 18 insertions(+), 19 deletions(-) diff --git a/output/cmf.eslint.txt b/output/cmf.eslint.txt index ce6e0c66b71..0fcbf9354f3 100644 --- a/output/cmf.eslint.txt +++ b/output/cmf.eslint.txt @@ -1,7 +1,7 @@ Lerna v2.0.0-beta.36 Scoping to packages that match 'react-cmf' -> react-cmf@0.74.2 lint:es /home/travis/build/Talend/ui/packages/cmf +> react-cmf@0.75.0 lint:es /home/travis/build/Talend/ui/packages/cmf > eslint --config .eslintrc --ext .js src The react/require-extension rule is deprecated. Please use the import/extensions rule from eslint-plugin-import instead. diff --git a/output/components.eslint.txt b/output/components.eslint.txt index fe9dd74b49a..79d63686d80 100644 --- a/output/components.eslint.txt +++ b/output/components.eslint.txt @@ -1,7 +1,7 @@ Lerna v2.0.0-beta.36 Scoping to packages that match 'react-talend-components' -> react-talend-components@0.74.2 lint:es /home/travis/build/Talend/ui/packages/components +> react-talend-components@0.75.0 lint:es /home/travis/build/Talend/ui/packages/components > eslint --config .eslintrc src diff --git a/output/components.sasslint.txt b/output/components.sasslint.txt index c36b5695c6a..6133209d889 100644 --- a/output/components.sasslint.txt +++ b/output/components.sasslint.txt @@ -1,6 +1,6 @@ Lerna v2.0.0-beta.36 Scoping to packages that match 'react-talend-components' -> react-talend-components@0.74.2 lint:style /home/travis/build/Talend/ui/packages/components +> react-talend-components@0.75.0 lint:style /home/travis/build/Talend/ui/packages/components > sass-lint -v -q diff --git a/output/containers.eslint.txt b/output/containers.eslint.txt index b8f76d6b044..f31409e75a8 100644 --- a/output/containers.eslint.txt +++ b/output/containers.eslint.txt @@ -1,7 +1,7 @@ Lerna v2.0.0-beta.36 Scoping to packages that match 'react-talend-containers' -> react-talend-containers@0.74.2 lint:es /home/travis/build/Talend/ui/packages/containers +> react-talend-containers@0.75.0 lint:es /home/travis/build/Talend/ui/packages/containers > eslint --config .eslintrc src The react/require-extension rule is deprecated. Please use the import/extensions rule from eslint-plugin-import instead. diff --git a/output/forms.eslint.txt b/output/forms.eslint.txt index 4c9b62a8bec..282e96247ce 100755 --- a/output/forms.eslint.txt +++ b/output/forms.eslint.txt @@ -1,7 +1,7 @@ Lerna v2.0.0-beta.36 Scoping to packages that match 'react-talend-forms' -> react-talend-forms@0.74.2 lint:es /home/travis/build/Talend/ui/packages/forms +> react-talend-forms@0.75.0 lint:es /home/travis/build/Talend/ui/packages/forms > eslint --config .eslintrc src The react/require-extension rule is deprecated. Please use the import/extensions rule from eslint-plugin-import instead. @@ -114,14 +114,13 @@ The react/require-extension rule is deprecated. Please use the import/extensions 71:21 error Missing trailing comma comma-dangle 77:16 error 'formContext' PropType is defined but prop is never used react/no-unused-prop-types +/home/travis/build/Talend/ui/packages/forms/src/UIForm/actions/form.actions.test.js + 16:38 error Block must not be padded by blank lines padded-blocks + /home/travis/build/Talend/ui/packages/forms/src/UIForm/actions/model.actions.js 3:1 error Prefer default export import/prefer-default-export -/home/travis/build/Talend/ui/packages/forms/src/UIForm/UIForm.component.js - 16:3 warning Unexpected console statement no-console - 16:39 error Missing semicolon semi - -✖ 103 problems (102 errors, 1 warning) +✖ 102 problems (102 errors, 0 warnings) Errored while running command 'npm' with arguments 'run lint:es' in 'react-talend-forms' Errored while running ExecCommand.execute diff --git a/output/forms.sasslint.txt b/output/forms.sasslint.txt index 8dd4c6947f0..ffbd24cdc1f 100644 --- a/output/forms.sasslint.txt +++ b/output/forms.sasslint.txt @@ -1,6 +1,6 @@ Lerna v2.0.0-beta.36 Scoping to packages that match 'react-talend-forms' -> react-talend-forms@0.74.2 lint:style /home/travis/build/Talend/ui/packages/forms +> react-talend-forms@0.75.0 lint:style /home/travis/build/Talend/ui/packages/forms > sass-lint -v -q diff --git a/output/logging.eslint.txt b/output/logging.eslint.txt index a9ab5255963..c605a34f62f 100644 --- a/output/logging.eslint.txt +++ b/output/logging.eslint.txt @@ -1,7 +1,7 @@ Lerna v2.0.0-beta.36 Scoping to packages that match 'talend-log' -> talend-log@0.74.2 lint:es /home/travis/build/Talend/ui/packages/logging +> talend-log@0.75.0 lint:es /home/travis/build/Talend/ui/packages/logging > eslint --config .eslintrc --ext .js src diff --git a/output/theme.sasslint.txt b/output/theme.sasslint.txt index 356215e247f..50334823f6c 100644 --- a/output/theme.sasslint.txt +++ b/output/theme.sasslint.txt @@ -1,7 +1,7 @@ Lerna v2.0.0-beta.36 Scoping to packages that match 'bootstrap-talend-theme' -> bootstrap-talend-theme@0.74.2 lint:style /home/travis/build/Talend/ui/packages/theme +> bootstrap-talend-theme@0.75.0 lint:style /home/travis/build/Talend/ui/packages/theme > sass-lint -q -v @@ -16,12 +16,12 @@ src/theme/_forms.scss 24:8 error Qualifying elements are not allowed for class selectors no-qualifying-elements 41:10 error Qualifying elements are not allowed for class selectors no-qualifying-elements 42:7 error Qualifying elements are not allowed for class selectors no-qualifying-elements - 79:8 error Qualifying elements are not allowed for class selectors no-qualifying-elements - 262:8 error Qualifying elements are not allowed for class selectors no-qualifying-elements - 275:8 error Qualifying elements are not allowed for class selectors no-qualifying-elements - 288:8 error Qualifying elements are not allowed for class selectors no-qualifying-elements - 311:9 error Qualifying elements are not allowed for class selectors no-qualifying-elements - 447:8 warning Vendor prefixes should not be used no-vendor-prefixes + 84:8 error Qualifying elements are not allowed for class selectors no-qualifying-elements + 267:8 error Qualifying elements are not allowed for class selectors no-qualifying-elements + 280:8 error Qualifying elements are not allowed for class selectors no-qualifying-elements + 293:8 error Qualifying elements are not allowed for class selectors no-qualifying-elements + 316:9 error Qualifying elements are not allowed for class selectors no-qualifying-elements + 457:8 warning Vendor prefixes should not be used no-vendor-prefixes ✖ 12 problems (11 errors, 1 warning) From e341f8796fa849ad56564f64fc0b382c0f7adaf4 Mon Sep 17 00:00:00 2001 From: Jimmy Somsanith Date: Fri, 2 Jun 2017 19:15:18 +0200 Subject: [PATCH 44/73] Unit tests --- .../__snapshots__/form.actions.test.js.snap | 48 +++++ .../__snapshots__/model.actions.test.js.snap | 13 ++ .../validation.actions.test.js.snap | 17 ++ .../src/UIForm/actions/form.actions.test.js | 38 +--- .../src/UIForm/actions/model.actions.test.js | 15 +- .../UIForm/actions/validation.actions.test.js | 25 +-- .../__snapshots__/form.reducer.test.js.snap | 125 ++++++++++++ .../forms/src/UIForm/reducers/form.reducer.js | 1 + .../src/UIForm/reducers/form.reducer.test.js | 188 ++++++++++++++++++ 9 files changed, 406 insertions(+), 64 deletions(-) create mode 100644 packages/forms/src/UIForm/actions/__snapshots__/form.actions.test.js.snap create mode 100644 packages/forms/src/UIForm/actions/__snapshots__/model.actions.test.js.snap create mode 100644 packages/forms/src/UIForm/actions/__snapshots__/validation.actions.test.js.snap create mode 100644 packages/forms/src/UIForm/reducers/__snapshots__/form.reducer.test.js.snap create mode 100644 packages/forms/src/UIForm/reducers/form.reducer.test.js diff --git a/packages/forms/src/UIForm/actions/__snapshots__/form.actions.test.js.snap b/packages/forms/src/UIForm/actions/__snapshots__/form.actions.test.js.snap new file mode 100644 index 00000000000..3d683dbd490 --- /dev/null +++ b/packages/forms/src/UIForm/actions/__snapshots__/form.actions.test.js.snap @@ -0,0 +1,48 @@ +exports[`Form actions #changeForm action should create the action payload 1`] = ` +Object { + "errors": Object { + "field": "errors", + }, + "formName": "formName", + "jsonSchema": Object { + "jsonSchema": "json", + }, + "properties": Object { + "props": "json", + }, + "type": "TF_CHANGE_FORM", + "uiSchema": Array [ + Object { + "uiSchema": "json", + }, + ], +} +`; + +exports[`Form actions #createForm action should create the action payload 1`] = ` +Object { + "errors": Object { + "field": "errors", + }, + "formName": "formName", + "jsonSchema": Object { + "jsonSchema": "json", + }, + "properties": Object { + "props": "json", + }, + "type": "TF_CREATE_FORM", + "uiSchema": Array [ + Object { + "uiSchema": "json", + }, + ], +} +`; + +exports[`Form actions #removeForm action should create the action payload 1`] = ` +Object { + "formName": "formName", + "type": "TF_REMOVE_FORM", +} +`; diff --git a/packages/forms/src/UIForm/actions/__snapshots__/model.actions.test.js.snap b/packages/forms/src/UIForm/actions/__snapshots__/model.actions.test.js.snap new file mode 100644 index 00000000000..6caacfb203c --- /dev/null +++ b/packages/forms/src/UIForm/actions/__snapshots__/model.actions.test.js.snap @@ -0,0 +1,13 @@ +exports[`Model actions #mutateValue action should create the action payload 1`] = ` +Object { + "error": "error", + "formName": "formName", + "schema": Object { + "jsonSchema": "json", + }, + "type": "TF_MUTATE_VALUE", + "value": Object { + "props": "json", + }, +} +`; diff --git a/packages/forms/src/UIForm/actions/__snapshots__/validation.actions.test.js.snap b/packages/forms/src/UIForm/actions/__snapshots__/validation.actions.test.js.snap new file mode 100644 index 00000000000..2e2a48700ae --- /dev/null +++ b/packages/forms/src/UIForm/actions/__snapshots__/validation.actions.test.js.snap @@ -0,0 +1,17 @@ +exports[`Validation actions #validate action should create the action payload 1`] = ` +Object { + "errors": "error", + "formName": "formName", + "type": "TF_SET_PARTIAL_ERROR", +} +`; + +exports[`Validation actions #validateAll action should create the action payload 1`] = ` +Object { + "errors": Array [ + "errors", + ], + "formName": "formName", + "type": "TF_SET_ALL_ERRORS", +} +`; diff --git a/packages/forms/src/UIForm/actions/form.actions.test.js b/packages/forms/src/UIForm/actions/form.actions.test.js index 8837f96a83a..85780f43e1b 100644 --- a/packages/forms/src/UIForm/actions/form.actions.test.js +++ b/packages/forms/src/UIForm/actions/form.actions.test.js @@ -1,4 +1,3 @@ -import { TF_CREATE_FORM, TF_CHANGE_FORM, TF_REMOVE_FORM } from './constants'; import { changeForm, createForm, @@ -7,63 +6,42 @@ import { const formName = 'formName'; const jsonSchema = { jsonSchema: 'json' }; -const uiSchema = { uiSchema: 'json' }; +const uiSchema = [{ uiSchema: 'json' }]; const properties = { props: 'json' }; -const errors = ['errors']; +const errors = { field: 'errors' }; describe('Form actions', () => { describe('#createForm action', () => { - it('should test the action', () => { - + it('should create the action payload', () => { // when const resultAction = createForm(formName, jsonSchema, uiSchema, properties, errors); // then - expect(resultAction).toEqual( - { - type: TF_CREATE_FORM, - errors: ['errors'], - formName: 'formName', - jsonSchema: { jsonSchema: 'json' }, - properties: { props: 'json' }, - uiSchema: { uiSchema: 'json' }, - } - ); + expect(resultAction).toMatchSnapshot(); }); }); describe('#changeForm action', () => { - it('should test the action', () => { + it('should create the action payload', () => { // given // when const resultAction = changeForm(formName, jsonSchema, uiSchema, properties, errors); // then - expect(resultAction).toEqual( - { - type: TF_CHANGE_FORM, - errors: ['errors'], - formName: 'formName', - jsonSchema: { jsonSchema: 'json' }, - properties: { props: 'json' }, - uiSchema: { uiSchema: 'json' }, - } - ); + expect(resultAction).toMatchSnapshot(); }); }); describe('#removeForm action', () => { - it('should test the action', () => { + it('should create the action payload', () => { // given // when const resultAction = removeForm(formName, jsonSchema, uiSchema, properties, errors); // then - expect(resultAction).toEqual( - { formName: 'formName', type: TF_REMOVE_FORM } - ); + expect(resultAction).toMatchSnapshot(); }); }); }); diff --git a/packages/forms/src/UIForm/actions/model.actions.test.js b/packages/forms/src/UIForm/actions/model.actions.test.js index 216ea2b938f..abcf46dd457 100644 --- a/packages/forms/src/UIForm/actions/model.actions.test.js +++ b/packages/forms/src/UIForm/actions/model.actions.test.js @@ -1,4 +1,3 @@ -import { TF_MUTATE_VALUE } from './constants'; import { mutateValue, } from './model.actions'; @@ -10,22 +9,12 @@ const error = 'error'; describe('Model actions', () => { describe('#mutateValue action', () => { - it('should test the action', () => { - // given - + it('should create the action payload', () => { // when const resultAction = mutateValue(formName, jsonSchema, value, error); // then - expect(resultAction).toEqual( - { - type: TF_MUTATE_VALUE, - error: 'error', - formName: 'formName', - schema: { jsonSchema: 'json' }, - value: { props: 'json' }, - } - ); + expect(resultAction).toMatchSnapshot(); }); }); }); diff --git a/packages/forms/src/UIForm/actions/validation.actions.test.js b/packages/forms/src/UIForm/actions/validation.actions.test.js index 5b68a631d02..c356f30acdb 100644 --- a/packages/forms/src/UIForm/actions/validation.actions.test.js +++ b/packages/forms/src/UIForm/actions/validation.actions.test.js @@ -1,4 +1,3 @@ -import { TF_SET_ALL_ERRORS, TF_SET_PARTIAL_ERROR } from './constants'; import { setError, setErrors, @@ -10,38 +9,22 @@ const errors = ['errors']; describe('Validation actions', () => { describe('#validate action', () => { - it('should test the action', () => { - // given - + it('should create the action payload', () => { // when const resultAction = setError(formName, error); // then - expect(resultAction).toEqual( - { - type: TF_SET_PARTIAL_ERROR, - errors: 'error', - formName: 'formName', - } - ); + expect(resultAction).toMatchSnapshot(); }); }); describe('#validateAll action', () => { - it('should test the action', () => { - // given - + it('should create the action payload', () => { // when const resultAction = setErrors(formName, errors); // then - expect(resultAction).toEqual( - { - type: TF_SET_ALL_ERRORS, - errors: ['errors'], - formName: 'formName', - } - ); + expect(resultAction).toMatchSnapshot(); }); }); }); diff --git a/packages/forms/src/UIForm/reducers/__snapshots__/form.reducer.test.js.snap b/packages/forms/src/UIForm/reducers/__snapshots__/form.reducer.test.js.snap new file mode 100644 index 00000000000..5119a2603ac --- /dev/null +++ b/packages/forms/src/UIForm/reducers/__snapshots__/form.reducer.test.js.snap @@ -0,0 +1,125 @@ +exports[`Form reducers #TF_CHANGE_FORM should replace the forms configurations 1`] = ` +Object { + "existingFormName": Object { + "errors": Object { + "field": "errors", + }, + "jsonSchema": Object { + "jsonSchema": "json", + }, + "properties": Object { + "props": "json", + }, + "uiSchema": Array [ + Object { + "uiSchema": "json", + }, + ], + }, +} +`; + +exports[`Form reducers #TF_CREATE_FORM should not create new form 1`] = ` +Object { + "existingFormName": Object { + "errors": Object { + "field": "oldError", + }, + "jsonSchema": Object { + "jsonSchema": "oldJson", + }, + "properties": Object { + "props": "oldJson", + }, + "uiSchema": Array [ + Object { + "uiSchema": "oldJson", + }, + ], + }, + "formName": Object { + "errors": Object { + "field": "errors", + }, + "jsonSchema": Object { + "jsonSchema": "json", + }, + "properties": Object { + "props": "json", + }, + "uiSchema": Array [ + Object { + "uiSchema": "json", + }, + ], + }, +} +`; + +exports[`Form reducers #TF_MUTATE_VALUE should mutate value 1`] = ` +Object { + "existingFormName": Object { + "errors": Object { + "field": "oldError", + }, + "jsonSchema": Object { + "jsonSchema": "oldJson", + }, + "properties": Object { + "props": "newJson", + }, + "uiSchema": Array [ + Object { + "uiSchema": "oldJson", + }, + ], + }, +} +`; + +exports[`Form reducers #TF_REMOVE_FORM should remove form 1`] = `Object {}`; + +exports[`Form reducers #TF_SET_ALL_ERRORS should replace all errors 1`] = ` +Object { + "existingFormName": Object { + "errors": Object { + "field1": "new error 1", + "field2": "new error 2", + }, + "jsonSchema": Object { + "jsonSchema": "oldJson", + }, + "properties": Object { + "props": "oldJson", + }, + "uiSchema": Array [ + Object { + "uiSchema": "oldJson", + }, + ], + }, +} +`; + +exports[`Form reducers #TF_SET_PARTIAL_ERROR should set errors 1`] = ` +Object { + "existingFormName": Object { + "errors": Object { + "field": "oldError", + "field1": "new error 1", + "field2": "new error 2", + }, + "jsonSchema": Object { + "jsonSchema": "oldJson", + }, + "properties": Object { + "props": "oldJson", + }, + "uiSchema": Array [ + Object { + "uiSchema": "oldJson", + }, + ], + }, +} +`; diff --git a/packages/forms/src/UIForm/reducers/form.reducer.js b/packages/forms/src/UIForm/reducers/form.reducer.js index 88ad2ff6b40..f833258dcc0 100644 --- a/packages/forms/src/UIForm/reducers/form.reducer.js +++ b/packages/forms/src/UIForm/reducers/form.reducer.js @@ -68,6 +68,7 @@ export default function formReducer(state = {}, action) { return { ...state, [action.formName]: { + ...form, properties: modelReducer(form.properties, action), errors: validationsReducer(form.errors, action), }, diff --git a/packages/forms/src/UIForm/reducers/form.reducer.test.js b/packages/forms/src/UIForm/reducers/form.reducer.test.js new file mode 100644 index 00000000000..77408e33ecc --- /dev/null +++ b/packages/forms/src/UIForm/reducers/form.reducer.test.js @@ -0,0 +1,188 @@ +import formReducer from './form.reducer'; +import { + TF_CREATE_FORM, + TF_CHANGE_FORM, + TF_REMOVE_FORM, + TF_MUTATE_VALUE, + TF_SET_ALL_ERRORS, + TF_SET_PARTIAL_ERROR, +} from '../actions'; + +const formName = 'formName'; +const jsonSchema = { jsonSchema: 'json' }; +const uiSchema = [{ uiSchema: 'json' }]; +const properties = { props: 'json' }; +const errors = { field: 'errors' }; + +const oldState = { + existingFormName: { + jsonSchema: { jsonSchema: 'oldJson' }, + uiSchema: [{ uiSchema: 'oldJson' }], + properties: { props: 'oldJson' }, + errors: { field: 'oldError' }, + }, +}; + +describe('Form reducers', () => { + describe('#TF_CREATE_FORM', () => { + it('should not create existing form', () => { + // given + const action = { + type: TF_CREATE_FORM, + formName: 'existingFormName', + jsonSchema, + uiSchema, + properties, + errors, + }; + + // when + const state = formReducer(oldState, action); + + // then + expect(state).toBe(oldState); + }); + + it('should not create new form', () => { + // given + const action = { + type: TF_CREATE_FORM, + formName, + jsonSchema, + uiSchema, + properties, + errors, + }; + + // when + const state = formReducer(oldState, action); + + // then + expect(state).toMatchSnapshot(); + }); + }); + + describe('#TF_CHANGE_FORM', () => { + it('should not replace anything when form does not exist', () => { + // given + const action = { + type: TF_CHANGE_FORM, + formName, + }; + + // when + const state = formReducer(oldState, action); + + // then + expect(state).toBe(oldState); + }); + + it('should not replace anything when there is no provided replacement', () => { + // given + const action = { + type: TF_CHANGE_FORM, + formName: 'existingFormName', + }; + + // when + const state = formReducer(oldState, action); + + // then + expect(state).toBe(oldState); + }); + + it('should replace the forms configurations', () => { + // given + const action = { + type: TF_CHANGE_FORM, + formName: 'existingFormName', + jsonSchema, + uiSchema, + properties, + errors, + }; + + // when + const state = formReducer(oldState, action); + + // then + expect(state).toMatchSnapshot(); + }); + }); + + describe('#TF_REMOVE_FORM', () => { + it('should remove form', () => { + // given + const action = { + type: TF_REMOVE_FORM, + formName: 'existingFormName', + }; + + // when + const state = formReducer(oldState, action); + + // then + expect(state).toMatchSnapshot(); + }); + }); + + describe('#TF_MUTATE_VALUE', () => { + it('should mutate value', () => { + // given + const action = { + type: TF_MUTATE_VALUE, + formName: 'existingFormName', + schema: { + key: ['props'], + }, + value: 'newJson', + }; + + // when + const state = formReducer(oldState, action); + + // then + expect(state).toMatchSnapshot(); + }); + }); + + describe('#TF_SET_ALL_ERRORS', () => { + it('should replace all errors', () => { + // given + const action = { + type: TF_SET_ALL_ERRORS, + formName: 'existingFormName', + errors: { + field1: 'new error 1', + field2: 'new error 2', + }, + }; + + // when + const state = formReducer(oldState, action); + + // then + expect(state).toMatchSnapshot(); + }); + }); + + describe('#TF_SET_PARTIAL_ERROR', () => { + it('should set errors', () => { + // given + const action = { + type: TF_SET_PARTIAL_ERROR, + formName: 'existingFormName', + errors: { + field1: 'new error 1', + field2: 'new error 2', + }, + }; + + // when + const state = formReducer(oldState, action); + + // then + expect(state).toMatchSnapshot(); + }); + }); +}); From 82b44b8bc733f8e2d72fa62dd2acf1a7671829b9 Mon Sep 17 00:00:00 2001 From: travis Date: Sun, 4 Jun 2017 16:16:12 +0000 Subject: [PATCH 45/73] test(ci): update code style outputs --- output/forms.eslint.txt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/output/forms.eslint.txt b/output/forms.eslint.txt index 282e96247ce..1c04d9808d9 100755 --- a/output/forms.eslint.txt +++ b/output/forms.eslint.txt @@ -114,13 +114,10 @@ The react/require-extension rule is deprecated. Please use the import/extensions 71:21 error Missing trailing comma comma-dangle 77:16 error 'formContext' PropType is defined but prop is never used react/no-unused-prop-types -/home/travis/build/Talend/ui/packages/forms/src/UIForm/actions/form.actions.test.js - 16:38 error Block must not be padded by blank lines padded-blocks - /home/travis/build/Talend/ui/packages/forms/src/UIForm/actions/model.actions.js 3:1 error Prefer default export import/prefer-default-export -✖ 102 problems (102 errors, 0 warnings) +✖ 101 problems (101 errors, 0 warnings) Errored while running command 'npm' with arguments 'run lint:es' in 'react-talend-forms' Errored while running ExecCommand.execute From 74dd53a29b812d4148ca893f76d699bbab7d19df Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Mon, 5 Jun 2017 10:55:57 +0200 Subject: [PATCH 46/73] Unit tests on model reducers --- .../__snapshots__/model.reducer.test.js.snap | 17 +++++++ .../src/UIForm/reducers/model.reducer.test.js | 45 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 packages/forms/src/UIForm/reducers/__snapshots__/model.reducer.test.js.snap create mode 100644 packages/forms/src/UIForm/reducers/model.reducer.test.js diff --git a/packages/forms/src/UIForm/reducers/__snapshots__/model.reducer.test.js.snap b/packages/forms/src/UIForm/reducers/__snapshots__/model.reducer.test.js.snap new file mode 100644 index 00000000000..5fdf48190e0 --- /dev/null +++ b/packages/forms/src/UIForm/reducers/__snapshots__/model.reducer.test.js.snap @@ -0,0 +1,17 @@ +exports[`Model reducers #TF_MUTATE_VALUE should mutate nested value 1`] = ` +Object { + "props": "oldProps", + "user": Object { + "firstname": "newName", + }, +} +`; + +exports[`Model reducers #TF_MUTATE_VALUE should mutate simple value 1`] = ` +Object { + "props": "newProps", + "user": Object { + "firstname": "oldName", + }, +} +`; diff --git a/packages/forms/src/UIForm/reducers/model.reducer.test.js b/packages/forms/src/UIForm/reducers/model.reducer.test.js new file mode 100644 index 00000000000..110e3fb9a86 --- /dev/null +++ b/packages/forms/src/UIForm/reducers/model.reducer.test.js @@ -0,0 +1,45 @@ +import modelReducer from './model.reducer'; +import { TF_MUTATE_VALUE } from '../actions'; + +const oldState = { + props: 'oldProps', + user: { firstname: 'oldName' }, +}; + +describe('Model reducers', () => { + describe('#TF_MUTATE_VALUE', () => { + it('should mutate simple value', () => { + // given + const action = { + type: TF_MUTATE_VALUE, + schema: { + key: ['props'], + }, + value: 'newProps', + }; + + // when + const state = modelReducer(oldState, action); + + // then + expect(state).toMatchSnapshot(); + }); + + it('should mutate nested value', () => { + // given + const action = { + type: TF_MUTATE_VALUE, + schema: { + key: ['user', 'firstname'], + }, + value: 'newName', + }; + + // when + const state = modelReducer(oldState, action); + + // then + expect(state).toMatchSnapshot(); + }); + }); +}); From 343c0976775cd9ed6ce21797306bb4850093fa12 Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Mon, 5 Jun 2017 11:02:52 +0200 Subject: [PATCH 47/73] Unit tests on validations reducers --- .../validations.reducer.test.js.snap | 14 ++++++ .../reducers/validations.reducer.test.js | 49 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 packages/forms/src/UIForm/reducers/__snapshots__/validations.reducer.test.js.snap create mode 100644 packages/forms/src/UIForm/reducers/validations.reducer.test.js diff --git a/packages/forms/src/UIForm/reducers/__snapshots__/validations.reducer.test.js.snap b/packages/forms/src/UIForm/reducers/__snapshots__/validations.reducer.test.js.snap new file mode 100644 index 00000000000..4bd58a681fa --- /dev/null +++ b/packages/forms/src/UIForm/reducers/__snapshots__/validations.reducer.test.js.snap @@ -0,0 +1,14 @@ +exports[`Validations reducers #TF_SET_ALL_ERRORS should replace all errors 1`] = ` +Object { + "field1": "new error 1", + "field2": "new error 2", +} +`; + +exports[`Validations reducers #TF_SET_PARTIAL_ERROR should set errors 1`] = ` +Object { + "field": "oldError", + "field1": "new error 1", + "field2": "new error 2", +} +`; diff --git a/packages/forms/src/UIForm/reducers/validations.reducer.test.js b/packages/forms/src/UIForm/reducers/validations.reducer.test.js new file mode 100644 index 00000000000..a27ab00a5d6 --- /dev/null +++ b/packages/forms/src/UIForm/reducers/validations.reducer.test.js @@ -0,0 +1,49 @@ +import validationsReducer from './validations.reducer'; +import { + TF_SET_ALL_ERRORS, + TF_SET_PARTIAL_ERROR, +} from '../actions'; + +const oldState = { + field: 'oldError', +}; + +describe('Validations reducers', () => { + describe('#TF_SET_ALL_ERRORS', () => { + it('should replace all errors', () => { + // given + const action = { + type: TF_SET_ALL_ERRORS, + errors: { + field1: 'new error 1', + field2: 'new error 2', + }, + }; + + // when + const state = validationsReducer(oldState, action); + + // then + expect(state).toMatchSnapshot(); + }); + }); + + describe('#TF_SET_PARTIAL_ERROR', () => { + it('should set errors', () => { + // given + const action = { + type: TF_SET_PARTIAL_ERROR, + errors: { + field1: 'new error 1', + field2: 'new error 2', + }, + }; + + // when + const state = validationsReducer(oldState, action); + + // then + expect(state).toMatchSnapshot(); + }); + }); +}); From bb6ff0214f670348787344975ad7ce93dcf02378 Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Mon, 5 Jun 2017 12:25:53 +0200 Subject: [PATCH 48/73] OnFormChange --> updateForm --- packages/forms/src/UIForm/UIForm.component.js | 8 +++---- packages/forms/src/UIForm/UIForm.connect.js | 12 +++++----- packages/forms/src/UIForm/UIForm.container.js | 10 ++++----- .../__snapshots__/form.actions.test.js.snap | 22 +++++++++---------- .../forms/src/UIForm/actions/constants.js | 2 +- .../forms/src/UIForm/actions/form.actions.js | 18 +++++++-------- .../src/UIForm/actions/form.actions.test.js | 6 ++--- packages/forms/src/UIForm/actions/index.js | 2 +- .../forms/src/UIForm/reducers/form.reducer.js | 2 ++ .../stories/{json => old-json}/listView.json | 0 10 files changed, 42 insertions(+), 40 deletions(-) rename packages/forms/stories/{json => old-json}/listView.json (100%) diff --git a/packages/forms/src/UIForm/UIForm.component.js b/packages/forms/src/UIForm/UIForm.component.js index 57787b2ae56..8359a419226 100644 --- a/packages/forms/src/UIForm/UIForm.component.js +++ b/packages/forms/src/UIForm/UIForm.component.js @@ -65,7 +65,7 @@ export default class UIForm extends React.Component { * @param value The field value */ onTrigger(event, type, schema, value) { - const { formName, onFormChange, onTrigger, setError, properties } = this.props; + const { formName, updateForm, onTrigger, setError, properties } = this.props; if (!onTrigger) { return; } @@ -76,7 +76,7 @@ export default class UIForm extends React.Component { value, // field value properties, // current properties values ) - .then(newForm => onFormChange( + .then(newForm => updateForm( formName, newForm.jsonSchema, newForm.uiSchema, @@ -172,11 +172,11 @@ if (process.env.NODE_ENV !== 'production') { /** State management impl: The change callback */ onChange: PropTypes.func.isRequired, - /** State management impl: The form change callback */ - onFormChange: PropTypes.func.isRequired, /** State management impl: Set Partial fields validation error */ setError: PropTypes.func, /** State management impl: Set All fields validations errors */ setErrors: PropTypes.func, + /** State management impl: The form update callback */ + updateForm: PropTypes.func.isRequired, }; } diff --git a/packages/forms/src/UIForm/UIForm.connect.js b/packages/forms/src/UIForm/UIForm.connect.js index 4cd082b2035..c9c5057135b 100644 --- a/packages/forms/src/UIForm/UIForm.connect.js +++ b/packages/forms/src/UIForm/UIForm.connect.js @@ -5,8 +5,8 @@ import UIFormComponent from './UIForm.component'; import { createForm, - changeForm, removeForm, + updateForm, mutateValue, setError, setErrors, @@ -79,9 +79,9 @@ class UIForm extends React.Component { widgets={this.props.widgets} onChange={this.onChange} - onFormChange={this.props.onFormChange} setError={this.props.setError} setErrors={this.props.setErrors} + updateForm={this.props.updateForm} /> ); } @@ -158,10 +158,10 @@ if (process.env.NODE_ENV !== 'production') { */ mutateValue: PropTypes.func, /** - * Form change action. + * Form update action. * This is injected by react-redux. See mapDispatchToProps */ - onFormChange: PropTypes.func, + updateForm: PropTypes.func, /** * Partial form validation action. * This is injected by react-redux. See mapDispatchToProps @@ -192,8 +192,8 @@ function mapDispatchToProps(dispatch) { return { createForm: bindActionCreators(createForm, dispatch), removeForm: bindActionCreators(removeForm, dispatch), - mutateValue: bindActionCreators(mutateValue, dispatch), - onFormChange: bindActionCreators(changeForm, dispatch), + mutateValue: bindActionCreators(mutateValue, dispatch), // TODO updateFormData + updateForm: bindActionCreators(updateForm, dispatch), // TODO updateForm setError: bindActionCreators(setError, dispatch), setErrors: bindActionCreators(setErrors, dispatch), }; diff --git a/packages/forms/src/UIForm/UIForm.container.js b/packages/forms/src/UIForm/UIForm.container.js index 6d6fbd7fa98..a6926dc83b9 100644 --- a/packages/forms/src/UIForm/UIForm.container.js +++ b/packages/forms/src/UIForm/UIForm.container.js @@ -2,7 +2,7 @@ import React, { PropTypes } from 'react'; import UIFormComponent from './UIForm.component'; import { formReducer, modelReducer, validationReducer } from './reducers'; -import { createForm, changeForm, mutateValue, setError, setErrors } from './actions'; +import { createForm, updateForm, mutateValue, setError, setErrors } from './actions'; export default class UIForm extends React.Component { constructor(props) { @@ -17,7 +17,7 @@ export default class UIForm extends React.Component { this.state = formReducer(undefined, action)[this.props.formName]; this.onChange = this.onChange.bind(this); - this.onFormChange = this.onFormChange.bind(this); + this.updateForm = this.updateForm.bind(this); this.setError = this.setError.bind(this); this.setErrors = this.setErrors.bind(this); } @@ -56,8 +56,8 @@ export default class UIForm extends React.Component { * @param values The values * @param errors The validation errors */ - onFormChange(formName, schema, values, errors) { - const action = changeForm(formName, schema, values, errors); + updateForm(formName, schema, values, errors) { + const action = updateForm(formName, schema, values, errors); const nextState = formReducer( { [formName]: this.state }, action @@ -104,9 +104,9 @@ export default class UIForm extends React.Component { widgets={this.props.widgets} onChange={this.onChange} - onFormChange={this.onFormChange} setError={this.setError} setErrors={this.setErrors} + updateForm={this.updateForm} /> ); } diff --git a/packages/forms/src/UIForm/actions/__snapshots__/form.actions.test.js.snap b/packages/forms/src/UIForm/actions/__snapshots__/form.actions.test.js.snap index 3d683dbd490..46b53e9768c 100644 --- a/packages/forms/src/UIForm/actions/__snapshots__/form.actions.test.js.snap +++ b/packages/forms/src/UIForm/actions/__snapshots__/form.actions.test.js.snap @@ -1,4 +1,4 @@ -exports[`Form actions #changeForm action should create the action payload 1`] = ` +exports[`Form actions #createForm action should create the action payload 1`] = ` Object { "errors": Object { "field": "errors", @@ -10,7 +10,7 @@ Object { "properties": Object { "props": "json", }, - "type": "TF_CHANGE_FORM", + "type": "TF_CREATE_FORM", "uiSchema": Array [ Object { "uiSchema": "json", @@ -19,7 +19,14 @@ Object { } `; -exports[`Form actions #createForm action should create the action payload 1`] = ` +exports[`Form actions #removeForm action should create the action payload 1`] = ` +Object { + "formName": "formName", + "type": "TF_REMOVE_FORM", +} +`; + +exports[`Form actions #updateForm action should create the action payload 1`] = ` Object { "errors": Object { "field": "errors", @@ -31,7 +38,7 @@ Object { "properties": Object { "props": "json", }, - "type": "TF_CREATE_FORM", + "type": "TF_UPDATE_FORM", "uiSchema": Array [ Object { "uiSchema": "json", @@ -39,10 +46,3 @@ Object { ], } `; - -exports[`Form actions #removeForm action should create the action payload 1`] = ` -Object { - "formName": "formName", - "type": "TF_REMOVE_FORM", -} -`; diff --git a/packages/forms/src/UIForm/actions/constants.js b/packages/forms/src/UIForm/actions/constants.js index 9f2265b12e6..20cd6a05ff2 100644 --- a/packages/forms/src/UIForm/actions/constants.js +++ b/packages/forms/src/UIForm/actions/constants.js @@ -2,5 +2,5 @@ export const TF_MUTATE_VALUE = 'TF_MUTATE_VALUE'; export const TF_SET_ALL_ERRORS = 'TF_SET_ALL_ERRORS'; export const TF_SET_PARTIAL_ERROR = 'TF_SET_PARTIAL_ERROR'; export const TF_CREATE_FORM = 'TF_CREATE_FORM'; -export const TF_CHANGE_FORM = 'TF_CHANGE_FORM'; export const TF_REMOVE_FORM = 'TF_REMOVE_FORM'; +export const TF_UPDATE_FORM = 'TF_UPDATE_FORM'; diff --git a/packages/forms/src/UIForm/actions/form.actions.js b/packages/forms/src/UIForm/actions/form.actions.js index 44db86f8213..acf9484399c 100644 --- a/packages/forms/src/UIForm/actions/form.actions.js +++ b/packages/forms/src/UIForm/actions/form.actions.js @@ -1,4 +1,4 @@ -import { TF_CREATE_FORM, TF_CHANGE_FORM, TF_REMOVE_FORM } from './constants'; +import { TF_CREATE_FORM, TF_UPDATE_FORM, TF_REMOVE_FORM } from './constants'; export function createForm(formName, jsonSchema, uiSchema, properties, errors) { return { @@ -11,20 +11,20 @@ export function createForm(formName, jsonSchema, uiSchema, properties, errors) { }; } -export function changeForm(formName, jsonSchema, uiSchema, properties, errors) { +export function removeForm(formName) { return { - type: TF_CHANGE_FORM, + type: TF_REMOVE_FORM, formName, - jsonSchema, - uiSchema, - properties, - errors, }; } -export function removeForm(formName) { +export function updateForm(formName, jsonSchema, uiSchema, properties, errors) { return { - type: TF_REMOVE_FORM, + type: TF_UPDATE_FORM, formName, + jsonSchema, + uiSchema, + properties, + errors, }; } diff --git a/packages/forms/src/UIForm/actions/form.actions.test.js b/packages/forms/src/UIForm/actions/form.actions.test.js index 85780f43e1b..9bc0b925f37 100644 --- a/packages/forms/src/UIForm/actions/form.actions.test.js +++ b/packages/forms/src/UIForm/actions/form.actions.test.js @@ -1,7 +1,7 @@ import { - changeForm, createForm, removeForm, + updateForm, } from './form.actions'; const formName = 'formName'; @@ -21,12 +21,12 @@ describe('Form actions', () => { }); }); - describe('#changeForm action', () => { + describe('#updateForm action', () => { it('should create the action payload', () => { // given // when - const resultAction = changeForm(formName, jsonSchema, uiSchema, properties, errors); + const resultAction = updateForm(formName, jsonSchema, uiSchema, properties, errors); // then expect(resultAction).toMatchSnapshot(); diff --git a/packages/forms/src/UIForm/actions/index.js b/packages/forms/src/UIForm/actions/index.js index d8e0ab48490..9a0fc818f0a 100644 --- a/packages/forms/src/UIForm/actions/index.js +++ b/packages/forms/src/UIForm/actions/index.js @@ -1,4 +1,4 @@ -export { createForm, changeForm, removeForm } from './form.actions'; +export { createForm, removeForm, updateForm } from './form.actions'; export { mutateValue } from './model.actions'; export { setError, setErrors } from './validation.actions'; export * from './constants'; diff --git a/packages/forms/src/UIForm/reducers/form.reducer.js b/packages/forms/src/UIForm/reducers/form.reducer.js index f833258dcc0..2827a46dc10 100644 --- a/packages/forms/src/UIForm/reducers/form.reducer.js +++ b/packages/forms/src/UIForm/reducers/form.reducer.js @@ -14,6 +14,8 @@ import validationsReducer from './validations.reducer'; * Form reducer, that manage multiple form state, identified by their formName * Format : { * [formName]: { + * jsonSchema: {}, + * uiSchema: [], * properties: {}, * errors: {}, * }, diff --git a/packages/forms/stories/json/listView.json b/packages/forms/stories/old-json/listView.json similarity index 100% rename from packages/forms/stories/json/listView.json rename to packages/forms/stories/old-json/listView.json From 84c9436c9f663179e50f880296ba8fc83bcd6244 Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Mon, 5 Jun 2017 12:31:13 +0200 Subject: [PATCH 49/73] mutateValue --> updateFormData --- packages/forms/src/UIForm/UIForm.connect.js | 16 ++++---- packages/forms/src/UIForm/UIForm.container.js | 38 +++++++++---------- .../__snapshots__/model.actions.test.js.snap | 4 +- .../forms/src/UIForm/actions/constants.js | 2 +- packages/forms/src/UIForm/actions/index.js | 2 +- .../forms/src/UIForm/actions/model.actions.js | 6 +-- .../src/UIForm/actions/model.actions.test.js | 8 ++-- .../__snapshots__/form.reducer.test.js.snap | 10 ++--- .../__snapshots__/model.reducer.test.js.snap | 4 +- .../forms/src/UIForm/reducers/form.reducer.js | 8 ++-- .../src/UIForm/reducers/model.reducer.js | 4 +- .../UIForm/reducers/validations.reducer.js | 4 +- 12 files changed, 52 insertions(+), 54 deletions(-) diff --git a/packages/forms/src/UIForm/UIForm.connect.js b/packages/forms/src/UIForm/UIForm.connect.js index c9c5057135b..de80da3d42a 100644 --- a/packages/forms/src/UIForm/UIForm.connect.js +++ b/packages/forms/src/UIForm/UIForm.connect.js @@ -7,7 +7,7 @@ import { createForm, removeForm, updateForm, - mutateValue, + updateFormData, setError, setErrors, } from './actions'; @@ -46,7 +46,7 @@ class UIForm extends React.Component { * @param error The validation error */ onChange(formName, schema, value, error) { - this.props.mutateValue( + this.props.updateFormData( formName, schema, value, @@ -153,15 +153,15 @@ if (process.env.NODE_ENV !== 'production') { */ removeForm: PropTypes.func, /** - * Value mutation action. + * Form update action. * This is injected by react-redux. See mapDispatchToProps */ - mutateValue: PropTypes.func, + updateForm: PropTypes.func, /** - * Form update action. + * Value mutation action. * This is injected by react-redux. See mapDispatchToProps */ - updateForm: PropTypes.func, + updateFormData: PropTypes.func, /** * Partial form validation action. * This is injected by react-redux. See mapDispatchToProps @@ -192,8 +192,8 @@ function mapDispatchToProps(dispatch) { return { createForm: bindActionCreators(createForm, dispatch), removeForm: bindActionCreators(removeForm, dispatch), - mutateValue: bindActionCreators(mutateValue, dispatch), // TODO updateFormData - updateForm: bindActionCreators(updateForm, dispatch), // TODO updateForm + updateFormData: bindActionCreators(updateFormData, dispatch), + updateForm: bindActionCreators(updateForm, dispatch), setError: bindActionCreators(setError, dispatch), setErrors: bindActionCreators(setErrors, dispatch), }; diff --git a/packages/forms/src/UIForm/UIForm.container.js b/packages/forms/src/UIForm/UIForm.container.js index a6926dc83b9..cdbfb9bab17 100644 --- a/packages/forms/src/UIForm/UIForm.container.js +++ b/packages/forms/src/UIForm/UIForm.container.js @@ -2,7 +2,7 @@ import React, { PropTypes } from 'react'; import UIFormComponent from './UIForm.component'; import { formReducer, modelReducer, validationReducer } from './reducers'; -import { createForm, updateForm, mutateValue, setError, setErrors } from './actions'; +import { createForm, updateForm, updateFormData, setError, setErrors } from './actions'; export default class UIForm extends React.Component { constructor(props) { @@ -31,7 +31,7 @@ export default class UIForm extends React.Component { * @param error The validation error */ onChange(formName, schema, value, error) { - const action = mutateValue(formName, schema, value, error); + const action = updateFormData(formName, schema, value, error); this.setState( { properties: modelReducer(this.state.properties, action), @@ -49,23 +49,6 @@ export default class UIForm extends React.Component { ); } - /** - * Update the form, the model and errors - * @param formName The form name - * @param schema The schema - * @param values The values - * @param errors The validation errors - */ - updateForm(formName, schema, values, errors) { - const action = updateForm(formName, schema, values, errors); - const nextState = formReducer( - { [formName]: this.state }, - action - )[formName]; - - this.setState(nextState); - } - /** * Set partial fields validation in state * @param formName the form name @@ -86,6 +69,23 @@ export default class UIForm extends React.Component { this.setState({ errors: validationReducer(this.state.errors, action) }); } + /** + * Update the form, the model and errors + * @param formName The form name + * @param schema The schema + * @param values The values + * @param errors The validation errors + */ + updateForm(formName, schema, values, errors) { + const action = updateForm(formName, schema, values, errors); + const nextState = formReducer( + { [formName]: this.state }, + action + )[formName]; + + this.setState(nextState); + } + render() { const { jsonSchema, uiSchema, properties, errors } = this.state; diff --git a/packages/forms/src/UIForm/actions/__snapshots__/model.actions.test.js.snap b/packages/forms/src/UIForm/actions/__snapshots__/model.actions.test.js.snap index 6caacfb203c..7fb2deeabd3 100644 --- a/packages/forms/src/UIForm/actions/__snapshots__/model.actions.test.js.snap +++ b/packages/forms/src/UIForm/actions/__snapshots__/model.actions.test.js.snap @@ -1,11 +1,11 @@ -exports[`Model actions #mutateValue action should create the action payload 1`] = ` +exports[`Model actions #updateFormData action should create the action payload 1`] = ` Object { "error": "error", "formName": "formName", "schema": Object { "jsonSchema": "json", }, - "type": "TF_MUTATE_VALUE", + "type": "TF_UPDATE_FORM_DATA", "value": Object { "props": "json", }, diff --git a/packages/forms/src/UIForm/actions/constants.js b/packages/forms/src/UIForm/actions/constants.js index 20cd6a05ff2..f8943cf6e7e 100644 --- a/packages/forms/src/UIForm/actions/constants.js +++ b/packages/forms/src/UIForm/actions/constants.js @@ -1,4 +1,4 @@ -export const TF_MUTATE_VALUE = 'TF_MUTATE_VALUE'; +export const TF_UPDATE_FORM_DATA = 'TF_UPDATE_FORM_DATA'; export const TF_SET_ALL_ERRORS = 'TF_SET_ALL_ERRORS'; export const TF_SET_PARTIAL_ERROR = 'TF_SET_PARTIAL_ERROR'; export const TF_CREATE_FORM = 'TF_CREATE_FORM'; diff --git a/packages/forms/src/UIForm/actions/index.js b/packages/forms/src/UIForm/actions/index.js index 9a0fc818f0a..6cece87d150 100644 --- a/packages/forms/src/UIForm/actions/index.js +++ b/packages/forms/src/UIForm/actions/index.js @@ -1,4 +1,4 @@ export { createForm, removeForm, updateForm } from './form.actions'; -export { mutateValue } from './model.actions'; +export { updateFormData } from './model.actions'; export { setError, setErrors } from './validation.actions'; export * from './constants'; diff --git a/packages/forms/src/UIForm/actions/model.actions.js b/packages/forms/src/UIForm/actions/model.actions.js index c848a9895fa..d9e8c37e2e3 100644 --- a/packages/forms/src/UIForm/actions/model.actions.js +++ b/packages/forms/src/UIForm/actions/model.actions.js @@ -1,8 +1,8 @@ -import { TF_MUTATE_VALUE } from './constants'; +import { TF_UPDATE_FORM_DATA } from './constants'; -export function mutateValue(formName, schema, value, error) { +export function updateFormData(formName, schema, value, error) { return { - type: TF_MUTATE_VALUE, + type: TF_UPDATE_FORM_DATA, error, formName, schema, diff --git a/packages/forms/src/UIForm/actions/model.actions.test.js b/packages/forms/src/UIForm/actions/model.actions.test.js index abcf46dd457..ce941d98b56 100644 --- a/packages/forms/src/UIForm/actions/model.actions.test.js +++ b/packages/forms/src/UIForm/actions/model.actions.test.js @@ -1,6 +1,4 @@ -import { - mutateValue, -} from './model.actions'; +import { updateFormData } from './model.actions'; const formName = 'formName'; const jsonSchema = { jsonSchema: 'json' }; @@ -8,10 +6,10 @@ const value = { props: 'json' }; const error = 'error'; describe('Model actions', () => { - describe('#mutateValue action', () => { + describe('#updateFormData action', () => { it('should create the action payload', () => { // when - const resultAction = mutateValue(formName, jsonSchema, value, error); + const resultAction = updateFormData(formName, jsonSchema, value, error); // then expect(resultAction).toMatchSnapshot(); diff --git a/packages/forms/src/UIForm/reducers/__snapshots__/form.reducer.test.js.snap b/packages/forms/src/UIForm/reducers/__snapshots__/form.reducer.test.js.snap index 5119a2603ac..12341097494 100644 --- a/packages/forms/src/UIForm/reducers/__snapshots__/form.reducer.test.js.snap +++ b/packages/forms/src/UIForm/reducers/__snapshots__/form.reducer.test.js.snap @@ -2,17 +2,17 @@ exports[`Form reducers #TF_CHANGE_FORM should replace the forms configurations 1 Object { "existingFormName": Object { "errors": Object { - "field": "errors", + "field": "oldError", }, "jsonSchema": Object { - "jsonSchema": "json", + "jsonSchema": "oldJson", }, "properties": Object { - "props": "json", + "props": "oldJson", }, "uiSchema": Array [ Object { - "uiSchema": "json", + "uiSchema": "oldJson", }, ], }, @@ -66,7 +66,7 @@ Object { "jsonSchema": "oldJson", }, "properties": Object { - "props": "newJson", + "props": "oldJson", }, "uiSchema": Array [ Object { diff --git a/packages/forms/src/UIForm/reducers/__snapshots__/model.reducer.test.js.snap b/packages/forms/src/UIForm/reducers/__snapshots__/model.reducer.test.js.snap index 5fdf48190e0..3d6bf137333 100644 --- a/packages/forms/src/UIForm/reducers/__snapshots__/model.reducer.test.js.snap +++ b/packages/forms/src/UIForm/reducers/__snapshots__/model.reducer.test.js.snap @@ -2,14 +2,14 @@ exports[`Model reducers #TF_MUTATE_VALUE should mutate nested value 1`] = ` Object { "props": "oldProps", "user": Object { - "firstname": "newName", + "firstname": "oldName", }, } `; exports[`Model reducers #TF_MUTATE_VALUE should mutate simple value 1`] = ` Object { - "props": "newProps", + "props": "oldProps", "user": Object { "firstname": "oldName", }, diff --git a/packages/forms/src/UIForm/reducers/form.reducer.js b/packages/forms/src/UIForm/reducers/form.reducer.js index 2827a46dc10..9b5e4197101 100644 --- a/packages/forms/src/UIForm/reducers/form.reducer.js +++ b/packages/forms/src/UIForm/reducers/form.reducer.js @@ -1,8 +1,8 @@ import { TF_CREATE_FORM, - TF_CHANGE_FORM, TF_REMOVE_FORM, - TF_MUTATE_VALUE, + TF_UPDATE_FORM, + TF_UPDATE_FORM_DATA, TF_SET_ALL_ERRORS, TF_SET_PARTIAL_ERROR, } from '../actions'; @@ -39,7 +39,7 @@ export default function formReducer(state = {}, action) { }, }; } - case TF_CHANGE_FORM: { + case TF_UPDATE_FORM: { const form = state[action.formName]; const { jsonSchema, uiSchema, properties, errors } = action; if (!form || (!jsonSchema && !uiSchema && !properties && !errors)) { @@ -60,7 +60,7 @@ export default function formReducer(state = {}, action) { } case TF_REMOVE_FORM: return omit(state, action.formName); - case TF_MUTATE_VALUE: + case TF_UPDATE_FORM_DATA: case TF_SET_ALL_ERRORS: case TF_SET_PARTIAL_ERROR: { const form = state[action.formName]; diff --git a/packages/forms/src/UIForm/reducers/model.reducer.js b/packages/forms/src/UIForm/reducers/model.reducer.js index c0837059d6b..ec441beb154 100644 --- a/packages/forms/src/UIForm/reducers/model.reducer.js +++ b/packages/forms/src/UIForm/reducers/model.reducer.js @@ -1,4 +1,4 @@ -import { TF_MUTATE_VALUE } from '../actions'; +import { TF_UPDATE_FORM_DATA } from '../actions'; /** * Mutate the properties, setting the value in the path identified by key @@ -27,7 +27,7 @@ function mutateValue(properties, key, value) { */ export default function modelReducer(state = {}, action) { switch (action.type) { - case TF_MUTATE_VALUE: + case TF_UPDATE_FORM_DATA: return mutateValue(state, action.schema.key, action.value); default: return state; diff --git a/packages/forms/src/UIForm/reducers/validations.reducer.js b/packages/forms/src/UIForm/reducers/validations.reducer.js index 870e6748102..fd044b304c0 100644 --- a/packages/forms/src/UIForm/reducers/validations.reducer.js +++ b/packages/forms/src/UIForm/reducers/validations.reducer.js @@ -1,4 +1,4 @@ -import { TF_MUTATE_VALUE, TF_SET_ALL_ERRORS, TF_SET_PARTIAL_ERROR } from '../actions'; +import { TF_UPDATE_FORM_DATA, TF_SET_ALL_ERRORS, TF_SET_PARTIAL_ERROR } from '../actions'; import { omit } from '../utils/properties'; /** @@ -8,7 +8,7 @@ import { omit } from '../utils/properties'; */ export default function validations(state = {}, action) { switch (action.type) { - case TF_MUTATE_VALUE: { + case TF_UPDATE_FORM_DATA: { const { schema, error } = action; if (error) { return { From 75459f72ece339603bd8af5838cb50d63903ecb3 Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Mon, 5 Jun 2017 15:00:00 +0200 Subject: [PATCH 50/73] Utils unit tests --- packages/forms/src/UIForm/fieldsets/Tabs.js | 2 +- .../forms/src/UIForm/utils/properties.test.js | 58 ++++++ packages/forms/src/UIForm/utils/validation.js | 2 +- .../forms/src/UIForm/utils/validation.test.js | 173 ++++++++++++++++++ 4 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 packages/forms/src/UIForm/utils/properties.test.js create mode 100644 packages/forms/src/UIForm/utils/validation.test.js diff --git a/packages/forms/src/UIForm/fieldsets/Tabs.js b/packages/forms/src/UIForm/fieldsets/Tabs.js index 6a35bc5e1df..5c2374794f5 100644 --- a/packages/forms/src/UIForm/fieldsets/Tabs.js +++ b/packages/forms/src/UIForm/fieldsets/Tabs.js @@ -2,7 +2,7 @@ import React, { PropTypes } from 'react'; import { Tabs as RBTabs, Tab as RBTab } from 'react-bootstrap'; import Fieldset from './Fieldset'; -import isValid from '../utils/validation'; +import { isValid } from '../utils/validation'; import theme from './Tabs.scss'; export default function Tabs(props) { diff --git a/packages/forms/src/UIForm/utils/properties.test.js b/packages/forms/src/UIForm/utils/properties.test.js new file mode 100644 index 00000000000..2c64f6fce49 --- /dev/null +++ b/packages/forms/src/UIForm/utils/properties.test.js @@ -0,0 +1,58 @@ +import { getValue, omit } from './properties'; + +describe('Properties utils', () => { + describe('getValue', () => { + it('should return undefined when key is falsy', () => { + // given + const properties = { + user: { + firstname: 'toto', + lastname: 'tata', + }, + }; + + // when + const value = getValue(properties, null); + + // then + expect(value).toBeUndefined(); + }); + + it('should return the requested value', () => { + // given + const properties = { + user: { + firstname: 'toto', + lastname: 'tata', + }, + }; + const key = ['user', 'firstname']; + + // when + const value = getValue(properties, key); + + // then + expect(value).toBe('toto'); + }); + }); + + describe('omit', () => { + it('should copy all properties except the omitted ones', () => { + // given + const properties = { + toKeep: 'toto', + toBeOmitted: 'tata', + other: 'titi', + }; + + // when + const result = omit(properties, 'toBeOmitted'); + + // then + expect(result).toEqual({ + toKeep: 'toto', + other: 'titi', + }); + }); + }); +}); diff --git a/packages/forms/src/UIForm/utils/validation.js b/packages/forms/src/UIForm/utils/validation.js index 2f43a95ad11..c839b2d1489 100644 --- a/packages/forms/src/UIForm/utils/validation.js +++ b/packages/forms/src/UIForm/utils/validation.js @@ -54,7 +54,7 @@ export function validateAll(mergedSchema, properties, customValidationFn) { * @param errors The errors * @returns {boolean} true if it is invalid, false otherwise */ -export default function isValid(schema, errors) { +export function isValid(schema, errors) { const { key, items } = schema; if (key && errors[key]) { return false; diff --git a/packages/forms/src/UIForm/utils/validation.test.js b/packages/forms/src/UIForm/utils/validation.test.js new file mode 100644 index 00000000000..7a1b27a6526 --- /dev/null +++ b/packages/forms/src/UIForm/utils/validation.test.js @@ -0,0 +1,173 @@ +import { validateValue, validateAll, isValid } from './validation'; + +const customError = 'This field is invalid'; +function customValidationFn() { + return customError; +} + +describe('Validation utils', () => { + describe('#validateValue', () => { + it('should validate schema static definition', () => { + // given + const schema = { + key: ['firstname'], + customValidation: true, + required: true, + schema: { + type: 'string', + }, + type: 'text', + }; + const value = ''; + const properties = { firstname: '' }; + + // when + const errors = validateValue(schema, value, properties, customValidationFn); + + // then + expect(errors).toBe('Missing required property: firstname'); + }); + + it('should return custom validation if static check is ok', () => { + // given + const schema = { + key: ['firstname'], + customValidation: true, + required: true, + schema: { + type: 'string', + }, + type: 'text', + }; + const value = 'my name'; + const properties = { firstname: 'my name' }; + + // when + const errors = validateValue(schema, value, properties, customValidationFn); + + // then + expect(errors).toBe(customError); + }); + + it('should return null when only static check is required and this one is ok', () => { + // given + const schema = { + key: ['firstname'], + customValidation: false, + required: true, + schema: { + type: 'string', + }, + type: 'text', + }; + const value = 'my name'; + const properties = { firstname: 'my name' }; + + // when + const errors = validateValue(schema, value, properties, customValidationFn); + + // then + expect(errors).toBe(null); + }); + }); + + describe('#validateAll', () => { + it('should validate all fields', () => { + // given + const mergedSchema = [ + { + key: ['user', 'lastname'], + required: true, + schema: { + type: 'string', + }, + type: 'text', + }, + { + key: ['user', 'firstname'], + customValidation: true, + required: true, + schema: { + type: 'string', + }, + type: 'text', + }, + { + key: ['comment'], + schema: { + type: 'string', + }, + type: 'textarea', + }, + ]; + const properties = { + user: { + lastname: '', + firstname: 'my name', + }, + comment: '', + }; + + // when + const errors = validateAll(mergedSchema, properties, customValidationFn); + + // then + expect(errors).toEqual({ + 'user,firstname': 'This field is invalid', // custom validation + 'user,lastname': 'Missing required property: lastname', + }); + }); + }); + + describe('#isValid', () => { + it('should return false on error', () => { + // given + const schema = { + key: ['firstname'], + }; + const errors = { firstname: 'this is not ok' }; + + // when + const valid = isValid(schema, errors); + + // then + expect(valid).toBe(false); + }); + + it('should return false on nested property error', () => { + // given + const schema = { + key: ['user'], + items: [ + { key: ['user', 'lastname'] }, + { key: ['user', 'firstname'] }, + ], + }; + const errors = { 'user,firstname': 'this is not ok' }; + + // when + const valid = isValid(schema, errors); + + // then + expect(valid).toBe(false); + }); + + it('should return true when schema has no error', () => { + // given + const schema = { + key: ['user'], + items: [ + { key: ['user', 'lastname'] }, + { key: ['user', 'firstname'] }, + ], + }; + const errors = {}; + + // when + const valid = isValid(schema, errors); + + // then + expect(valid).toBe(true); + }); + }); +}); From 12bdd2ca7d1662047ffe0b05f5de686b7d7a2946 Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Mon, 5 Jun 2017 15:09:27 +0200 Subject: [PATCH 51/73] Message unit tests --- .../UIForm/Message/Message.component.test.js | 47 +++++++++++++++++++ .../Message.component.test.js.snap | 17 +++++++ 2 files changed, 64 insertions(+) create mode 100644 packages/forms/src/UIForm/Message/Message.component.test.js create mode 100644 packages/forms/src/UIForm/Message/__snapshots__/Message.component.test.js.snap diff --git a/packages/forms/src/UIForm/Message/Message.component.test.js b/packages/forms/src/UIForm/Message/Message.component.test.js new file mode 100644 index 00000000000..4edeae8f94f --- /dev/null +++ b/packages/forms/src/UIForm/Message/Message.component.test.js @@ -0,0 +1,47 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import Message from './Message.component'; + +describe('Message component', () => { + it('should render provided description if the field is valid', () => { + // when + const wrapper = shallow( + + ); + + // then + expect(wrapper.node).toMatchSnapshot(); + }); + + it('should render provided error message if the field is invalid', () => { + // when + const wrapper = shallow( + + ); + + // then + expect(wrapper.node).toMatchSnapshot(); + }); + + it('should render nothing when field is valid and no description is provided', () => { + // when + const wrapper = shallow( + + ); + + // then + expect(wrapper.node).toMatchSnapshot(); + }); +}); diff --git a/packages/forms/src/UIForm/Message/__snapshots__/Message.component.test.js.snap b/packages/forms/src/UIForm/Message/__snapshots__/Message.component.test.js.snap new file mode 100644 index 00000000000..1f341967732 --- /dev/null +++ b/packages/forms/src/UIForm/Message/__snapshots__/Message.component.test.js.snap @@ -0,0 +1,17 @@ +exports[`Message component should render nothing when field is valid and no description is provided 1`] = `null`; + +exports[`Message component should render provided description if the field is valid 1`] = ` +
+ My description +
+`; + +exports[`Message component should render provided error message if the field is invalid 1`] = ` +
+ My error message +
+`; From 2ef3c5e0bc06bfc502359862434f9ac7f5a33245 Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Mon, 5 Jun 2017 15:36:06 +0200 Subject: [PATCH 52/73] Widget unit tests --- .../UIForm/Widget/Widget.component.test.js | 127 ++++++++++++++++++ .../Widget.component.test.js.snap | 69 ++++++++++ 2 files changed, 196 insertions(+) create mode 100644 packages/forms/src/UIForm/Widget/Widget.component.test.js create mode 100644 packages/forms/src/UIForm/Widget/__snapshots__/Widget.component.test.js.snap diff --git a/packages/forms/src/UIForm/Widget/Widget.component.test.js b/packages/forms/src/UIForm/Widget/Widget.component.test.js new file mode 100644 index 00000000000..a5866a6e76f --- /dev/null +++ b/packages/forms/src/UIForm/Widget/Widget.component.test.js @@ -0,0 +1,127 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import Widget from './Widget.component'; + +describe('Widget component', () => { + const schema = { + key: ['user', 'firstname'], + type: 'text', + }; + const errors = { + 'user,firstname': 'This is not ok', + }; + const properties = { + user: { + firstname: 'my firstname', + lastname: 'my lastname', + }, + comment: '', + }; + + it('should render widget', () => { + // when + const wrapper = shallow( + + ); + + // then + expect(wrapper.node).toMatchSnapshot(); + }); + + it('should render nothing if widget does not exist', () => { + // given + const unknownWidgetSchema = { + ...schema, + type: 'unknown', + }; + + // when + const wrapper = shallow( + + ); + + // then + expect(wrapper.node).toMatchSnapshot(); + }); + + it('should render custom widget', () => { + // given + const widgets = { + customWidget() { + return (
my widget
); + }, + }; + const customWidgetSchema = { + ...schema, + type: 'customWidget', + }; + + // when + const wrapper = shallow( + + ); + + // then + expect(wrapper.node).toMatchSnapshot(); + }); + + it('should pass validation message from schema over message from errors', () => { + // given + const customValidationMessageSchema = { + ...schema, + validationMessage: 'My custom validation message', + }; + + // when + const wrapper = shallow( + + ); + + // then + expect(wrapper.node.props.errorMessage).toBe('My custom validation message'); + }); + + it('should pass message from errors when there is no validation message in schema', () => { + // when + const wrapper = shallow( + + ); + + // then + expect(wrapper.node.props.errorMessage).toBe('This is not ok'); + }); +}); diff --git a/packages/forms/src/UIForm/Widget/__snapshots__/Widget.component.test.js.snap b/packages/forms/src/UIForm/Widget/__snapshots__/Widget.component.test.js.snap new file mode 100644 index 00000000000..57641a717ea --- /dev/null +++ b/packages/forms/src/UIForm/Widget/__snapshots__/Widget.component.test.js.snap @@ -0,0 +1,69 @@ +exports[`Widget component should render custom widget 1`] = ` + +`; + +exports[`Widget component should render nothing if widget does not exist 1`] = `null`; + +exports[`Widget component should render widget 1`] = ` + +`; From c9b456a4e4961050821e1445788106e6ef99ec22 Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Mon, 5 Jun 2017 16:17:42 +0200 Subject: [PATCH 53/73] Fields widgets unit tests --- .../forms/src/UIForm/fields/Button.test.js | 66 +++++++++ packages/forms/src/UIForm/fields/Text.test.js | 125 ++++++++++++++++++ .../fields/__snapshots__/Button.test.js.snap | 33 +++++ .../fields/__snapshots__/Text.test.js.snap | 77 +++++++++++ 4 files changed, 301 insertions(+) create mode 100644 packages/forms/src/UIForm/fields/Button.test.js create mode 100644 packages/forms/src/UIForm/fields/Text.test.js create mode 100644 packages/forms/src/UIForm/fields/__snapshots__/Button.test.js.snap create mode 100644 packages/forms/src/UIForm/fields/__snapshots__/Text.test.js.snap diff --git a/packages/forms/src/UIForm/fields/Button.test.js b/packages/forms/src/UIForm/fields/Button.test.js new file mode 100644 index 00000000000..5064898b71f --- /dev/null +++ b/packages/forms/src/UIForm/fields/Button.test.js @@ -0,0 +1,66 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import Button from './Button'; + +describe('Button field', () => { + const schema = { + description: 'Click here to trigger a trigger', + title: 'Boom !', + triggers: ['after'], + type: 'button', + }; + + it('should render button', () => { + // when + const wrapper = shallow( + + +
+`; + +exports[`Button field should render error button if it is not valid 1`] = ` +
+ + +
+`; diff --git a/packages/forms/src/UIForm/fields/__snapshots__/Text.test.js.snap b/packages/forms/src/UIForm/fields/__snapshots__/Text.test.js.snap new file mode 100644 index 00000000000..2b67e23622e --- /dev/null +++ b/packages/forms/src/UIForm/fields/__snapshots__/Text.test.js.snap @@ -0,0 +1,77 @@ +exports[`Text field should render disabled input 1`] = ` +
+ + + +
+`; + +exports[`Text field should render input 1`] = ` +
+ + + +
+`; + +exports[`Text field should render readonly input 1`] = ` +
+ + + +
+`; From 38f131f07933277efc44720cac7fb8b0e88625c8 Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Mon, 5 Jun 2017 16:35:29 +0200 Subject: [PATCH 54/73] Fieldsets unit tests --- .../src/UIForm/fieldsets/Fieldset.test.js | 31 ++++ .../forms/src/UIForm/fieldsets/Tabs.test.js | 88 ++++++++++++ .../__snapshots__/Fieldset.test.js.snap | 36 +++++ .../fieldsets/__snapshots__/Tabs.test.js.snap | 133 ++++++++++++++++++ 4 files changed, 288 insertions(+) create mode 100644 packages/forms/src/UIForm/fieldsets/Fieldset.test.js create mode 100644 packages/forms/src/UIForm/fieldsets/Tabs.test.js create mode 100644 packages/forms/src/UIForm/fieldsets/__snapshots__/Fieldset.test.js.snap create mode 100644 packages/forms/src/UIForm/fieldsets/__snapshots__/Tabs.test.js.snap diff --git a/packages/forms/src/UIForm/fieldsets/Fieldset.test.js b/packages/forms/src/UIForm/fieldsets/Fieldset.test.js new file mode 100644 index 00000000000..537e5a92ef6 --- /dev/null +++ b/packages/forms/src/UIForm/fieldsets/Fieldset.test.js @@ -0,0 +1,31 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import Fieldset from './Fieldset'; + +describe('Fieldset widget', () => { + it('should render fieldset', () => { + // given + const schema = { + title: 'My fieldset', + items: [ + { + key: ['user', 'firstname'], + type: 'text', + schema: { type: 'string' }, + }, + { + key: ['user', 'lastname'], + type: 'text', + schema: { type: 'string' }, + }, + ], + }; + + // when + const wrapper = shallow(
); + + // then + expect(wrapper.node).toMatchSnapshot(); + }); +}); diff --git a/packages/forms/src/UIForm/fieldsets/Tabs.test.js b/packages/forms/src/UIForm/fieldsets/Tabs.test.js new file mode 100644 index 00000000000..12de758019c --- /dev/null +++ b/packages/forms/src/UIForm/fieldsets/Tabs.test.js @@ -0,0 +1,88 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import Tabs from './Tabs'; + +describe('Tabs widget', () => { + it('should render fieldset', () => { + // given + const errors = {}; + const schema = { + title: 'My Tabs', + items: [ + { + title: 'User', + items: [ + { + key: ['user', 'firstname'], + type: 'text', + schema: { type: 'string' }, + }, + { + key: ['user', 'lastname'], + type: 'text', + schema: { type: 'string' }, + }, + ], + }, + { + title: 'Other', + items: [ + { + key: ['comment'], + type: 'text', + schema: { type: 'string' }, + }, + ], + }, + ], + }; + + // when + const wrapper = shallow(); + + // then + expect(wrapper.node).toMatchSnapshot(); + }); + + it('should render invalid tab', () => { + // given + const errors = { 'user,firstname': 'This is wrong' }; + const schema = { + title: 'My Tabs', + items: [ + { + title: 'User', + items: [ + { + key: ['user', 'firstname'], + type: 'text', + schema: { type: 'string' }, + }, + { + key: ['user', 'lastname'], + type: 'text', + schema: { type: 'string' }, + }, + ], + }, + { + title: 'Other', + items: [ + { + key: ['comment'], + type: 'text', + schema: { type: 'string' }, + }, + ], + }, + ], + }; + + // when + const wrapper = shallow(); + + // then + expect(wrapper.node).toMatchSnapshot(); + }); +}); diff --git a/packages/forms/src/UIForm/fieldsets/__snapshots__/Fieldset.test.js.snap b/packages/forms/src/UIForm/fieldsets/__snapshots__/Fieldset.test.js.snap new file mode 100644 index 00000000000..0ce4884c4ca --- /dev/null +++ b/packages/forms/src/UIForm/fieldsets/__snapshots__/Fieldset.test.js.snap @@ -0,0 +1,36 @@ +exports[`Fieldset widget should render fieldset 1`] = ` +
+ + My fieldset + + + +
+`; diff --git a/packages/forms/src/UIForm/fieldsets/__snapshots__/Tabs.test.js.snap b/packages/forms/src/UIForm/fieldsets/__snapshots__/Tabs.test.js.snap new file mode 100644 index 00000000000..67f42732a61 --- /dev/null +++ b/packages/forms/src/UIForm/fieldsets/__snapshots__/Tabs.test.js.snap @@ -0,0 +1,133 @@ +exports[`Tabs widget should render fieldset 1`] = ` + + +
+ + +
+ + +`; + +exports[`Tabs widget should render invalid tab 1`] = ` + + +
+ + +
+ + +`; From 15033d40b247e881dfedf4e46be9b68f7ac170a9 Mon Sep 17 00:00:00 2001 From: jsomsanith Date: Mon, 5 Jun 2017 17:30:00 +0200 Subject: [PATCH 55/73] Start UIForm component unit tests --- .../forms/src/UIForm/UIForm.component.test.js | 109 ++++++++++++++++++ packages/forms/src/UIForm/UIForm.test.js | 13 --- .../UIForm.component.test.js.snap | 58 ++++++++++ .../UIForm/__snapshots__/UIForm.test.js.snap | 104 ----------------- 4 files changed, 167 insertions(+), 117 deletions(-) create mode 100644 packages/forms/src/UIForm/UIForm.component.test.js delete mode 100644 packages/forms/src/UIForm/UIForm.test.js create mode 100644 packages/forms/src/UIForm/__snapshots__/UIForm.component.test.js.snap delete mode 100644 packages/forms/src/UIForm/__snapshots__/UIForm.test.js.snap diff --git a/packages/forms/src/UIForm/UIForm.component.test.js b/packages/forms/src/UIForm/UIForm.component.test.js new file mode 100644 index 00000000000..1d4b6775d75 --- /dev/null +++ b/packages/forms/src/UIForm/UIForm.component.test.js @@ -0,0 +1,109 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import UIForm from './UIForm.component'; + +describe('UIForm component', () => { + const props = { + autocomplete: 'off', + customValidation: jest.fn(), + formName: 'myForm', + onChange: jest.fn(), + onTrigger: jest.fn(), + onSubmit: jest.fn(), + }; + + const data = { + jsonSchema: { + type: 'object', + title: 'Comment', + properties: { + lastname: { + type: 'string', + }, + firstname: { + type: 'string', + }, + }, + required: [ + 'firstname', + ], + }, + uiSchema: [ + { + key: 'lastname', + title: 'Last Name (with description)', + description: 'Hint: this is the last name', + autoFocus: true, + }, + { + key: 'firstname', + title: 'First Name (with placeholder)', + placeholder: 'Enter your firstname here', + }, + ], + properties: {}, + }; + + it('should render form', () => { + // when + const wrapper = shallow(); + + // then + expect(wrapper.node).toMatchSnapshot(); + }); + + describe('#onChange', () => { + it('should validate change', () => { + // given + const wrapper = shallow(); + + // when + // renderer._instance._instance.onChange + }); + + it('should call onChange callback', () => { + + }); + + it('should trigger "after" trigger', () => { + + }); + }); + + describe('#onTrigger', () => { + it('should do nothing if there is no trigger callback', () => { + + }); + + it('should call trigger callback', () => { + + }); + + it('should updateForm on trigger success', () => { + + }); + + it('should setError after trigger failure', () => { + + }); + }); + + describe('#submit', () => { + it('should prevent default submit', () => { + + }); + + it('should validate all fields', () => { + + }); + + it('should not call submit callback when form is invalid', () => { + + }); + + it('should call submit callback when form is valid', () => { + + }); + }); +}); diff --git a/packages/forms/src/UIForm/UIForm.test.js b/packages/forms/src/UIForm/UIForm.test.js deleted file mode 100644 index ceb205da709..00000000000 --- a/packages/forms/src/UIForm/UIForm.test.js +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; - -import UIForm from './UIForm.component'; - -describe('UIForm', () => { - it('should render', () => { - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/packages/forms/src/UIForm/__snapshots__/UIForm.component.test.js.snap b/packages/forms/src/UIForm/__snapshots__/UIForm.component.test.js.snap new file mode 100644 index 00000000000..67fdc050ee5 --- /dev/null +++ b/packages/forms/src/UIForm/__snapshots__/UIForm.component.test.js.snap @@ -0,0 +1,58 @@ +exports[`UIForm component should render form 1`] = ` +
+ + + + +`; diff --git a/packages/forms/src/UIForm/__snapshots__/UIForm.test.js.snap b/packages/forms/src/UIForm/__snapshots__/UIForm.test.js.snap deleted file mode 100644 index 6c30d16369f..00000000000 --- a/packages/forms/src/UIForm/__snapshots__/UIForm.test.js.snap +++ /dev/null @@ -1,104 +0,0 @@ -exports[`UIForm should render 1`] = ` -ShallowWrapper { - "complexSelector": ComplexSelector { - "buildPredicate": [Function], - "childrenOfNode": [Function], - "findWhereUnwrapped": [Function], - }, - "length": 1, - "node":
- -
, - "nodes": Array [ -
- -
, - ], - "options": Object {}, - "renderer": ReactShallowRenderer { - "_instance": ShallowComponentWrapper { - "_calledComponentWillUnmount": false, - "_compositeType": 0, - "_context": Object {}, - "_currentElement": , - "_debugID": 1, - "_hostContainerInfo": null, - "_hostParent": null, - "_instance": UIForm { - "_reactInternalInstance": [Circular], - "context": Object {}, - "onChange": [Function], - "onTrigger": [Function], - "props": Object {}, - "refs": Object {}, - "state": Object { - "mergedSchema": Array [], - }, - "submit": [Function], - "updater": Object { - "enqueueCallback": [Function], - "enqueueCallbackInternal": [Function], - "enqueueElementInternal": [Function], - "enqueueForceUpdate": [Function], - "enqueueReplaceState": [Function], - "enqueueSetState": [Function], - "isMounted": [Function], - "validateCallback": [Function], - }, - }, - "_mountOrder": 1, - "_pendingCallbacks": null, - "_pendingElement": null, - "_pendingForceUpdate": false, - "_pendingReplaceState": false, - "_pendingStateQueue": null, - "_renderedComponent": NoopInternalComponent { - "_currentElement":
- -
, - "_debugID": 2, - "_renderedOutput":
- -
, - }, - "_renderedNodeType": 0, - "_rootNodeID": 0, - "_topLevelWrapper": null, - "_updateBatchNumber": null, - "_warnedAboutRefsInRender": false, - }, - "getRenderOutput": [Function], - "render": [Function], - }, - "root": [Circular], - "unrendered": , -} -`; From 75a94aad01d5db9a0c6e5b90532956bf263d6ceb Mon Sep 17 00:00:00 2001 From: travis Date: Mon, 5 Jun 2017 15:39:52 +0000 Subject: [PATCH 56/73] test(ci): update code style outputs --- output/forms.eslint.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/output/forms.eslint.txt b/output/forms.eslint.txt index 1c04d9808d9..c0020664b13 100755 --- a/output/forms.eslint.txt +++ b/output/forms.eslint.txt @@ -117,7 +117,10 @@ The react/require-extension rule is deprecated. Please use the import/extensions /home/travis/build/Talend/ui/packages/forms/src/UIForm/actions/model.actions.js 3:1 error Prefer default export import/prefer-default-export -✖ 101 problems (101 errors, 0 warnings) +/home/travis/build/Talend/ui/packages/forms/src/UIForm/UIForm.component.test.js + 59:10 error 'wrapper' is assigned a value but never used no-unused-vars + +✖ 102 problems (102 errors, 0 warnings) Errored while running command 'npm' with arguments 'run lint:es' in 'react-talend-forms' Errored while running ExecCommand.execute From 985c923287647ee97ba27cfcdd98bf6fefeb7ef7 Mon Sep 17 00:00:00 2001 From: Jimmy Somsanith Date: Mon, 5 Jun 2017 23:20:41 +0200 Subject: [PATCH 57/73] Continue UIForm component unit tests --- .../forms/src/UIForm/UIForm.component.test.js | 203 +++++++++++++----- .../UIForm.component.test.js.snap | 29 ++- 2 files changed, 179 insertions(+), 53 deletions(-) diff --git a/packages/forms/src/UIForm/UIForm.component.test.js b/packages/forms/src/UIForm/UIForm.component.test.js index 1d4b6775d75..852d2cfdc8e 100644 --- a/packages/forms/src/UIForm/UIForm.component.test.js +++ b/packages/forms/src/UIForm/UIForm.component.test.js @@ -1,50 +1,92 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, mount } from 'enzyme'; import UIForm from './UIForm.component'; -describe('UIForm component', () => { - const props = { - autocomplete: 'off', - customValidation: jest.fn(), - formName: 'myForm', - onChange: jest.fn(), - onTrigger: jest.fn(), - onSubmit: jest.fn(), - }; - - const data = { - jsonSchema: { - type: 'object', - title: 'Comment', - properties: { - lastname: { - type: 'string', - }, - firstname: { - type: 'string', - }, - }, - required: [ - 'firstname', - ], - }, - uiSchema: [ - { - key: 'lastname', - title: 'Last Name (with description)', - description: 'Hint: this is the last name', - autoFocus: true, +const props = { + autocomplete: 'off', + customValidation: jest.fn(), + formName: 'myForm', + onChange: jest.fn(), + onTrigger: jest.fn(), + onSubmit: jest.fn(), + setError: jest.fn(), + setErrors: jest.fn(), + updateForm: jest.fn(), +}; + +const data = { + jsonSchema: { + type: 'object', + title: 'Comment', + properties: { + lastname: { + type: 'string', + minLength: 10, }, - { - key: 'firstname', - title: 'First Name (with placeholder)', - placeholder: 'Enter your firstname here', + firstname: { + type: 'string', }, + check: {}, + }, + required: [ + 'firstname', ], - properties: {}, - }; + }, + uiSchema: [ + { + key: 'lastname', + title: 'Last Name (with description)', + description: 'Hint: this is the last name', + autoFocus: true, + }, + { + key: 'firstname', + title: 'First Name (with placeholder)', + placeholder: 'Enter your firstname here', + triggers: ['after'], + }, + { + key: 'check', + type: 'button', + title: 'Check the thing', + triggers: ['after'], + }, + ], + properties: {}, + errors: {}, +}; + +const mergedSchema = [ + { + autoFocus: true, + description: 'Hint: this is the last name', + key: ['lastname'], + minlength: 10, + ngModelOptions: {}, + schema: { minLength: 10, type: 'string' }, + title: 'Last Name (with description)', + type: 'text', + }, + { + key: ['firstname'], + ngModelOptions: {}, + placeholder: 'Enter your firstname here', + required: true, + schema: { type: 'string' }, + title: 'First Name (with placeholder)', + triggers: ['after'], + type: 'text', + }, + { + key: ['check'], + title: 'Check the thing', + triggers: ['after'], + type: 'button', + }, +]; +describe('UIForm component', () => { it('should render form', () => { // when const wrapper = shallow(); @@ -54,34 +96,93 @@ describe('UIForm component', () => { }); describe('#onChange', () => { - it('should validate change', () => { + it('should call onChange callback', () => { // given - const wrapper = shallow(); + const wrapper = mount(); + const newValue = 'toto'; + const event = { target: { value: newValue } }; + const inputValidationError = 'String is too short (4 chars), minimum 10'; + expect(props.onChange).not.toBeCalled(); // when - // renderer._instance._instance.onChange - }); - - it('should call onChange callback', () => { - + wrapper.find('input').at(0).simulate('change', event); + + // then + expect(props.onChange).toBeCalledWith( + props.formName, + mergedSchema[0], + newValue, + inputValidationError, + ); + expect(props.onTrigger).not.toBeCalled(); }); it('should trigger "after" trigger', () => { + // given + const wrapper = mount(); + const newValue = 'toto'; + const event = { target: { value: newValue } }; + expect(props.onTrigger).not.toHaveBeenCalled(); + props.onTrigger.mockReturnValueOnce(Promise.resolve({})); + // when + wrapper.find('input').at(1).simulate('change', event); + + // then + expect(props.onTrigger).toHaveBeenCalledWith( + 'after', + mergedSchema[1], + newValue, + data.properties, + ); }); }); describe('#onTrigger', () => { - it('should do nothing if there is no trigger callback', () => { - - }); - it('should call trigger callback', () => { + // given + const wrapper = mount(); + props.onTrigger.mockReturnValueOnce(Promise.resolve({})); + // when + wrapper.find('button').at(0).simulate('click'); + + // then + expect(props.onTrigger).toHaveBeenCalledWith( + 'after', + mergedSchema[2], + undefined, + data.properties, + ); }); it('should updateForm on trigger success', () => { - + // given + // const wrapper = mount(); + // const nextData = { + // jsonSchema: { + // type: 'object', + // title: 'User', + // properties: { + // name: { type: 'string' }, + // }, + // }, + // uiSchema: ['name'], + // properties: { name: 'toto' }, + // errors: { name: 'This field is required' }, + // }; + // props.onTrigger.mockReturnValueOnce(Promise.resolve(nextData).then(done)); + // + // // when + // wrapper.find('button').at(0).simulate('click'); + // + // // then + // expect(props.updateForm).toHaveBeenCalledWith( + // 'after', + // mergedSchema[2], + // undefined, + // data.properties, + // ); }); it('should setError after trigger failure', () => { diff --git a/packages/forms/src/UIForm/__snapshots__/UIForm.component.test.js.snap b/packages/forms/src/UIForm/__snapshots__/UIForm.component.test.js.snap index 67fdc050ee5..13be90964a0 100644 --- a/packages/forms/src/UIForm/__snapshots__/UIForm.component.test.js.snap +++ b/packages/forms/src/UIForm/__snapshots__/UIForm.component.test.js.snap @@ -4,7 +4,7 @@ exports[`UIForm component should render form 1`] = ` noValidate={true} onSubmit={[Function]}> +