From 2bd67deeea3f6b2d74f496a3129bd1c35729949b Mon Sep 17 00:00:00 2001 From: Patrick Kang <1489148+patrickkang@users.noreply.github.com> Date: Wed, 18 Sep 2019 09:35:32 -0400 Subject: [PATCH] Adds support for 'radio' type in interactive dialogs (#3212) --- .../__snapshots__/radio_setting.test.js.snap | 189 +++++++++++++++++ .../widgets/settings/radio_setting.js | 191 ++++++++++++++++++ .../widgets/settings/radio_setting.test.js | 69 +++++++ .../widgets/settings/text_setting.js | 5 +- .../interactive_dialog/dialog_element.js | 14 ++ .../interactive_dialog/dialog_element.test.js | 23 ++- package-lock.json | 32 +-- package.json | 2 +- 8 files changed, 507 insertions(+), 18 deletions(-) create mode 100644 app/components/widgets/settings/__snapshots__/radio_setting.test.js.snap create mode 100644 app/components/widgets/settings/radio_setting.js create mode 100644 app/components/widgets/settings/radio_setting.test.js diff --git a/app/components/widgets/settings/__snapshots__/radio_setting.test.js.snap b/app/components/widgets/settings/__snapshots__/radio_setting.test.js.snap new file mode 100644 index 000000000..b9053a11d --- /dev/null +++ b/app/components/widgets/settings/__snapshots__/radio_setting.test.js.snap @@ -0,0 +1,189 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/widgets/settings/RadioSetting should match snapshot when error is present 1`] = ` + + + + some label + + + * + + + + + + + + this is engineering + + + + + + + + + + this is sales + + + + + + + + + + this is administration + + + + + + + + + some error message + + + +`; diff --git a/app/components/widgets/settings/radio_setting.js b/app/components/widgets/settings/radio_setting.js new file mode 100644 index 000000000..c312d3d79 --- /dev/null +++ b/app/components/widgets/settings/radio_setting.js @@ -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 ( + + ); + } + return null; + } + + renderRowSeparator = (idx, separatorStyle) => { + const {options} = this.props; + if (idx === options.length - 1) { + return null; + } + return ; + } + + render() { + const { + theme, + label, + helpText, + errorText, + isLandscape, + } = this.props; + const style = getStyleSheet(theme); + + let helpTextContent; + if (helpText) { + helpTextContent = ( + + {helpText} + + ); + } + let errorTextContent; + if (errorText) { + errorTextContent = ( + + {errorText} + + ); + } + + let additionalTextContent; + if (errorText || helpText) { + additionalTextContent = ( + + {helpTextContent} + {errorTextContent} + + ); + } + + const options = []; + for (const [i, {value, text}] of this.props.options.entries()) { + options.push( + this.handleChange(value)} + key={value} + > + + + {text} + + {this.renderCheckMark(value, style.checkMark)} + + {this.renderRowSeparator(i, style.separator)} + + ); + } + return ( + + + {label} + {' *'} + + + + {options} + + {additionalTextContent} + + ); + } +} + +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, + }, + }; +}); diff --git a/app/components/widgets/settings/radio_setting.test.js b/app/components/widgets/settings/radio_setting.test.js new file mode 100644 index 000000000..fc7aa3f36 --- /dev/null +++ b/app/components/widgets/settings/radio_setting.test.js @@ -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( + + ); + 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( + + ); + + 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( + + ); + + expect(wrapper.find(CheckMark)).toHaveLength(0); + }); +}); diff --git a/app/components/widgets/settings/text_setting.js b/app/components/widgets/settings/text_setting.js index 5c0567917..1775e4ede 100644 --- a/app/components/widgets/settings/text_setting.js +++ b/app/components/widgets/settings/text_setting.js @@ -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 = {' *'}; } diff --git a/app/screens/interactive_dialog/dialog_element.js b/app/screens/interactive_dialog/dialog_element.js index 89b5f734e..85f90c4d6 100644 --- a/app/screens/interactive_dialog/dialog_element.js +++ b/app/screens/interactive_dialog/dialog_element.js @@ -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 ( + + ); } else if (type === 'bool') { return ( { @@ -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( + + ); + expect(wrapper.find(RadioSetting).find({options: radioOptions, default: radioOptions[1].value}).exists()).toBe(true); + }); + }); }); diff --git a/package-lock.json b/package-lock.json index 0a17b5acb..220211ee0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" diff --git a/package.json b/package.json index f44c12ed1..04754fb5e 100644 --- a/package.json +++ b/package.json @@ -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",