[sdc] docker file fix for cassandra
[sdc.git] / openecomp-ui / src / nfvo-components / input / validation / ValidationInput.jsx
1 /**
2  * Used for inputs on a validation form.
3  * All properties will be passed on to the input element.
4  *
5  * The following properties can be set for OOB validations and callbacks:
6  - required: Boolean:  Should be set to true if the input must have a value
7  - numeric: Boolean : Should be set to true id the input should be an integer
8  - onChange : Function :  Will be called to validate the value if the default validations are not sufficient, should return a boolean value
9  indicating whether the value is valid
10  - didUpdateCallback :Function: Will be called after the state has been updated and the component has rerendered. This can be used if
11  there are dependencies between inputs in a form.
12  *
13  * The following properties of the state can be set to determine
14  * the state of the input from outside components:
15  - isValid : Boolean - whether the value is valid
16  - value : value for the input field,
17  - disabled : Boolean,
18  - required : Boolean - whether the input value must be filled out.
19  */
20 import React from 'react';
21 import ReactDOM from 'react-dom';
22 import Validator from 'validator';
23 import FormGroup from 'react-bootstrap/lib/FormGroup.js';
24 import Input from 'react-bootstrap/lib/Input.js';
25 import Overlay from 'react-bootstrap/lib/Overlay.js';
26 import Tooltip from 'react-bootstrap/lib/Tooltip.js';
27 import isEqual from 'lodash/isEqual.js';
28 import i18n from 'nfvo-utils/i18n/i18n.js';
29 import JSONSchema from 'nfvo-utils/json/JSONSchema.js';
30 import JSONPointer from 'nfvo-utils/json/JSONPointer.js';
31
32
33 import InputOptions  from '../inputOptions/InputOptions.jsx';
34
35 const globalValidationFunctions = {
36         required: value => value !== '',
37         maxLength: (value, length) => Validator.isLength(value, {max: length}),
38         minLength: (value, length) => Validator.isLength(value, {min: length}),
39         pattern: (value, pattern) => Validator.matches(value, pattern),
40         numeric: value => {
41                 if (value === '') {
42                         // to allow empty value which is not zero
43                         return true;
44                 }
45                 return Validator.isNumeric(value);
46         },
47         maxValue: (value, maxValue) => value < maxValue,
48         minValue: (value, minValue) => value >= minValue,
49         alphanumeric: value => Validator.isAlphanumeric(value),
50         alphanumericWithSpaces: value => Validator.isAlphanumeric(value.replace(/ /g, '')),
51         validateName: value => Validator.isAlphanumeric(value.replace(/\s|\.|\_|\-/g, ''), 'en-US'),
52         validateVendorName: value => Validator.isAlphanumeric(value.replace(/[\x7F-\xFF]|\s/g, ''), 'en-US'),
53         freeEnglishText: value => Validator.isAlphanumeric(value.replace(/\s|\.|\_|\-|\,|\(|\)|\?/g, ''), 'en-US'),
54         email: value => Validator.isEmail(value),
55         ip: value => Validator.isIP(value),
56         url: value => Validator.isURL(value)
57 };
58
59 const globalValidationMessagingFunctions = {
60         required: () => i18n('Field is required'),
61         maxLength: (value, maxLength) => i18n('Field value has exceeded it\'s limit, {maxLength}. current length: {length}', {
62                 length: value.length,
63                 maxLength
64         }),
65         minLength: (value, minLength) => i18n('Field value should contain at least {minLength} characters.', {minLength}),
66         pattern: (value, pattern) => i18n('Field value should match the pattern: {pattern}.', {pattern}),
67         numeric: () => i18n('Field value should contain numbers only.'),
68         maxValue: (value, maxValue) => i18n('Field value should be less than: {maxValue}.', {maxValue}),
69         minValue: (value, minValue) => i18n('Field value should be at least: {minValue}.', {minValue}),
70         alphanumeric: () => i18n('Field value should contain letters or digits only.'),
71         alphanumericWithSpaces: () => i18n('Field value should contain letters, digits or spaces only.'),
72         validateName: ()=> i18n('Field value should contain English letters, digits , spaces, underscores, dashes and dots only.'),
73         validateVendorName: ()=> i18n('Field value should contain English letters digits and spaces only.'),
74         freeEnglishText: ()=> i18n('Field value should contain  English letters, digits , spaces, underscores, dashes and dots only.'),
75         email: () => i18n('Field value should be a valid email address.'),
76         ip: () => i18n('Field value should be a valid ip address.'),
77         url: () => i18n('Field value should be a valid url address.'),
78         general: () => i18n('Field value is invalid.')
79 };
80
81 class ValidationInput extends React.Component {
82
83         static contextTypes = {
84                 validationParent: React.PropTypes.any,
85                 isReadOnlyMode: React.PropTypes.bool,
86                 validationSchema: React.PropTypes.instanceOf(JSONSchema),
87                 validationData: React.PropTypes.object
88         };
89
90         static defaultProps = {
91                 onChange: null,
92                 disabled: null,
93                 didUpdateCallback: null,
94                 validations: {},
95                 value: ''
96         };
97
98         static propTypes = {
99                 type: React.PropTypes.string.isRequired,
100                 onChange: React.PropTypes.func,
101                 disabled: React.PropTypes.bool,
102                 didUpdateCallback: React.PropTypes.func,
103                 validations: React.PropTypes.object,
104                 isMultiSelect: React.PropTypes.bool,
105                 onOtherChange: React.PropTypes.func,
106                 pointer: React.PropTypes.string
107         };
108
109
110         state = {
111                 isValid: true,
112                 style: null,
113                 value: this.props.value,
114                 error: {},
115                 previousErrorMessage: '',
116                 wasInvalid: false,
117                 validations: this.props.validations,
118                 isMultiSelect: this.props.isMultiSelect
119         };
120
121         componentWillMount() {
122                 if (this.context.validationSchema) {
123                         let {validationSchema: schema, validationData: data} = this.context,
124                                 {pointer} = this.props;
125
126                         if (!schema.exists(pointer)) {
127                                 console.error(`Field doesn't exists in the schema ${pointer}`);
128                         }
129
130                         let value = JSONPointer.getValue(data, pointer);
131                         if (value === undefined) {
132                                 value = schema.getDefault(pointer);
133                                 if (value === undefined) {
134                                         value = '';
135                                 }
136                         }
137                         this.setState({value});
138
139                         let enums = schema.getEnum(pointer);
140                         if (enums) {
141                                 let values = enums.map(value => ({enum: value, title: value, groupName: pointer})),
142                                         isMultiSelect = schema.isArray(pointer);
143
144                                 if (!isMultiSelect && this.props.type !== 'radiogroup') {
145                                         values = [{enum: '', title: i18n('Select...')}, ...values];
146                                 }
147                                 if (isMultiSelect && Array.isArray(value) && value.length === 0) {
148                                         value = '';
149                                 }
150
151                                 this.setState({
152                                         isMultiSelect,
153                                         values,
154                                         onEnumChange: value => this.changedInputOptions(value),
155                                         value
156                                 });
157                         }
158
159                         this.setState({validations: this.extractValidationsFromSchema(schema, pointer, this.props)});
160                 }
161         }
162
163         extractValidationsFromSchema(schema, pointer, props) {
164                 /* props are here to get precedence over the scheme definitions */
165                 let validations = {};
166
167                 if (schema.isRequired(pointer)) {
168                         validations.required = true;
169                 }
170
171                 if (schema.isNumber(pointer)) {
172                         validations.numeric = true;
173
174                         const maxValue = props.validations.maxValue || schema.getMaxValue(pointer);
175                         if (maxValue !== undefined) {
176                                 validations.maxValue = maxValue;
177                         }
178
179                         const minValue = props.validations.minValue || schema.getMinValue(pointer);
180                         if (minValue !== undefined) {
181                                 validations.minValue = minValue;
182                         }
183                 }
184
185
186                 if (schema.isString(pointer)) {
187
188                         const pattern = schema.getPattern(pointer);
189                         if (pattern) {
190                                 validations.pattern = pattern;
191                         }
192
193                         const maxLength = schema.getMaxLength(pointer);
194                         if (maxLength !== undefined) {
195                                 validations.maxLength = maxLength;
196                         }
197
198                         const minLength = schema.getMinLength(pointer);
199                         if (minLength !== undefined) {
200                                 validations.minLength = minLength;
201                         }
202                 }
203
204                 return validations;
205         }
206
207         componentWillReceiveProps({value: nextValue, validations: nextValidations, pointer: nextPointer}, nextContext) {
208                 const {validations, value} = this.props;
209                 const validationsChanged = !isEqual(validations, nextValidations);
210                 if (nextContext.validationSchema) {
211                         if (this.props.pointer !== nextPointer ||
212                                 this.context.validationData !== nextContext.validationData) {
213                                 let currentValue = JSONPointer.getValue(this.context.validationData, this.props.pointer),
214                                         nextValue = JSONPointer.getValue(nextContext.validationData, nextPointer);
215                                 if(nextValue === undefined) {
216                                         nextValue = '';
217                                 }
218                                 if (this.state.isMultiSelect && Array.isArray(nextValue) && nextValue.length === 0) {
219                                         nextValue = '';
220                                 }
221                                 if (currentValue !== nextValue) {
222                                         this.setState({value: nextValue});
223                                 }
224                                 if (validationsChanged) {
225                                         this.setState({
226                                                 validations: this.extractValidationsFromSchema(nextContext.validationSchema, nextPointer, {validations: nextValidations})
227                                         });
228                                 }
229                         }
230                 } else {
231                         if (validationsChanged) {
232                                 this.setState({validations: nextValidations});
233                         }
234                         if (this.state.wasInvalid && (value !== nextValue || validationsChanged)) {
235                                 this.validate(nextValue, nextValidations);
236                         } else if (value !== nextValue) {
237                                 this.setState({value: nextValue});
238                         }
239                 }
240         }
241
242         shouldTypeBeNumberBySchemeDefinition(pointer) {
243                 return this.context.validationSchema &&
244                         this.context.validationSchema.isNumber(pointer);
245         }
246
247         hasEnum(pointer) {
248                 return this.context.validationSchema &&
249                         this.context.validationSchema.getEnum(pointer);
250         }
251
252         render() {
253                 let {value, isMultiSelect, values, onEnumChange, style, isValid, validations} = this.state;
254                 let {onOtherChange, type, pointer} = this.props;
255                 if (this.shouldTypeBeNumberBySchemeDefinition(pointer) && !this.hasEnum(pointer)) {
256                         type = 'number';
257                 }
258                 let props = {...this.props};
259
260                 let groupClasses = this.props.groupClassName || '';
261                 if (validations.required) {
262                         groupClasses += ' required';
263                 }
264                 let isReadOnlyMode = this.context.isReadOnlyMode;
265
266                 if (value === true && (type === 'checkbox' || type === 'radio')) {
267                         props.checked = true;
268                 }
269                 return (
270                         <div className='validation-input-wrapper'>
271                                 {
272                                         !isMultiSelect && !onOtherChange && type !== 'select' && type !== 'radiogroup'
273                                         && <Input
274                                                 {...props}
275                                                 type={type}
276                                                 groupClassName={groupClasses}
277                                                 ref={'_myInput'}
278                                                 value={value}
279                                                 disabled={isReadOnlyMode || Boolean(this.props.disabled)}
280                                                 bsStyle={style}
281                                                 onChange={() => this.changedInput()}
282                                                 onBlur={() => this.blurInput()}>
283                                                 {this.props.children}
284                                         </Input>
285                                 }
286                                 {
287                                         type === 'radiogroup'
288                                         && <FormGroup>
289                                                 {
290                                                         values.map(val =>
291                                                                 <Input disabled={isReadOnlyMode || Boolean(this.props.disabled)}
292                                                                         inline={true}
293                                                                         ref={'_myInput' + (typeof val.enum === 'string' ? val.enum.replace(/\W/g, '_') : val.enum)}
294                                                                         value={val.enum} checked={value === val.enum}
295                                                                         type='radio' label={val.title}
296                                                                         name={val.groupName}
297                                                                         onChange={() => this.changedInput()}/>
298                                                         )
299                                                 }
300                                         </FormGroup>
301                                 }
302                                 {
303                                         (isMultiSelect || onOtherChange || type === 'select')
304                                         && <InputOptions
305                                                 onInputChange={() => this.changedInput()}
306                                                 onBlur={() => this.blurInput()}
307                                                 hasError={!isValid}
308                                                 ref={'_myInput'}
309                                                 isMultiSelect={isMultiSelect}
310                                                 values={values}
311                                                 onEnumChange={onEnumChange}
312                                                 selectedEnum={value}
313                                                 multiSelectedEnum={value}
314                                                 {...props} />
315                                 }
316                                 {this.renderOverlay()}
317                         </div>
318                 );
319         }
320
321         renderOverlay() {
322                 let position = 'right';
323                 if (this.props.type === 'text'
324                         || this.props.type === 'email'
325                         || this.props.type === 'number'
326                         || this.props.type === 'password'
327
328                 ) {
329                         position = 'bottom';
330                 }
331
332                 let validationMessage = this.state.error.message || this.state.previousErrorMessage;
333                 return (
334                         <Overlay
335                                 show={!this.state.isValid}
336                                 placement={position}
337                                 target={() => {
338                                         let target = ReactDOM.findDOMNode(this.refs._myInput);
339                                         return target.offsetParent ? target : undefined;
340                                 }}
341                                 container={this}>
342                                 <Tooltip
343                                         id={`error-${validationMessage.replace(' ', '-')}`}
344                                         className='validation-error-message'>
345                                         {validationMessage}
346                                 </Tooltip>
347                         </Overlay>
348                 );
349         }
350
351         componentDidMount() {
352                 if (this.context.validationParent) {
353                         this.context.validationParent.register(this);
354                 }
355         }
356
357         componentDidUpdate(prevProps, prevState) {
358                 if (this.context.validationParent) {
359                         if (prevState.isValid !== this.state.isValid) {
360                                 this.context.validationParent.childValidStateChanged(this, this.state.isValid);
361                         }
362                 }
363                 if (this.props.didUpdateCallback) {
364                         this.props.didUpdateCallback();
365                 }
366
367         }
368
369         componentWillUnmount() {
370                 if (this.context.validationParent) {
371                         this.context.validationParent.unregister(this);
372                 }
373         }
374
375         isNumberInputElement() {
376                 return this.props.type === 'number' || this.refs._myInput.props.type === 'number';
377         }
378
379         /***
380          * Adding same method as the actual input component
381          * @returns {*}
382          */
383         getValue() {
384                 if (this.props.type === 'checkbox') {
385                         return this.refs._myInput.getChecked();
386                 }
387                 if (this.props.type === 'radiogroup') {
388                         for (let key in this.refs) { // finding the value of the radio button that was checked
389                                 if (this.refs[key].getChecked()) {
390                                         return this.refs[key].getValue();
391                                 }
392                         }
393                 }
394                 if (this.isNumberInputElement()) {
395                         return Number(this.refs._myInput.getValue());
396                 }
397
398                 return this.refs._myInput.getValue();
399         }
400
401         resetValue() {
402                 this.setState({value: this.props.value});
403         }
404
405
406         /***
407          * internal method that validated the value. includes callback to the onChange method
408          * @param value
409          * @param validations - map containing validation id and the limitation describing the validation.
410          * @returns {object}
411          */
412         validateValue = (value, validations) => {
413                 let {customValidationFunction} = validations;
414                 let error = {};
415                 let isValid = true;
416                 for (let validation in validations) {
417                         if ('customValidationFunction' !== validation) {
418                                 if (validations[validation]) {
419                                         if (!globalValidationFunctions[validation](value, validations[validation])) {
420                                                 error.id = validation;
421                                                 error.message = globalValidationMessagingFunctions[validation](value, validations[validation]);
422                                                 isValid = false;
423                                                 break;
424                                         }
425                                 }
426                         } else {
427                                 let customValidationResult = customValidationFunction(value);
428
429                                 if (customValidationResult !== true) {
430                                         error.id = 'custom';
431                                         isValid = false;
432                                         if (typeof customValidationResult === 'string') {//custom validation error message supplied.
433                                                 error.message = customValidationResult;
434                                         } else {
435                                                 error.message = globalValidationMessagingFunctions.general();
436                                         }
437                                         break;
438                                 }
439
440
441                         }
442                 }
443
444                 return {
445                         isValid,
446                         error
447                 };
448         };
449
450         /***
451          * Internal method that handles the change event of the input. validates and updates the state.
452          */
453         changedInput() {
454
455                 let {isValid, error} = this.state.wasInvalid ? this.validate() : this.state;
456                 let onChange = this.props.onChange;
457                 if (onChange) {
458                         onChange(this.getValue(), isValid, error);
459                 }
460                 if (this.context.validationSchema) {
461                         let value = this.getValue();
462                         if (this.state.isMultiSelect && value === '') {
463                                 value = [];
464                         }
465                         if (this.shouldTypeBeNumberBySchemeDefinition(this.props.pointer)) {
466                                 value = Number(value);
467                         }
468                         this.context.validationParent.onValueChanged(this.props.pointer, value, isValid, error);
469                 }
470         }
471
472         changedInputOptions(value) {
473                 this.context.validationParent.onValueChanged(this.props.pointer, value, true);
474         }
475
476         blurInput() {
477                 if (!this.state.wasInvalid) {
478                         this.setState({wasInvalid: true});
479                 }
480
481                 let {isValid, error} = !this.state.wasInvalid ? this.validate() : this.state;
482                 let onBlur = this.props.onBlur;
483                 if (onBlur) {
484                         onBlur(this.getValue(), isValid, error);
485                 }
486         }
487
488         validate(value = this.getValue(), validations = this.state.validations) {
489                 let validationStatus = this.validateValue(value, validations);
490                 let {isValid, error} = validationStatus;
491                 let _style = isValid ? null : 'error';
492                 this.setState({
493                         isValid,
494                         error,
495                         value,
496                         previousErrorMessage: this.state.error.message || '',
497                         style: _style,
498                         wasInvalid: !isValid || this.state.wasInvalid
499                 });
500
501                 return validationStatus;
502         }
503
504         isValid() {
505                 return this.state.isValid;
506         }
507
508 }
509 export default ValidationInput;