From e53b9a107ac378352486c7957c31c0604581fb4f Mon Sep 17 00:00:00 2001 From: Jean-Michel FRANCOIS Date: Thu, 8 Jun 2017 00:06:11 +0200 Subject: [PATCH] feat(UIForms) : rendering lib based on json-schema-form-core (#415) --- output/cmf.eslint.txt | 2 +- output/components.eslint.txt | 2 +- output/components.sasslint.txt | 2 +- output/containers.eslint.txt | 2 +- output/forms.eslint.txt | 2 +- output/forms.sasslint.txt | 2 +- output/logging.eslint.txt | 2 +- output/theme.sasslint.txt | 14 +- packages/forms/.storybook/config.js | 1 + packages/forms/__mocks__/data.js | 99 ++++ packages/forms/package.json | 10 +- .../src/UIForm/Message/Message.component.js | 29 ++ .../UIForm/Message/Message.component.test.js | 47 ++ .../Message.component.test.js.snap | 17 + packages/forms/src/UIForm/Message/index.js | 3 + packages/forms/src/UIForm/README.md | 428 ++++++++++++++++++ packages/forms/src/UIForm/UIForm.component.js | 181 ++++++++ .../forms/src/UIForm/UIForm.component.test.js | 201 ++++++++ packages/forms/src/UIForm/UIForm.connect.js | 205 +++++++++ .../forms/src/UIForm/UIForm.connect.test.js | 108 +++++ packages/forms/src/UIForm/UIForm.container.js | 160 +++++++ .../forms/src/UIForm/UIForm.container.test.js | 116 +++++ .../src/UIForm/Widget/Widget.component.js | 54 +++ .../UIForm/Widget/Widget.component.test.js | 127 ++++++ .../Widget.component.test.js.snap | 69 +++ packages/forms/src/UIForm/Widget/index.js | 3 + .../UIForm.component.test.js.snap | 95 ++++ .../__snapshots__/UIForm.connect.test.js.snap | 224 +++++++++ .../UIForm.container.test.js.snap | 288 ++++++++++++ .../__snapshots__/form.actions.test.js.snap | 48 ++ .../__snapshots__/model.actions.test.js.snap | 13 + .../validation.actions.test.js.snap | 17 + .../forms/src/UIForm/actions/constants.js | 6 + .../forms/src/UIForm/actions/form.actions.js | 30 ++ .../src/UIForm/actions/form.actions.test.js | 47 ++ packages/forms/src/UIForm/actions/index.js | 4 + .../forms/src/UIForm/actions/model.actions.js | 12 + .../src/UIForm/actions/model.actions.test.js | 18 + .../src/UIForm/actions/validation.actions.js | 17 + .../UIForm/actions/validation.actions.test.js | 30 ++ packages/forms/src/UIForm/fields/Button.js | 41 ++ .../forms/src/UIForm/fields/Button.test.js | 66 +++ packages/forms/src/UIForm/fields/Text.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 ++++ .../forms/src/UIForm/fieldsets/Fieldset.js | 29 ++ .../src/UIForm/fieldsets/Fieldset.test.js | 31 ++ packages/forms/src/UIForm/fieldsets/Tabs.js | 44 ++ packages/forms/src/UIForm/fieldsets/Tabs.scss | 12 + .../forms/src/UIForm/fieldsets/Tabs.test.js | 88 ++++ .../__snapshots__/Fieldset.test.js.snap | 36 ++ .../fieldsets/__snapshots__/Tabs.test.js.snap | 133 ++++++ packages/forms/src/UIForm/index.js | 11 + .../__snapshots__/form.reducer.test.js.snap | 125 +++++ .../__snapshots__/model.reducer.test.js.snap | 17 + .../validations.reducer.test.js.snap | 14 + .../forms/src/UIForm/reducers/form.reducer.js | 81 ++++ .../src/UIForm/reducers/form.reducer.test.js | 188 ++++++++ packages/forms/src/UIForm/reducers/index.js | 5 + .../src/UIForm/reducers/model.reducer.js | 35 ++ .../src/UIForm/reducers/model.reducer.test.js | 45 ++ .../UIForm/reducers/validations.reducer.js | 36 ++ .../reducers/validations.reducer.test.js | 49 ++ packages/forms/src/UIForm/utils/properties.js | 33 ++ .../forms/src/UIForm/utils/properties.test.js | 58 +++ packages/forms/src/UIForm/utils/triggers.js | 1 + packages/forms/src/UIForm/utils/validation.js | 72 +++ .../forms/src/UIForm/utils/validation.test.js | 173 +++++++ packages/forms/src/UIForm/utils/widgets.js | 18 + .../forms/stories-core/customWidgetStory.js | 56 +++ packages/forms/stories-core/index.js | 42 ++ .../forms/stories-core/json/core-buttons.json | 24 + .../json/core-custom-validation.json | 32 ++ .../stories-core/json/core-fieldset.json | 93 ++++ .../forms/stories-core/json/core-simple.json | 91 ++++ .../json/core-structured-model.json | 90 ++++ .../forms/stories-core/json/core-tabs.json | 96 ++++ .../stories-core/json/core-trigger-after.json | 31 ++ packages/forms/stories-core/jsonStories.js | 83 ++++ packages/forms/yarn.lock | 68 ++- 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 packages/theme/src/theme/_colors.scss | 5 + packages/theme/src/theme/_forms.scss | 16 +- packages/theme/src/theme/_variables.scss | 2 +- 88 files changed, 5171 insertions(+), 35 deletions(-) create mode 100644 packages/forms/__mocks__/data.js create mode 100644 packages/forms/src/UIForm/Message/Message.component.js 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 create mode 100644 packages/forms/src/UIForm/Message/index.js create mode 100644 packages/forms/src/UIForm/README.md create mode 100644 packages/forms/src/UIForm/UIForm.component.js create mode 100644 packages/forms/src/UIForm/UIForm.component.test.js create mode 100644 packages/forms/src/UIForm/UIForm.connect.js create mode 100644 packages/forms/src/UIForm/UIForm.connect.test.js create mode 100644 packages/forms/src/UIForm/UIForm.container.js create mode 100644 packages/forms/src/UIForm/UIForm.container.test.js create mode 100644 packages/forms/src/UIForm/Widget/Widget.component.js 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 create mode 100644 packages/forms/src/UIForm/Widget/index.js create mode 100644 packages/forms/src/UIForm/__snapshots__/UIForm.component.test.js.snap create mode 100644 packages/forms/src/UIForm/__snapshots__/UIForm.connect.test.js.snap create mode 100644 packages/forms/src/UIForm/__snapshots__/UIForm.container.test.js.snap 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/actions/constants.js create mode 100644 packages/forms/src/UIForm/actions/form.actions.js create mode 100644 packages/forms/src/UIForm/actions/form.actions.test.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/model.actions.test.js create mode 100644 packages/forms/src/UIForm/actions/validation.actions.js create mode 100644 packages/forms/src/UIForm/actions/validation.actions.test.js create mode 100644 packages/forms/src/UIForm/fields/Button.js create mode 100644 packages/forms/src/UIForm/fields/Button.test.js create mode 100644 packages/forms/src/UIForm/fields/Text.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 create mode 100644 packages/forms/src/UIForm/fieldsets/Fieldset.js create mode 100644 packages/forms/src/UIForm/fieldsets/Fieldset.test.js 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/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 create mode 100644 packages/forms/src/UIForm/index.js create mode 100644 packages/forms/src/UIForm/reducers/__snapshots__/form.reducer.test.js.snap create mode 100644 packages/forms/src/UIForm/reducers/__snapshots__/model.reducer.test.js.snap create mode 100644 packages/forms/src/UIForm/reducers/__snapshots__/validations.reducer.test.js.snap create mode 100644 packages/forms/src/UIForm/reducers/form.reducer.js create mode 100644 packages/forms/src/UIForm/reducers/form.reducer.test.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/model.reducer.test.js create mode 100644 packages/forms/src/UIForm/reducers/validations.reducer.js create mode 100644 packages/forms/src/UIForm/reducers/validations.reducer.test.js create mode 100644 packages/forms/src/UIForm/utils/properties.js create mode 100644 packages/forms/src/UIForm/utils/properties.test.js create mode 100644 packages/forms/src/UIForm/utils/triggers.js create mode 100644 packages/forms/src/UIForm/utils/validation.js create mode 100644 packages/forms/src/UIForm/utils/validation.test.js create mode 100644 packages/forms/src/UIForm/utils/widgets.js create mode 100644 packages/forms/stories-core/customWidgetStory.js create mode 100644 packages/forms/stories-core/index.js create mode 100644 packages/forms/stories-core/json/core-buttons.json create mode 100644 packages/forms/stories-core/json/core-custom-validation.json create mode 100644 packages/forms/stories-core/json/core-fieldset.json create mode 100644 packages/forms/stories-core/json/core-simple.json create mode 100644 packages/forms/stories-core/json/core-structured-model.json create mode 100644 packages/forms/stories-core/json/core-tabs.json create mode 100644 packages/forms/stories-core/json/core-trigger-after.json create mode 100644 packages/forms/stories-core/jsonStories.js diff --git a/output/cmf.eslint.txt b/output/cmf.eslint.txt index 4907794d947..81c3c34c5b0 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.79.0 lint:es /home/travis/build/Talend/ui/packages/cmf +> react-cmf@0.80.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 8bcfd509ae3..7cdfd328094 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.79.0 lint:es /home/travis/build/Talend/ui/packages/components +> react-talend-components@0.80.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 4abdedbad52..1da7ef2b402 100644 --- a/output/components.sasslint.txt +++ b/output/components.sasslint.txt @@ -1,7 +1,7 @@ Lerna v2.0.0-beta.36 Scoping to packages that match 'react-talend-components' -> react-talend-components@0.79.0 lint:style /home/travis/build/Talend/ui/packages/components +> react-talend-components@0.80.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 7e227326c6e..77087482d22 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.79.0 lint:es /home/travis/build/Talend/ui/packages/containers +> react-talend-containers@0.80.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 efa2108787c..2deaa1201ca 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.79.0 lint:es /home/travis/build/Talend/ui/packages/forms +> react-talend-forms@0.80.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. diff --git a/output/forms.sasslint.txt b/output/forms.sasslint.txt index 75a66ddd372..fb0812b062b 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.79.0 lint:style /home/travis/build/Talend/ui/packages/forms +> react-talend-forms@0.80.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 e0391750cbb..0d7fab8ef92 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.79.0 lint:es /home/travis/build/Talend/ui/packages/logging +> talend-log@0.80.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 58cd9ec5b0c..13e5442cd65 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.79.0 lint:style /home/travis/build/Talend/ui/packages/theme +> bootstrap-talend-theme@0.80.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 src/theme/_tables.scss 21:2 warning Vendor prefixes should not be used no-vendor-prefixes diff --git a/packages/forms/.storybook/config.js b/packages/forms/.storybook/config.js index 74e5a1eb102..e2bc8834b39 100644 --- a/packages/forms/.storybook/config.js +++ b/packages/forms/.storybook/config.js @@ -7,6 +7,7 @@ setAddon(infoAddon); function loadStories() { require('../stories'); + require('../stories-core'); } configure(loadStories, module); diff --git a/packages/forms/__mocks__/data.js b/packages/forms/__mocks__/data.js new file mode 100644 index 00000000000..9d4a2ae1cfd --- /dev/null +++ b/packages/forms/__mocks__/data.js @@ -0,0 +1,99 @@ +import React from 'react'; +import configureMockStore from 'redux-mock-store'; + +export const data = { + jsonSchema: { + type: 'object', + title: 'Comment', + properties: { + lastname: { + type: 'string', + minLength: 10, + }, + firstname: { + type: 'string', + }, + check: {}, + }, + 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', + triggers: ['after'], + }, + { + key: 'check', + type: 'button', + title: 'Check the thing', + triggers: ['after'], + }, + ], + properties: {}, + errors: {}, +}; + +export 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', + }, +]; + +export function initProps() { + return { + autoComplete: 'off', + customValidation: jest.fn(), + formName: 'myFormName', + id: 'myFormId', + onChange: jest.fn(), + onSubmit: jest.fn(), + onTrigger: jest.fn(), + widgets: { + custom: () => (
Custom
), + }, + }; +} + +export function initStore(formName, form) { + const mockStore = configureMockStore(); + const state = { forms: {} }; + + if (formName && form) { + state.forms[formName] = { ...form }; + } + + return mockStore(state); +} diff --git a/packages/forms/package.json b/packages/forms/package.json index 2d4c5a46cd3..0685842bee5 100644 --- a/packages/forms/package.json +++ b/packages/forms/package.json @@ -46,9 +46,12 @@ "react-bootstrap": "^0.30.3", "react-css-transition": "^0.7.4", "react-dom": "^15.4.0", + "react-redux": "^5.0.4", "react-talend-components": "^0.80.0", "react-test-renderer": "15.4.0", "react-virtualized": "^9.1.0", + "redux": "^3.6.0", + "redux-mock-store": "^1.2.3", "rimraf": "^2.5.4", "sass-lint": "^1.10.2", "sass-loader": "^4.1.1", @@ -60,13 +63,16 @@ "classnames": "^2.2.5", "keycode": "^2.1.8", "react-autowhatever": "^7.0.0", - "react-jsonschema-form": "^0.42.0" + "react-jsonschema-form": "^0.42.0", + "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.80.0" + "react-redux": "^5.0.4", + "react-talend-components": "^0.80.0", + "redux": "^3.6.0" }, "scripts": { "prepublish": "rimraf lib && babel -d lib ./src/ && cpx \"./src/**/*.scss\" lib", diff --git a/packages/forms/src/UIForm/Message/Message.component.js b/packages/forms/src/UIForm/Message/Message.component.js new file mode 100644 index 00000000000..a782fde71f6 --- /dev/null +++ b/packages/forms/src/UIForm/Message/Message.component.js @@ -0,0 +1,29 @@ +import React, { PropTypes } from 'react'; + +export default function Message(props) { + const { + errorMessage, + description, + isValid, + } = props; + + const message = isValid ? description : errorMessage; + return message ? + ( +

+ { message } +

+ ) : + null; +} + +if (process.env.NODE_ENV !== 'production') { + Message.propTypes = { + errorMessage: PropTypes.string, + description: PropTypes.string, + isValid: PropTypes.bool, + }; +} 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..a9c93a89c04 --- /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 +

+`; 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/README.md b/packages/forms/src/UIForm/README.md new file mode 100644 index 00000000000..2d94128cdad --- /dev/null +++ b/packages/forms/src/UIForm/README.md @@ -0,0 +1,428 @@ +# Guide to UIForms + +## Implementation principles + +### TD;LR + +1. User provide initial schema and data (jsonSchema, uiSchema, properties, errors) + +2. json-schema-form-core lib process the jsonSchema and uiSchema to produce a mergedSchema + +3. The mergedSchema describe what widgets to render. + +4. The form is autonomous, it has its own lifecycle, but there are ways to change things from outside. + +### Schema and data + +#### jsonSchema +It defines the properties model. It should define the expected value (type, pattern, etc). +Those info will be used for synchronous validation on the frontend side. + +```json +{ + "jsonSchema": { + "type": "object", + "title": "Comment", + "properties": { + "lastname": { + "type": "string" + }, + "firstname": { + "type": "string" + }, + "age": { + "type": "number" + }, + "email": { + "type": "string", + "pattern": "^\\S+@\\S+$" + }, + "comment": { + "type": "string", + "maxLength": 20 + } + }, + "required": [ + "lastname", + "firstname", + "email" + ] + } +} +``` + +This will produce a flat properties : + +```json +{ + "lastname": "", + "firstname": "", + "age": 0, + "email": "", + "comment": "", +} +``` + +You can structure it like the following example : + +```json +{ + "jsonSchema": { + "type": "object", + "title": "Comment", + "properties": { + "user": { + "type": "object", + "properties": { + "lastname": { + "type": "string" + }, + "firstname": { + "type": "string" + }, + "age": { + "type": "number" + } + }, + "required": [ + "lastname", + "firstname" + ] + }, + "email": { + "type": "string", + "pattern": "^\\S+@\\S+$" + }, + "comment": { + "type": "string", + "maxLength": 20 + } + }, + "required": [ + "email" + ] + } +} +``` + +This will produce a structured properties : + +```json +{ + "user": { + "lastname": "", + "firstname": "", + "age": 0 + }, + "email": "", + "comment": "" +} +``` + +#### uiSchema +It defines the form fields model. It is an ordered array, each element can represent a field or fieldsets. + +**Field example** +For simple inputs with nothing special, you can only pass the key from jsonSchema +```json +[ + "user.lastname", + "user.firstname", + "user.age", + "email", + "comment" +] +``` + +For more complicated inputs, you can pass objects with additional properties +```json +[ + "user.lastname", + { + "key": "user.firstname", + "type": "my-widget", + "title": "First Name (with placeholder)", + "placeholder": "Enter your firstname here" + }, + "user.age", + "email", + "comment" +] +``` + +| Mandatory property | Description | +|---|---| +| key | The corresponding key in jsonSchema | +| type | The widget name in widget mapping | + +The additional values depends on the widget you use. Refers to the widget for that. +Example for the `type: "text"` type : + +| Additional property | Description | Mandatory | +|---|---|---| +| title | The input title/label | false | +| placeholder | The input placeholder | false | +| description | A comment under the input. Can be hints/instructions | false | +| validationMessage | A custom validation message if synchronous validation fails | false | +| readOnly | Specifies if the input is in readonly mode | false | + +**Fieldsets example** + +What we define as `fieldset` is all the complex widgets that manage fieldsets (fieldsets, tabs, columns, ...). + +Each of those widgets should be defined as an object in the uiSchema array. +```json +[{ + "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!" + } + ] + } + ] +}] +``` + +| Mandatory property | Description | +|---|---| +| type | The widget name in widget mapping | +| items | The array of contents of this type of fieldset manager. For tab widget, it represents each tab. Each tab content is a fieldset. | + + +#### properties + +This is a plain object that follows the jsonSchema model. It provides initial values. + +#### errors + +It represents the validation errors. The format is the error message for the composed key. A field is invalid if it has a error message. + +```json +{ + "user,lastname": "Please enter your lastname", + "user,firstname": "Please enter your firstname", + "age": "You must be at least 18 years old" +} +``` + +### JSFC (json-schema-form-core) + +We use [json-schema-form-core](https://github.com/json-schema-form/json-schema-form-core). It takes the jsonSchema and uiSchema, process them, and merge them to have only 1 array of widgets to render. + +For example, it transforms the user lastname jsonSchema/uiSchema into this mergedSchema : +```json +{ + "description": "Hint: this is the last name", + "key": ["user", "lastname"], + "required": true, + "schema": { + "type": "string" + }, + "title": "Last Name (with description)", + "type": "text" +} +``` + +The content depends on the jsonSchema/uiSchema and is the entry that configures the widget. + +### Lifecycle + +The jsonSchema/uiSchema/properties/errors that is provided to UIForm are the initial values. Those values are stored (state or redux) and live their lives. They may be modified depending on user's actions. + +**Validations** + +As the user type, the value is validated by : + +* the static validation (ex: pattern, required, etc) +* the provided customValidation function if the static validation pass. + +Those validations change the `errors` object accordingly. + +**Triggers** + +This is a way to alter everything in the form. To add a trigger in a field, you must pass the additionnal property in it's uiSchema. + +```json +[ + ... + { + "key": "user.gender", + "triggers": ["after"] + } + ... +] +``` + +There is at least 2 ways to trigger a trigger: + +* onChange on a field with an `"after"` trigger in uiSchema +* onClick on a button, passing the trigger type +* other ways depending on the widgets + +The `onTrigger` function is called. +Triggers are concepts introduced with Daikkon. The goal is to write an `onTrigger` compatible with Daikkon forms as available implementation. + +```javascript +function onTrigger(type, properties, schema, value) { + ... + + return new Promise(() => ({ + jsonSchema: {}, // the new jsonSchema + uiSchema: [], // the new uiSchema + properties: {}, // the new properties + errors: {}, // the errors to add/alter to the current errors + })) +} +``` + +The `onTrigger` should return a promise that resolves an object containing + +| Result | Description | +|---|---| +| jsonSchema | This replace the current jsonSchema | +| uiSchema | This replace the current uiSchema | +| properties | This replace the current properties | +| errors | This is merged with the current errors | + +**Redux actions** + +If you use the redux implementation of UIForm, you dispatch the actions to alter the form configurations. + +Take a look at + +* form.actions.js : actions creators on the form +* model.actions.js : actions creators to alter the properties +* validation.actions.js : actions creators to alter the errors + +## How to use + +### React state based + +```javascript +import React from 'react'; +import { UIForm } from 'react-talend-forms/lib/UIForm'; + +class MyComponent extends React.Component { + customValidation(schema, value, properties) { + return `The field ${schema.key} is not valid. Value: ${value}`; + } + + onChange(schema, value, properties) { + ... + } + + onTrigger(type, schema, value, properties) { + ... + } + + render() { + return (); + } +} +``` + +### Redux based + +```javascript +import { createStore, combineReducers } from 'redux'; +import { formReducer } from 'react-talend-forms/lib/UIForm'; + +const reducers = { + // ... your other reducers here ... + form: formReducer +} +const reducer = combineReducers(reducers) +const store = createStore(reducer) +``` + +```javascript +import React from 'react'; +import { ConnectedUIForm } from 'react-talend-forms/lib/UIForm'; + +class MyComponent extends React.Component { + constructor(props) { + super(props); + this.customValidation = this.customValidation.bind(this); + this.onChange = this.onChange.bind(this); + this.onTrigger = this.onTrigger.bind(this); + this.onSubmit = this.onSubmit.bind(this); + } + + customValidation(schema, value, properties) { + return `The field ${schema.key} is not valid. Value: ${value}`; + } + + onChange(schema, value, properties) { + ... + } + + onTrigger(type, schema, value, properties) { + ... + } + + onSubmit(event, properties) { + ... // properties is the model values + } + + render() { + return (); + } +} +``` diff --git a/packages/forms/src/UIForm/UIForm.component.js b/packages/forms/src/UIForm/UIForm.component.js new file mode 100644 index 00000000000..8cd0d926d92 --- /dev/null +++ b/packages/forms/src/UIForm/UIForm.component.js @@ -0,0 +1,181 @@ +import React, { PropTypes } from 'react'; +import { merge } from 'talend-json-schema-form-core'; + +import { TRIGGER_AFTER } from './utils/triggers'; +import { validateValue, validateAll } from './utils/validation'; +import Widget from './Widget'; + +export default class UIForm extends React.Component { + constructor(props) { + super(props); + const { jsonSchema, uiSchema } = props; + this.state = { + mergedSchema: merge(jsonSchema, uiSchema), + }; + + this.onChange = this.onChange.bind(this); + this.onTrigger = this.onTrigger.bind(this); + this.submit = this.submit.bind(this); + } + + /** + * Update the state with the new schema. + * @param jsonSchema + * @param uiSchema + */ + componentWillReceiveProps({ jsonSchema, uiSchema }) { + if (!jsonSchema || !uiSchema) { + return; + } + this.setState({ + mergedSchema: merge(jsonSchema, uiSchema), + }); + } + + /** + * Fire callbacks while interacting with form fields + * - onChange: for each field change + * - onTrigger: when trigger is provided and its value is "after" + * @param event The event that triggered the callback + * @param schema The field schema + * @param value The new value + */ + onChange(event, schema, value) { + const { + formName, + onChange, + properties, + customValidation, + } = this.props; + const error = validateValue(schema, value, properties, customValidation); + onChange(formName, schema, value, error); + + const { triggers } = schema; + if (triggers && triggers.includes(TRIGGER_AFTER)) { + 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, updateForm, onTrigger, setError, properties } = this.props; + if (!onTrigger) { + return null; + } + + return onTrigger( + type, // type of trigger + schema, // field schema + value, // field value + properties, // current properties values + ) + .then(newForm => updateForm( + formName, + newForm.jsonSchema, + newForm.uiSchema, + newForm.properties, + newForm.errors) + ) + .catch(({ errors }) => setError(formName, errors)); + } + + /** + * Triggers submit callback if form is valid + * @param event the submit event + */ + submit(event) { + event.preventDefault(); + const { mergedSchema } = this.state; + const { formName, properties, customValidation } = this.props; + const errors = validateAll(mergedSchema, properties, customValidation); + this.props.setErrors(formName, errors); + + const isValid = !Object.keys(errors).length; + if (isValid) { + this.props.onSubmit(event, properties); + } + } + + render() { + const { autoComplete, errors, formName, id, properties, widgets } = this.props; + return ( +
+ { + this.state.mergedSchema.map((nextSchema, index) => ( + + )) + } + + + ); + } +} + +if (process.env.NODE_ENV !== 'production') { + UIForm.propTypes = { + /** Form auto complete */ + autoComplete: PropTypes.string, + /** Form definition: The form name that will be used to create ids */ + formName: PropTypes.string, + /** The form id */ + id: PropTypes.string, + /** Form definition: Json schema that specify the data model */ + jsonSchema: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + /** + * Form definition: Form fields values. + * Note that it should contains @definitionName for triggers. + */ + properties: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + /** Form definition: UI schema that specify how to render the fields */ + uiSchema: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types + /** Form definition: The forms errors { [fieldKey]: errorMessage } */ + errors: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + + /** + * User callback: Custom validation function. + * Prototype: function customValidation(schema, value, properties) + * Return format : errorMessage String | falsy + * This is triggered on fields that has their uiSchema > customValidation : true + */ + customValidation: PropTypes.func, + /** User callback: Form submit callback */ + onSubmit: PropTypes.func.isRequired, + /** + * User callback: Trigger > after callback. + * Prototype: function onTrigger(type, schema, value, properties) + * This is executed on changes on fields with uiSchema > triggers : ['after'] + */ + onTrigger: PropTypes.func, + /** Custom widgets */ + widgets: PropTypes.object, // eslint-disable-line react/forbid-prop-types + + /** State management impl: The change callback */ + onChange: 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.component.test.js b/packages/forms/src/UIForm/UIForm.component.test.js new file mode 100644 index 00000000000..3a633b02edf --- /dev/null +++ b/packages/forms/src/UIForm/UIForm.component.test.js @@ -0,0 +1,201 @@ +import React from 'react'; +import { shallow, mount } from 'enzyme'; +import { data, mergedSchema, initProps } from '../../__mocks__/data'; + +import UIForm from './UIForm.component'; + +describe('UIForm component', () => { + let props; + beforeEach(() => { + props = { + ...initProps(), + setError: jest.fn(), + setErrors: jest.fn(), + updateForm: jest.fn(), + }; + }); + + it('should render form', () => { + // when + const wrapper = shallow(); + + // then + expect(wrapper.node).toMatchSnapshot(); + }); + + describe('#onChange', () => { + it('should call onChange callback', () => { + // given + const wrapper = mount(); + const newValue = 'toto'; + const event = { target: { value: newValue } }; + const inputValidationError = 'String is too short (4 chars), minimum 10'; + + // when + 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 } }; + props.onTrigger.mockReturnValueOnce(Promise.resolve({})); + + // when + wrapper.find('input').at(1).simulate('change', event); + + // then + expect(props.onTrigger).toBeCalledWith( + 'after', + mergedSchema[1], + newValue, + data.properties, + ); + }); + }); + + describe('#onTrigger', () => { + 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).toBeCalledWith( + 'after', + mergedSchema[2], + undefined, + data.properties, + ); + }); + + it('should updateForm on trigger success', (done) => { + // given + const wrapper = shallow(); + 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)); + + // when + const trigger = wrapper + .instance() + .onTrigger(null, 'after', mergedSchema[2], null); + + // then + trigger.then(() => { + expect(props.updateForm).toBeCalledWith( + props.formName, + nextData.jsonSchema, + nextData.uiSchema, + nextData.properties, + nextData.errors + ); + expect(props.setError).not.toBeCalled(); + done(); + }); + }); + + it('should setError after trigger failure', (done) => { + // given + const wrapper = shallow(); + const triggerErrors = { errors: { check: 'Error while triggeringthe trigger' } }; + props.onTrigger.mockReturnValueOnce(Promise.reject(triggerErrors)); + + // when + const trigger = wrapper + .instance() + .onTrigger(null, 'after', mergedSchema[2], null); + + // then + trigger.then(() => { + expect(props.updateForm).not.toBeCalled(); + expect(props.setError).toBeCalledWith( + props.formName, + triggerErrors.errors + ); + done(); + }); + }); + }); + + describe('#submit', () => { + it('should prevent event default', () => { + // given + const wrapper = shallow(); + const event = { preventDefault: jest.fn() }; + + // when + wrapper.instance().submit(event); + + // then + expect(event.preventDefault).toBeCalled(); + }); + + it('should validate all fields', () => { + // given + const wrapper = shallow(); + const event = { preventDefault: jest.fn() }; + + // when + wrapper.instance().submit(event); + + // then + expect(props.setErrors).toBeCalledWith( + props.formName, + { firstname: 'Missing required property: firstname' } + ); + }); + + it('should not call submit callback when form is invalid', () => { + // given + const wrapper = shallow(); + const event = { preventDefault: jest.fn() }; + + // when + wrapper.instance().submit(event); + + // then + expect(props.onSubmit).not.toBeCalled(); + }); + + it('should call submit callback when form is valid', () => { + // given + const validProperties = { + ...data.properties, + lastname: 'This has at least 10 characters', + firstname: 'This is required', + }; + const wrapper = shallow(); + const event = { preventDefault: jest.fn() }; + + // when + wrapper.instance().submit(event); + + // then + expect(props.onSubmit).toBeCalled(); + }); + }); +}); diff --git a/packages/forms/src/UIForm/UIForm.connect.js b/packages/forms/src/UIForm/UIForm.connect.js new file mode 100644 index 00000000000..7235c6ffb1c --- /dev/null +++ b/packages/forms/src/UIForm/UIForm.connect.js @@ -0,0 +1,205 @@ +import React, { PropTypes } from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import UIFormComponent from './UIForm.component'; + +import { + createForm, + removeForm, + updateForm, + updateFormData, + setError, + setErrors, +} from './actions'; + +class UIForm extends React.PureComponent { + constructor(props) { + super(props); + this.onChange = this.onChange.bind(this); + } + + /** + * Create form on mount + */ + componentWillMount() { + this.props.createForm( + this.props.formName, + this.props.data.jsonSchema, + this.props.data.uiSchema, + 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 formName The form name + * @param schema The schema + * @param value The new value + * @param error The validation error + */ + onChange(formName, schema, value, error) { + this.props.updateFormData( + formName, + schema, + value, + error + ); + if (this.props.onChange) { + this.props.onChange( + schema, + value, + this.props.form.properties // TODO fix that, old props + ); + } + } + + render() { + const { form } = this.props; + + return ( + + ); + } +} + +if (process.env.NODE_ENV !== 'production') { + UIForm.propTypes = { + /** Form schema initial 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, + }), + /** Form auto complete */ + autoComplete: PropTypes.string, + /** + * Custom validation function. + * Prototype: function customValidation(schema, value, properties) + * 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 form id */ + id: PropTypes.string, + /** + * The change callback. + * Prototype: function onChange(schema, value, properties) + */ + onChange: PropTypes.func, + /** Form submit callback */ + onSubmit: PropTypes.func.isRequired, + /** + * Tigger callback. + * Prototype: function onTrigger(type, schema, value, properties) + * This is executed on changes on fields with uiSchema > triggers : ['after'] + */ + onTrigger: PropTypes.func, + /** Custom widgets */ + widgets: PropTypes.object, // eslint-disable-line react/forbid-prop-types + + /** + * Form data from store. + * 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 */ + 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, + /** + * Form update action. + * This is injected by react-redux. See mapDispatchToProps + */ + updateForm: PropTypes.func, + /** + * Value mutation action. + * This is injected by react-redux. See mapDispatchToProps + */ + updateFormData: PropTypes.func, + /** + * Partial form validation action. + * This is injected by react-redux. See mapDispatchToProps + */ + setError: PropTypes.func, + /** + * Form validation action. + * This is injected by react-redux. See mapDispatchToProps + */ + setErrors: PropTypes.func, + }; +} + +UIForm.defaultProps = { + form: { + jsonSchema: {}, + uiSchema: [], + properties: {}, + errors: {}, + }, +}; + +function mapStateToProps(state, ownProps) { + return { form: state.forms[ownProps.formName] }; +} + +function mapDispatchToProps(dispatch) { + return { + createForm: bindActionCreators(createForm, dispatch), + removeForm: bindActionCreators(removeForm, dispatch), + updateFormData: bindActionCreators(updateFormData, dispatch), + updateForm: bindActionCreators(updateForm, dispatch), + setError: bindActionCreators(setError, dispatch), + setErrors: bindActionCreators(setErrors, dispatch), + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(UIForm); diff --git a/packages/forms/src/UIForm/UIForm.connect.test.js b/packages/forms/src/UIForm/UIForm.connect.test.js new file mode 100644 index 00000000000..e8b27cee885 --- /dev/null +++ b/packages/forms/src/UIForm/UIForm.connect.test.js @@ -0,0 +1,108 @@ +import React from 'react'; +import { shallow, mount } from 'enzyme'; + +import { data, mergedSchema, initProps, initStore } from '../../__mocks__/data'; +import UIForm from './UIForm.connect'; + +describe('UIForm connect', () => { + let props; + beforeEach(() => { + props = initProps(); + }); + + it('should render form', () => { + // when + const wrapper = shallow( + + ); + + // then + expect(wrapper.node).toMatchSnapshot(); + }); + + it('should create form state on mount', () => { + // given + const store = initStore(); + + // when + mount( + + ); + + // then + const actions = store.getActions(); + expect(actions.length).toBe(1); + expect(actions[0]).toMatchSnapshot(); + }); + + it('should remove form state on unmount', () => { + // given + const store = initStore(); + const wrapper = mount( + + ); + expect(store.getActions().length).toBe(1); + + // when + wrapper.unmount(); + + // then + const actions = store.getActions(); + expect(actions.length).toBe(2); + expect(actions[1]).toMatchSnapshot(); + }); + + describe('#onChange', () => { + it('should update state properties and errors', () => { + // given + const store = initStore(props.formName, data); + const wrapper = mount( + + ); + expect(store.getActions().length).toBe(1); + const event = { target: { value: 'toto' } }; + + // when + wrapper.find('input').at(0).simulate('change', event); + + // then + expect(store.getActions().length).toBe(2); + expect(store.getActions()[1]).toMatchSnapshot(); + }); + + it('should trigger onChange callback', () => { + // given + const store = initStore(props.formName, data); + const wrapper = mount( + + ); + const event = { target: { value: 'toto' } }; + + // when + wrapper.find('input').at(0).simulate('change', event); + + // then + expect(props.onChange).toBeCalledWith(mergedSchema[0], 'toto', data.properties); + }); + }); +}); diff --git a/packages/forms/src/UIForm/UIForm.container.js b/packages/forms/src/UIForm/UIForm.container.js new file mode 100644 index 00000000000..95023976561 --- /dev/null +++ b/packages/forms/src/UIForm/UIForm.container.js @@ -0,0 +1,160 @@ +import React, { PropTypes } from 'react'; +import UIFormComponent from './UIForm.component'; + +import { formReducer, modelReducer, validationReducer } from './reducers'; +import { createForm, updateForm, updateFormData, setError, setErrors } from './actions'; + +export default class UIForm extends React.Component { + constructor(props) { + super(props); + + 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.updateForm = this.updateForm.bind(this); + this.setError = this.setError.bind(this); + this.setErrors = this.setErrors.bind(this); + } + + /** + * 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(formName, schema, value, error) { + const action = updateFormData(formName, schema, value, error); + this.setState( + { + properties: modelReducer(this.state.properties, action), + errors: validationReducer(this.state.errors, action), + }, + () => { + if (this.props.onChange) { + this.props.onChange( + schema, + value, + this.state.properties + ); + } + } + ); + } + + /** + * Set partial fields validation in state + * @param formName the form name + * @param errors the validation errors + */ + setError(formName, errors) { + const action = setError(formName, errors); + this.setState({ errors: validationReducer(this.state.errors, action) }); + } + + /** + * Set all fields validation in state + * @param formName the form name + * @param errors the validation errors + */ + setErrors(formName, errors) { + const action = setErrors(formName, errors); + this.setState({ errors: validationReducer(this.state.errors, action) }); + } + + /** + * Update the form, the model and errors + * @param formName The form name + * @param jsonSchema The model schema + * @param uiSchema The UI schema + * @param properties The values + * @param errors The validation errors + */ + updateForm(formName, jsonSchema, uiSchema, properties, errors) { + const action = updateForm(formName, jsonSchema, uiSchema, properties, errors); + const nextState = formReducer( + { [formName]: this.state }, + action + )[formName]; + + this.setState(nextState); + } + + render() { + const { jsonSchema, uiSchema, 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 initial values. + * Note that it should contains @definitionName for triggers. + */ + properties: PropTypes.object, + }), + /** Form auto complete */ + autoComplete: PropTypes.string, + /** + * Custom validation function. + * Prototype: function customValidation(schema, value, properties) + * 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 form id */ + id: PropTypes.string, + /** + * The change callback. + * Prototype: function onChange(schema, value, properties) + */ + onChange: PropTypes.func, + /** Form submit callback */ + onSubmit: PropTypes.func.isRequired, + /** + * Tigger callback. + * Prototype: function onTrigger(type, schema, value, properties) + * This is executed on changes on fields with uiSchema > triggers : ['after'] + */ + onTrigger: PropTypes.func, + /** Custom widgets */ + widgets: PropTypes.object, // eslint-disable-line react/forbid-prop-types + }; +} diff --git a/packages/forms/src/UIForm/UIForm.container.test.js b/packages/forms/src/UIForm/UIForm.container.test.js new file mode 100644 index 00000000000..071262da2c9 --- /dev/null +++ b/packages/forms/src/UIForm/UIForm.container.test.js @@ -0,0 +1,116 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { data, mergedSchema, initProps } from '../../__mocks__/data'; +import UIForm from './UIForm.container'; + +describe('UIForm container', () => { + let props; + beforeEach(() => { + props = initProps(); + }); + + it('should render form', () => { + // when + const wrapper = shallow(); + + // then + expect(wrapper // eslint-disable-line no-underscore-dangle + .instance() + .state + ).toMatchSnapshot(); + expect(wrapper.node).toMatchSnapshot(); + }); + + describe('#onChange', () => { + it('should update state properties and errors', () => { + // given + const wrapper = shallow(); + const instance = wrapper.instance(); + + // when + instance.onChange( + props.formName, + mergedSchema[0], + 'toto', + 'too short', + ); + + // then + expect(instance.state).toMatchSnapshot(); + }); + + it('should trigger onChange callback', () => { + // given + const wrapper = shallow(); + const instance = wrapper.instance(); + + // when + instance.onChange( + props.formName, + mergedSchema[0], + 'toto', + 'too short', + ); + + // then + expect(props.onChange).toBeCalledWith(mergedSchema[0], 'toto', { lastname: 'toto' }); + }); + }); + + describe('#setErrors', () => { + it('should update state errors', () => { + // given + const wrapper = shallow(); + const instance = wrapper.instance(); + const errors = { firstname: 'my firstname is invalid' }; + + // when + instance.setErrors(props.formName, errors); + + // then + expect(instance.state).toMatchSnapshot(); + }); + }); + + describe('#setError', () => { + it('should update state error', () => { + // given + const wrapper = shallow(); + const instance = wrapper.instance(); + const errors = { firstname: 'my firstname is invalid' }; + + // when + instance.setError(props.formName, errors); + + // then + expect(instance.state).toMatchSnapshot(); + }); + }); + + describe('#updateForm', () => { + it('should update state form', () => { + // given + const wrapper = shallow(); + const instance = wrapper.instance(); + const jsonSchema = { + type: 'object', + title: 'title', + properties: { + lastname: { + type: 'string', + }, + }, + }; + const uiSchema = ['lastname']; + const properties = { lastname: 'lol' }; + const errors = { lastname: 'my lastname is invalid' }; + + // when + instance.updateForm(props.formName, jsonSchema, uiSchema, properties, errors); + + // then + expect(instance.state).toMatchSnapshot(); + }); + }); +}); 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..7a9d3cae335 --- /dev/null +++ b/packages/forms/src/UIForm/Widget/Widget.component.js @@ -0,0 +1,54 @@ +import React, { PropTypes } from 'react'; +import { sfPath } from 'talend-json-schema-form-core'; + +import defaultWidgets from '../utils/widgets'; +import { getValue } from '../utils/properties'; + +export default function Widget(props) { + const { errors, formName, onChange, onTrigger, properties, schema, widgets } = props; + const { key, type, validationMessage } = schema; + const WidgetImpl = widgets[type] || defaultWidgets[type]; + + if (!WidgetImpl) { + return null; + } + + const id = sfPath.name(key, '-', formName); + const error = errors[key]; + const errorMessage = validationMessage || error; + return ( + + ); +} + +if (process.env.NODE_ENV !== 'production') { + Widget.propTypes = { + 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, + validationMessage: PropTypes.string, + }).isRequired, + properties: PropTypes.object, // eslint-disable-line react/forbid-prop-types + widgets: PropTypes.object, // eslint-disable-line react/forbid-prop-types + }; +} + +Widget.defaultProps = { + widgets: [], +}; 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`] = ` + +`; 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/UIForm/__snapshots__/UIForm.component.test.js.snap b/packages/forms/src/UIForm/__snapshots__/UIForm.component.test.js.snap new file mode 100644 index 00000000000..df4bb0d0413 --- /dev/null +++ b/packages/forms/src/UIForm/__snapshots__/UIForm.component.test.js.snap @@ -0,0 +1,95 @@ +exports[`UIForm component should render form 1`] = ` +
+ + + + + +`; diff --git a/packages/forms/src/UIForm/__snapshots__/UIForm.connect.test.js.snap b/packages/forms/src/UIForm/__snapshots__/UIForm.connect.test.js.snap new file mode 100644 index 00000000000..4558984b044 --- /dev/null +++ b/packages/forms/src/UIForm/__snapshots__/UIForm.connect.test.js.snap @@ -0,0 +1,224 @@ +exports[`UIForm connect #onChange should update state properties and errors 1`] = ` +Object { + "error": "String is too short (4 chars), minimum 10", + "formName": "myFormName", + "schema": Object { + "autoFocus": true, + "description": "Hint: this is the last name", + "key": Array [ + "lastname", + ], + "minlength": 10, + "ngModelOptions": Object {}, + "schema": Object { + "minLength": 10, + "type": "string", + }, + "title": "Last Name (with description)", + "type": "text", + }, + "type": "TF_UPDATE_FORM_DATA", + "value": "toto", +} +`; + +exports[`UIForm connect should create form state on mount 1`] = ` +Object { + "errors": undefined, + "formName": "myFormName", + "jsonSchema": Object { + "properties": Object { + "check": Object {}, + "firstname": Object { + "type": "string", + }, + "lastname": Object { + "minLength": 10, + "type": "string", + }, + }, + "required": Array [ + "firstname", + ], + "title": "Comment", + "type": "object", + }, + "properties": Object {}, + "type": "TF_CREATE_FORM", + "uiSchema": Array [ + Object { + "autoFocus": true, + "description": "Hint: this is the last name", + "key": "lastname", + "title": "Last Name (with description)", + }, + Object { + "key": "firstname", + "placeholder": "Enter your firstname here", + "title": "First Name (with placeholder)", + "triggers": Array [ + "after", + ], + }, + Object { + "key": "check", + "title": "Check the thing", + "triggers": Array [ + "after", + ], + "type": "button", + }, + ], +} +`; + +exports[`UIForm connect should remove form state on unmount 1`] = ` +Object { + "formName": "myFormName", + "type": "TF_REMOVE_FORM", +} +`; + +exports[`UIForm connect should render form 1`] = ` + +`; diff --git a/packages/forms/src/UIForm/__snapshots__/UIForm.container.test.js.snap b/packages/forms/src/UIForm/__snapshots__/UIForm.container.test.js.snap new file mode 100644 index 00000000000..edd3ec6d11d --- /dev/null +++ b/packages/forms/src/UIForm/__snapshots__/UIForm.container.test.js.snap @@ -0,0 +1,288 @@ +exports[`UIForm container #onChange should update state properties and errors 1`] = ` +Object { + "errors": Object { + "lastname": "too short", + }, + "jsonSchema": Object { + "properties": Object { + "check": Object {}, + "firstname": Object { + "type": "string", + }, + "lastname": Object { + "minLength": 10, + "type": "string", + }, + }, + "required": Array [ + "firstname", + ], + "title": "Comment", + "type": "object", + }, + "properties": Object { + "lastname": "toto", + }, + "uiSchema": Array [ + Object { + "autoFocus": true, + "description": "Hint: this is the last name", + "key": "lastname", + "title": "Last Name (with description)", + }, + Object { + "key": "firstname", + "placeholder": "Enter your firstname here", + "title": "First Name (with placeholder)", + "triggers": Array [ + "after", + ], + }, + Object { + "key": "check", + "title": "Check the thing", + "triggers": Array [ + "after", + ], + "type": "button", + }, + ], +} +`; + +exports[`UIForm container #setError should update state error 1`] = ` +Object { + "errors": Object { + "firstname": "my firstname is invalid", + }, + "jsonSchema": Object { + "properties": Object { + "check": Object {}, + "firstname": Object { + "type": "string", + }, + "lastname": Object { + "minLength": 10, + "type": "string", + }, + }, + "required": Array [ + "firstname", + ], + "title": "Comment", + "type": "object", + }, + "properties": Object {}, + "uiSchema": Array [ + Object { + "autoFocus": true, + "description": "Hint: this is the last name", + "key": "lastname", + "title": "Last Name (with description)", + }, + Object { + "key": "firstname", + "placeholder": "Enter your firstname here", + "title": "First Name (with placeholder)", + "triggers": Array [ + "after", + ], + }, + Object { + "key": "check", + "title": "Check the thing", + "triggers": Array [ + "after", + ], + "type": "button", + }, + ], +} +`; + +exports[`UIForm container #setErrors should update state errors 1`] = ` +Object { + "errors": Object { + "firstname": "my firstname is invalid", + }, + "jsonSchema": Object { + "properties": Object { + "check": Object {}, + "firstname": Object { + "type": "string", + }, + "lastname": Object { + "minLength": 10, + "type": "string", + }, + }, + "required": Array [ + "firstname", + ], + "title": "Comment", + "type": "object", + }, + "properties": Object {}, + "uiSchema": Array [ + Object { + "autoFocus": true, + "description": "Hint: this is the last name", + "key": "lastname", + "title": "Last Name (with description)", + }, + Object { + "key": "firstname", + "placeholder": "Enter your firstname here", + "title": "First Name (with placeholder)", + "triggers": Array [ + "after", + ], + }, + Object { + "key": "check", + "title": "Check the thing", + "triggers": Array [ + "after", + ], + "type": "button", + }, + ], +} +`; + +exports[`UIForm container #updateForm should update state form 1`] = ` +Object { + "errors": Object { + "lastname": "my lastname is invalid", + }, + "jsonSchema": Object { + "properties": Object { + "lastname": Object { + "type": "string", + }, + }, + "title": "title", + "type": "object", + }, + "properties": Object { + "lastname": "lol", + }, + "uiSchema": Array [ + "lastname", + ], +} +`; + +exports[`UIForm container should render form 1`] = ` +Object { + "errors": Object {}, + "jsonSchema": Object { + "properties": Object { + "check": Object {}, + "firstname": Object { + "type": "string", + }, + "lastname": Object { + "minLength": 10, + "type": "string", + }, + }, + "required": Array [ + "firstname", + ], + "title": "Comment", + "type": "object", + }, + "properties": Object {}, + "uiSchema": Array [ + Object { + "autoFocus": true, + "description": "Hint: this is the last name", + "key": "lastname", + "title": "Last Name (with description)", + }, + Object { + "key": "firstname", + "placeholder": "Enter your firstname here", + "title": "First Name (with placeholder)", + "triggers": Array [ + "after", + ], + }, + Object { + "key": "check", + "title": "Check the thing", + "triggers": Array [ + "after", + ], + "type": "button", + }, + ], +} +`; + +exports[`UIForm container should render form 2`] = ` + +`; 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..46b53e9768c --- /dev/null +++ b/packages/forms/src/UIForm/actions/__snapshots__/form.actions.test.js.snap @@ -0,0 +1,48 @@ +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", +} +`; + +exports[`Form actions #updateForm action should create the action payload 1`] = ` +Object { + "errors": Object { + "field": "errors", + }, + "formName": "formName", + "jsonSchema": Object { + "jsonSchema": "json", + }, + "properties": Object { + "props": "json", + }, + "type": "TF_UPDATE_FORM", + "uiSchema": Array [ + Object { + "uiSchema": "json", + }, + ], +} +`; 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..7fb2deeabd3 --- /dev/null +++ b/packages/forms/src/UIForm/actions/__snapshots__/model.actions.test.js.snap @@ -0,0 +1,13 @@ +exports[`Model actions #updateFormData action should create the action payload 1`] = ` +Object { + "error": "error", + "formName": "formName", + "schema": Object { + "jsonSchema": "json", + }, + "type": "TF_UPDATE_FORM_DATA", + "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/constants.js b/packages/forms/src/UIForm/actions/constants.js new file mode 100644 index 00000000000..f8943cf6e7e --- /dev/null +++ b/packages/forms/src/UIForm/actions/constants.js @@ -0,0 +1,6 @@ +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'; +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 new file mode 100644 index 00000000000..acf9484399c --- /dev/null +++ b/packages/forms/src/UIForm/actions/form.actions.js @@ -0,0 +1,30 @@ +import { TF_CREATE_FORM, TF_UPDATE_FORM, TF_REMOVE_FORM } from './constants'; + +export function createForm(formName, jsonSchema, uiSchema, properties, errors) { + return { + type: TF_CREATE_FORM, + formName, + jsonSchema, + uiSchema, + properties, + errors, + }; +} + +export function removeForm(formName) { + return { + type: TF_REMOVE_FORM, + formName, + }; +} + +export function updateForm(formName, jsonSchema, uiSchema, properties, errors) { + return { + 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 new file mode 100644 index 00000000000..9bc0b925f37 --- /dev/null +++ b/packages/forms/src/UIForm/actions/form.actions.test.js @@ -0,0 +1,47 @@ +import { + createForm, + removeForm, + updateForm, +} from './form.actions'; + +const formName = 'formName'; +const jsonSchema = { jsonSchema: 'json' }; +const uiSchema = [{ uiSchema: 'json' }]; +const properties = { props: 'json' }; +const errors = { field: 'errors' }; + +describe('Form actions', () => { + describe('#createForm action', () => { + it('should create the action payload', () => { + // when + const resultAction = createForm(formName, jsonSchema, uiSchema, properties, errors); + + // then + expect(resultAction).toMatchSnapshot(); + }); + }); + + describe('#updateForm action', () => { + it('should create the action payload', () => { + // given + + // when + const resultAction = updateForm(formName, jsonSchema, uiSchema, properties, errors); + + // then + expect(resultAction).toMatchSnapshot(); + }); + }); + + describe('#removeForm action', () => { + it('should create the action payload', () => { + // given + + // when + const resultAction = removeForm(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 new file mode 100644 index 00000000000..6cece87d150 --- /dev/null +++ b/packages/forms/src/UIForm/actions/index.js @@ -0,0 +1,4 @@ +export { createForm, removeForm, updateForm } from './form.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 new file mode 100644 index 00000000000..b3c164de5d9 --- /dev/null +++ b/packages/forms/src/UIForm/actions/model.actions.js @@ -0,0 +1,12 @@ +import { TF_UPDATE_FORM_DATA } from './constants'; + +// eslint-disable-next-line import/prefer-default-export +export function updateFormData(formName, schema, value, error) { + return { + type: TF_UPDATE_FORM_DATA, + error, + formName, + schema, + value, + }; +} diff --git a/packages/forms/src/UIForm/actions/model.actions.test.js b/packages/forms/src/UIForm/actions/model.actions.test.js new file mode 100644 index 00000000000..ce941d98b56 --- /dev/null +++ b/packages/forms/src/UIForm/actions/model.actions.test.js @@ -0,0 +1,18 @@ +import { updateFormData } from './model.actions'; + +const formName = 'formName'; +const jsonSchema = { jsonSchema: 'json' }; +const value = { props: 'json' }; +const error = 'error'; + +describe('Model actions', () => { + describe('#updateFormData action', () => { + it('should create the action payload', () => { + // when + const resultAction = updateFormData(formName, jsonSchema, value, error); + + // then + expect(resultAction).toMatchSnapshot(); + }); + }); +}); 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..495fb53bd0c --- /dev/null +++ b/packages/forms/src/UIForm/actions/validation.actions.js @@ -0,0 +1,17 @@ +import { TF_SET_ALL_ERRORS, TF_SET_PARTIAL_ERROR } from './constants'; + +export function setError(formName, errors) { + return { + type: TF_SET_PARTIAL_ERROR, + formName, + errors, + }; +} + +export function setErrors(formName, errors) { + return { + type: TF_SET_ALL_ERRORS, + formName, + errors, + }; +} diff --git a/packages/forms/src/UIForm/actions/validation.actions.test.js b/packages/forms/src/UIForm/actions/validation.actions.test.js new file mode 100644 index 00000000000..c356f30acdb --- /dev/null +++ b/packages/forms/src/UIForm/actions/validation.actions.test.js @@ -0,0 +1,30 @@ +import { + setError, + setErrors, +} from './validation.actions'; + +const formName = 'formName'; +const error = 'error'; +const errors = ['errors']; + +describe('Validation actions', () => { + describe('#validate action', () => { + it('should create the action payload', () => { + // when + const resultAction = setError(formName, error); + + // then + expect(resultAction).toMatchSnapshot(); + }); + }); + + describe('#validateAll action', () => { + it('should create the action payload', () => { + // when + const resultAction = setErrors(formName, errors); + + // then + expect(resultAction).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/forms/src/UIForm/fields/Button.js b/packages/forms/src/UIForm/fields/Button.js new file mode 100644 index 00000000000..d98c875043a --- /dev/null +++ b/packages/forms/src/UIForm/fields/Button.js @@ -0,0 +1,41 @@ +import React, { PropTypes } from 'react'; +import classNames from 'classnames'; + +import Message from '../Message'; + +export default function Button(props) { + const { id, errorMessage, isValid, onTrigger, schema } = props; + const { description, title, triggers, type } = schema; + + return ( +
+ + +
+ ); +} + +if (process.env.NODE_ENV !== 'production') { + Button.propTypes = { + id: PropTypes.string, + isValid: PropTypes.bool, + errorMessage: PropTypes.string, + onTrigger: PropTypes.func, + schema: PropTypes.shape({ + description: PropTypes.string, + title: PropTypes.string, + type: PropTypes.string, + }), + }; +} 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`] = ` +
+ + + +
+`; diff --git a/packages/forms/src/UIForm/fieldsets/Fieldset.js b/packages/forms/src/UIForm/fieldsets/Fieldset.js new file mode 100644 index 00000000000..956405e2705 --- /dev/null +++ b/packages/forms/src/UIForm/fieldsets/Fieldset.js @@ -0,0 +1,29 @@ +import React, { PropTypes } from 'react'; +import Widget from '../Widget'; + +export default function Fieldset(props) { + const { schema, ...restProps } = props; + const { title, items } = schema; + + return ( +
+ {title && ({title})} + {items.map((itemSchema, index) => ( + + ))} +
+ ); +} + +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/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.js b/packages/forms/src/UIForm/fieldsets/Tabs.js new file mode 100644 index 00000000000..a4164506b9c --- /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 classNames from 'classnames'; + +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 tabIsValid = isValid(tabSchema, restProps.errors); + return ( + +
+ + ); + })} + + ); +} + +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({ + 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/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..1061062b772 --- /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`] = ` + + +
+ + +
+ + +`; diff --git a/packages/forms/src/UIForm/index.js b/packages/forms/src/UIForm/index.js new file mode 100644 index 00000000000..cdedd60387c --- /dev/null +++ b/packages/forms/src/UIForm/index.js @@ -0,0 +1,11 @@ +import UIForm from './UIForm.container'; +import ConnectedUIForm from './UIForm.connect'; +import { formReducer } from './reducers'; +import * as triggers from './utils/triggers'; + +export { + ConnectedUIForm, + UIForm, + formReducer, + triggers, +}; 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..12341097494 --- /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": "oldError", + }, + "jsonSchema": Object { + "jsonSchema": "oldJson", + }, + "properties": Object { + "props": "oldJson", + }, + "uiSchema": Array [ + Object { + "uiSchema": "oldJson", + }, + ], + }, +} +`; + +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": "oldJson", + }, + "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/__snapshots__/model.reducer.test.js.snap b/packages/forms/src/UIForm/reducers/__snapshots__/model.reducer.test.js.snap new file mode 100644 index 00000000000..3d6bf137333 --- /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": "oldName", + }, +} +`; + +exports[`Model reducers #TF_MUTATE_VALUE should mutate simple value 1`] = ` +Object { + "props": "oldProps", + "user": Object { + "firstname": "oldName", + }, +} +`; 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/form.reducer.js b/packages/forms/src/UIForm/reducers/form.reducer.js new file mode 100644 index 00000000000..fac6e035548 --- /dev/null +++ b/packages/forms/src/UIForm/reducers/form.reducer.js @@ -0,0 +1,81 @@ +import { + TF_CREATE_FORM, + TF_REMOVE_FORM, + TF_UPDATE_FORM, + TF_UPDATE_FORM_DATA, + TF_SET_ALL_ERRORS, + TF_SET_PARTIAL_ERROR, +} from '../actions'; +import { omit } from '../utils/properties'; +import modelReducer from './model.reducer'; +import validationsReducer from './validations.reducer'; + +/** + * Form reducer, that manage multiple form state, identified by their formName + * Format : { + * [formName]: { + * jsonSchema: {}, + * uiSchema: [], + * properties: {}, + * errors: {}, + * }, + * ... + * } + */ +export default function formReducer(state = {}, action) { + const form = state[action.formName]; + + switch (action.type) { + case TF_CREATE_FORM: { + if (form) { + return state; + } + return { + ...state, + [action.formName]: { + jsonSchema: action.jsonSchema, + uiSchema: action.uiSchema, + properties: action.properties || {}, + errors: action.errors || {}, + }, + }; + } + case TF_UPDATE_FORM: { + const { jsonSchema, uiSchema, properties, errors } = action; + if (!form || (!jsonSchema && !uiSchema && !properties && !errors)) { + return state; + } + return { + ...state, + [action.formName]: { + jsonSchema: action.jsonSchema || form.jsonSchema, + uiSchema: action.uiSchema || form.uiSchema, + properties: action.properties || form.properties, + errors: { + ...form.errors, + ...action.errors, + }, + }, + }; + } + case TF_REMOVE_FORM: + return omit(state, action.formName); + case TF_UPDATE_FORM_DATA: + case TF_SET_ALL_ERRORS: + case TF_SET_PARTIAL_ERROR: { + if (!form) { + return state; + } + return { + ...state, + [action.formName]: { + ...form, + properties: modelReducer(form.properties, action), + errors: validationsReducer(form.errors, action), + }, + }; + } + default: + return state; + } +} 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(); + }); + }); +}); diff --git a/packages/forms/src/UIForm/reducers/index.js b/packages/forms/src/UIForm/reducers/index.js new file mode 100644 index 00000000000..af18c19bbd8 --- /dev/null +++ b/packages/forms/src/UIForm/reducers/index.js @@ -0,0 +1,5 @@ +import formReducer from './form.reducer'; +import modelReducer from './model.reducer'; +import validationReducer from './validations.reducer'; + +export { formReducer, 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..ec441beb154 --- /dev/null +++ b/packages/forms/src/UIForm/reducers/model.reducer.js @@ -0,0 +1,35 @@ +import { TF_UPDATE_FORM_DATA } 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 modelReducer(state = {}, action) { + switch (action.type) { + case TF_UPDATE_FORM_DATA: + return mutateValue(state, action.schema.key, action.value); + default: + return state; + } +} 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(); + }); + }); +}); 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..fd044b304c0 --- /dev/null +++ b/packages/forms/src/UIForm/reducers/validations.reducer.js @@ -0,0 +1,36 @@ +import { TF_UPDATE_FORM_DATA, TF_SET_ALL_ERRORS, TF_SET_PARTIAL_ERROR } from '../actions'; +import { omit } from '../utils/properties'; + +/** + * 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 TF_UPDATE_FORM_DATA: { + const { schema, error } = action; + if (error) { + return { + ...state, + [schema.key]: error, + }; + } + return omit(state, schema.key.toString()); + } + case TF_SET_PARTIAL_ERROR: { + if (Object.keys(action.errors).length === 0) { + return state; + } + return { + ...state, + ...action.errors, + }; + } + case TF_SET_ALL_ERRORS: { + return action.errors; + } + default: + return state; + } +} 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(); + }); + }); +}); diff --git a/packages/forms/src/UIForm/utils/properties.js b/packages/forms/src/UIForm/utils/properties.js new file mode 100644 index 00000000000..4bf68ddcf2b --- /dev/null +++ b/packages/forms/src/UIForm/utils/properties.js @@ -0,0 +1,33 @@ +/** + * 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) { + if (!key) { + return undefined; + } + + return key.reduce( + (accu, nextKey) => accu && accu[nextKey], + properties + ); +} + +/** + * Omit a property 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/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/triggers.js b/packages/forms/src/UIForm/utils/triggers.js new file mode 100644 index 00000000000..3ccd5750ac8 --- /dev/null +++ b/packages/forms/src/UIForm/utils/triggers.js @@ -0,0 +1 @@ +export const TRIGGER_AFTER = 'after'; // eslint-disable-line import/prefer-default-export diff --git a/packages/forms/src/UIForm/utils/validation.js b/packages/forms/src/UIForm/utils/validation.js new file mode 100644 index 00000000000..c839b2d1489 --- /dev/null +++ b/packages/forms/src/UIForm/utils/validation.js @@ -0,0 +1,72 @@ +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(schema, value, properties); + } + 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 : + * - 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 errors The errors + * @returns {boolean} true if it is invalid, false otherwise + */ +export function isValid(schema, errors) { + const { key, items } = schema; + if (key && errors[key]) { + return false; + } + + if (items) { + for (const itemSchema of items) { + if (!isValid(itemSchema, errors)) { + return false; + } + } + } + + return true; +} 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); + }); + }); +}); diff --git a/packages/forms/src/UIForm/utils/widgets.js b/packages/forms/src/UIForm/utils/widgets.js new file mode 100644 index 00000000000..b767e3ccef8 --- /dev/null +++ b/packages/forms/src/UIForm/utils/widgets.js @@ -0,0 +1,18 @@ +import Fieldset from '../fieldsets/Fieldset'; +import Tabs from '../fieldsets/Tabs'; + +import Button from '../fields/Button'; +import Text from '../fields/Text'; + +const widgets = { + // fieldsets + fieldset: Fieldset, + tabs: Tabs, + + // fields + button: Button, + number: Text, + text: Text, +}; + +export default widgets; diff --git a/packages/forms/stories-core/customWidgetStory.js b/packages/forms/stories-core/customWidgetStory.js new file mode 100644 index 00000000000..4d9628fc12d --- /dev/null +++ b/packages/forms/stories-core/customWidgetStory.js @@ -0,0 +1,56 @@ +import React from 'react'; +import { UIForm } from '../src/UIForm'; + +function CustomWidget(props) { + const { value } = props; + + return ( +
+
+

