diff options
Diffstat (limited to 'openecomp-ui/src/nfvo-components/input')
15 files changed, 1009 insertions, 981 deletions
diff --git a/openecomp-ui/src/nfvo-components/input/ExpandableInput.jsx b/openecomp-ui/src/nfvo-components/input/ExpandableInput.jsx index 3ac3fcad28..e2ee40fcd2 100644 --- a/openecomp-ui/src/nfvo-components/input/ExpandableInput.jsx +++ b/openecomp-ui/src/nfvo-components/input/ExpandableInput.jsx @@ -1,77 +1,115 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ import React from 'react'; -import FontAwesome from 'react-fontawesome'; -import classnames from 'classnames'; -import Input from 'react-bootstrap/lib/Input'; +import ReactDOM from 'react-dom'; +import SVGIcon from 'nfvo-components/icon/SVGIcon.jsx'; +import Input from 'nfvo-components/input/validation/InputWrapper.jsx'; +const ExpandableInputClosed = ({iconType, onClick}) => ( + <SVGIcon className='expandable-input-wrapper closed' name={iconType} onClick={onClick} /> +); -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); +class ExpandableInputOpened extends React.Component { + componentDidMount(){ + this.rawDomNode = ReactDOM.findDOMNode(this.searchInputNode.inputWrapper); + this.rawDomNode.focus(); } - toggleInput(){ - if (!this.state.showInput){ - this.searchInputNode.refs.input.focus(); - } else { - this.setState({showInput: false}); + componentWillReceiveProps(newProps){ + if (!newProps.value){ + if (!(document.activeElement === this.rawDomNode)){ + this.props.handleBlur(); + } } } - handleInput(e){ - let {onChange} = this.props; + handleClose(){ + this.props.onChange(''); + this.rawDomNode.focus(); + } - this.setState({value: e.target.value}); - onChange(e); + handleKeyDown(e){ + if (e.key === 'Escape'){ + e.preventDefault(); + if (this.props.value) { + this.handleClose(); + } else { + this.rawDomNode.blur(); + } + }; } - handleClose(){ - this.handleInput({target: {value: ''}}); - this.searchInputNode.refs.input.focus(); + render() { + let {iconType, value, onChange, handleBlur} = this.props; + return ( + <div className='expandable-input-wrapper opened' key='expandable'> + <Input + type='text' + value={value} + ref={(input) => this.searchInputNode = input} + className='expandable-active' + groupClassName='expandable-input-control' + onChange={e => onChange(e)} + onKeyDown={e => this.handleKeyDown(e)} + onBlur={handleBlur}/> + {value && <SVGIcon onClick={() => this.handleClose()} name='close' />} + {!value && <SVGIcon name={iconType} onClick={handleBlur}/>} + </div> + ); } +} + +class ExpandableInput extends React.Component { - handleFocus(){ - if (!this.state.showInput){ - this.setState({showInput: true}); + static propTypes = { + iconType: React.PropTypes.string, + onChange: React.PropTypes.func, + value: React.PropTypes.string + }; + + state = {showInput: false}; + + closeInput(){ + if (!this.props.value) { + this.setState({showInput: false}); } } getValue(){ - return this.state.value; + return this.props.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} - ); - + let {iconType, value, onChange = false} = this.props; 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 className='expandable-input-top'> + {this.state.showInput && + <ExpandableInputOpened + key='open' + iconType={iconType} + onChange={onChange} + value={value} + handleKeyDown={(e) => this.handleKeyDown(e)} + handleBlur={() => this.closeInput()}/> + } + {!this.state.showInput && <ExpandableInputClosed key='closed' iconType={iconType} onClick={() => this.setState({showInput: true})} />} </div> - ); + ); } } + export default ExpandableInput; diff --git a/openecomp-ui/src/nfvo-components/input/SelectInput.jsx b/openecomp-ui/src/nfvo-components/input/SelectInput.jsx index 1036ac41c3..03c727379e 100644 --- a/openecomp-ui/src/nfvo-components/input/SelectInput.jsx +++ b/openecomp-ui/src/nfvo-components/input/SelectInput.jsx @@ -1,3 +1,18 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ /** * 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. @@ -20,8 +35,9 @@ class SelectInput extends Component { render() { let {label, value, ...other} = this.props; + const dataTestId = this.props['data-test-id'] ? {'data-test-id': this.props['data-test-id']} : {}; return ( - <div className='validation-input-wrapper dropdown-multi-select'> + <div {...dataTestId} 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} /> diff --git a/openecomp-ui/src/nfvo-components/input/ToggleInput.jsx b/openecomp-ui/src/nfvo-components/input/ToggleInput.jsx index 873d3ded65..7bbafa3696 100644 --- a/openecomp-ui/src/nfvo-components/input/ToggleInput.jsx +++ b/openecomp-ui/src/nfvo-components/input/ToggleInput.jsx @@ -1,3 +1,18 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ import React from 'react'; export default diff --git a/openecomp-ui/src/nfvo-components/input/dualListbox/DualListboxView.jsx b/openecomp-ui/src/nfvo-components/input/dualListbox/DualListboxView.jsx index 171bead9bb..c60d6f777e 100644 --- a/openecomp-ui/src/nfvo-components/input/dualListbox/DualListboxView.jsx +++ b/openecomp-ui/src/nfvo-components/input/dualListbox/DualListboxView.jsx @@ -1,6 +1,21 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ import React from 'react'; -import FontAwesome from 'react-fontawesome'; -import Input from 'react-bootstrap/lib/Input.js'; +import SVGIcon from 'nfvo-components/icon/SVGIcon.jsx'; +import Input from 'nfvo-components/input/validation/InputWrapper.jsx'; class DualListboxView extends React.Component { @@ -30,37 +45,32 @@ class DualListboxView extends React.Component { state = { availableListFilter: '', - selectedValuesListFilter: '' - }; - - static contextTypes = { - isReadOnlyMode: React.PropTypes.bool + selectedValuesListFilter: '', + selectedValues: [] }; render() { - let {availableList, selectedValuesList, filterTitle} = this.props; + let {availableList, selectedValuesList, filterTitle, isReadOnlyMode} = 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})} + onChange: (value) => this.setState({availableListFilter: value}) + }, {ref: 'availableValues', disabled: isReadOnlyMode, testId: 'available',})} {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})} + onChange: (value) => this.setState({selectedValuesListFilter: value}) + }, {ref: 'selectedValues', disabled: isReadOnlyMode, testId: 'selected'})} </div> ); } @@ -69,21 +79,25 @@ class DualListboxView extends React.Component { 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'/> + <Input data-test-id={`${props.testId}-search-input`} + name='search-input-control' type='text' + groupClassName='search-input-control' + {...filterProps}/> + <SVGIcon name='search' className='search-icon'/> </div> <Input multiple + onChange={(event) => this.onSelectItems(event.target.selectedOptions)} groupClassName='dual-list-box-multi-select' type='select' name='dual-list-box-multi-select' - {...props}> + data-test-id={`${props.testId}-select-input`} + disabled={props.disabled} + ref={props.ref}> {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))} @@ -92,6 +106,11 @@ class DualListboxView extends React.Component { ); } + onSelectItems(selectedOptions) { + let selectedValues = Object.keys(selectedOptions).map((k) => selectedOptions[k].value); + this.setState({selectedValues}); + } + renderOption(value, name) { return (<option className='dual-list-box-multi-select-text' key={value} value={value}>{name}</option>); } @@ -107,17 +126,19 @@ class DualListboxView extends React.Component { ); } - renderOperationBarButton(onClick, fontAwesomeIconName){ - return (<div className='dual-list-option' onClick={onClick}><FontAwesome name={fontAwesomeIconName}/></div>); + renderOperationBarButton(onClick, iconName){ + return (<div className='dual-list-option' data-test-id={`operation-icon-${iconName}`} onClick={onClick}><SVGIcon name={iconName}/></div>); } addToSelectedList() { - this.props.onChange(this.props.selectedValuesList.concat(this.refs.availableValues.getValue())); + this.props.onChange(this.props.selectedValuesList.concat(this.state.selectedValues)); + this.setState({selectedValues: []}); } removeFromSelectedList() { - const selectedValues = this.refs.selectedValues.getValue(); + const selectedValues = this.state.selectedValues; this.props.onChange(this.props.selectedValuesList.filter(value => !selectedValues.find(selectedValue => selectedValue === value))); + this.setState({selectedValues: []}); } addAllToSelectedList() { diff --git a/openecomp-ui/src/nfvo-components/input/inputOptions/InputOptions.jsx b/openecomp-ui/src/nfvo-components/input/inputOptions/InputOptions.jsx index 5daaffea41..e8aadc4357 100644 --- a/openecomp-ui/src/nfvo-components/input/inputOptions/InputOptions.jsx +++ b/openecomp-ui/src/nfvo-components/input/inputOptions/InputOptions.jsx @@ -1,3 +1,18 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ import React from 'react'; import i18n from 'nfvo-utils/i18n/i18n.js'; import classNames from 'classnames'; @@ -13,15 +28,21 @@ class InputOptions extends React.Component { title: React.PropTypes.string })), isEnabledOther: React.PropTypes.bool, - title: React.PropTypes.string, + label: React.PropTypes.string, selectedValue: React.PropTypes.string, - multiSelectedEnum: React.PropTypes.array, + multiSelectedEnum: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.array + ]), selectedEnum: React.PropTypes.string, otherValue: React.PropTypes.string, onEnumChange: React.PropTypes.func, onOtherChange: React.PropTypes.func, + onBlur: React.PropTypes.func, isRequired: React.PropTypes.bool, - isMultiSelect: React.PropTypes.bool + isMultiSelect: React.PropTypes.bool, + hasError: React.PropTypes.bool, + disabled: React.PropTypes.bool }; @@ -41,7 +62,7 @@ class InputOptions extends React.Component { render() { let {label, isRequired, values, otherValue, onOtherChange, isMultiSelect, onBlur, multiSelectedEnum, selectedEnum, hasError, validations, children} = this.props; - + const dataTestId = this.props['data-test-id'] ? {'data-test-id': this.props['data-test-id']} : {}; let currentMultiSelectedEnum = []; let currentSelectedEnum = ''; let {otherInputDisabled} = this.state; @@ -54,14 +75,18 @@ class InputOptions extends React.Component { else if(selectedEnum){ currentSelectedEnum = selectedEnum; } + if (!onBlur) { + onBlur = () => {}; + } let isReadOnlyMode = this.context.isReadOnlyMode; return( - <div className={classNames('form-group', {'required' : validations.required , 'has-error' : hasError})}> + <div className={classNames('form-group', {'required' : (validations && validations.required) || isRequired, 'has-error' : hasError})}> {label && <label className='control-label'>{label}</label>} {isMultiSelect && otherInputDisabled ? <Select + {...dataTestId} ref='_myInput' value={currentMultiSelectedEnum} className='options-input' @@ -74,18 +99,18 @@ class InputOptions extends React.Component { multi/> : <div className={classNames('input-options',{'has-error' : hasError})}> <select + {...dataTestId} ref={'_myInput'} label={label} className='form-control input-options-select' value={currentSelectedEnum} - style={{'width' : otherInputDisabled ? '100%' : '95px'}} + style={{'width' : otherInputDisabled ? '100%' : '100px'}} onBlur={() => onBlur()} disabled={isReadOnlyMode || Boolean(this.props.disabled)} onChange={ value => this.enumChanged(value)} type='select'> - {values && values.length && values.map(val => this.renderOptions(val))} + {children || (values && values.length && values.map((val, index) => this.renderOptions(val, index)))} {onOtherChange && <option key='other' value={other.OTHER}>{i18n(other.OTHER)}</option>} - {children} </select> {!otherInputDisabled && <div className='input-options-separator'/>} @@ -104,9 +129,9 @@ class InputOptions extends React.Component { ); } - renderOptions(val){ - return( - <option key={val.enum} value={val.enum}>{val.title}</option> + renderOptions(val, index){ + return ( + <option key={index} value={val.enum}>{val.title}</option> ); } @@ -154,9 +179,9 @@ class InputOptions extends React.Component { enumChanged() { let enumValue = this.refs._myInput.value; - let {onEnumChange, isMultiSelect, onChange} = this.props; + let {onEnumChange, onOtherChange, isMultiSelect, onChange} = this.props; this.setState({ - otherInputDisabled: enumValue !== other.OTHER + otherInputDisabled: !Boolean(onOtherChange) || enumValue !== other.OTHER }); let value = isMultiSelect ? [enumValue] : enumValue; @@ -169,7 +194,7 @@ class InputOptions extends React.Component { } multiSelectEnumChanged(enumValue) { - let {onEnumChange} = this.props; + let {onEnumChange, onOtherChange} = this.props; let selectedValues = enumValue.map(enumVal => { return enumVal.value; }); @@ -182,7 +207,7 @@ class InputOptions extends React.Component { } this.setState({ - otherInputDisabled: !selectedValues.includes(i18n(other.OTHER)) + otherInputDisabled: !Boolean(onOtherChange) || !selectedValues.includes(i18n(other.OTHER)) }); onEnumChange(selectedValues); } diff --git a/openecomp-ui/src/nfvo-components/input/validation/Form.jsx b/openecomp-ui/src/nfvo-components/input/validation/Form.jsx new file mode 100644 index 0000000000..47922f86a0 --- /dev/null +++ b/openecomp-ui/src/nfvo-components/input/validation/Form.jsx @@ -0,0 +1,114 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import React from 'react'; +import ValidationButtons from './ValidationButtons.jsx'; + +class Form extends React.Component { + + static defaultProps = { + hasButtons : true, + onSubmit : null, + onReset : null, + labledButtons: true, + onValidChange : null, + isValid: true + }; + + static propTypes = { + isValid : React.PropTypes.bool, + formReady : 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, + onValidateForm: React.PropTypes.func + }; + + constructor(props) { + super(props); + } + + + render() { + // eslint-disable-next-line no-unused-vars + let {isValid, formReady, onValidateForm, isReadOnlyMode, hasButtons, onSubmit, labledButtons, onValidChange, onValidityChanged, onDataChanged, children, ...formProps} = this.props; + return ( + <form {...formProps} ref={(form) => this.form = form} onSubmit={event => this.handleFormValidation(event)}> + <div className='validation-form-content'> + <fieldset disabled={isReadOnlyMode}> + {children} + </fieldset> + </div> + {hasButtons && <ValidationButtons labledButtons={labledButtons} ref={(buttons) => this.buttons = buttons} isReadOnlyMode={isReadOnlyMode}/>} + </form> + ); + } + + handleFormValidation(event) { + event.preventDefault(); + if (this.props.onValidateForm && !this.props.formReady){ + return this.props.onValidateForm(); + } else { + return this.handleFormSubmit(event); + } + } + handleFormSubmit(event) { + if (event) { + event.preventDefault(); + } + if(this.props.onSubmit) { + return this.props.onSubmit(event); + } + } + + componentDidMount() { + if (this.props.hasButtons) { + this.buttons.setState({isValid: this.props.isValid}); + } + } + + + + componentDidUpdate(prevProps) { + // 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.buttons.setState({isValid: this.props.isValid}); + } + // callback in case form is part of bigger picture in view + if (this.props.onValidChange) { + this.props.onValidChange(this.props.isValid); + } + + // TODO - maybe this has to be part of componentWillUpdate + if(this.props.onValidityChanged) { + this.props.onValidityChanged(this.props.isValid); + } + } + if (this.props.formReady) { // if form validation succeeded -> continue with submit + this.handleFormSubmit(); + } + } + +} + + +export default Form; diff --git a/openecomp-ui/src/nfvo-components/input/validation/Input.jsx b/openecomp-ui/src/nfvo-components/input/validation/Input.jsx new file mode 100644 index 0000000000..59c35d7993 --- /dev/null +++ b/openecomp-ui/src/nfvo-components/input/validation/Input.jsx @@ -0,0 +1,180 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +import React from 'react'; +import ReactDOM from 'react-dom'; +import classNames from 'classnames'; +import Checkbox from 'react-bootstrap/lib/Checkbox.js'; +import Radio from 'react-bootstrap/lib/Radio.js'; +import FormGroup from 'react-bootstrap/lib/FormGroup.js'; +import FormControl from 'react-bootstrap/lib/FormControl.js'; +import Overlay from 'react-bootstrap/lib/Overlay.js'; +import Tooltip from 'react-bootstrap/lib/Tooltip.js'; + +class Input extends React.Component { + + state = { + value: this.props.value, + checked: this.props.checked, + selectedValues: [] + } + + render() { + const {label, isReadOnlyMode, value, onBlur, onKeyDown, type, disabled, checked, name} = this.props; + // eslint-disable-next-line no-unused-vars + const {groupClassName, isValid = true, errorText, isRequired, ...inputProps} = this.props; + let wrapperClassName = (type !== 'radio') ? 'validation-input-wrapper' : 'form-group'; + if (disabled) { + wrapperClassName += ' disabled'; + } + return( + <div className={wrapperClassName}> + <FormGroup className={classNames('form-group', [groupClassName], {'required' : isRequired , 'has-error' : !isValid})} > + {(label && (type !== 'checkbox' && type !== 'radio')) && <label className='control-label'>{label}</label>} + {(type === 'text' || type === 'number') && + <FormControl + bsClass={'form-control input-options-other'} + onChange={(e) => this.onChange(e)} + disabled={isReadOnlyMode || Boolean(disabled)} + onBlur={onBlur} + onKeyDown={onKeyDown} + value={value || ''} + inputRef={(input) => this.input = input} + type={type} + data-test-id={this.props['data-test-id']}/>} + + {type === 'textarea' && + <FormControl + className='form-control input-options-other' + disabled={isReadOnlyMode || Boolean(disabled)} + value={value || ''} + onBlur={onBlur} + onKeyDown={onKeyDown} + componentClass={type} + onChange={(e) => this.onChange(e)} + inputRef={(input) => this.input = input} + data-test-id={this.props['data-test-id']}/>} + + {type === 'checkbox' && + <Checkbox + className={classNames({'required' : isRequired , 'has-error' : !isValid})} + onChange={(e)=>this.onChangeCheckBox(e)} + disabled={isReadOnlyMode || Boolean(disabled)} + checked={value} + data-test-id={this.props['data-test-id']}>{label}</Checkbox>} + + {type === 'radio' && + <Radio name={name} + checked={checked} + disabled={isReadOnlyMode || Boolean(disabled)} + value={value} + onChange={(e)=>this.onChangeRadio(e)} + data-test-id={this.props['data-test-id']}>{label}</Radio>} + {type === 'select' && + <FormControl onClick={ (e) => this.optionSelect(e) } + componentClass={type} + inputRef={(input) => this.input = input} + name={name} {...inputProps} + data-test-id={this.props['data-test-id']}/>} + </FormGroup> + { this.renderErrorOverlay() } + </div> + ); + } + + getValue() { + return this.props.type !== 'select' ? this.state.value : this.state.selectedValues; + } + + getChecked() { + return this.state.checked; + } + + optionSelect(e) { + let selectedValues = []; + if (e.target.value) { + selectedValues.push(e.target.value); + } + this.setState({ + selectedValues + }); + } + + onChange(e) { + const {onChange, type} = this.props; + let value = e.target.value; + if (type === 'number') { + value = Number(value); + } + this.setState({ + value + }); + onChange(value); + } + + onChangeCheckBox(e) { + let {onChange} = this.props; + this.setState({ + checked: e.target.checked + }); + onChange(e.target.checked); + } + + onChangeRadio(e) { + let {onChange} = this.props; + this.setState({ + checked: e.target.checked + }); + onChange(this.state.value); + } + + focus() { + ReactDOM.findDOMNode(this.input).focus(); + } + + renderErrorOverlay() { + let position = 'right'; + const {errorText = '', isValid = true, type, overlayPos} = this.props; + + if (overlayPos) { + position = overlayPos; + } + else if (type === 'text' + || type === 'email' + || type === 'number' + || type === 'password') { + position = 'bottom'; + } + + return ( + <Overlay + show={!isValid} + placement={position} + target={() => { + let target = ReactDOM.findDOMNode(this.input); + return target.offsetParent ? target : undefined; + }} + container={this}> + <Tooltip + id={`error-${errorText.replace(' ', '-')}`} + className='validation-error-message'> + {errorText} + </Tooltip> + </Overlay> + ); + } + +} +export default Input; diff --git a/openecomp-ui/src/nfvo-components/input/validation/InputOptions.jsx b/openecomp-ui/src/nfvo-components/input/validation/InputOptions.jsx new file mode 100644 index 0000000000..6e54254eb0 --- /dev/null +++ b/openecomp-ui/src/nfvo-components/input/validation/InputOptions.jsx @@ -0,0 +1,279 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +import React from 'react'; +import ReactDOM from 'react-dom'; +import i18n from 'nfvo-utils/i18n/i18n.js'; +import classNames from 'classnames'; +import Select from 'nfvo-components/input/SelectInput.jsx'; +import Overlay from 'react-bootstrap/lib/Overlay.js'; +import Tooltip from 'react-bootstrap/lib/Tooltip.js'; + +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, + label: React.PropTypes.string, + selectedValue: React.PropTypes.string, + multiSelectedEnum: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.array + ]), + selectedEnum: React.PropTypes.string, + otherValue: React.PropTypes.string, + overlayPos: React.PropTypes.string, + onEnumChange: React.PropTypes.func, + onOtherChange: React.PropTypes.func, + onBlur: React.PropTypes.func, + isRequired: React.PropTypes.bool, + isMultiSelect: React.PropTypes.bool, + isValid: React.PropTypes.bool, + disabled: React.PropTypes.bool + }; + + state = { + otherInputDisabled: !this.props.otherValue + }; + + oldProps = { + selectedEnum: '', + otherValue: '', + multiSelectedEnum: [] + }; + + render() { + let {label, isRequired, values, otherValue, onOtherChange, isMultiSelect, onBlur, multiSelectedEnum, selectedEnum, isValid, children, isReadOnlyMode} = this.props; + const dataTestId = this.props['data-test-id'] ? {'data-test-id': this.props['data-test-id']} : {}; + let currentMultiSelectedEnum = []; + let currentSelectedEnum = ''; + let {otherInputDisabled} = this.state; + if (isMultiSelect) { + currentMultiSelectedEnum = multiSelectedEnum; + if(!otherInputDisabled) { + currentSelectedEnum = multiSelectedEnum ? multiSelectedEnum.toString() : undefined; + } + } + else if(selectedEnum){ + currentSelectedEnum = selectedEnum; + } + if (!onBlur) { + onBlur = () => {}; + } + + return( + <div className='validation-input-wrapper' > + <div className={classNames('form-group', {'required' : isRequired, 'has-error' : !isValid})} > + {label && <label className='control-label'>{label}</label>} + {isMultiSelect && otherInputDisabled ? + <Select + {...dataTestId} + ref={(input) => this.input = input} + 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' : !isValid})} > + <select + {...dataTestId} + ref={(input) => this.input = input} + label={label} + className='form-control input-options-select' + value={currentSelectedEnum} + style={{'width' : otherInputDisabled ? '100%' : '100px'}} + onBlur={() => onBlur()} + disabled={isReadOnlyMode || Boolean(this.props.disabled)} + onChange={ value => this.enumChanged(value)} + type='select'> + {children || (values && values.length && values.map((val, index) => this.renderOptions(val, index)))} + {onOtherChange && <option key='other' value={other.OTHER}>{i18n(other.OTHER)}</option>} + </select> + + {!otherInputDisabled && <div className='input-options-separator'/>} + <input + className='form-control input-options-other' + placeholder={i18n('other')} + ref={(otherValue) => this.otherValue = otherValue} + style={{'display' : otherInputDisabled ? 'none' : 'block'}} + disabled={isReadOnlyMode || Boolean(this.props.disabled)} + value={otherValue || ''} + onBlur={() => onBlur()} + onChange={() => this.changedOtherInput()}/> + </div> + } + </div> + { this.renderErrorOverlay() } + </div> + ); + } + + renderOptions(val, index){ + return ( + <option key={index} 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; + } + + renderErrorOverlay() { + let position = 'right'; + const {errorText = '', isValid = true, type, overlayPos} = this.props; + + if (overlayPos) { + position = overlayPos; + } + else if (type === 'text' + || type === 'email' + || type === 'number' + || type === 'password') { + position = 'bottom'; + } + + return ( + <Overlay + show={!isValid} + placement={position} + target={() => { + let {otherInputDisabled} = this.state; + let target = otherInputDisabled ? ReactDOM.findDOMNode(this.input) : ReactDOM.findDOMNode(this.otherValue); + return target.offsetParent ? target : undefined; + }} + container={this}> + <Tooltip + id={`error-${errorText.replace(' ', '-')}`} + className='validation-error-message'> + {errorText} + </Tooltip> + </Overlay> + ); + } + + getValue() { + let res = ''; + let {isMultiSelect} = this.props; + let {otherInputDisabled} = this.state; + + if (otherInputDisabled) { + res = isMultiSelect ? this.input.getValue() : this.input.value; + } else { + res = this.otherValue.value; + } + return res; + } + + enumChanged() { + let enumValue = this.input.value; + let {onEnumChange, onOtherChange, isMultiSelect, onChange} = this.props; + this.setState({ + otherInputDisabled: !Boolean(onOtherChange) || enumValue !== other.OTHER + }); + + let value = isMultiSelect ? [enumValue] : enumValue; + if (onEnumChange) { + onEnumChange(value); + } + if (onChange) { + onChange(value); + } + } + + multiSelectEnumChanged(enumValue) { + let {onEnumChange, onOtherChange} = 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: !Boolean(onOtherChange) || !selectedValues.includes(i18n(other.OTHER)) + }); + onEnumChange(selectedValues); + } + + changedOtherInput() { + let {onOtherChange} = this.props; + onOtherChange(this.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/InputWrapper.jsx b/openecomp-ui/src/nfvo-components/input/validation/InputWrapper.jsx new file mode 100644 index 0000000000..5ca716cc20 --- /dev/null +++ b/openecomp-ui/src/nfvo-components/input/validation/InputWrapper.jsx @@ -0,0 +1,134 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +import React from 'react'; +import ReactDOM from 'react-dom'; +import classNames from 'classnames'; +import Checkbox from 'react-bootstrap/lib/Checkbox.js'; +import Radio from 'react-bootstrap/lib/Radio.js'; +import FormGroup from 'react-bootstrap/lib/FormGroup.js'; +import FormControl from 'react-bootstrap/lib/FormControl.js'; + +class InputWrapper extends React.Component { + + state = { + value: this.props.value, + checked: this.props.checked, + selectedValues: [] + } + + render() { + const {label, hasError, validations = {}, isReadOnlyMode, value, onBlur, onKeyDown, type, disabled, checked, name} = this.props; + const {groupClassName, ...inputProps} = this.props; + return( + <FormGroup className={classNames('form-group', [groupClassName], {'required' : validations.required , 'has-error' : hasError})} > + {(label && (type !== 'checkbox' && type !== 'radio')) && <label className='control-label'>{label}</label>} + {(type === 'text' || type === 'number') && + <FormControl + bsClass={'form-control input-options-other'} + onChange={(e) => this.onChange(e)} + disabled={isReadOnlyMode || Boolean(disabled)} + onBlur={onBlur} + onKeyDown={onKeyDown} + value={value || ''} + ref={(input) => this.inputWrapper = input} + type={type} + data-test-id={this.props['data-test-id']}/>} + + {type === 'textarea' && + <FormControl + className='form-control input-options-other' + disabled={isReadOnlyMode || Boolean(disabled)} + value={value || ''} + onBlur={onBlur} + onKeyDown={onKeyDown} + componentClass={type} + onChange={(e) => this.onChange(e)} + data-test-id={this.props['data-test-id']}/>} + + {type === 'checkbox' && + <Checkbox + className={classNames({'required' : validations.required , 'has-error' : hasError})} + onChange={(e)=>this.onChangeCheckBox(e)} + disabled={isReadOnlyMode || Boolean(disabled)} + checked={value} + data-test-id={this.props['data-test-id']}>{label}</Checkbox>} + + {type === 'radio' && + <Radio name={name} + checked={checked} + disabled={isReadOnlyMode || Boolean(disabled)} + value={value} + onChange={(e)=>this.onChangeRadio(e)} + data-test-id={this.props['data-test-id']}>{label}</Radio>} + {type === 'select' && + <FormControl onClick={ (e) => this.optionSelect(e) } + componentClass={type} + name={name} {...inputProps} + data-test-id={this.props['data-test-id']}/>} + + </FormGroup> + + ); + } + + getValue() { + return this.props.type !== 'select' ? this.state.value : this.state.selectedValues; + } + + getChecked() { + return this.state.checked; + } + + optionSelect(e) { + let selectedValues = []; + if (e.target.value) { + selectedValues.push(e.target.value); + } + this.setState({ + selectedValues + }); + } + + onChange(e) { + let {onChange} = this.props; + this.setState({ + value: e.target.value + }); + onChange(e.target.value); + } + + onChangeCheckBox(e) { + let {onChange} = this.props; + this.setState({ + checked: e.target.checked + }); + onChange(e.target.checked); + } + + onChangeRadio(e) { + let {onChange} = this.props; + this.setState({ + checked: e.target.checked + }); + onChange(this.state.value); + } + + focus() { + ReactDOM.findDOMNode(this.inputWrapper).focus(); + } + +} +export default InputWrapper; diff --git a/openecomp-ui/src/nfvo-components/input/validation/Tabs.jsx b/openecomp-ui/src/nfvo-components/input/validation/Tabs.jsx new file mode 100644 index 0000000000..95144b1468 --- /dev/null +++ b/openecomp-ui/src/nfvo-components/input/validation/Tabs.jsx @@ -0,0 +1,79 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +import React from 'react'; +import ReactDOM from 'react-dom'; +import {default as BTabs} 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 Tabs extends React.Component { + + static propTypes = { + children: React.PropTypes.node + }; + + cloneTab(element) { + const {invalidTabs} = this.props; + return React.cloneElement( + element, + { + key: element.props.eventKey, + tabClassName: invalidTabs.indexOf(element.props.eventKey) > -1 ? 'invalid-tab' : 'valid-tab' + } + ); + } + + showTabsError() { + const {invalidTabs} = this.props; + const showError = ((invalidTabs.length === 1 && invalidTabs[0] !== this.props.activeKey) || (invalidTabs.length > 1)); + return showError; + } + + render() { + // eslint-disable-next-line no-unused-vars + let {invalidTabs, ...tabProps} = this.props; + return ( + <div> + <BTabs {...tabProps} ref='tabsList' id='tabsList' > + {this.props.children.map(element => this.cloneTab(element))} + </BTabs> + <Overlay + animation={false} + show={this.showTabsError()} + placement='bottom' + containerPadding={50} + 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={() => { + let target = ReactDOM.findDOMNode(this.refs.tabsList).querySelector('ul > li.invalid-tab:not(.active):nth-of-type(n)'); + return target && target.offsetParent ? target.offsetParent : this; + }}> + <Tooltip + id='error-some-tabs-contain-errors' + className='validation-error-message'> + {i18n('One or more tabs are invalid')} + </Tooltip> + </Overlay> + </div> + ); + } +} diff --git a/openecomp-ui/src/nfvo-components/input/validation/ValidationButtons.jsx b/openecomp-ui/src/nfvo-components/input/validation/ValidationButtons.jsx index a87c8d6f40..ebb1473c04 100644 --- a/openecomp-ui/src/nfvo-components/input/validation/ValidationButtons.jsx +++ b/openecomp-ui/src/nfvo-components/input/validation/ValidationButtons.jsx @@ -1,3 +1,18 @@ +/*! + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ /** * Holds the buttons for save/reset for forms. * Used by the ValidationForm that changes the state of the buttons according to its own state. @@ -8,7 +23,7 @@ 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'; +import SVGIcon from 'nfvo-components/icon/SVGIcon.jsx'; class ValidationButtons extends React.Component { @@ -22,8 +37,8 @@ class ValidationButtons extends React.Component { }; 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'/>; + var submitBtn = this.props.labledButtons ? i18n('Save') : <SVGIcon className='check' name='check'/>; + var closeBtn = this.props.labledButtons ? i18n('Cancel') : <SVGIcon className='close' name='close'/>; return ( <div className='validation-buttons'> {!this.props.isReadOnlyMode ? diff --git a/openecomp-ui/src/nfvo-components/input/validation/ValidationForm.jsx b/openecomp-ui/src/nfvo-components/input/validation/ValidationForm.jsx deleted file mode 100644 index 098ccf1fd4..0000000000 --- a/openecomp-ui/src/nfvo-components/input/validation/ValidationForm.jsx +++ /dev/null @@ -1,200 +0,0 @@ -/** - * 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 deleted file mode 100644 index 0f14307645..0000000000 --- a/openecomp-ui/src/nfvo-components/input/validation/ValidationInput.jsx +++ /dev/null @@ -1,509 +0,0 @@ -/** - * 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 deleted file mode 100644 index 6036518288..0000000000 --- a/openecomp-ui/src/nfvo-components/input/validation/ValidationTab.jsx +++ /dev/null @@ -1,107 +0,0 @@ -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 deleted file mode 100644 index 6eda4b9827..0000000000 --- a/openecomp-ui/src/nfvo-components/input/validation/ValidationTabs.jsx +++ /dev/null @@ -1,72 +0,0 @@ -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> - ); - } -} |