Adds support for 'radio' type in interactive dialogs (#3212)

This commit is contained in:
Patrick Kang 2019-09-18 09:35:32 -04:00 committed by Ben Schumacher
parent e9db272a9e
commit 2bd67deeea
8 changed files with 507 additions and 18 deletions

View file

@ -0,0 +1,189 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/widgets/settings/RadioSetting should match snapshot when error is present 1`] = `
<View>
<View
style={
Object {
"flexDirection": "row",
"marginBottom": 10,
"marginTop": 15,
}
}
>
<Text
style={
Object {
"color": "#3d3c40",
"fontSize": 14,
"marginLeft": 15,
}
}
>
some label
</Text>
<Text
style={
Object {
"color": "#fd5960",
"fontSize": 14,
}
}
>
*
</Text>
</View>
<View
style={
Array [
Object {
"backgroundColor": "#ffffff",
"borderBottomColor": "rgba(61,60,64,0.1)",
"borderBottomWidth": 1,
"borderTopColor": "rgba(61,60,64,0.1)",
"borderTopWidth": 1,
},
null,
]
}
>
<TouchableOpacity
activeOpacity={0.2}
onPress={[Function]}
>
<View
style={
Array [
Object {
"alignItems": "center",
"flexDirection": "row",
"paddingHorizontal": 15,
},
null,
]
}
>
<View
style={
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"height": 45,
}
}
>
<Text>
this is engineering
</Text>
</View>
</View>
<View
style={
Object {
"backgroundColor": "rgba(61,60,64,0.1)",
"flex": 1,
"height": 1,
"marginLeft": 15,
}
}
/>
</TouchableOpacity>
<TouchableOpacity
activeOpacity={0.2}
onPress={[Function]}
>
<View
style={
Array [
Object {
"alignItems": "center",
"flexDirection": "row",
"paddingHorizontal": 15,
},
null,
]
}
>
<View
style={
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"height": 45,
}
}
>
<Text>
this is sales
</Text>
</View>
</View>
<View
style={
Object {
"backgroundColor": "rgba(61,60,64,0.1)",
"flex": 1,
"height": 1,
"marginLeft": 15,
}
}
/>
</TouchableOpacity>
<TouchableOpacity
activeOpacity={0.2}
onPress={[Function]}
>
<View
style={
Array [
Object {
"alignItems": "center",
"flexDirection": "row",
"paddingHorizontal": 15,
},
null,
]
}
>
<View
style={
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"height": 45,
}
}
>
<Text>
this is administration
</Text>
</View>
<CheckMark
color="#2389d7"
height={12}
width={12}
/>
</View>
</TouchableOpacity>
</View>
<View
style={null}
>
<Text
style={
Object {
"color": "#fd5960",
"fontSize": 12,
"marginHorizontal": 15,
"marginTop": 10,
}
}
>
some error message
</Text>
</View>
</View>
`;

View file

@ -0,0 +1,191 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Text, TouchableOpacity, View} from 'react-native';
import CheckMark from 'app/components/checkmark';
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
export default class RadioSetting extends PureComponent {
static propTypes = {
id: PropTypes.string.isRequired,
label: PropTypes.node.isRequired,
options: PropTypes.array.isRequired,
default: PropTypes.string,
onChange: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
isLandscape: PropTypes.bool.isRequired,
helpText: PropTypes.node,
errorText: PropTypes.node,
};
static defaultProps = {
isLandscape: false,
};
constructor(props) {
super(props);
this.state = {
value: typeof props.default === 'undefined' ? props.options[0] : props.default,
};
}
handleChange = (item) => {
const {onChange, id} = this.props;
onChange(id, item);
this.setState({value: item});
}
renderCheckMark = (value, {width, height, color}) => {
if (value === this.state.value) {
return (
<CheckMark
width={width}
height={height}
color={color}
/>
);
}
return null;
}
renderRowSeparator = (idx, separatorStyle) => {
const {options} = this.props;
if (idx === options.length - 1) {
return null;
}
return <View style={separatorStyle}/>;
}
render() {
const {
theme,
label,
helpText,
errorText,
isLandscape,
} = this.props;
const style = getStyleSheet(theme);
let helpTextContent;
if (helpText) {
helpTextContent = (
<Text style={style.helpText}>
{helpText}
</Text>
);
}
let errorTextContent;
if (errorText) {
errorTextContent = (
<Text style={style.errorText}>
{errorText}
</Text>
);
}
let additionalTextContent;
if (errorText || helpText) {
additionalTextContent = (
<View style={padding(isLandscape)}>
{helpTextContent}
{errorTextContent}
</View>
);
}
const options = [];
for (const [i, {value, text}] of this.props.options.entries()) {
options.push(
<TouchableOpacity
onPress={() => this.handleChange(value)}
key={value}
>
<View style={[style.container, padding(isLandscape)]}>
<View style={style.rowContainer}>
<Text>{text}</Text>
</View>
{this.renderCheckMark(value, style.checkMark)}
</View>
{this.renderRowSeparator(i, style.separator)}
</TouchableOpacity>
);
}
return (
<View>
<View style={style.titleContainer}>
<Text style={style.title}>{label}</Text>
<Text style={style.asterisk}>{' *'}</Text>
</View>
<View style={[style.items, padding(isLandscape)]}>
{options}
</View>
{additionalTextContent}
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 15,
},
rowContainer: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
height: 45,
},
items: {
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderBottomWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1),
},
helpText: {
fontSize: 12,
color: changeOpacity(theme.centerChannelColor, 0.5),
marginHorizontal: 15,
marginVertical: 10,
},
errorText: {
fontSize: 12,
color: theme.errorTextColor,
marginHorizontal: 15,
marginTop: 10,
},
asterisk: {
color: theme.errorTextColor,
fontSize: 14,
},
title: {
fontSize: 14,
color: theme.centerChannelColor,
marginLeft: 15,
},
titleContainer: {
flexDirection: 'row',
marginTop: 15,
marginBottom: 10,
},
separator: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
flex: 1,
height: 1,
marginLeft: 15,
},
checkMark: {
width: 12,
height: 12,
color: theme.linkColor,
},
};
});

View file

@ -0,0 +1,69 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import Preferences from 'mattermost-redux/constants/preferences';
import {TouchableOpacity} from 'react-native';
import RadioSetting from './radio_setting.js';
import CheckMark from 'app/components/checkmark';
describe('components/widgets/settings/RadioSetting', () => {
const theme = Preferences.THEMES.default;
const options = [
{text: 'this is engineering', value: 'Engineering'},
{text: 'this is sales', value: 'Sales'},
{text: 'this is administration', value: 'Administration'},
];
test('onChange', () => {
const onChange = jest.fn();
const wrapper = shallow(
<RadioSetting
id='string.id'
label='some label'
options={options}
default={'Administration'}
onChange={onChange}
theme={theme}
/>
);
wrapper.find(TouchableOpacity).at(1).props().onPress();
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith('string.id', 'Sales');
});
test('should match snapshot when error is present', () => {
const onChange = jest.fn();
const wrapper = shallow(
<RadioSetting
id='string.id'
label='some label'
options={options}
errorText={'some error message'}
default={'Administration'}
onChange={onChange}
theme={theme}
/>
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('no option should be checked when the value is not in provided options', () => {
const onChange = jest.fn();
const wrapper = shallow(
<RadioSetting
id='string.id'
label='some label'
options={options}
default={'invalid-option-value'}
onChange={onChange}
theme={theme}
/>
);
expect(wrapper.find(CheckMark)).toHaveLength(0);
});
});

View file

@ -39,7 +39,6 @@ export default class TextSetting extends PureComponent {
onChange: PropTypes.func.isRequired,
value: PropTypes.string.isRequired,
multiline: PropTypes.bool,
showRequiredAsterisk: PropTypes.bool,
isLandscape: PropTypes.bool.isRequired,
keyboardType: PropTypes.oneOf([
'default',
@ -57,7 +56,6 @@ export default class TextSetting extends PureComponent {
optional: false,
disabled: false,
multiline: false,
showRequiredAsterisk: false,
keyboardType: 'default',
isLandscape: false,
secureTextEntry: false,
@ -80,7 +78,6 @@ export default class TextSetting extends PureComponent {
errorText,
value,
multiline,
showRequiredAsterisk,
isLandscape,
secureTextEntry,
} = this.props;
@ -109,7 +106,7 @@ export default class TextSetting extends PureComponent {
defaultMessage='(optional)'
/>
);
} else if (showRequiredAsterisk) {
} else {
asterisk = <Text style={style.asterisk}>{' *'}</Text>;
}

View file

@ -7,6 +7,7 @@ import PropTypes from 'prop-types';
import BoolSetting from 'app/components/widgets/settings/bool_setting';
import TextSetting from 'app/components/widgets/settings/text_setting';
import AutocompleteSelector from 'app/components/autocomplete_selector';
import RadioSetting from 'app/components/widgets/settings/radio_setting';
const TEXT_DEFAULT_MAX_LENGTH = 150;
const TEXTAREA_DEFAULT_MAX_LENGTH = 3000;
@ -136,6 +137,19 @@ export default class DialogElement extends PureComponent {
roundedBorders={false}
/>
);
} else if (type === 'radio') {
return (
<RadioSetting
id={name}
label={displayName}
helpText={helpText}
errorText={errorText}
options={options}
theme={theme}
default={value}
onChange={this.onChange}
/>
);
} else if (type === 'bool') {
return (
<BoolSetting

View file

@ -5,7 +5,7 @@ import React from 'react';
import {shallow} from 'enzyme';
import Preferences from 'mattermost-redux/constants/preferences';
import RadioSetting from 'app/components/widgets/settings/radio_setting';
import DialogElement from './dialog_element.js';
describe('DialogElement', () => {
@ -15,6 +15,7 @@ describe('DialogElement', () => {
type: 'text',
isLandscape: false,
};
const theme = Preferences.THEMES.default;
test('secureTextEntry is true and multiline is false when subtype is password', () => {
const wrapper = shallow(
@ -37,4 +38,24 @@ describe('DialogElement', () => {
);
expect(wrapper.find({secureTextEntry: false}).exists()).toBe(true);
});
describe('radioSetting', () => {
const radioOptions = [
{value: 'foo', text: 'foo-text'},
{value: 'bar', text: 'bar-text'},
];
test('The default value can be specified from the list', () => {
const wrapper = shallow(
<DialogElement
{...baseDialogProps}
theme={theme}
type='radio'
options={radioOptions}
value={radioOptions[1].value}
/>
);
expect(wrapper.find(RadioSetting).find({options: radioOptions, default: radioOptions[1].value}).exists()).toBe(true);
});
});
});

32
package-lock.json generated
View file

@ -4490,7 +4490,7 @@
},
"ansi-escapes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz",
"resolved": "http://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz",
"integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw=="
},
"ansi-gray": {
@ -7473,7 +7473,7 @@
},
"external-editor": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz",
"resolved": "http://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz",
"integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==",
"requires": {
"chardet": "^0.4.0",
@ -7673,7 +7673,7 @@
"dependencies": {
"json5": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
"resolved": "http://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
"integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=",
"dev": true
}
@ -14056,8 +14056,8 @@
"integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A=="
},
"mattermost-redux": {
"version": "github:mattermost/mattermost-redux#6ea71dc77808041d7e953ac15cbc12e1c483ac69",
"from": "github:mattermost/mattermost-redux#6ea71dc77808041d7e953ac15cbc12e1c483ac69",
"version": "github:mattermost/mattermost-redux#9800f16a94b46fd77e95132c1c33f18752bf8a98",
"from": "github:mattermost/mattermost-redux#9800f16a94b46fd77e95132c1c33f18752bf8a98",
"requires": {
"deep-equal": "1.0.1",
"eslint-plugin-header": "3.0.0",
@ -14070,7 +14070,7 @@
"redux": "4.0.4",
"redux-action-buffer": "1.2.0",
"redux-batched-actions": "0.4.1",
"redux-offline": "git+https://github.com/enahum/redux-offline.git#4bd85e7e3b279a2b11fb4d587808d583d2b5e7b5",
"redux-offline": "git+https://github.com/enahum/redux-offline.git#885024de96b6ec73650c340c8928066585c413df",
"redux-persist": "4.9.1",
"redux-thunk": "2.3.0",
"reselect": "4.0.0",
@ -17064,7 +17064,7 @@
},
"readable-stream": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"requires": {
"core-util-is": "~1.0.0",
@ -17451,10 +17451,18 @@
}
},
"redux-offline": {
"version": "git+https://github.com/enahum/redux-offline.git#4bd85e7e3b279a2b11fb4d587808d583d2b5e7b5",
"from": "git+https://github.com/enahum/redux-offline.git#4bd85e7e3b279a2b11fb4d587808d583d2b5e7b5",
"version": "git+https://github.com/enahum/redux-offline.git#885024de96b6ec73650c340c8928066585c413df",
"from": "git+https://github.com/enahum/redux-offline.git#885024de96b6ec73650c340c8928066585c413df",
"requires": {
"@react-native-community/netinfo": "^4.1.3",
"redux-persist": "^4.5.0"
},
"dependencies": {
"@react-native-community/netinfo": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-4.2.1.tgz",
"integrity": "sha512-kAnmYp8vXpZToPw8rgE7uO+MqmqHSR9VEDPkuZT0DnFMBJmIXCSD2NLAD28HGKVY/kujVWCknC/FuVWr5/A3uA=="
}
}
},
"redux-persist": {
@ -18263,7 +18271,7 @@
"dependencies": {
"async": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/async/-/async-2.0.0.tgz",
"resolved": "http://registry.npmjs.org/async/-/async-2.0.0.tgz",
"integrity": "sha1-0JAK04WvE4BFQKEJxCFm4657K50=",
"dev": true,
"requires": {
@ -18685,7 +18693,7 @@
},
"inquirer": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz",
"resolved": "http://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz",
"integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==",
"dev": true,
"requires": {
@ -19110,7 +19118,7 @@
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"requires": {
"ansi-regex": "^2.0.0"

View file

@ -21,7 +21,7 @@
"intl": "1.2.5",
"jail-monkey": "2.2.0",
"jsc-android": "241213.2.0",
"mattermost-redux": "github:mattermost/mattermost-redux#6ea71dc77808041d7e953ac15cbc12e1c483ac69",
"mattermost-redux": "github:mattermost/mattermost-redux#9800f16a94b46fd77e95132c1c33f18752bf8a98",
"mime-db": "1.40.0",
"moment-timezone": "0.5.25",
"prop-types": "15.7.2",