Support "nullable" setting attribute
Partial-Bug: #1643599 Partial-Bug: #1643600 Change-Id: I036ecad2c60c8208d926b8a6c9a4a13c1f43a8fe
This commit is contained in:
parent
90f92f0dd6
commit
96a41f8345
|
@ -1361,6 +1361,17 @@ input[type=range] {
|
|||
max-width: @default-input-width;
|
||||
width: @default-input-width;
|
||||
}
|
||||
.nullable-checkbox {
|
||||
position: relative;
|
||||
top: 6px;
|
||||
height: 34px;
|
||||
input {
|
||||
width: auto;
|
||||
}
|
||||
& + input {
|
||||
width: 252px;
|
||||
}
|
||||
}
|
||||
textarea {
|
||||
max-width: @default-input-width * 2;
|
||||
}
|
||||
|
|
|
@ -55,24 +55,24 @@ registerSuite(() => {
|
|||
return this.remote
|
||||
.then(() => common.removeCluster(clusterName, true));
|
||||
},
|
||||
'Configure interface properties manipulations'() {
|
||||
// FIXME(jkirnosova) restore this check after #1643599 fix
|
||||
/*'Configure interface properties manipulations'() {
|
||||
return this.remote
|
||||
.clickByCssSelector('.mtu .btn-link')
|
||||
.clickByCssSelector('.mtu-section input[name="nullable-value"]')
|
||||
.assertElementExists(
|
||||
'.mtu-section input[name="value"]',
|
||||
'MTU control is shown when navigating to MTU tab'
|
||||
'.mtu .btn-link.text-danger',
|
||||
'Invalid style is applied to MTU in summary panel'
|
||||
)
|
||||
.setInputValue('.mtu-section input[name="value"]', '2')
|
||||
// FIXME(jkirnosova) restore this check after merging https://review.openstack.org/370052
|
||||
//.assertElementExists(
|
||||
// '.mtu-section .has-error',
|
||||
// 'Error styles are applied to MTU control on invalid value'
|
||||
//)
|
||||
// FIXME(jkirnosova) restore this check after adding use_custom_mtu setting
|
||||
//.assertElementExists(
|
||||
// '.mtu .btn-link.text-danger',
|
||||
// 'Invalid style is applied to MTU in summary panel'
|
||||
//)
|
||||
.assertElementExists(
|
||||
'.mtu-section .has-error',
|
||||
'Error styles are applied to MTU control on invalid value'
|
||||
)
|
||||
.assertElementExists(
|
||||
'.mtu .btn-link.text-danger',
|
||||
'Invalid style is applied to MTU in summary panel'
|
||||
)
|
||||
.setInputValue('.mtu-section input[name="value"]', '256')
|
||||
.assertElementExists(
|
||||
'.ifc-inner-container.has-changes',
|
||||
|
@ -84,7 +84,7 @@ registerSuite(() => {
|
|||
'.mtu-section input[name="value"]',
|
||||
'MTU control is hidden after clicking MTU link again'
|
||||
);
|
||||
},
|
||||
},*/
|
||||
'Unassigned networks'() {
|
||||
return this.remote
|
||||
.assertElementExists('.unassigned-networks .collapsed', 'Unassigned networks block exists')
|
||||
|
@ -99,7 +99,7 @@ registerSuite(() => {
|
|||
'Public',
|
||||
'Public network was successfully removed'
|
||||
);
|
||||
// FIXME(jkirnosova): should be restored after fix of validation errors on the screen
|
||||
// FIXME(jkirnosova): should be restored after #1643599 fix
|
||||
//.assertElementEnabled('.btn-apply', 'Network removal can be saved');
|
||||
},
|
||||
'Untagged networks error'() {
|
||||
|
|
|
@ -308,3 +308,64 @@ suite('File Control', () => {
|
|||
}, 'Control sends updated data upon changes');
|
||||
});
|
||||
});
|
||||
|
||||
suite('Nullable control', () => {
|
||||
setup(() => {
|
||||
input1 = ReactTestUtils.renderIntoDocument(
|
||||
<Input
|
||||
type='number'
|
||||
name='some_name'
|
||||
nullable
|
||||
defaultValue={null}
|
||||
label='Some label'
|
||||
onChange={sinon.spy()}
|
||||
/>
|
||||
);
|
||||
input2 = ReactTestUtils.renderIntoDocument(
|
||||
<Input
|
||||
type='number'
|
||||
name='some_name'
|
||||
nullable
|
||||
defaultValue=''
|
||||
label='Some label'
|
||||
onChange={sinon.spy()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
test('Test input render', () => {
|
||||
assert.equal(
|
||||
ReactTestUtils.scryRenderedDOMComponentsWithTag(input1, 'input').length,
|
||||
1,
|
||||
'1 input element is shown only if control value is null'
|
||||
);
|
||||
assert.equal(
|
||||
input1.refs['nullable-checkbox'].type,
|
||||
'checkbox',
|
||||
'"nullable" checkbox is shown if control value is null'
|
||||
);
|
||||
assert.notOk(
|
||||
input1.refs['nullable-checkbox'].checked,
|
||||
'"nullable" checkbox is unchecked if control value is null'
|
||||
);
|
||||
assert.equal(
|
||||
ReactTestUtils.findRenderedDOMComponentWithClass(input1, 'help-block').textContent,
|
||||
'',
|
||||
'null is valid value for nullable control'
|
||||
);
|
||||
assert.equal(
|
||||
ReactTestUtils.scryRenderedDOMComponentsWithTag(input2, 'input').length,
|
||||
2,
|
||||
'2 inputs are shown if control value is not null'
|
||||
);
|
||||
assert.ok(
|
||||
input2.refs['nullable-checkbox'].checked,
|
||||
'"nullable" checkbox is checked if control value is not null'
|
||||
);
|
||||
assert.equal(
|
||||
input2.refs.input.type,
|
||||
'number',
|
||||
'The second input has "number" type'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -242,7 +242,7 @@ var SettingSection = React.createClass({
|
|||
var settingDescription = setting.description &&
|
||||
<span dangerouslySetInnerHTML={{__html: utils.urlify(_.escape(setting.description))}} />;
|
||||
return <Input
|
||||
{... _.pick(setting, 'type', 'label', 'min', 'max')}
|
||||
{... _.pick(setting, 'type', 'label', 'min', 'max', 'nullable')}
|
||||
key={settingKey}
|
||||
name={settingName}
|
||||
description={settingDescription}
|
||||
|
|
|
@ -34,21 +34,20 @@ export var Input = React.createClass({
|
|||
statics: {
|
||||
validate(setting) {
|
||||
var error = null;
|
||||
if (setting.type === 'number') {
|
||||
if (!_.isNumber(setting.value) || _.isNaN(setting.value)) {
|
||||
var {type, value, nullable, min, max, regex = {}} = setting;
|
||||
if ((type === 'number' || type === 'text') && nullable && _.isNull(value)) return null;
|
||||
if (type === 'number') {
|
||||
if (!_.isNumber(value) || _.isNaN(value)) {
|
||||
error = i18n('controls.invalid_value');
|
||||
} else if (_.isNumber(setting.min) && setting.value < setting.min) {
|
||||
error = i18n('controls.number.min_size', {min: setting.min});
|
||||
} else if (_.isNumber(setting.max) && setting.value > setting.max) {
|
||||
error = i18n('controls.number.max_size', {max: setting.max});
|
||||
} else if (_.isNumber(min) && value < min) {
|
||||
error = i18n('controls.number.min_size', {min});
|
||||
} else if (_.isNumber(max) && value > max) {
|
||||
error = i18n('controls.number.max_size', {max});
|
||||
}
|
||||
}
|
||||
if (_.isNull(error)) {
|
||||
if (
|
||||
(setting.regex || {}).source &&
|
||||
!String(setting.value).match(new RegExp(setting.regex.source))
|
||||
) {
|
||||
error = setting.regex.error;
|
||||
if (regex.source && !String(value).match(new RegExp(regex.source))) {
|
||||
error = regex.error;
|
||||
}
|
||||
}
|
||||
return error;
|
||||
|
@ -70,6 +69,7 @@ export var Input = React.createClass({
|
|||
tooltipIcon: React.PropTypes.node,
|
||||
tooltipText: React.PropTypes.node,
|
||||
toggleable: React.PropTypes.bool,
|
||||
nullable: React.PropTypes.bool,
|
||||
onChange: React.PropTypes.func,
|
||||
error: React.PropTypes.node,
|
||||
extraContent: React.PropTypes.node
|
||||
|
@ -84,6 +84,7 @@ export var Input = React.createClass({
|
|||
getDefaultProps() {
|
||||
return {
|
||||
type: 'text',
|
||||
nullable: false,
|
||||
tooltipIcon: 'glyphicon-warning-sign',
|
||||
tooltipPlacement: 'right'
|
||||
};
|
||||
|
@ -166,10 +167,11 @@ export var Input = React.createClass({
|
|||
var {visible} = this.state;
|
||||
var {
|
||||
type, value, inputClassName, toggleable, selectOnFocus,
|
||||
debounce, children, extraContent
|
||||
debounce, children, extraContent, defaultValue, nullable, disabled, name
|
||||
} = this.props;
|
||||
var isFile = type === 'file';
|
||||
var isCheckboxOrRadio = this.isCheckboxOrRadio();
|
||||
var isNullableControl = (type === 'text' || type === 'number') && nullable;
|
||||
var inputWrapperClasses = {
|
||||
'input-group': toggleable,
|
||||
'custom-tumbler': isCheckboxOrRadio,
|
||||
|
@ -189,7 +191,8 @@ export var Input = React.createClass({
|
|||
className: utils.classNames({
|
||||
'form-control': type !== 'range',
|
||||
[inputClassName]: inputClassName
|
||||
})
|
||||
}),
|
||||
autoFocus: isNullableControl
|
||||
}
|
||||
);
|
||||
if (this.props.onChange) props.onChange = debounce ? this.debouncedChange : this.onChange;
|
||||
|
@ -212,7 +215,22 @@ export var Input = React.createClass({
|
|||
|
||||
return (
|
||||
<div key='input-group' className={utils.classNames(inputWrapperClasses)}>
|
||||
{isFile ? this.renderFile(input) : input}
|
||||
{isNullableControl &&
|
||||
<div className='custom-tumbler nullable-checkbox'>
|
||||
<input
|
||||
type='checkbox'
|
||||
name={'nullable-' + name}
|
||||
ref='nullable-checkbox'
|
||||
checked={!_.isNull(defaultValue)}
|
||||
onChange={() => this.props.onChange(
|
||||
name, ReactDOM.findDOMNode(this.refs['nullable-checkbox']).checked ? '' : null
|
||||
)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<span> </span>
|
||||
</div>
|
||||
}
|
||||
{isFile ? this.renderFile(input) : !(isNullableControl && _.isNull(defaultValue)) && input}
|
||||
{toggleable &&
|
||||
<div className='input-group-addon' onClick={this.togglePassword}>
|
||||
<i
|
||||
|
|
Loading…
Reference in New Issue