Custom widget

+
+
+ Form was instantiated with a custom widget to display its selected value + {value}. +
+
+ ); +} + +CustomWidget.propTypes = { + value: React.PropTypes.string, +}; + +function story() { + const widgets = { custom: CustomWidget }; + const schema = { + jsonSchema: { + title: 'Unknown widget', + type: 'object', + properties: { + list: { + type: 'string', + enum: ['one', 'two', 'three'], + enumNames: ['One', 'Two', 'Three'], + }, + }, + }, + properties: { + list: 'two', + }, + uiSchema: [ + { + key: 'list', + type: 'custom', + }, + ], + }; + return ( + + ); +} + +export default { + name: 'Core Custom widget', + story, +}; diff --git a/packages/forms/stories-core/index.js b/packages/forms/stories-core/index.js new file mode 100644 index 00000000000..db66361e401 --- /dev/null +++ b/packages/forms/stories-core/index.js @@ -0,0 +1,42 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import a11y from 'react-a11y'; +import { Provider } from 'react-redux'; +import { storiesOf } from '@kadira/storybook'; +import { withKnobs } from '@kadira/storybook-addon-knobs'; + +import Well from 'react-bootstrap/lib/Well'; + +import { createStore, combineReducers } from 'redux'; +import { formReducer } from '../src/UIForm'; + +import jsonStories from './jsonStories'; +import customWidgetStory from './customWidgetStory'; + +const reducers = { forms: formReducer }; +const reducer = combineReducers(reducers); +const store = createStore(reducer); + +a11y(ReactDOM); + +const decoratedStories = storiesOf('Form', module) + .addDecorator(withKnobs) + .addDecorator(story => ( + +
+
+ + {story()} + +
+
+
+ )); + +jsonStories.forEach(({ name, story }) => { + decoratedStories.add(name, story); +}); +decoratedStories.add(customWidgetStory.name, customWidgetStory.story); diff --git a/packages/forms/stories-core/json/core-buttons.json b/packages/forms/stories-core/json/core-buttons.json new file mode 100644 index 00000000000..33039c75f58 --- /dev/null +++ b/packages/forms/stories-core/json/core-buttons.json @@ -0,0 +1,24 @@ +{ + "jsonSchema": { + "type": "object", + "title": "Comment", + "properties": {} + }, + "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": {} +} diff --git a/packages/forms/stories-core/json/core-custom-validation.json b/packages/forms/stories-core/json/core-custom-validation.json new file mode 100644 index 00000000000..4559c937793 --- /dev/null +++ b/packages/forms/stories-core/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": {} +} diff --git a/packages/forms/stories-core/json/core-fieldset.json b/packages/forms/stories-core/json/core-fieldset.json new file mode 100644 index 00000000000..68c5b22dc29 --- /dev/null +++ b/packages/forms/stories-core/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" + } +} diff --git a/packages/forms/stories-core/json/core-simple.json b/packages/forms/stories-core/json/core-simple.json new file mode 100644 index 00000000000..6618bf0037a --- /dev/null +++ b/packages/forms/stories-core/json/core-simple.json @@ -0,0 +1,91 @@ +{ + "jsonSchema": { + "type": "object", + "title": "Comment", + "properties": { + "name": { + "type": "string" + }, + "lastname": { + "type": "string" + }, + "firstname": { + "type": "string" + }, + "age": { + "type": "number" + }, + "readonlyField": { + "type": "string" + }, + "disabledField": { + "type": "string" + }, + "email": { + "type": "string", + "pattern": "^\\S+@\\S+$" + }, + "comment": { + "type": "string", + "maxLength": 20 + } + }, + "required": [ + "name", + "firstname", + "email", + "comment" + ] + }, + "uiSchema": [ + { + "key": "name", + "title": "Name" + }, + { + "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" + }, + { + "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": "readonlyField", + "title": "Field (read only mode)", + "readOnly": true + }, + { + "key": "disabledField", + "title": "Field (disabled mode)", + "disabled": true + }, + { + "key": "comment", + "type": "textarea", + "title": "Comment", + "placeholder": "Make a comment", + "validationMessage": "Don't be greedy!" + } + ], + "properties": { + "name": "Chuck Norris", + "readonlyField": "You can't change that", + "disabledField": "You can't change that", + "email": "ChuckyFTW@gmail.com", + "comment": "lol" + } +} diff --git a/packages/forms/stories-core/json/core-structured-model.json b/packages/forms/stories-core/json/core-structured-model.json new file mode 100644 index 00000000000..7da37c72051 --- /dev/null +++ b/packages/forms/stories-core/json/core-structured-model.json @@ -0,0 +1,90 @@ +{ + "jsonSchema": { + "type": "object", + "title": "Comment", + "properties": { + "user": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "lastname": { + "type": "string" + }, + "firstname": { + "type": "string" + }, + "age": { + "type": "number" + } + }, + "required": [ + "name", + "firstname" + ] + }, + "nochange": { + "type": "string" + }, + "email": { + "type": "string", + "pattern": "^\\S+@\\S+$" + }, + "comment": { + "type": "string", + "maxLength": 20 + } + }, + "required": [ + "email", + "comment" + ] + }, + "uiSchema": [ + { + "key": "user.name", + "title": "Name" + }, + { + "key": "user.lastname", + "title": "Last Name (with description)", + "description": "Hint: this is the last name" + }, + { + "key": "user.firstname", + "title": "First Name (with placeholder)", + "placeholder": "Enter your firstname here" + }, + { + "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", + "title": "Field (read only mode)", + "readOnly": true + }, + { + "key": "comment", + "type": "textarea", + "title": "Comment", + "placeholder": "Make a comment", + "validationMessage": "Don't be greedy!" + } + ], + "properties": { + "user": { + "name": "Chuck Norris" + }, + "nochange": "You can't change that", + "email": "ChuckyFTW@gmail.com", + "comment": "lol" + } +} diff --git a/packages/forms/stories-core/json/core-tabs.json b/packages/forms/stories-core/json/core-tabs.json new file mode 100644 index 00000000000..4aad6fcbb0b --- /dev/null +++ b/packages/forms/stories-core/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" + } +} diff --git a/packages/forms/stories-core/json/core-trigger-after.json b/packages/forms/stories-core/json/core-trigger-after.json new file mode 100644 index 00000000000..4d79f93dd30 --- /dev/null +++ b/packages/forms/stories-core/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": {} +} diff --git a/packages/forms/stories-core/jsonStories.js b/packages/forms/stories-core/jsonStories.js new file mode 100644 index 00000000000..4a8e14f1ba5 --- /dev/null +++ b/packages/forms/stories-core/jsonStories.js @@ -0,0 +1,83 @@ +import React from 'react'; +import { action } from '@kadira/storybook'; +import { object } from '@kadira/storybook-addon-knobs'; +import { Tabs, Tab } from 'react-bootstrap'; +import IconsProvider from 'react-talend-components/lib/IconsProvider'; +import { UIForm, ConnectedUIForm } from '../src/UIForm'; + +const sampleFilenames = require.context('./json', true, /.(js|json)$/); +const sampleFilenameRegex = /^.\/(.*).js/; +const stories = []; + +function capitalizeFirstLetter(string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} + +function createCommonProps() { + return { + autocomplete: 'off', + // onBlur: action('Blur'), + customValidation(schema, value, properties) { + action('customValidation')(schema, value, properties); + return value.length >= 5 && + 'Custom validation : The value should be less than 5 chars'; + }, + formName: 'my-form', + onChange: action('Change'), + onTrigger(type, schema, value, properties) { + action('Trigger')(type, schema, value, properties); + 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'), + }; +} + +function createStory(filename) { + const sampleNameMatches = filename.match(sampleFilenameRegex); + const sampleName = sampleNameMatches[sampleNameMatches.length - 1]; + const name = capitalizeFirstLetter(sampleName); + const props = createCommonProps(); + + return { + name, + story() { + return ( +
+ + + + + + + + + + +
+ ); + }, + }; +} + +sampleFilenames + .keys() + .forEach((filename) => { stories.push(createStory(filename)); }); + +export default stories; diff --git a/packages/forms/yarn.lock b/packages/forms/yarn.lock index 8e13733bcc1..11053fdb9f1 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.74.2: - version "0.74.2" - resolved "https://registry.yarnpkg.com/bootstrap-talend-theme/-/bootstrap-talend-theme-0.74.2.tgz#33dc36934523bedd75d69f9ed1ac339ecebe7c13" +bootstrap-talend-theme@^0.75.0: + version "0.75.1" + resolved "https://registry.yarnpkg.com/bootstrap-talend-theme/-/bootstrap-talend-theme-0.75.1.tgz#62b6eb1b99b0c931425e98ccb0504ef561225772" dependencies: bootstrap-sass "^3.3.7" @@ -1696,6 +1696,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" @@ -2865,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" @@ -3017,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: version "2.2.2" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" dependencies: @@ -3875,7 +3882,7 @@ locate-path@^2.0.0: p-locate "^2.0.0" path-exists "^3.0.0" -lodash-es@^4.2.1: +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" @@ -4504,7 +4511,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" @@ -4560,6 +4567,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" @@ -5061,7 +5072,7 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -prop-types@^15.5.6: +prop-types@^15.0.0, prop-types@^15.5.6: version "15.5.10" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154" dependencies: @@ -5301,6 +5312,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" @@ -5314,9 +5337,9 @@ react-stubber@^1.0.0: dependencies: babel-runtime "^6.5.0" -react-talend-components@^0.74.2: - version "0.74.2" - resolved "https://registry.yarnpkg.com/react-talend-components/-/react-talend-components-0.74.2.tgz#f5c29c8fc73bf571a90ea0609bc2ff037c9955de" +react-talend-components@^0.75.0: + version "0.75.1" + resolved "https://registry.yarnpkg.com/react-talend-components/-/react-talend-components-0.75.1.tgz#b160356015a89d8348fd9278ed0e3d7b2ad56f5c" dependencies: lodash "^4.17.4" react-autowhatever "^7.0.0" @@ -5461,7 +5484,11 @@ reduce-function-call@^1.0.1: dependencies: balanced-match "^0.4.2" -redux@^3.5.2: +redux-mock-store@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.2.3.tgz#1b3ad299da91cb41ba30d68e3b6f024475fb9e1b" + +redux@^3.5.2, redux@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/redux/-/redux-3.6.0.tgz#887c2b3d0b9bd86eca2be70571c27654c19e188d" dependencies: @@ -6044,9 +6071,16 @@ table@^3.7.8: slice-ansi "0.0.4" string-width "^2.0.0" -talend-icons@^0.74.2: - version "0.74.2" - resolved "https://registry.yarnpkg.com/talend-icons/-/talend-icons-0.74.2.tgz#42ee59ee43163f200f3f024276f872891af647ab" +talend-icons@^0.75.0: + version "0.75.1" + resolved "https://registry.yarnpkg.com/talend-icons/-/talend-icons-0.75.1.tgz#b80f913dbc3d85c9b1bec4fbc539fdaeac9537f1" + +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" @@ -6159,6 +6193,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" 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 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 TpvAzw5{S~nCvrsyqrm?Hed#7x diff --git a/packages/theme/src/theme/_colors.scss b/packages/theme/src/theme/_colors.scss index d582d2a10a6..e426d3bb712 100644 --- a/packages/theme/src/theme/_colors.scss +++ b/packages/theme/src/theme/_colors.scss @@ -59,6 +59,11 @@ $dove-gray: #666; /// @type Color $silver: #C0C0C0; +/// Used for input disabled colors, etc.. +/// +/// @type Color +$silver-chalice: #A0A0A0; + /// Used for backgrounds etc. /// /// @type Color diff --git a/packages/theme/src/theme/_forms.scss b/packages/theme/src/theme/_forms.scss index 37cb6f4a2bd..634880dda4c 100644 --- a/packages/theme/src/theme/_forms.scss +++ b/packages/theme/src/theme/_forms.scss @@ -73,6 +73,11 @@ form { font-size: $font-size-large; } } + + &::placeholder { + color: $silver; + font-style: oblique; + } } select, @@ -329,6 +334,7 @@ form { position: absolute; top: 2.2rem; left: 0; + color: $dove-gray; font-size: $font-size-base; transition: opacity 0.1s linear, font-size 0.1s linear, top 0.1s linear; pointer-events: none; @@ -353,14 +359,18 @@ form { input[type='date'], input[type='datetime-local'] { + label { - top: 0; + color: $silver; font-size: $font-size-small; + top: 0; } } - &-control:disabled { + &-control:disabled, + &-control[value]:not([value='']):disabled { + color: $silver-chalice; + + label { - opacity: 0.4; + color: $silver-chalice; } } } diff --git a/packages/theme/src/theme/_variables.scss b/packages/theme/src/theme/_variables.scss index cffc641ac1c..539541877e3 100644 --- a/packages/theme/src/theme/_variables.scss +++ b/packages/theme/src/theme/_variables.scss @@ -206,7 +206,7 @@ $input-bg: transparent !default; $input-bg-disabled: $gray-lighter !default; //** Text color for ``s -$input-color: $gray !default; +$input-color: $black !default; //** `` border color $input-border: #CCC !default;