@@ -64,10 +64,13 @@ class MainSignUpPageWidget extends React.Component {
};
}
- getInputProps() {
+ getInputProps(password) {
return {
- inputType: 'secondary',
- className: 'signup-widget__input'
+ className: 'signup-widget__input',
+ fieldProps: {
+ size: 'medium',
+ password: password
+ }
};
}
diff --git a/client/src/core-components/__tests__/form-field-test.js b/client/src/core-components/__tests__/form-field-test.js
new file mode 100644
index 00000000..43fece59
--- /dev/null
+++ b/client/src/core-components/__tests__/form-field-test.js
@@ -0,0 +1,449 @@
+const Input = ReactMock();
+const Checkbox = ReactMock();
+const DropDown = ReactMock();
+const TextEditor = ReactMock();
+
+const {EditorState} = require('draft-js');
+
+const FormField = requireUnit('core-components/form-field', {
+ 'core-components/input': Input,
+ 'core-components/checkbox': Checkbox,
+ 'core-components/drop-down': DropDown,
+ 'core-components/text-editor': TextEditor
+});
+
+
+describe('FormField component', function () {
+ let component, innerField;
+
+ function renderFormField(props = { field: 'input'}) {
+ let fields = {
+ 'input': Input,
+ 'checkbox': Checkbox,
+ 'select': DropDown,
+ 'textarea': TextEditor
+ };
+
+ component = reRenderIntoDocument(
+
+ );
+ innerField = TestUtils.scryRenderedComponentsWithType(component, fields[props.field])[0];
+ }
+
+ describe('when calling static getDefaultValue', function () {
+ it('should return correct values', function () {
+ expect(FormField.getDefaultValue('input')).to.equal('');
+ expect(FormField.getDefaultValue('checkbox')).to.equal(false);
+ expect(FormField.getDefaultValue('select')).to.equal(0);
+ expect(FormField.getDefaultValue('textarea') instanceof EditorState).to.equal(true);
+ });
+ });
+
+ describe('when rendering an input field', function () {
+
+ beforeEach(function () {
+ renderFormField({
+ field: 'input',
+ name: 'MOCK_NAME',
+ label: 'MOCK_LABEL',
+ error: 'MOCK_ERROR',
+ value: 'VALUE_MOCK',
+ required: true,
+ validation: 'MOCK_VALIDATION',
+ fieldProps: {
+ prop1: 'value1',
+ prop2: 'value2',
+ prop3: 'value3'
+ },
+ onChange: stub(),
+ onBlur: stub()
+ });
+ });
+
+ it('should be wrapped in a label', function () {
+ expect(ReactDOM.findDOMNode(component).tagName).to.equal('LABEL');
+ expect(ReactDOM.findDOMNode(component).className).to.include('form-field');
+ expect(ReactDOM.findDOMNode(component).className).to.include('form-field_errored');
+ expect(ReactDOM.findDOMNode(component).className).to.not.include('form-field_select');
+ expect(ReactDOM.findDOMNode(component).className).to.not.include('form-field_checkbox');
+
+ renderFormField({
+ field: 'input',
+ name: 'MOCK_NAME',
+ label: 'MOCK_LABEL',
+ error: '',
+ value: 'VALUE_MOCK',
+ required: true,
+ validation: 'MOCK_VALIDATION',
+ fieldProps: {
+ prop1: 'value1',
+ prop2: 'value2',
+ prop3: 'value3'
+ },
+ onChange: stub(),
+ onBlur: stub()
+ });
+ expect(ReactDOM.findDOMNode(component).className).to.include('form-field');
+ expect(ReactDOM.findDOMNode(component).className).to.not.include('form-field_errored');
+ expect(ReactDOM.findDOMNode(component).className).to.not.include('form-field_checkbox');
+ expect(ReactDOM.findDOMNode(component).className).to.not.include('form-field_select');
+ });
+
+ it('should render error and label in right order and pass correct classes', function () {
+ expect(ReactDOM.findDOMNode(component).children[0].tagName).to.equal('SPAN');
+ expect(ReactDOM.findDOMNode(component).children[0].className).to.equal('form-field__label');
+ expect(ReactDOM.findDOMNode(component).children[0].textContent).to.equal('MOCK_LABEL');
+
+ expect(ReactDOM.findDOMNode(component).children[1]).to.equal(ReactDOM.findDOMNode(innerField));
+
+ expect(ReactDOM.findDOMNode(component).children[2].tagName).to.equal('SPAN');
+ expect(ReactDOM.findDOMNode(component).children[2].className).to.equal('form-field__error');
+ expect(ReactDOM.findDOMNode(component).children[2].textContent).to.equal('MOCK_ERROR');
+ });
+
+ it('should pass props correctly to Input', function () {
+ expect(innerField.props).to.include({
+ prop1: 'value1',
+ prop2: 'value2',
+ prop3: 'value3',
+ errored: true,
+ name: 'MOCK_NAME',
+ onBlur: component.props.onBlur,
+ required: true,
+ value: 'VALUE_MOCK'
+ });
+ });
+
+ it('should pass disable field when context is loading', function () {
+ component.context.loading = true;
+ component.forceUpdate();
+
+ expect(innerField.props.disabled).to.equal(true);
+ component.context.loading = false;
+ });
+
+ it('should pass callbacks correctly', function () {
+ component.props.onChange.reset();
+ component.props.onBlur.reset();
+
+ innerField.props.onChange({ target: { value: 'SOME_VALUE_2'}});
+ innerField.props.onBlur();
+
+ expect(component.props.onBlur).to.have.been.called;
+ expect(component.props.onChange).to.have.been.calledWithMatch({target: { value: 'SOME_VALUE_2'}});
+ });
+
+ it('should pass focus to the field', function () {
+ innerField.focus = stub();
+ component.focus();
+
+ expect(innerField.focus).to.have.been.called;
+ });
+ });
+
+ describe('when rendering a checkbox field', function () {
+
+ beforeEach(function () {
+ renderFormField({
+ field: 'checkbox',
+ name: 'MOCK_NAME',
+ label: 'MOCK_LABEL',
+ error: 'MOCK_ERROR',
+ value: false,
+ required: true,
+ validation: 'MOCK_VALIDATION',
+ fieldProps: {
+ prop1: 'value1',
+ prop2: 'value2',
+ prop3: 'value3'
+ },
+ onChange: stub(),
+ onBlur: stub()
+ });
+ });
+
+ it('should be wrapped in a label', function () {
+ expect(ReactDOM.findDOMNode(component).tagName).to.equal('LABEL');
+ expect(ReactDOM.findDOMNode(component).className).to.include('form-field');
+ expect(ReactDOM.findDOMNode(component).className).to.include('form-field_checkbox');
+ expect(ReactDOM.findDOMNode(component).className).to.include('form-field_errored');
+
+ renderFormField({
+ field: 'checkbox',
+ name: 'MOCK_NAME',
+ label: 'MOCK_LABEL',
+ error: '',
+ value: false,
+ required: true,
+ validation: 'MOCK_VALIDATION',
+ fieldProps: {
+ prop1: 'value1',
+ prop2: 'value2',
+ prop3: 'value3'
+ },
+ onChange: stub(),
+ onBlur: stub()
+ });
+ expect(ReactDOM.findDOMNode(component).className).to.include('form-field');
+ expect(ReactDOM.findDOMNode(component).className).to.include('form-field_checkbox');
+ expect(ReactDOM.findDOMNode(component).className).to.not.include('form-field_errored');
+ });
+
+ it('should render error and label in right order and pass correct classes', function () {
+ expect(ReactDOM.findDOMNode(component).children[0]).to.equal(ReactDOM.findDOMNode(innerField));
+
+ expect(ReactDOM.findDOMNode(component).children[1].tagName).to.equal('SPAN');
+ expect(ReactDOM.findDOMNode(component).children[1].className).to.equal('form-field__label');
+ expect(ReactDOM.findDOMNode(component).children[1].textContent).to.equal('MOCK_LABEL');
+
+ expect(ReactDOM.findDOMNode(component).children[2].tagName).to.equal('SPAN');
+ expect(ReactDOM.findDOMNode(component).children[2].className).to.equal('form-field__error');
+ expect(ReactDOM.findDOMNode(component).children[2].textContent).to.equal('MOCK_ERROR');
+ });
+
+ it('should pass props correctly to Checkbox', function () {
+ expect(innerField.props).to.include({
+ prop1: 'value1',
+ prop2: 'value2',
+ prop3: 'value3',
+ errored: true,
+ name: 'MOCK_NAME',
+ onBlur: component.props.onBlur,
+ required: true,
+ value: false
+ });
+ });
+
+ it('should pass disable field when context is loading', function () {
+ component.context.loading = true;
+ component.forceUpdate();
+
+ expect(innerField.props.disabled).to.equal(true);
+ component.context.loading = false;
+ });
+
+ it('should pass callbacks correctly', function () {
+ component.props.onChange.reset();
+ component.props.onBlur.reset();
+
+ innerField.props.onChange({ target: { checked: true }});
+ innerField.props.onBlur();
+
+ expect(component.props.onBlur).to.have.been.called;
+ expect(component.props.onChange).to.have.been.calledWithMatch({target: { value: true}});
+ });
+
+ it('should pass focus to the field', function () {
+ innerField.focus = stub();
+ component.focus();
+
+ expect(innerField.focus).to.have.been.called;
+ });
+ });
+
+ describe('when rendering an select field', function () {
+
+ beforeEach(function () {
+ renderFormField({
+ field: 'select',
+ name: 'MOCK_NAME',
+ label: 'MOCK_LABEL',
+ error: 'MOCK_ERROR',
+ value: 5,
+ required: true,
+ validation: 'MOCK_VALIDATION',
+ fieldProps: {
+ prop1: 'value1',
+ prop2: 'value2',
+ prop3: 'value3',
+ items: []
+ },
+ onChange: stub(),
+ onBlur: stub()
+ });
+ });
+
+ it('should be wrapped in a label', function () {
+ expect(ReactDOM.findDOMNode(component).tagName).to.equal('LABEL');
+ expect(ReactDOM.findDOMNode(component).className).to.include('form-field');
+ expect(ReactDOM.findDOMNode(component).className).to.include('form-field_errored');
+ expect(ReactDOM.findDOMNode(component).className).to.include('form-field_select');
+ expect(ReactDOM.findDOMNode(component).className).to.not.include('form-field_checkbox');
+
+ renderFormField({
+ field: 'select',
+ name: 'MOCK_NAME',
+ label: 'MOCK_LABEL',
+ error: '',
+ value: 5,
+ required: true,
+ validation: 'MOCK_VALIDATION',
+ fieldProps: {
+ prop1: 'value1',
+ prop2: 'value2',
+ prop3: 'value3',
+ items: []
+ },
+ onChange: stub(),
+ onBlur: stub()
+ });
+ expect(ReactDOM.findDOMNode(component).className).to.include('form-field');
+ expect(ReactDOM.findDOMNode(component).className).to.not.include('form-field_errored');
+ expect(ReactDOM.findDOMNode(component).className).to.include('form-field_select');
+ expect(ReactDOM.findDOMNode(component).className).to.not.include('form-field_checkbox');
+ });
+
+ it('should render error and label in right order and pass correct classes', function () {
+ expect(ReactDOM.findDOMNode(component).children[0].tagName).to.equal('SPAN');
+ expect(ReactDOM.findDOMNode(component).children[0].className).to.equal('form-field__label');
+ expect(ReactDOM.findDOMNode(component).children[0].textContent).to.equal('MOCK_LABEL');
+
+ expect(ReactDOM.findDOMNode(component).children[1]).to.equal(ReactDOM.findDOMNode(innerField));
+
+ expect(ReactDOM.findDOMNode(component).children[2].tagName).to.equal('SPAN');
+ expect(ReactDOM.findDOMNode(component).children[2].className).to.equal('form-field__error');
+ expect(ReactDOM.findDOMNode(component).children[2].textContent).to.equal('MOCK_ERROR');
+ });
+
+ it('should pass props correctly to DropDown', function () {
+ expect(innerField.props).to.include({
+ prop1: 'value1',
+ prop2: 'value2',
+ prop3: 'value3',
+ errored: true,
+ name: 'MOCK_NAME',
+ onBlur: component.props.onBlur,
+ required: true,
+ selectedIndex: 5
+ });
+ });
+
+ it('should pass disable field when context is loading', function () {
+ component.context.loading = true;
+ component.forceUpdate();
+
+ expect(innerField.props.disabled).to.equal(true);
+ component.context.loading = false;
+ });
+
+ it('should pass callbacks correctly', function () {
+ component.props.onChange.reset();
+ component.props.onBlur.reset();
+
+ innerField.props.onChange({index: 2});
+ innerField.props.onBlur();
+
+ expect(component.props.onBlur).to.have.been.called;
+ expect(component.props.onChange).to.have.been.calledWithMatch({target: {value: 2}});
+ });
+
+ it('should pass focus to the field', function () {
+ innerField.focus = stub();
+ component.focus();
+
+ expect(innerField.focus).to.have.been.called;
+ });
+ });
+
+ describe('when rendering an textarea field', function () {
+
+ beforeEach(function () {
+ renderFormField({
+ field: 'textarea',
+ name: 'MOCK_NAME',
+ label: 'MOCK_LABEL',
+ error: 'MOCK_ERROR',
+ value: {value: 'VALUE_MOCk'},
+ required: true,
+ validation: 'MOCK_VALIDATION',
+ fieldProps: {
+ prop1: 'value1',
+ prop2: 'value2',
+ prop3: 'value3'
+ },
+ onChange: stub(),
+ onBlur: stub()
+ });
+ });
+
+ it('should be wrapped in a div', function () {
+ expect(ReactDOM.findDOMNode(component).tagName).to.equal('DIV');
+ expect(ReactDOM.findDOMNode(component).className).to.include('form-field');
+ expect(ReactDOM.findDOMNode(component).className).to.include('form-field_errored');
+ expect(ReactDOM.findDOMNode(component).className).to.not.include('form-field_select');
+ expect(ReactDOM.findDOMNode(component).className).to.not.include('form-field_checkbox');
+
+ renderFormField({
+ field: 'textarea',
+ name: 'MOCK_NAME',
+ label: 'MOCK_LABEL',
+ error: '',
+ value: {value: 'VALUE_MOCk'},
+ required: true,
+ validation: 'MOCK_VALIDATION',
+ fieldProps: {
+ prop1: 'value1',
+ prop2: 'value2',
+ prop3: 'value3'
+ },
+ onChange: stub(),
+ onBlur: stub()
+ });
+ expect(ReactDOM.findDOMNode(component).className).to.include('form-field');
+ expect(ReactDOM.findDOMNode(component).className).to.not.include('form-field_errored');
+ expect(ReactDOM.findDOMNode(component).className).to.not.include('form-field_checkbox');
+ expect(ReactDOM.findDOMNode(component).className).to.not.include('form-field_select');
+ });
+
+ it('should render error and label in right order and pass correct classes', function () {
+ expect(ReactDOM.findDOMNode(component).children[0].tagName).to.equal('SPAN');
+ expect(ReactDOM.findDOMNode(component).children[0].className).to.equal('form-field__label');
+ expect(ReactDOM.findDOMNode(component).children[0].textContent).to.equal('MOCK_LABEL');
+
+ expect(ReactDOM.findDOMNode(component).children[1]).to.equal(ReactDOM.findDOMNode(innerField));
+
+ expect(ReactDOM.findDOMNode(component).children[2].tagName).to.equal('SPAN');
+ expect(ReactDOM.findDOMNode(component).children[2].className).to.equal('form-field__error');
+ expect(ReactDOM.findDOMNode(component).children[2].textContent).to.equal('MOCK_ERROR');
+ });
+
+ it('should pass props correctly to TextEditor', function () {
+ expect(innerField.props).to.include({
+ prop1: 'value1',
+ prop2: 'value2',
+ prop3: 'value3',
+ errored: true,
+ name: 'MOCK_NAME',
+ onBlur: component.props.onBlur,
+ required: true,
+ });
+ expect(innerField.props.value).to.deep.equal({value: 'VALUE_MOCk'});
+ });
+
+ it('should pass disable field when context is loading', function () {
+ component.context.loading = true;
+ component.forceUpdate();
+
+ expect(innerField.props.disabled).to.equal(true);
+ component.context.loading = false;
+ });
+
+ it('should pass callbacks correctly', function () {
+ component.props.onChange.reset();
+ component.props.onBlur.reset();
+
+ innerField.props.onChange({ target: { value: 'SOME_VALUE_2'}});
+ innerField.props.onBlur();
+
+ expect(component.props.onBlur).to.have.been.called;
+ expect(component.props.onChange).to.have.been.calledWithMatch({target: { value: 'SOME_VALUE_2'}});
+ });
+
+ it('should pass focus to the field', function () {
+ innerField.focus = stub();
+ component.focus();
+
+ expect(innerField.focus).to.have.been.called;
+ });
+ });
+});
\ No newline at end of file
diff --git a/client/src/core-components/__tests__/form-test.js b/client/src/core-components/__tests__/form-test.js
index 46136923..a778d6b4 100644
--- a/client/src/core-components/__tests__/form-test.js
+++ b/client/src/core-components/__tests__/form-test.js
@@ -1,11 +1,16 @@
// MOCKS
const ValidationFactoryMock = require('lib-app/__mocks__/validations/validation-factory-mock');
-const Input = ReactMock();
+const FormField = ReactMock();
+const {EditorState} = require('draft-js');
+const draftJsExportHTML = {
+ stateToHTML: stub().returns('HTML_CODE')
+};
// COMPONENT
const Form = requireUnit('core-components/form', {
'lib-app/validations/validations-factory': ValidationFactoryMock,
- 'core-components/input': Input
+ 'draft-js-export-html': draftJsExportHTML,
+ 'core-components/form-field': FormField
});
describe('Form component', function () {
@@ -15,13 +20,13 @@ describe('Form component', function () {
form = TestUtils.renderIntoDocument(
);
- fields = TestUtils.scryRenderedComponentsWithType(form, Input);
+ fields = TestUtils.scryRenderedComponentsWithType(form, FormField);
}
function resetStubs() {
@@ -117,7 +122,7 @@ describe('Form component', function () {
});
afterEach(resetStubs);
- it('should pass the errors to inputs', function () {
+ it('should pass the errors to fields', function () {
expect(fields[0].props.error).to.equal('MOCK_ERROR_CONTROLLED');
expect(fields[1].props.error).to.equal(undefined);
});
@@ -138,13 +143,13 @@ describe('Form component', function () {
form = reRenderIntoDocument(
);
- fields = TestUtils.scryRenderedComponentsWithType(form, Input);
+ fields = TestUtils.scryRenderedComponentsWithType(form, FormField);
}
setErrorsOrRender();
@@ -171,6 +176,19 @@ describe('Form component', function () {
expect(form.props.onSubmit).to.have.been.calledWith(form.state.form);
});
+ it('should tranform EditorState to HTML usign draft-js-export-html library', function () {
+ draftJsExportHTML.stateToHTML.reset();
+ form.state.form.first = EditorState.createEmpty();
+
+ TestUtils.Simulate.submit(ReactDOM.findDOMNode(form));
+ expect(draftJsExportHTML.stateToHTML).to.have.been.calledWith(form.state.form.first.getCurrentContent());
+ expect(form.props.onSubmit).to.have.been.calledWith({
+ first: 'HTML_CODE',
+ second: 'value2',
+ third: 'value3'
+ });
+ });
+
it('should validate all fields and not call onSubmit if there are errors', function () {
ValidationFactoryMock.validators.defaultValidatorMock.performValidation = stub().returns('MOCK_ERROR');
ValidationFactoryMock.validators.customValidatorMock.performValidation = stub().returns('MOCK_ERROR_2');
diff --git a/client/src/core-components/button.js b/client/src/core-components/button.js
index 755004f9..8ad69522 100644
--- a/client/src/core-components/button.js
+++ b/client/src/core-components/button.js
@@ -6,6 +6,9 @@ import classNames from 'classnames';
// CORE LIBS
import callback from 'lib-core/callback';
+// CORE COMPONENTS
+import Icon from 'core-components/icon';
+
class Button extends React.Component {
static contextTypes = {
@@ -16,6 +19,7 @@ class Button extends React.Component {
children: React.PropTypes.node,
type: React.PropTypes.oneOf([
'primary',
+ 'primary-icon',
'clean',
'link'
]),
@@ -23,7 +27,8 @@ class Button extends React.Component {
to: React.PropTypes. string.isRequired,
params: React.PropTypes.object,
query: React.PropTypes.query
- })
+ }),
+ iconName: React.PropTypes.string
};
static defaultProps = {
@@ -33,7 +38,7 @@ class Button extends React.Component {
render() {
return (
);
}
@@ -45,6 +50,7 @@ class Button extends React.Component {
props.className = this.getClass();
delete props.route;
+ delete props.iconName;
delete props.type;
return props;
diff --git a/client/src/core-components/button.scss b/client/src/core-components/button.scss
index 974cf04e..34c58bea 100644
--- a/client/src/core-components/button.scss
+++ b/client/src/core-components/button.scss
@@ -2,7 +2,8 @@
.button {
- &-primary {
+ &-primary,
+ &-primary-icon {
background-color: $primary-red;
border: solid transparent;
border-radius: 4px;
@@ -12,6 +13,11 @@
width: 239px;
}
+ &-primary-icon {
+ width: initial;
+ height: initial;
+ }
+
&-clean {
background: none;
border: none;
diff --git a/client/src/core-components/checkbox.js b/client/src/core-components/checkbox.js
index 8394772d..28a48ffa 100644
--- a/client/src/core-components/checkbox.js
+++ b/client/src/core-components/checkbox.js
@@ -28,13 +28,12 @@ class CheckBox extends React.Component {
render() {
return (
-
+
);
}
@@ -49,7 +48,7 @@ class CheckBox extends React.Component {
props.onChange = callback(this.handleChange.bind(this), this.props.onChange);
delete props.alignment;
- delete props.error;
+ delete props.errored;
delete props.label;
delete props.value;
diff --git a/client/src/core-components/checkbox.scss b/client/src/core-components/checkbox.scss
index 3213846f..1e1301d6 100644
--- a/client/src/core-components/checkbox.scss
+++ b/client/src/core-components/checkbox.scss
@@ -18,12 +18,6 @@
}
}
- &--label {
- margin-left: 10px;
- font-size: 14px;
- user-select: none;
- }
-
&_checked {
.checkbox--icon {
color: $primary-red;
diff --git a/client/src/core-components/drop-down.js b/client/src/core-components/drop-down.js
index 969bc589..70c3500d 100644
--- a/client/src/core-components/drop-down.js
+++ b/client/src/core-components/drop-down.js
@@ -10,7 +10,8 @@ class DropDown extends React.Component {
static propTypes = {
defaultSelectedIndex: React.PropTypes.number,
selectedIndex: React.PropTypes.number,
- items: Menu.propTypes.items
+ items: Menu.propTypes.items,
+ size: React.PropTypes.oneOf(['small', 'medium', 'large'])
};
static defaultProps = {
@@ -66,7 +67,7 @@ class DropDown extends React.Component {
};
return (
-
+
);
@@ -76,11 +77,11 @@ class DropDown extends React.Component {
var iconNode = null;
if (item.icon) {
- iconNode =
;
+ iconNode =
;
}
return (
-
+
{iconNode}{item.content}
);
@@ -91,6 +92,7 @@ class DropDown extends React.Component {
'drop-down': true,
'drop-down_closed': !this.state.opened,
+ ['drop-down_' + this.props.size]: true,
[this.props.className]: (this.props.className)
};
diff --git a/client/src/core-components/drop-down.scss b/client/src/core-components/drop-down.scss
index 74eb1270..12e74a5f 100644
--- a/client/src/core-components/drop-down.scss
+++ b/client/src/core-components/drop-down.scss
@@ -7,19 +7,24 @@
user-select: none;
cursor: pointer;
- &--current-item {
+ &__current-item {
background-color: $light-grey;
border-radius: 4px 4px 0 0;
color: $primary-black;
padding: 6px;
+
+ &:focus {
+ outline: none;
+ background-color: $medium-grey;
+ }
}
- &--current-item-icon {
+ &__current-item-icon {
margin-right: 8px;
margin-bottom: 2px;
}
- &--list-container {
+ &__list-container {
position: absolute;
width: 150px;
z-index: 5;
@@ -27,8 +32,21 @@
&_closed {
- .drop-down--list-container {
+ .drop-down__list-container {
pointer-events: none;
}
}
+
+ &_medium {
+ width: 200px;
+
+ .drop-down__current-item {
+ border-radius: 4px;
+ }
+
+ .drop-down__list-container {
+ width: 200px;
+ border: 1px solid $light-grey;
+ }
+ }
}
\ No newline at end of file
diff --git a/client/src/core-components/form-field.js b/client/src/core-components/form-field.js
new file mode 100644
index 00000000..98b9dd35
--- /dev/null
+++ b/client/src/core-components/form-field.js
@@ -0,0 +1,151 @@
+import React from 'react';
+import {EditorState} from 'draft-js';
+import classNames from 'classnames';
+import _ from 'lodash';
+
+import Input from 'core-components/input';
+import DropDown from 'core-components/drop-down';
+import Checkbox from 'core-components/checkbox';
+import TextEditor from 'core-components/text-editor';
+
+class FormField extends React.Component {
+ static contextTypes = {
+ loading: React.PropTypes.bool
+ };
+
+ static propTypes = {
+ validation: React.PropTypes.string,
+ onChange: React.PropTypes.func,
+ onBlur: React.PropTypes.func,
+ required: React.PropTypes.bool,
+ error: React.PropTypes.string,
+ value: React.PropTypes.any,
+ field: React.PropTypes.oneOf(['input', 'textarea', 'select', 'checkbox']),
+ fieldProps: React.PropTypes.object
+ };
+
+ static defaultProps = {
+ field: 'input'
+ };
+
+ static getDefaultValue(field) {
+ if (field === 'input') {
+ return '';
+ }
+ else if (field === 'checkbox') {
+ return false;
+ }
+ else if (field === 'textarea') {
+ return EditorState.createEmpty();
+ }
+ else if (field === 'select') {
+ return 0;
+ }
+ }
+
+ render() {
+ const Wrapper = (this.props.field === 'textarea') ? 'div' : 'label';
+ const fieldContent = [
+
{this.props.label},
+ this.renderField(),
+ this.renderError()
+ ];
+
+ if (this.props.field === 'checkbox') {
+ fieldContent.swap(0, 1);
+ }
+ return (
+
+ {fieldContent}
+
+ );
+ }
+
+ renderField() {
+ const Field = {
+ 'input': Input,
+ 'textarea': TextEditor,
+ 'select': DropDown,
+ 'checkbox': Checkbox
+ }[this.props.field];
+
+ return
;
+ }
+
+ renderError() {
+ let error = null;
+
+ if (this.props.error) {
+ error =
{this.props.error};
+ }
+
+ return error;
+ }
+
+ getClass() {
+ let classes = {
+ 'form-field': true,
+ 'form-field_errored': (this.props.error),
+ 'form-field_checkbox': (this.props.field === 'checkbox'),
+ 'form-field_select': (this.props.field === 'select'),
+
+ [this.props.className]: (this.props.className)
+ };
+
+ return classNames(classes);
+ }
+
+ getFieldProps() {
+ let props = _.extend({}, this.props.fieldProps, {
+ disabled: this.context.loading,
+ errored: !!this.props.error,
+ name: this.props.name,
+ placeholder: this.props.placeholder,
+ key: 'nativeField',
+ onChange: this.onChange.bind(this),
+ onBlur: this.props.onBlur,
+ ref: 'nativeField',
+ required: this.props.required
+ });
+
+ if (this.props.field === 'select') {
+ props.selectedIndex = this.props.value;
+ } else {
+ props.value = this.props.value;
+ }
+
+ return props;
+ }
+
+ onChange(nativeEvent) {
+ let event = nativeEvent;
+
+ if (this.props.field === 'checkbox') {
+ event = {
+ target: {
+ value: event.target.checked
+ }
+ };
+ }
+
+ if (this.props.field === 'select') {
+ event = {
+ target: {
+ value: event.index
+ }
+ };
+ }
+
+ if (this.props.onChange) {
+ this.props.onChange(event)
+ }
+ }
+
+ focus() {
+ if (this.refs.nativeField) {
+ this.refs.nativeField.focus();
+ }
+ }
+}
+
+export default FormField;
\ No newline at end of file
diff --git a/client/src/core-components/form-field.scss b/client/src/core-components/form-field.scss
new file mode 100644
index 00000000..0b2ee428
--- /dev/null
+++ b/client/src/core-components/form-field.scss
@@ -0,0 +1,42 @@
+@import "../scss/vars";
+
+.form-field {
+ display: block;
+ margin-bottom: 20px;
+
+ &__label {
+ color: $primary-black;
+ font-size: 15px;
+ display: block;
+ padding: 3px 0;
+ text-align: left;
+ }
+
+ &_errored {
+ .form-field__error {
+ color: $primary-red;
+ font-size: $font-size--sm;
+ display: block;
+ position: absolute;
+ }
+ }
+
+ &_checkbox {
+ display: inline-block;
+ margin-bottom: 0;
+
+ .form-field__label {
+ display: inline-block;
+
+ margin-left: 10px;
+ font-size: 14px;
+ user-select: none;
+ }
+ }
+
+ &_select {
+ .form-field__label {
+ padding-bottom: 10px;
+ }
+ }
+}
\ No newline at end of file
diff --git a/client/src/core-components/form.js b/client/src/core-components/form.js
index 54ec26e3..8de7cbd8 100644
--- a/client/src/core-components/form.js
+++ b/client/src/core-components/form.js
@@ -1,12 +1,13 @@
import React from 'react';
import _ from 'lodash';
import classNames from 'classnames';
+import {EditorState} from 'draft-js';
+import {stateToHTML} from 'draft-js-export-html';
import {reactDFS, renderChildrenWithProps} from 'lib-core/react-dfs';
import ValidationFactory from 'lib-app/validations/validations-factory';
-import Input from 'core-components/input';
-import Checkbox from 'core-components/checkbox';
+import FormField from 'core-components/form-field';
class Form extends React.Component {
@@ -75,14 +76,14 @@ class Form extends React.Component {
getFieldProps({props, type}) {
let additionalProps = {};
- if (type === Input || type === Checkbox) {
+ if (this.isValidField({type})) {
let fieldName = props.name;
additionalProps = {
ref: fieldName,
value: this.state.form[fieldName] || props.value,
error: this.getFieldError(fieldName),
- onChange: this.handleFieldChange.bind(this, fieldName, type),
+ onChange: this.handleFieldChange.bind(this, fieldName),
onBlur: this.validateField.bind(this, fieldName)
}
}
@@ -138,13 +139,8 @@ class Form extends React.Component {
reactDFS(this.props.children, (child) => {
- if (this.isValidFieldType(child)) {
- if (child.type === Input) {
- form[child.props.name] = child.props.value || '';
- }
- else if (child.type === Checkbox) {
- form[child.props.name] = child.props.checked || false;
- }
+ if (this.isValidField(child)) {
+ form[child.props.name] = child.props.value || FormField.getDefaultValue(child.props.field);
if (child.props.required) {
validations[child.props.name] = ValidationFactory.getValidator(child.props.validation || 'DEFAULT');
@@ -161,29 +157,33 @@ class Form extends React.Component {
handleSubmit(event) {
event.preventDefault();
+ const form = _.mapValues(this.state.form, (field) => {
+ if (field instanceof EditorState) {
+ return stateToHTML(field.getCurrentContent());
+ } else {
+ return field;
+ }
+ });
+
if (this.hasFormErrors()) {
this.updateErrors(this.getAllFieldErrors(), this.focusFirstErrorField.bind(this));
} else if (this.props.onSubmit) {
- this.props.onSubmit(this.state.form);
+ this.props.onSubmit(form);
}
}
- handleFieldChange(fieldName, type, event) {
+ handleFieldChange(fieldName, event) {
let form = _.clone(this.state.form);
form[fieldName] = event.target.value;
- if (type === Checkbox) {
- form[fieldName] = event.target.checked || false;
- }
-
this.setState({
form: form
});
}
- isValidFieldType(child) {
- return child.type === Input || child.type === Checkbox;
+ isValidField(node) {
+ return node.type === FormField;
}
hasFormErrors() {
diff --git a/client/src/core-components/form.scss b/client/src/core-components/form.scss
deleted file mode 100644
index cc2c9aac..00000000
--- a/client/src/core-components/form.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-.form {
- .input {
- margin-bottom: 20px;
- }
-}
\ No newline at end of file
diff --git a/client/src/core-components/header.js b/client/src/core-components/header.js
new file mode 100644
index 00000000..31741183
--- /dev/null
+++ b/client/src/core-components/header.js
@@ -0,0 +1,27 @@
+import React from 'react';
+
+class Header extends React.Component {
+ static propTypes = {
+ title: React.PropTypes.string.isRequired,
+ description: React.PropTypes.string
+ };
+
+ render() {
+ return (
+
+
{this.props.title}
+ {(this.props.description) ? this.renderDescription() : null}
+
+ )
+ }
+
+ renderDescription() {
+ return (
+
+ {this.props.description}
+
+ )
+ }
+}
+
+export default Header;
\ No newline at end of file
diff --git a/client/src/core-components/header.scss b/client/src/core-components/header.scss
new file mode 100644
index 00000000..ebb0cc3b
--- /dev/null
+++ b/client/src/core-components/header.scss
@@ -0,0 +1,17 @@
+@import '../scss/vars';
+
+.header {
+ margin-bottom: 30px;
+ text-align: left;
+
+ &__title {
+ margin: 5px 0 14px;
+ font-size: 24px;
+ }
+
+ &__description {
+ font-size: 13px;
+ color: $dark-grey;
+ font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
+ }
+}
\ No newline at end of file
diff --git a/client/src/core-components/input.js b/client/src/core-components/input.js
index 44dec9fe..d1a01476 100644
--- a/client/src/core-components/input.js
+++ b/client/src/core-components/input.js
@@ -14,7 +14,7 @@ class Input extends React.Component {
value: React.PropTypes.string,
validation: React.PropTypes.string,
onChange: React.PropTypes.func,
- inputType: React.PropTypes.string,
+ size: React.PropTypes.oneOf(['small', 'medium', 'large']),
password: React.PropTypes.bool,
required: React.PropTypes.bool,
icon: React.PropTypes.string,
@@ -22,30 +22,18 @@ class Input extends React.Component {
};
static defaultProps = {
- inputType: 'primary'
+ size: 'small'
};
render() {
return (
-
+
);
}
- renderError() {
- let error = null;
-
- if (this.props.error){
- error =
{this.props.error} ;
- }
-
- return error;
- }
-
renderIcon() {
let icon = null;
@@ -62,12 +50,12 @@ class Input extends React.Component {
props['aria-required'] = this.props.required;
props.type = (this.props.password) ? 'password' : 'text';
props.ref = 'nativeInput';
- props.disabled = this.context.loading;
+ delete props.errored;
delete props.required;
delete props.validation;
delete props.inputType;
- delete props.error;
+ delete props.errored;
delete props.password;
return props;
@@ -77,8 +65,8 @@ class Input extends React.Component {
let classes = {
'input': true,
'input_with-icon': (this.props.icon),
- 'input_with-error': (this.props.error),
- ['input_' + this.props.inputType]: true,
+ 'input_errored': (this.props.errored),
+ ['input_' + this.props.size]: true,
[this.props.className]: (this.props.className)
};
diff --git a/client/src/core-components/input.scss b/client/src/core-components/input.scss
index 4e5066f9..d3b88eee 100644
--- a/client/src/core-components/input.scss
+++ b/client/src/core-components/input.scss
@@ -19,22 +19,18 @@
}
}
- &__label {
- color: $primary-black;
- font-size: 15px;
- display: block;
- padding: 3px 0;
- text-align: left;
- }
-
- &_primary {
+ &_small {
width: 200px;
}
- &_secondary {
+ &_medium {
width: 250px;
}
+ &_large {
+ width: 350px;
+ }
+
&_with-icon {
position: relative;
@@ -52,16 +48,9 @@
}
}
- &_with-error {
- .input__error {
- color: $primary-red;
- font-size: $font-size--sm;
- display: block;
- position: absolute;
- }
+ &_errored {
.input__text {
border: 1px solid $primary-red;
}
}
-
}
\ No newline at end of file
diff --git a/client/src/core-components/text-editor.js b/client/src/core-components/text-editor.js
new file mode 100644
index 00000000..d546d8a0
--- /dev/null
+++ b/client/src/core-components/text-editor.js
@@ -0,0 +1,118 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import classNames from 'classnames';
+import _ from 'lodash';
+import {Editor, EditorState, RichUtils} from 'draft-js';
+import Button from 'core-components/button';
+
+class TextEditor extends React.Component {
+ static propTypes = {
+ errored: React.PropTypes.bool,
+ onChange: React.PropTypes.func,
+ value: React.PropTypes.object
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ editorState: EditorState.createEmpty(),
+ focused: false
+ };
+ }
+
+ render() {
+ return (
+
+ {this.renderEditOptions()}
+
event.preventDefault()}>
+ event.stopPropagation()}>
+
+
+
+
+ );
+ }
+
+ renderEditOptions() {
+ const onBoldClick = (event) => {
+ event.preventDefault();
+ this.onEditorChange(RichUtils.toggleInlineStyle(this.state.editorState, 'BOLD'));
+ };
+ const onItalicsClick = (event) => {
+ event.preventDefault();
+ this.onEditorChange(RichUtils.toggleInlineStyle(this.state.editorState, 'ITALIC'));
+ };
+ const onUnderlineClick = (event) => {
+ event.preventDefault();
+ this.onEditorChange(RichUtils.toggleInlineStyle(this.state.editorState, 'UNDERLINE'));
+ };
+
+
+ return (
+
+
+ )
+ }
+
+ getClass() {
+ let classes = {
+ 'text-editor': true,
+ 'text-editor_errored': (this.props.errored),
+ 'text-editor_focused': (this.state.focused),
+
+ [this.props.className]: (this.props.className)
+ };
+
+ return classNames(classes);
+ }
+
+ getEditorProps() {
+ return {
+ editorState: this.props.value || this.state.editorState,
+ ref: 'editor',
+ onChange: this.onEditorChange.bind(this),
+ onFocus: this.onEditorFocus.bind(this),
+ onBlur: this.onBlur.bind(this)
+ };
+ }
+
+ onEditorChange(editorState) {
+ this.setState({editorState});
+
+ if (this.props.onChange) {
+ this.props.onChange({
+ target: {
+ value: editorState
+ }
+ });
+ }
+ }
+
+ onEditorFocus(event) {
+ this.setState({focused: true});
+
+ if(this.props.onFocus) {
+ this.props.onFocus(event)
+ }
+ }
+
+ onBlur(event) {
+ this.setState({focused: false});
+
+ if(this.props.onBlur) {
+ this.props.onBlur(event)
+ }
+ }
+
+ focus() {
+ if (this.refs.editor) {
+ this.refs.editor.focus();
+ }
+ }
+}
+
+export default TextEditor;
\ No newline at end of file
diff --git a/client/src/core-components/text-editor.scss b/client/src/core-components/text-editor.scss
new file mode 100644
index 00000000..c1536d01
--- /dev/null
+++ b/client/src/core-components/text-editor.scss
@@ -0,0 +1,39 @@
+@import "../scss/vars";
+
+.text-editor {
+
+ &__editor {
+ border: 1px solid $grey;
+ border-radius: 3px;
+ padding: 8px;
+ width: 100%;
+ height: 200px;
+ text-align: left;
+ overflow: auto;
+
+ &:hover {
+ border-color: $medium-grey;
+ }
+ }
+
+ &__options {
+ text-align: left;
+ margin-bottom: 5px;
+
+ .button {
+ margin-right: 3px;
+ }
+ }
+
+ &_focused {
+ .text-editor__editor {
+ border-color: $primary-blue;
+ }
+ }
+
+ &_errored {
+ .text-editor__editor {
+ border: 1px solid $primary-red;
+ }
+ }
+}
\ No newline at end of file
diff --git a/client/src/data/fixtures/ticket-fixtures.js b/client/src/data/fixtures/ticket-fixtures.js
new file mode 100644
index 00000000..16d169cd
--- /dev/null
+++ b/client/src/data/fixtures/ticket-fixtures.js
@@ -0,0 +1,25 @@
+module.exports = [
+ {
+ path: '/ticket/create',
+ time: 2000,
+ response: function (data) {
+ let response;
+
+ if (data.title !== 'error') {
+ response = {
+ status: 'success',
+ data: {
+ 'ticketNumber': 121444
+ }
+ };
+ } else {
+ response = {
+ status: 'fail',
+ message: 'Ticket could not be created'
+ };
+ }
+
+ return response;
+ }
+ }
+];
\ No newline at end of file
diff --git a/client/src/data/languages/en.js b/client/src/data/languages/en.js
index acd007f8..940b4baf 100644
--- a/client/src/data/languages/en.js
+++ b/client/src/data/languages/en.js
@@ -9,23 +9,29 @@ export default {
'NEW_PASSWORD': 'New password',
'REPEAT_NEW_PASSWORD': 'Repeat new password',
'BACK_LOGIN_FORM': 'Back to login form',
- 'TICKET_LIST': 'Ticket List',
- 'CREATE_TICKET': 'Create Ticket',
'VIEW_ARTICLES': 'View Articles',
'EDIT_PROFILE': 'Edit Profile',
'CLOSE_SESSION': 'Close session',
+ 'CREATE_TICKET': 'Create Ticket',
+ 'CREATE_TICKET_DESCRIPTION': 'This is a form for creating tickets. Fill the form and send us your issues/doubts/suggestions. Our support system will answer it as soon as possible.',
+ 'TICKET_LIST': 'Ticket List',
+ 'TICKET_LIST_DESCRIPTION': 'Here you can find a list of all tickets you have sent to our support team.',
//ERRORS
'EMAIL_NOT_EXIST': 'Email does not exist',
'ERROR_EMPTY': 'Invalid value',
'ERROR_PASSWORD': 'Invalid password',
'ERROR_NAME': 'Invalid name',
+ 'ERROR_TITLE': 'Invalid title',
'ERROR_EMAIL': 'Invalid email',
+ 'ERROR_CONTENT_SHORT': 'Content too short',
'PASSWORD_NOT_MATCH': 'Password does not match',
'INVALID_RECOVER': 'Invalid recover data',
+ 'TICKET_SENT_ERROR': 'An error occurred while trying to create the ticket.',
//MESSAGES
'SIGNUP_SUCCESS': 'You have registered successfully in our support system.',
+ 'TICKET_SENT': 'Ticket has been created successfully.',
'VALID_RECOVER': 'Password recovered successfully',
'EMAIL_EXISTS': 'Email already exists, please try to log in or recover password'
};
\ No newline at end of file
diff --git a/client/src/index.js b/client/src/index.js
index 7d53e718..00c95300 100644
--- a/client/src/index.js
+++ b/client/src/index.js
@@ -7,6 +7,13 @@ import ConfigActions from 'actions/config-actions';
import routes from 'app/Routes';
import store from 'app/store';
+Array.prototype.swap = function (x,y) {
+ var b = this[x];
+ this[x] = this[y];
+ this[y] = b;
+ return this;
+};
+
if ( process.env.NODE_ENV !== 'production' ) {
// Enable React devtools
window.React = React;
diff --git a/client/src/lib-app/fixtures-loader.js b/client/src/lib-app/fixtures-loader.js
index 8cbb9dbd..fcf645a2 100644
--- a/client/src/lib-app/fixtures-loader.js
+++ b/client/src/lib-app/fixtures-loader.js
@@ -17,6 +17,7 @@ let fixtures = (function () {
// FIXTURES
fixtures.add(require('data/fixtures/user-fixtures'));
+fixtures.add(require('data/fixtures/ticket-fixtures'));
fixtures.add(require('data/fixtures/system-fixtures'));
_.each(fixtures.getAll(), function (fixture) {
diff --git a/client/src/lib-app/validations/length-validator.js b/client/src/lib-app/validations/length-validator.js
index b91e3e16..3951c31e 100644
--- a/client/src/lib-app/validations/length-validator.js
+++ b/client/src/lib-app/validations/length-validator.js
@@ -1,3 +1,5 @@
+import {EditorState} from 'draft-js';
+
import Validator from 'lib-app/validations/validator';
class LengthValidator extends Validator {
@@ -9,6 +11,10 @@ class LengthValidator extends Validator {
}
validate(value, form) {
+ if (value instanceof EditorState) {
+ value = value.getCurrentContent().getPlainText();
+ }
+
if (value.length < this.minlength) return this.getError(this.errorKey);
}
}
diff --git a/client/src/lib-app/validations/validations-factory.js b/client/src/lib-app/validations/validations-factory.js
index 00e443f9..6749d3b5 100644
--- a/client/src/lib-app/validations/validations-factory.js
+++ b/client/src/lib-app/validations/validations-factory.js
@@ -7,7 +7,9 @@ import LengthValidator from 'lib-app/validations/length-validator';
let validators = {
'DEFAULT': new Validator(),
'NAME': new AlphaNumericValidator('ERROR_NAME', new LengthValidator(2, 'ERROR_NAME')),
+ 'TITLE': new AlphaNumericValidator('ERROR_TITLE', new LengthValidator(2, 'ERROR_TITLE')),
'EMAIL': new EmailValidator(),
+ 'TEXT_AREA': new LengthValidator(10, 'ERROR_CONTENT_SHORT'),
'PASSWORD': new LengthValidator(6, 'ERROR_PASSWORD'),
'REPEAT_PASSWORD': new RepeatPasswordValidator()
};
diff --git a/client/src/lib-app/validations/validator.js b/client/src/lib-app/validations/validator.js
index 5eeb7e35..b02ab078 100644
--- a/client/src/lib-app/validations/validator.js
+++ b/client/src/lib-app/validations/validator.js
@@ -1,4 +1,6 @@
-const i18n = require('lib-app/i18n');
+import {EditorState} from 'draft-js';
+
+import i18n from 'lib-app/i18n';
class Validator {
constructor(validator = null) {
@@ -18,7 +20,11 @@ class Validator {
}
validate(value, form) {
- if (!value.length) return this.getError('ERROR_EMPTY');
+ if (value instanceof EditorState) {
+ value = value.getCurrentContent().getPlainText()
+ }
+
+ if (value.length === 0) return this.getError('ERROR_EMPTY');
}
getError(errorKey) {
diff --git a/client/src/lib-test/preprocessor.js b/client/src/lib-test/preprocessor.js
index 59744b44..e3a1e44d 100644
--- a/client/src/lib-test/preprocessor.js
+++ b/client/src/lib-test/preprocessor.js
@@ -34,4 +34,11 @@ global.reRenderIntoDocument = (function () {
})();
global.ReduxMock = {
connect: stub().returns(stub().returnsArg(0))
-};
\ No newline at end of file
+};
+
+Array.prototype.swap = function (x,y) {
+ var b = this[x];
+ this[x] = this[y];
+ this[y] = b;
+ return this;
+};