summaryrefslogtreecommitdiffstats
path: root/openecomp-ui/src/nfvo-components/input
diff options
context:
space:
mode:
Diffstat (limited to 'openecomp-ui/src/nfvo-components/input')
-rw-r--r--openecomp-ui/src/nfvo-components/input/ExpandableInput.jsx77
-rw-r--r--openecomp-ui/src/nfvo-components/input/SelectInput.jsx52
-rw-r--r--openecomp-ui/src/nfvo-components/input/ToggleInput.jsx53
-rw-r--r--openecomp-ui/src/nfvo-components/input/dualListbox/DualListboxView.jsx132
-rw-r--r--openecomp-ui/src/nfvo-components/input/inputOptions/InputOptions.jsx221
-rw-r--r--openecomp-ui/src/nfvo-components/input/validation/ValidationButtons.jsx40
-rw-r--r--openecomp-ui/src/nfvo-components/input/validation/ValidationForm.jsx200
-rw-r--r--openecomp-ui/src/nfvo-components/input/validation/ValidationInput.jsx509
-rw-r--r--openecomp-ui/src/nfvo-components/input/validation/ValidationTab.jsx107
-rw-r--r--openecomp-ui/src/nfvo-components/input/validation/ValidationTabs.jsx72
10 files changed, 1463 insertions, 0 deletions
diff --git a/openecomp-ui/src/nfvo-components/input/ExpandableInput.jsx b/openecomp-ui/src/nfvo-components/input/ExpandableInput.jsx
new file mode 100644
index 0000000000..3ac3fcad28
--- /dev/null
+++ b/openecomp-ui/src/nfvo-components/input/ExpandableInput.jsx
@@ -0,0 +1,77 @@
+import React from 'react';
+import FontAwesome from 'react-fontawesome';
+import classnames from 'classnames';
+import Input from 'react-bootstrap/lib/Input';
+
+
+class ExpandableInput extends React.Component {
+ constructor(props){
+ super(props);
+ this.state = {showInput: false, value: ''};
+ this.toggleInput = this.toggleInput.bind(this);
+ this.handleFocus = this.handleFocus.bind(this);
+ this.handleInput = this.handleInput.bind(this);
+ this.handleClose = this.handleClose.bind(this);
+ }
+
+ toggleInput(){
+ if (!this.state.showInput){
+ this.searchInputNode.refs.input.focus();
+ } else {
+ this.setState({showInput: false});
+ }
+ }
+
+ handleInput(e){
+ let {onChange} = this.props;
+
+ this.setState({value: e.target.value});
+ onChange(e);
+ }
+
+ handleClose(){
+ this.handleInput({target: {value: ''}});
+ this.searchInputNode.refs.input.focus();
+ }
+
+ handleFocus(){
+ if (!this.state.showInput){
+ this.setState({showInput: true});
+ }
+ }
+
+ getValue(){
+ return this.state.value;
+ }
+
+ render(){
+ let {iconType} = this.props;
+
+ let inputClasses = classnames({
+ 'expandable-active': this.state.showInput,
+ 'expandable-not-active': !this.state.showInput
+ });
+
+ let iconClasses = classnames(
+ 'expandable-icon',
+ {'expandable-icon-active': this.state.showInput}
+ );
+
+ return (
+ <div className='expandable-input-wrapper'>
+ <Input
+ type='text'
+ value={this.state.value}
+ ref={(input) => this.searchInputNode = input}
+ className={inputClasses}
+ groupClassName='expandable-input-control'
+ onChange={e => this.handleInput(e)}
+ onFocus={this.handleFocus}/>
+ {this.state.showInput && this.state.value && <FontAwesome onClick={this.handleClose} name='close' className='expandable-close-button'/>}
+ {!this.state.value && <FontAwesome onClick={this.toggleInput} name={iconType} className={iconClasses}/>}
+ </div>
+ );
+ }
+}
+
+export default ExpandableInput;
diff --git a/openecomp-ui/src/nfvo-components/input/SelectInput.jsx b/openecomp-ui/src/nfvo-components/input/SelectInput.jsx
new file mode 100644
index 0000000000..1036ac41c3
--- /dev/null
+++ b/openecomp-ui/src/nfvo-components/input/SelectInput.jsx
@@ -0,0 +1,52 @@
+/**
+ * The HTML structure here is aligned with bootstrap HTML structure for form elements.
+ * In this way we have proper styling and it is aligned with other form elements on screen.
+ *
+ * Select and MultiSelect options:
+ *
+ * label - the label to be shown which paired with the input
+ *
+ * all other "react-select" props - as documented on
+ * http://jedwatson.github.io/react-select/
+ * or
+ * https://github.com/JedWatson/react-select
+ */
+import React, {Component} from 'react';
+import Select from 'react-select';
+
+class SelectInput extends Component {
+
+ inputValue = [];
+
+ render() {
+ let {label, value, ...other} = this.props;
+ return (
+ <div className='validation-input-wrapper dropdown-multi-select'>
+ <div className='form-group'>
+ {label && <label className='control-label'>{label}</label>}
+ <Select ref='_myInput' onChange={value => this.onSelectChanged(value)} {...other} value={value} />
+ </div>
+ </div>
+ );
+ }
+
+ getValue() {
+ return this.inputValue && this.inputValue.length ? this.inputValue : '';
+ }
+
+ onSelectChanged(value) {
+ this.props.onMultiSelectChanged(value);
+ }
+
+ componentDidMount() {
+ let {value} = this.props;
+ this.inputValue = value ? value : [];
+ }
+ componentDidUpdate() {
+ if (this.inputValue !== this.props.value) {
+ this.inputValue = this.props.value;
+ }
+ }
+}
+
+export default SelectInput;
diff --git a/openecomp-ui/src/nfvo-components/input/ToggleInput.jsx b/openecomp-ui/src/nfvo-components/input/ToggleInput.jsx
new file mode 100644
index 0000000000..873d3ded65
--- /dev/null
+++ b/openecomp-ui/src/nfvo-components/input/ToggleInput.jsx
@@ -0,0 +1,53 @@
+import React from 'react';
+
+export default
+class ToggleInput extends React.Component {
+
+ static propTypes = {
+ label: React.PropTypes.node,
+ value: React.PropTypes.bool,
+ onChange: React.PropTypes.func,
+ disabled: React.PropTypes.bool
+ }
+
+ static defaultProps = {
+ value: false,
+ label: ''
+ }
+
+ state = {
+ value: this.props.value
+ }
+
+ status() {
+ return this.state.value ? 'on' : 'off';
+ }
+
+ render() {
+ let {label, disabled} = this.props;
+ let checked = this.status() === 'on';
+ return (
+ <div className='toggle-input-wrapper form-group' onClick={!disabled && this.click}>
+ <div className='toggle-input-label'>{label}</div>
+ <div className='toggle-switch'>
+ <input className='toggle toggle-round-flat' type='checkbox' checked={checked} readOnly/>
+ <label></label>
+ </div>
+ </div>
+ );
+ }
+
+ click = () => {
+ let value = !this.state.value;
+ this.setState({value});
+
+ let onChange = this.props.onChange;
+ if (onChange) {
+ onChange(value);
+ }
+ }
+
+ getValue() {
+ return this.state.value;
+ }
+}
diff --git a/openecomp-ui/src/nfvo-components/input/dualListbox/DualListboxView.jsx b/openecomp-ui/src/nfvo-components/input/dualListbox/DualListboxView.jsx
new file mode 100644
index 0000000000..171bead9bb
--- /dev/null
+++ b/openecomp-ui/src/nfvo-components/input/dualListbox/DualListboxView.jsx
@@ -0,0 +1,132 @@
+import React from 'react';
+import FontAwesome from 'react-fontawesome';
+import Input from 'react-bootstrap/lib/Input.js';
+
+class DualListboxView extends React.Component {
+
+ static propTypes = {
+
+ availableList: React.PropTypes.arrayOf(React.PropTypes.shape({
+ id: React.PropTypes.string.isRequired,
+ name: React.PropTypes.string.isRequired
+ })),
+ filterTitle: React.PropTypes.shape({
+ left: React.PropTypes.string,
+ right: React.PropTypes.string
+ }),
+ selectedValuesList: React.PropTypes.arrayOf(React.PropTypes.string),
+
+ onChange: React.PropTypes.func.isRequired
+ };
+
+ static defaultProps = {
+ selectedValuesList: [],
+ availableList: [],
+ filterTitle: {
+ left: '',
+ right: ''
+ }
+ };
+
+ state = {
+ availableListFilter: '',
+ selectedValuesListFilter: ''
+ };
+
+ static contextTypes = {
+ isReadOnlyMode: React.PropTypes.bool
+ };
+
+ render() {
+ let {availableList, selectedValuesList, filterTitle} = this.props;
+ let {availableListFilter, selectedValuesListFilter} = this.state;
+ let isReadOnlyMode = this.context.isReadOnlyMode;
+
+ let unselectedList = availableList.filter(availableItem => !selectedValuesList.find(value => value === availableItem.id));
+ let selectedList = availableList.filter(availableItem => selectedValuesList.find(value => value === availableItem.id));
+ selectedList = selectedList.sort((a, b) => selectedValuesList.indexOf(a.id) - selectedValuesList.indexOf(b.id));
+
+ return (
+ <div className='dual-list-box'>
+ {this.renderListbox(filterTitle.left, unselectedList, {
+ value: availableListFilter,
+ ref: 'availableListFilter',
+ disabled: isReadOnlyMode,
+ onChange: () => this.setState({availableListFilter: this.refs.availableListFilter.getValue()})
+ }, {ref: 'availableValues', disabled: isReadOnlyMode})}
+ {this.renderOperationsBar(isReadOnlyMode)}
+ {this.renderListbox(filterTitle.right, selectedList, {
+ value: selectedValuesListFilter,
+ ref: 'selectedValuesListFilter',
+ disabled: isReadOnlyMode,
+ onChange: () => this.setState({selectedValuesListFilter: this.refs.selectedValuesListFilter.getValue()})
+ }, {ref: 'selectedValues', disabled: isReadOnlyMode})}
+ </div>
+ );
+ }
+
+ renderListbox(filterTitle, list, filterProps, props) {
+ let regExFilter = new RegExp(escape(filterProps.value), 'i');
+ let matchedItems = list.filter(item => item.name.match(regExFilter));
+ let unMatchedItems = list.filter(item => !item.name.match(regExFilter));
+
+
+ return (
+ <div className='dual-search-multi-select-section'>
+ <p>{filterTitle}</p>
+ <div className='dual-text-box-search search-wrapper'>
+ <Input name='search-input-control' type='text' groupClassName='search-input-control' {...filterProps}/>
+ <FontAwesome name='search' className='search-icon'/>
+ </div>
+ <Input
+ multiple
+ groupClassName='dual-list-box-multi-select'
+ type='select'
+ name='dual-list-box-multi-select'
+ {...props}>
+ {matchedItems.map(item => this.renderOption(item.id, item.name))}
+ {matchedItems.length && unMatchedItems.length && <option style={{pointerEvents: 'none'}}>--------------------</option>}
+ {unMatchedItems.map(item => this.renderOption(item.id, item.name))}
+ </Input>
+ </div>
+ );
+ }
+
+ renderOption(value, name) {
+ return (<option className='dual-list-box-multi-select-text' key={value} value={value}>{name}</option>);
+ }
+
+ renderOperationsBar(isReadOnlyMode) {
+ return (
+ <div className={`dual-list-options-bar${isReadOnlyMode ? ' disabled' : ''}`}>
+ {this.renderOperationBarButton(() => this.addToSelectedList(), 'angle-right')}
+ {this.renderOperationBarButton(() => this.removeFromSelectedList(), 'angle-left')}
+ {this.renderOperationBarButton(() => this.addAllToSelectedList(), 'angle-double-right')}
+ {this.renderOperationBarButton(() => this.removeAllFromSelectedList(), 'angle-double-left')}
+ </div>
+ );
+ }
+
+ renderOperationBarButton(onClick, fontAwesomeIconName){
+ return (<div className='dual-list-option' onClick={onClick}><FontAwesome name={fontAwesomeIconName}/></div>);
+ }
+
+ addToSelectedList() {
+ this.props.onChange(this.props.selectedValuesList.concat(this.refs.availableValues.getValue()));
+ }
+
+ removeFromSelectedList() {
+ const selectedValues = this.refs.selectedValues.getValue();
+ this.props.onChange(this.props.selectedValuesList.filter(value => !selectedValues.find(selectedValue => selectedValue === value)));
+ }
+
+ addAllToSelectedList() {
+ this.props.onChange(this.props.availableList.map(item => item.id));
+ }
+
+ removeAllFromSelectedList() {
+ this.props.onChange([]);
+ }
+}
+
+export default DualListboxView;
diff --git a/openecomp-ui/src/nfvo-components/input/inputOptions/InputOptions.jsx b/openecomp-ui/src/nfvo-components/input/inputOptions/InputOptions.jsx
new file mode 100644
index 0000000000..5daaffea41
--- /dev/null
+++ b/openecomp-ui/src/nfvo-components/input/inputOptions/InputOptions.jsx
@@ -0,0 +1,221 @@
+import React from 'react';
+import i18n from 'nfvo-utils/i18n/i18n.js';
+import classNames from 'classnames';
+import Select from 'nfvo-components/input/SelectInput.jsx';
+
+export const other = {OTHER: 'Other'};
+
+class InputOptions extends React.Component {
+
+ static propTypes = {
+ values: React.PropTypes.arrayOf(React.PropTypes.shape({
+ enum: React.PropTypes.string,
+ title: React.PropTypes.string
+ })),
+ isEnabledOther: React.PropTypes.bool,
+ title: React.PropTypes.string,
+ selectedValue: React.PropTypes.string,
+ multiSelectedEnum: React.PropTypes.array,
+ selectedEnum: React.PropTypes.string,
+ otherValue: React.PropTypes.string,
+ onEnumChange: React.PropTypes.func,
+ onOtherChange: React.PropTypes.func,
+ isRequired: React.PropTypes.bool,
+ isMultiSelect: React.PropTypes.bool
+ };
+
+
+ static contextTypes = {
+ isReadOnlyMode: React.PropTypes.bool
+ };
+
+ state = {
+ otherInputDisabled: !this.props.otherValue
+ };
+
+ oldProps = {
+ selectedEnum: '',
+ otherValue: '',
+ multiSelectedEnum: []
+ };
+
+ render() {
+ let {label, isRequired, values, otherValue, onOtherChange, isMultiSelect, onBlur, multiSelectedEnum, selectedEnum, hasError, validations, children} = this.props;
+
+ let currentMultiSelectedEnum = [];
+ let currentSelectedEnum = '';
+ let {otherInputDisabled} = this.state;
+ if (isMultiSelect) {
+ currentMultiSelectedEnum = multiSelectedEnum;
+ if(!otherInputDisabled) {
+ currentSelectedEnum = multiSelectedEnum ? multiSelectedEnum.toString() : undefined;
+ }
+ }
+ else if(selectedEnum){
+ currentSelectedEnum = selectedEnum;
+ }
+
+ let isReadOnlyMode = this.context.isReadOnlyMode;
+
+ return(
+ <div className={classNames('form-group', {'required' : validations.required , 'has-error' : hasError})}>
+ {label && <label className='control-label'>{label}</label>}
+ {isMultiSelect && otherInputDisabled ?
+ <Select
+ ref='_myInput'
+ value={currentMultiSelectedEnum}
+ className='options-input'
+ clearable={false}
+ required={isRequired}
+ disabled={isReadOnlyMode || Boolean(this.props.disabled)}
+ onBlur={() => onBlur()}
+ onMultiSelectChanged={value => this.multiSelectEnumChanged(value)}
+ options={this.renderMultiSelectOptions(values)}
+ multi/> :
+ <div className={classNames('input-options',{'has-error' : hasError})}>
+ <select
+ ref={'_myInput'}
+ label={label}
+ className='form-control input-options-select'
+ value={currentSelectedEnum}
+ style={{'width' : otherInputDisabled ? '100%' : '95px'}}
+ onBlur={() => onBlur()}
+ disabled={isReadOnlyMode || Boolean(this.props.disabled)}
+ onChange={ value => this.enumChanged(value)}
+ type='select'>
+ {values && values.length && values.map(val => this.renderOptions(val))}
+ {onOtherChange && <option key='other' value={other.OTHER}>{i18n(other.OTHER)}</option>}
+ {children}
+ </select>
+
+ {!otherInputDisabled && <div className='input-options-separator'/>}
+ <input
+ className='form-control input-options-other'
+ placeholder={i18n('other')}
+ ref='_otherValue'
+ style={{'display' : otherInputDisabled ? 'none' : 'block'}}
+ disabled={isReadOnlyMode || Boolean(this.props.disabled)}
+ value={otherValue || ''}
+ onBlur={() => onBlur()}
+ onChange={() => this.changedOtherInput()}/>
+ </div>
+ }
+ </div>
+ );
+ }
+
+ renderOptions(val){
+ return(
+ <option key={val.enum} value={val.enum}>{val.title}</option>
+ );
+ }
+
+
+ renderMultiSelectOptions(values) {
+ let {onOtherChange} = this.props;
+ let optionsList = [];
+ if (onOtherChange) {
+ optionsList = values.map(option => {
+ return {
+ label: option.title,
+ value: option.enum,
+ };
+ }).concat([{
+ label: i18n(other.OTHER),
+ value: i18n(other.OTHER),
+ }]);
+ }
+ else {
+ optionsList = values.map(option => {
+ return {
+ label: option.title,
+ value: option.enum,
+ };
+ });
+ }
+ if (optionsList.length > 0 && optionsList[0].value === '') {
+ optionsList.shift();
+ }
+ return optionsList;
+ }
+
+ getValue() {
+ let res = '';
+ let {isMultiSelect} = this.props;
+ let {otherInputDisabled} = this.state;
+
+ if (otherInputDisabled) {
+ res = isMultiSelect ? this.refs._myInput.getValue() : this.refs._myInput.value;
+ } else {
+ res = this.refs._otherValue.value;
+ }
+ return res;
+ }
+
+ enumChanged() {
+ let enumValue = this.refs._myInput.value;
+ let {onEnumChange, isMultiSelect, onChange} = this.props;
+ this.setState({
+ otherInputDisabled: enumValue !== other.OTHER
+ });
+
+ let value = isMultiSelect ? [enumValue] : enumValue;
+ if (onEnumChange) {
+ onEnumChange(value);
+ }
+ if (onChange) {
+ onChange(value);
+ }
+ }
+
+ multiSelectEnumChanged(enumValue) {
+ let {onEnumChange} = this.props;
+ let selectedValues = enumValue.map(enumVal => {
+ return enumVal.value;
+ });
+
+ if (this.state.otherInputDisabled === false) {
+ selectedValues.shift();
+ }
+ else if (selectedValues.includes(i18n(other.OTHER))) {
+ selectedValues = [i18n(other.OTHER)];
+ }
+
+ this.setState({
+ otherInputDisabled: !selectedValues.includes(i18n(other.OTHER))
+ });
+ onEnumChange(selectedValues);
+ }
+
+ changedOtherInput() {
+ let {onOtherChange} = this.props;
+ onOtherChange(this.refs._otherValue.value);
+ }
+
+ componentDidUpdate() {
+ let {otherValue, selectedEnum, onInputChange, multiSelectedEnum} = this.props;
+ if (this.oldProps.otherValue !== otherValue
+ || this.oldProps.selectedEnum !== selectedEnum
+ || this.oldProps.multiSelectedEnum !== multiSelectedEnum) {
+ this.oldProps = {
+ otherValue,
+ selectedEnum,
+ multiSelectedEnum
+ };
+ onInputChange();
+ }
+ }
+
+ static getTitleByName(values, name) {
+ for (let key of Object.keys(values)) {
+ let option = values[key].find(option => option.enum === name);
+ if (option) {
+ return option.title;
+ }
+ }
+ return name;
+ }
+
+}
+
+export default InputOptions;
diff --git a/openecomp-ui/src/nfvo-components/input/validation/ValidationButtons.jsx b/openecomp-ui/src/nfvo-components/input/validation/ValidationButtons.jsx
new file mode 100644
index 0000000000..a87c8d6f40
--- /dev/null
+++ b/openecomp-ui/src/nfvo-components/input/validation/ValidationButtons.jsx
@@ -0,0 +1,40 @@
+/**
+ * Holds the buttons for save/reset for forms.
+ * Used by the ValidationForm that changes the state of the buttons according to its own state.
+ *
+ * properties:
+ * labledButtons - whether or not to use labeled buttons or icons only
+ */
+import React from 'react';
+import i18n from 'nfvo-utils/i18n/i18n.js';
+import Button from 'react-bootstrap/lib/Button.js';
+import FontAwesome from 'react-fontawesome';
+
+class ValidationButtons extends React.Component {
+
+ static propTypes = {
+ labledButtons: React.PropTypes.bool.isRequired,
+ isReadOnlyMode: React.PropTypes.bool
+ };
+
+ state = {
+ isValid: this.props.formValid
+ };
+
+ render() {
+ var submitBtn = this.props.labledButtons ? i18n('Save') : <FontAwesome className='check' name='check'/>;
+ var closeBtn = this.props.labledButtons ? i18n('Cancel') : <FontAwesome className='close' name='close'/>;
+ return (
+ <div className='validation-buttons'>
+ {!this.props.isReadOnlyMode ?
+ <div>
+ <Button bsStyle='primary' ref='submitbutton' type='submit' disabled={!this.state.isValid}>{submitBtn}</Button>
+ <Button type='reset'>{closeBtn}</Button>
+ </div>
+ : <Button type='reset'>{i18n('Close')}</Button>
+ }
+ </div>
+ );
+ }
+}
+export default ValidationButtons;
diff --git a/openecomp-ui/src/nfvo-components/input/validation/ValidationForm.jsx b/openecomp-ui/src/nfvo-components/input/validation/ValidationForm.jsx
new file mode 100644
index 0000000000..098ccf1fd4
--- /dev/null
+++ b/openecomp-ui/src/nfvo-components/input/validation/ValidationForm.jsx
@@ -0,0 +1,200 @@
+/**
+ * ValidationForm should be used in order to have a form that handles it's internal validation state.
+ * All ValidationInputs inside the form are checked for validity and the styling and submit buttons
+ * are updated accordingly.
+ *
+ * The properties that ahould be given to the form:
+ * labledButtons - whether or not use icons only as the form default buttons or use buttons with labels
+ * onSubmit - function for click on the submit button
+ * onReset - function for click on the reset button
+ */
+import React from 'react';
+import JSONSchema from 'nfvo-utils/json/JSONSchema.js';
+import JSONPointer from 'nfvo-utils/json/JSONPointer.js';
+import ValidationButtons from './ValidationButtons.jsx';
+
+class ValidationForm extends React.Component {
+
+ static childContextTypes = {
+ validationParent: React.PropTypes.any,
+ isReadOnlyMode: React.PropTypes.bool,
+ validationSchema: React.PropTypes.instanceOf(JSONSchema),
+ validationData: React.PropTypes.object
+ };
+
+ static defaultProps = {
+ hasButtons : true,
+ onSubmit : null,
+ onReset : null,
+ labledButtons: true,
+ onValidChange : null,
+ isValid: true
+ };
+
+ static propTypes = {
+ isValid : React.PropTypes.bool,
+ isReadOnlyMode : React.PropTypes.bool,
+ hasButtons : React.PropTypes.bool,
+ onSubmit : React.PropTypes.func,
+ onReset : React.PropTypes.func,
+ labledButtons: React.PropTypes.bool,
+ onValidChange : React.PropTypes.func,
+ onValidityChanged: React.PropTypes.func,
+ schema: React.PropTypes.object,
+ data: React.PropTypes.object
+ };
+
+ state = {
+ isValid: this.props.isValid
+ };
+
+ constructor(props) {
+ super(props);
+ this.validationComponents = [];
+ }
+
+ componentWillMount() {
+ let {schema, data} = this.props;
+ if (schema) {
+ this.processSchema(schema, data);
+ }
+ }
+
+ componentWillReceiveProps(nextProps) {
+ let {schema, data} = this.props;
+ let {schema: nextSchema, data: nextData} = nextProps;
+
+ if (schema !== nextSchema || data !== nextData) {
+ if (!schema || !nextSchema) {
+ throw new Error('ValidationForm: dynamically adding/removing schema is not supported');
+ }
+
+ if (schema !== nextSchema) {
+ this.processSchema(nextSchema, nextData);
+ } else {
+ this.setState({data: nextData});
+ }
+ }
+ }
+
+ processSchema(rawSchema, rawData) {
+ let schema = new JSONSchema();
+ schema.setSchema(rawSchema);
+ let data = schema.processData(rawData);
+ this.setState({
+ schema,
+ data
+ });
+ }
+
+ render() {
+ // eslint-disable-next-line no-unused-vars
+ let {isValid, isReadOnlyMode, hasButtons, onSubmit, labledButtons, onValidChange, onValidityChanged, schema, data, children, ...formProps} = this.props;
+ return (
+ <form {...formProps} onSubmit={event => this.handleFormSubmit(event)}>
+ <div className='validation-form-content'>{children}</div>
+ {hasButtons && <ValidationButtons labledButtons={labledButtons} ref='buttons' isReadOnlyMode={isReadOnlyMode}/>}
+ </form>
+ );
+ }
+
+ handleFormSubmit(event) {
+ event.preventDefault();
+ let isFormValid = true;
+ this.validationComponents.forEach(validationComponent => {
+ const isInputValid = validationComponent.validate().isValid;
+ isFormValid = isInputValid && isFormValid;
+ });
+ if(isFormValid && this.props.onSubmit) {
+ return this.props.onSubmit(event);
+ } else if(!isFormValid) {
+ this.setState({isValid: false});
+ }
+ }
+
+ componentWillUpdate(nextProps, nextState) {
+ if(this.state.isValid !== nextState.isValid && this.props.onValidityChanged) {
+ this.props.onValidityChanged(nextState.isValid);
+ }
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ // only handling this programatically if the validation of the form is done outside of the view
+ // (example with a form that is dependent on the state of other forms)
+ if (prevProps.isValid !== this.props.isValid) {
+ if (this.props.hasButtons) {
+ this.refs.buttons.setState({isValid: this.state.isValid});
+ }
+ } else if(this.state.isValid !== prevState.isValid) {
+ if (this.props.hasButtons) {
+ this.refs.buttons.setState({isValid: this.state.isValid});
+ }
+ // callback in case form is part of bigger picture in view
+ if (this.props.onValidChange) {
+ this.props.onValidChange(this.state.isValid);
+ }
+ }
+ }
+
+ componentDidMount() {
+ if (this.props.hasButtons) {
+ this.refs.buttons.setState({isValid: this.state.isValid});
+ }
+ }
+
+
+ getChildContext() {
+ return {
+ validationParent: this,
+ isReadOnlyMode: this.props.isReadOnlyMode,
+ validationSchema: this.state.schema,
+ validationData: this.state.data
+ };
+ }
+
+
+ /***
+ * Used by ValidationInput in order to let the (parent) form know
+ * the valid state. If there is a change in the state of the form,
+ * the buttons will be updated.
+ *
+ * @param validationComponent
+ * @param isValid
+ */
+ childValidStateChanged(validationComponent, isValid) {
+ if (isValid !== this.state.isValid) {
+ let oldState = this.state.isValid;
+ let newState = isValid && this.validationComponents.filter(otherValidationComponent => validationComponent !== otherValidationComponent).every(otherValidationComponent => {
+ return otherValidationComponent.isValid();
+ });
+
+ if (oldState !== newState) {
+ this.setState({isValid: newState});
+ }
+ }
+ }
+
+ register(validationComponent) {
+ if (this.state.schema) {
+ // TODO: register
+ } else {
+ this.validationComponents.push(validationComponent);
+ }
+ }
+
+ unregister(validationComponent) {
+ this.childValidStateChanged(validationComponent, true);
+ this.validationComponents = this.validationComponents.filter(otherValidationComponent => validationComponent !== otherValidationComponent);
+ }
+
+ onValueChanged(pointer, value, isValid, error) {
+ this.props.onDataChanged({
+ data: JSONPointer.setValue(this.props.data, pointer, value),
+ isValid,
+ error
+ });
+ }
+}
+
+
+export default ValidationForm;
diff --git a/openecomp-ui/src/nfvo-components/input/validation/ValidationInput.jsx b/openecomp-ui/src/nfvo-components/input/validation/ValidationInput.jsx
new file mode 100644
index 0000000000..0f14307645
--- /dev/null
+++ b/openecomp-ui/src/nfvo-components/input/validation/ValidationInput.jsx
@@ -0,0 +1,509 @@
+/**
+ * Used for inputs on a validation form.
+ * All properties will be passed on to the input element.
+ *
+ * The following properties can be set for OOB validations and callbacks:
+ - required: Boolean: Should be set to true if the input must have a value
+ - numeric: Boolean : Should be set to true id the input should be an integer
+ - onChange : Function : Will be called to validate the value if the default validations are not sufficient, should return a boolean value
+ indicating whether the value is valid
+ - didUpdateCallback :Function: Will be called after the state has been updated and the component has rerendered. This can be used if
+ there are dependencies between inputs in a form.
+ *
+ * The following properties of the state can be set to determine
+ * the state of the input from outside components:
+ - isValid : Boolean - whether the value is valid
+ - value : value for the input field,
+ - disabled : Boolean,
+ - required : Boolean - whether the input value must be filled out.
+ */
+import React from 'react';
+import ReactDOM from 'react-dom';
+import Validator from 'validator';
+import FormGroup from 'react-bootstrap/lib/FormGroup.js';
+import Input from 'react-bootstrap/lib/Input.js';
+import Overlay from 'react-bootstrap/lib/Overlay.js';
+import Tooltip from 'react-bootstrap/lib/Tooltip.js';
+import isEqual from 'lodash/isEqual.js';
+import i18n from 'nfvo-utils/i18n/i18n.js';
+import JSONSchema from 'nfvo-utils/json/JSONSchema.js';
+import JSONPointer from 'nfvo-utils/json/JSONPointer.js';
+
+
+import InputOptions from '../inputOptions/InputOptions.jsx';
+
+const globalValidationFunctions = {
+ required: value => value !== '',
+ maxLength: (value, length) => Validator.isLength(value, {max: length}),
+ minLength: (value, length) => Validator.isLength(value, {min: length}),
+ pattern: (value, pattern) => Validator.matches(value, pattern),
+ numeric: value => {
+ if (value === '') {
+ // to allow empty value which is not zero
+ return true;
+ }
+ return Validator.isNumeric(value);
+ },
+ maxValue: (value, maxValue) => value < maxValue,
+ minValue: (value, minValue) => value >= minValue,
+ alphanumeric: value => Validator.isAlphanumeric(value),
+ alphanumericWithSpaces: value => Validator.isAlphanumeric(value.replace(/ /g, '')),
+ validateName: value => Validator.isAlphanumeric(value.replace(/\s|\.|\_|\-/g, ''), 'en-US'),
+ validateVendorName: value => Validator.isAlphanumeric(value.replace(/[\x7F-\xFF]|\s/g, ''), 'en-US'),
+ freeEnglishText: value => Validator.isAlphanumeric(value.replace(/\s|\.|\_|\-|\,|\(|\)|\?/g, ''), 'en-US'),
+ email: value => Validator.isEmail(value),
+ ip: value => Validator.isIP(value),
+ url: value => Validator.isURL(value)
+};
+
+const globalValidationMessagingFunctions = {
+ required: () => i18n('Field is required'),
+ maxLength: (value, maxLength) => i18n('Field value has exceeded it\'s limit, {maxLength}. current length: {length}', {
+ length: value.length,
+ maxLength
+ }),
+ minLength: (value, minLength) => i18n('Field value should contain at least {minLength} characters.', {minLength}),
+ pattern: (value, pattern) => i18n('Field value should match the pattern: {pattern}.', {pattern}),
+ numeric: () => i18n('Field value should contain numbers only.'),
+ maxValue: (value, maxValue) => i18n('Field value should be less than: {maxValue}.', {maxValue}),
+ minValue: (value, minValue) => i18n('Field value should be at least: {minValue}.', {minValue}),
+ alphanumeric: () => i18n('Field value should contain letters or digits only.'),
+ alphanumericWithSpaces: () => i18n('Field value should contain letters, digits or spaces only.'),
+ validateName: ()=> i18n('Field value should contain English letters, digits , spaces, underscores, dashes and dots only.'),
+ validateVendorName: ()=> i18n('Field value should contain English letters digits and spaces only.'),
+ freeEnglishText: ()=> i18n('Field value should contain English letters, digits , spaces, underscores, dashes and dots only.'),
+ email: () => i18n('Field value should be a valid email address.'),
+ ip: () => i18n('Field value should be a valid ip address.'),
+ url: () => i18n('Field value should be a valid url address.'),
+ general: () => i18n('Field value is invalid.')
+};
+
+class ValidationInput extends React.Component {
+
+ static contextTypes = {
+ validationParent: React.PropTypes.any,
+ isReadOnlyMode: React.PropTypes.bool,
+ validationSchema: React.PropTypes.instanceOf(JSONSchema),
+ validationData: React.PropTypes.object
+ };
+
+ static defaultProps = {
+ onChange: null,
+ disabled: null,
+ didUpdateCallback: null,
+ validations: {},
+ value: ''
+ };
+
+ static propTypes = {
+ type: React.PropTypes.string.isRequired,
+ onChange: React.PropTypes.func,
+ disabled: React.PropTypes.bool,
+ didUpdateCallback: React.PropTypes.func,
+ validations: React.PropTypes.object,
+ isMultiSelect: React.PropTypes.bool,
+ onOtherChange: React.PropTypes.func,
+ pointer: React.PropTypes.string
+ };
+
+
+ state = {
+ isValid: true,
+ style: null,
+ value: this.props.value,
+ error: {},
+ previousErrorMessage: '',
+ wasInvalid: false,
+ validations: this.props.validations,
+ isMultiSelect: this.props.isMultiSelect
+ };
+
+ componentWillMount() {
+ if (this.context.validationSchema) {
+ let {validationSchema: schema, validationData: data} = this.context,
+ {pointer} = this.props;
+
+ if (!schema.exists(pointer)) {
+ console.error(`Field doesn't exists in the schema ${pointer}`);
+ }
+
+ let value = JSONPointer.getValue(data, pointer);
+ if (value === undefined) {
+ value = schema.getDefault(pointer);
+ if (value === undefined) {
+ value = '';
+ }
+ }
+ this.setState({value});
+
+ let enums = schema.getEnum(pointer);
+ if (enums) {
+ let values = enums.map(value => ({enum: value, title: value, groupName: pointer})),
+ isMultiSelect = schema.isArray(pointer);
+
+ if (!isMultiSelect && this.props.type !== 'radiogroup') {
+ values = [{enum: '', title: i18n('Select...')}, ...values];
+ }
+ if (isMultiSelect && Array.isArray(value) && value.length === 0) {
+ value = '';
+ }
+
+ this.setState({
+ isMultiSelect,
+ values,
+ onEnumChange: value => this.changedInputOptions(value),
+ value
+ });
+ }
+
+ this.setState({validations: this.extractValidationsFromSchema(schema, pointer, this.props)});
+ }
+ }
+
+ extractValidationsFromSchema(schema, pointer, props) {
+ /* props are here to get precedence over the scheme definitions */
+ let validations = {};
+
+ if (schema.isRequired(pointer)) {
+ validations.required = true;
+ }
+
+ if (schema.isNumber(pointer)) {
+ validations.numeric = true;
+
+ const maxValue = props.validations.maxValue || schema.getMaxValue(pointer);
+ if (maxValue !== undefined) {
+ validations.maxValue = maxValue;
+ }
+
+ const minValue = props.validations.minValue || schema.getMinValue(pointer);
+ if (minValue !== undefined) {
+ validations.minValue = minValue;
+ }
+ }
+
+
+ if (schema.isString(pointer)) {
+
+ const pattern = schema.getPattern(pointer);
+ if (pattern) {
+ validations.pattern = pattern;
+ }
+
+ const maxLength = schema.getMaxLength(pointer);
+ if (maxLength !== undefined) {
+ validations.maxLength = maxLength;
+ }
+
+ const minLength = schema.getMinLength(pointer);
+ if (minLength !== undefined) {
+ validations.minLength = minLength;
+ }
+ }
+
+ return validations;
+ }
+
+ componentWillReceiveProps({value: nextValue, validations: nextValidations, pointer: nextPointer}, nextContext) {
+ const {validations, value} = this.props;
+ const validationsChanged = !isEqual(validations, nextValidations);
+ if (nextContext.validationSchema) {
+ if (this.props.pointer !== nextPointer ||
+ this.context.validationData !== nextContext.validationData) {
+ let currentValue = JSONPointer.getValue(this.context.validationData, this.props.pointer),
+ nextValue = JSONPointer.getValue(nextContext.validationData, nextPointer);
+ if(nextValue === undefined) {
+ nextValue = '';
+ }
+ if (this.state.isMultiSelect && Array.isArray(nextValue) && nextValue.length === 0) {
+ nextValue = '';
+ }
+ if (currentValue !== nextValue) {
+ this.setState({value: nextValue});
+ }
+ if (validationsChanged) {
+ this.setState({
+ validations: this.extractValidationsFromSchema(nextContext.validationSchema, nextPointer, {validations: nextValidations})
+ });
+ }
+ }
+ } else {
+ if (validationsChanged) {
+ this.setState({validations: nextValidations});
+ }
+ if (this.state.wasInvalid && (value !== nextValue || validationsChanged)) {
+ this.validate(nextValue, nextValidations);
+ } else if (value !== nextValue) {
+ this.setState({value: nextValue});
+ }
+ }
+ }
+
+ shouldTypeBeNumberBySchemeDefinition(pointer) {
+ return this.context.validationSchema &&
+ this.context.validationSchema.isNumber(pointer);
+ }
+
+ hasEnum(pointer) {
+ return this.context.validationSchema &&
+ this.context.validationSchema.getEnum(pointer);
+ }
+
+ render() {
+ let {value, isMultiSelect, values, onEnumChange, style, isValid, validations} = this.state;
+ let {onOtherChange, type, pointer} = this.props;
+ if (this.shouldTypeBeNumberBySchemeDefinition(pointer) && !this.hasEnum(pointer)) {
+ type = 'number';
+ }
+ let props = {...this.props};
+
+ let groupClasses = this.props.groupClassName || '';
+ if (validations.required) {
+ groupClasses += ' required';
+ }
+ let isReadOnlyMode = this.context.isReadOnlyMode;
+
+ if (value === true && (type === 'checkbox' || type === 'radio')) {
+ props.checked = true;
+ }
+ return (
+ <div className='validation-input-wrapper'>
+ {
+ !isMultiSelect && !onOtherChange && type !== 'select' && type !== 'radiogroup'
+ && <Input
+ {...props}
+ type={type}
+ groupClassName={groupClasses}
+ ref={'_myInput'}
+ value={value}
+ disabled={isReadOnlyMode || Boolean(this.props.disabled)}
+ bsStyle={style}
+ onChange={() => this.changedInput()}
+ onBlur={() => this.blurInput()}>
+ {this.props.children}
+ </Input>
+ }
+ {
+ type === 'radiogroup'
+ && <FormGroup>
+ {
+ values.map(val =>
+ <Input disabled={isReadOnlyMode || Boolean(this.props.disabled)}
+ inline={true}
+ ref={'_myInput' + (typeof val.enum === 'string' ? val.enum.replace(/\W/g, '_') : val.enum)}
+ value={val.enum} checked={value === val.enum}
+ type='radio' label={val.title}
+ name={val.groupName}
+ onChange={() => this.changedInput()}/>
+ )
+ }
+ </FormGroup>
+ }
+ {
+ (isMultiSelect || onOtherChange || type === 'select')
+ && <InputOptions
+ onInputChange={() => this.changedInput()}
+ onBlur={() => this.blurInput()}
+ hasError={!isValid}
+ ref={'_myInput'}
+ isMultiSelect={isMultiSelect}
+ values={values}
+ onEnumChange={onEnumChange}
+ selectedEnum={value}
+ multiSelectedEnum={value}
+ {...props} />
+ }
+ {this.renderOverlay()}
+ </div>
+ );
+ }
+
+ renderOverlay() {
+ let position = 'right';
+ if (this.props.type === 'text'
+ || this.props.type === 'email'
+ || this.props.type === 'number'
+ || this.props.type === 'password'
+
+ ) {
+ position = 'bottom';
+ }
+
+ let validationMessage = this.state.error.message || this.state.previousErrorMessage;
+ return (
+ <Overlay
+ show={!this.state.isValid}
+ placement={position}
+ target={() => {
+ let target = ReactDOM.findDOMNode(this.refs._myInput);
+ return target.offsetParent ? target : undefined;
+ }}
+ container={this}>
+ <Tooltip
+ id={`error-${validationMessage.replace(' ', '-')}`}
+ className='validation-error-message'>
+ {validationMessage}
+ </Tooltip>
+ </Overlay>
+ );
+ }
+
+ componentDidMount() {
+ if (this.context.validationParent) {
+ this.context.validationParent.register(this);
+ }
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if (this.context.validationParent) {
+ if (prevState.isValid !== this.state.isValid) {
+ this.context.validationParent.childValidStateChanged(this, this.state.isValid);
+ }
+ }
+ if (this.props.didUpdateCallback) {
+ this.props.didUpdateCallback();
+ }
+
+ }
+
+ componentWillUnmount() {
+ if (this.context.validationParent) {
+ this.context.validationParent.unregister(this);
+ }
+ }
+
+ isNumberInputElement() {
+ return this.props.type === 'number' || this.refs._myInput.props.type === 'number';
+ }
+
+ /***
+ * Adding same method as the actual input component
+ * @returns {*}
+ */
+ getValue() {
+ if (this.props.type === 'checkbox') {
+ return this.refs._myInput.getChecked();
+ }
+ if (this.props.type === 'radiogroup') {
+ for (let key in this.refs) { // finding the value of the radio button that was checked
+ if (this.refs[key].getChecked()) {
+ return this.refs[key].getValue();
+ }
+ }
+ }
+ if (this.isNumberInputElement()) {
+ return Number(this.refs._myInput.getValue());
+ }
+
+ return this.refs._myInput.getValue();
+ }
+
+ resetValue() {
+ this.setState({value: this.props.value});
+ }
+
+
+ /***
+ * internal method that validated the value. includes callback to the onChange method
+ * @param value
+ * @param validations - map containing validation id and the limitation describing the validation.
+ * @returns {object}
+ */
+ validateValue = (value, validations) => {
+ let {customValidationFunction} = validations;
+ let error = {};
+ let isValid = true;
+ for (let validation in validations) {
+ if ('customValidationFunction' !== validation) {
+ if (validations[validation]) {
+ if (!globalValidationFunctions[validation](value, validations[validation])) {
+ error.id = validation;
+ error.message = globalValidationMessagingFunctions[validation](value, validations[validation]);
+ isValid = false;
+ break;
+ }
+ }
+ } else {
+ let customValidationResult = customValidationFunction(value);
+
+ if (customValidationResult !== true) {
+ error.id = 'custom';
+ isValid = false;
+ if (typeof customValidationResult === 'string') {//custom validation error message supplied.
+ error.message = customValidationResult;
+ } else {
+ error.message = globalValidationMessagingFunctions.general();
+ }
+ break;
+ }
+
+
+ }
+ }
+
+ return {
+ isValid,
+ error
+ };
+ };
+
+ /***
+ * Internal method that handles the change event of the input. validates and updates the state.
+ */
+ changedInput() {
+
+ let {isValid, error} = this.state.wasInvalid ? this.validate() : this.state;
+ let onChange = this.props.onChange;
+ if (onChange) {
+ onChange(this.getValue(), isValid, error);
+ }
+ if (this.context.validationSchema) {
+ let value = this.getValue();
+ if (this.state.isMultiSelect && value === '') {
+ value = [];
+ }
+ if (this.shouldTypeBeNumberBySchemeDefinition(this.props.pointer)) {
+ value = Number(value);
+ }
+ this.context.validationParent.onValueChanged(this.props.pointer, value, isValid, error);
+ }
+ }
+
+ changedInputOptions(value) {
+ this.context.validationParent.onValueChanged(this.props.pointer, value, true);
+ }
+
+ blurInput() {
+ if (!this.state.wasInvalid) {
+ this.setState({wasInvalid: true});
+ }
+
+ let {isValid, error} = !this.state.wasInvalid ? this.validate() : this.state;
+ let onBlur = this.props.onBlur;
+ if (onBlur) {
+ onBlur(this.getValue(), isValid, error);
+ }
+ }
+
+ validate(value = this.getValue(), validations = this.state.validations) {
+ let validationStatus = this.validateValue(value, validations);
+ let {isValid, error} = validationStatus;
+ let _style = isValid ? null : 'error';
+ this.setState({
+ isValid,
+ error,
+ value,
+ previousErrorMessage: this.state.error.message || '',
+ style: _style,
+ wasInvalid: !isValid || this.state.wasInvalid
+ });
+
+ return validationStatus;
+ }
+
+ isValid() {
+ return this.state.isValid;
+ }
+
+}
+export default ValidationInput;
diff --git a/openecomp-ui/src/nfvo-components/input/validation/ValidationTab.jsx b/openecomp-ui/src/nfvo-components/input/validation/ValidationTab.jsx
new file mode 100644
index 0000000000..6036518288
--- /dev/null
+++ b/openecomp-ui/src/nfvo-components/input/validation/ValidationTab.jsx
@@ -0,0 +1,107 @@
+import React from 'react';
+import Tab from 'react-bootstrap/lib/Tab.js';
+
+export default
+class ValidationTab extends React.Component {
+
+ static propTypes = {
+ children: React.PropTypes.node,
+ eventKey: React.PropTypes.any.isRequired,
+ onValidationStateChange: React.PropTypes.func //This property is assigned dynamically via React.cloneElement. lookup ValidationTabs.jsx. therefore it cannot be stated as required!
+ };
+
+ constructor(props) {
+ super(props);
+ this.validationComponents = [];
+ }
+
+ static childContextTypes = {
+ validationParent: React.PropTypes.any
+ };
+
+ static contextTypes = {
+ validationParent: React.PropTypes.any
+ };
+
+ getChildContext() {
+ return {validationParent: this};
+ }
+
+ state = {
+ isValid: true,
+ notifyParent: false
+ };
+
+ componentDidMount() {
+ let validationParent = this.context.validationParent;
+ if (validationParent) {
+ validationParent.register(this);
+ }
+ }
+
+ componentWillUnmount() {
+ let validationParent = this.context.validationParent;
+ if (validationParent) {
+ validationParent.unregister(this);
+ }
+ }
+
+ register(validationComponent) {
+ this.validationComponents.push(validationComponent);
+ }
+
+ unregister(validationComponent) {
+ this.childValidStateChanged(validationComponent, true);
+ this.validationComponents = this.validationComponents.filter(otherValidationComponent => validationComponent !== otherValidationComponent);
+ }
+
+ notifyValidStateChangedToParent(isValid) {
+
+ let validationParent = this.context.validationParent;
+ if (validationParent) {
+ validationParent.childValidStateChanged(this, isValid);
+ }
+ }
+
+ childValidStateChanged(validationComponent, isValid) {
+
+ const currentValidState = this.state.isValid;
+ if (isValid !== currentValidState) {
+ let filteredValidationComponents = this.validationComponents.filter(otherValidationComponent => validationComponent !== otherValidationComponent);
+ let newValidState = isValid && filteredValidationComponents.every(otherValidationComponent => {
+ return otherValidationComponent.isValid();
+ });
+ this.setState({isValid: newValidState, notifyParent: true});
+ }
+ }
+
+ validate() {
+ let isValid = true;
+ this.validationComponents.forEach(validationComponent => {
+ const isValidationComponentValid = validationComponent.validate().isValid;
+ isValid = isValidationComponentValid && isValid;
+ });
+ this.setState({isValid, notifyParent: false});
+ return {isValid};
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if(prevState.isValid !== this.state.isValid) {
+ if(this.state.notifyParent) {
+ this.notifyValidStateChangedToParent(this.state.isValid);
+ }
+ this.props.onValidationStateChange(this.props.eventKey, this.state.isValid);
+ }
+ }
+
+ isValid() {
+ return this.state.isValid;
+ }
+
+ render() {
+ let {children, ...tabProps} = this.props;
+ return (
+ <Tab {...tabProps}>{children}</Tab>
+ );
+ }
+}
diff --git a/openecomp-ui/src/nfvo-components/input/validation/ValidationTabs.jsx b/openecomp-ui/src/nfvo-components/input/validation/ValidationTabs.jsx
new file mode 100644
index 0000000000..6eda4b9827
--- /dev/null
+++ b/openecomp-ui/src/nfvo-components/input/validation/ValidationTabs.jsx
@@ -0,0 +1,72 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import Tabs from 'react-bootstrap/lib/Tabs.js';
+import Overlay from 'react-bootstrap/lib/Overlay.js';
+import Tooltip from 'react-bootstrap/lib/Tooltip.js';
+
+import i18n from 'nfvo-utils/i18n/i18n.js';
+
+export default
+class ValidationTab extends React.Component {
+
+ static propTypes = {
+ children: React.PropTypes.node
+ };
+
+ state = {
+ invalidTabs: []
+ };
+
+ cloneTab(element) {
+ const {invalidTabs} = this.state;
+ return React.cloneElement(
+ element,
+ {
+ key: element.props.eventKey,
+ tabClassName: invalidTabs.indexOf(element.props.eventKey) > -1 ? 'invalid-tab' : 'valid-tab',
+ onValidationStateChange: (eventKey, isValid) => this.validTabStateChanged(eventKey, isValid)
+ }
+ );
+ }
+
+ validTabStateChanged(eventKey, isValid) {
+ let {invalidTabs} = this.state;
+ let invalidTabIndex = invalidTabs.indexOf(eventKey);
+ if (isValid && invalidTabIndex > -1) {
+ this.setState({invalidTabs: invalidTabs.filter(otherEventKey => eventKey !== otherEventKey)});
+ } else if (!isValid && invalidTabIndex === -1) {
+ this.setState({invalidTabs: [...invalidTabs, eventKey]});
+ }
+ }
+
+ showTabsError() {
+ const {invalidTabs} = this.state;
+ return invalidTabs.length > 0 && (invalidTabs.length > 1 || invalidTabs[0] !== this.props.activeKey);
+ }
+
+ render() {
+ return (
+ <div>
+ <Tabs {...this.props} ref='tabsList'>
+ {this.props.children.map(element => this.cloneTab(element))}
+ </Tabs>
+ <Overlay
+ animation={false}
+ show={this.showTabsError()}
+ placement='bottom'
+ target={() => {
+ let target = ReactDOM.findDOMNode(this.refs.tabsList).querySelector('ul > li.invalid-tab:not(.active):nth-of-type(n)');
+ return target && target.offsetParent ? target : undefined;
+ }
+ }
+ container={this}>
+ <Tooltip
+ id='error-some-tabs-contain-errors'
+ className='validation-error-message'>
+ {i18n('One or more tabs are invalid')}
+ </Tooltip>
+ </Overlay>
+ </div>
+ );
+ }
+}