MM-12845 Add widgets and interactive dialogs (#2353)

* Add widgets and interactive dialogs

* Update snapshots

* Updates per feedback and fix slash command

* Fix style

* Update styling

* Updates per feedback

* Updates per feedback

* More styling changes

* Remove extra space above message menu
This commit is contained in:
Joram Wilander 2018-11-23 12:30:12 -05:00 committed by GitHub
parent 8097764d87
commit e431bd36c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1226 additions and 385 deletions

View file

@ -1,6 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {IntegrationTypes} from 'mattermost-redux/action_types';
import {executeCommand as executeCommandService} from 'mattermost-redux/actions/integrations';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
@ -27,6 +28,12 @@ export function executeCommand(message, channelId, rootId) {
const cmd = msg.substring(0, cmdLength).toLowerCase();
msg = cmd + msg.substring(cmdLength, msg.length);
return executeCommandService(msg, args)(dispatch, getState);
const {data, error} = await dispatch(executeCommandService(msg, args));
if (data.trigger_id) {
dispatch({type: IntegrationTypes.RECEIVED_DIALOG_TRIGGER_ID, data: data.trigger_id});
}
return {data, error};
};
}

View file

@ -41,7 +41,7 @@ export function sendAddToChannelEphemeralPost(user, addedUsername, message, chan
};
}
export function setMenuActionSelector(dataSource, onSelect, options) {
export function setAutocompleteSelector(dataSource, onSelect, options) {
return {
type: ViewTypes.SELECTED_ACTION_MENU,
data: {
@ -52,14 +52,14 @@ export function setMenuActionSelector(dataSource, onSelect, options) {
};
}
export function selectAttachmentMenuAction(postId, actionId, displayText, value) {
export function selectAttachmentMenuAction(postId, actionId, text, value) {
return (dispatch) => {
dispatch({
type: ViewTypes.SUBMIT_ATTACHMENT_MENU_ACTION,
postId,
data: {
[actionId]: {
displayText,
text,
value,
},
},

View file

@ -0,0 +1,289 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {Text, TouchableOpacity, View} from 'react-native';
import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import Icon from 'react-native-vector-icons/FontAwesome';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
import FormattedText from 'app/components/formatted_text';
import {preventDoubleTap} from 'app/utils/tap';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
import {ViewTypes} from 'app/constants';
export default class AutocompleteSelector extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
setAutocompleteSelector: PropTypes.func.isRequired,
}).isRequired,
label: PropTypes.string,
placeholder: PropTypes.string.isRequired,
dataSource: PropTypes.string,
options: PropTypes.arrayOf(PropTypes.object),
selected: PropTypes.object,
optional: PropTypes.bool,
showRequiredAsterisk: PropTypes.bool,
teammateNameDisplay: PropTypes.string,
theme: PropTypes.object.isRequired,
navigator: PropTypes.object,
onSelected: PropTypes.func,
helpText: PropTypes.node,
errorText: PropTypes.node,
roundedBorders: PropTypes.bool,
};
static contextTypes = {
intl: intlShape,
};
static defaultProps = {
optional: false,
showRequiredAsterisk: false,
roundedBorders: true,
};
constructor(props) {
super(props);
this.state = {
selectedText: null,
};
}
static getDerivedStateFromProps(props, state) {
if (props.selected && props.selected !== state.selected) {
return {
selectedText: props.selected.text,
selected: props.selected,
};
}
return null;
}
handleSelect = (selected) => {
if (!selected) {
return;
}
const {
dataSource,
teammateNameDisplay,
} = this.props;
let selectedText;
let selectedValue;
if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
selectedText = displayUsername(selected, teammateNameDisplay);
selectedValue = selected.id;
} else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
selectedText = selected.display_name;
selectedValue = selected.id;
} else {
selectedText = selected.text;
selectedValue = selected.value;
}
this.setState({selectedText});
if (this.props.onSelected) {
this.props.onSelected({text: selectedText, value: selectedValue});
}
};
goToSelectorScreen = preventDoubleTap(() => {
const {formatMessage} = this.context.intl;
const {navigator, theme, actions, dataSource, options, placeholder} = this.props;
actions.setAutocompleteSelector(dataSource, this.handleSelect, options);
navigator.push({
backButtonTitle: '',
screen: 'SelectorScreen',
title: placeholder || formatMessage({id: 'mobile.action_menu.select', defaultMessage: 'Select an option'}),
animated: true,
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
},
});
});
render() {
const {intl} = this.context;
const {
placeholder,
theme,
label,
helpText,
errorText,
optional,
showRequiredAsterisk,
roundedBorders,
} = this.props;
const {selectedText} = this.state;
const style = getStyleSheet(theme);
let text = placeholder || intl.formatMessage({id: 'mobile.action_menu.select', defaultMessage: 'Select an option'});
let selectedStyle = style.dropdownPlaceholder;
if (selectedText) {
text = selectedText;
selectedStyle = style.dropdownSelected;
}
let inputStyle = style.input;
if (roundedBorders) {
inputStyle = style.roundedInput;
}
let optionalContent;
let asterisk;
if (optional) {
optionalContent = (
<FormattedText
style={style.optional}
id='channel_modal.optional'
defaultMessage='(optional)'
/>
);
} else if (showRequiredAsterisk) {
asterisk = <Text style={style.asterisk}>{' *'}</Text>;
}
let labelContent;
if (label) {
labelContent = (
<View style={style.labelContainer}>
<Text style={style.label}>
{label}
</Text>
{asterisk}
{optionalContent}
</View>
);
}
let helpTextContent;
if (helpText) {
helpTextContent = (
<Text style={style.helpText}>
{helpText}
</Text>
);
}
let errorTextContent;
if (errorText) {
errorTextContent = (
<Text style={style.errorText}>
{errorText}
</Text>
);
}
return (
<View style={style.container}>
{labelContent}
<TouchableOpacity
style={style.flex}
onPress={this.goToSelectorScreen}
>
<View style={inputStyle}>
<Text
style={selectedStyle}
numberOfLines={1}
>
{text}
</Text>
<Icon
name='chevron-down'
color={changeOpacity(theme.centerChannelColor, 0.5)}
style={style.icon}
/>
</View>
</TouchableOpacity>
{helpTextContent}
{errorTextContent}
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
const input = {
borderWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.1),
backgroundColor: changeOpacity(theme.centerChannelBg, 0.9),
paddingLeft: 10,
paddingRight: 30,
paddingVertical: 7,
height: 40,
};
return {
container: {
width: '100%',
marginBottom: 2,
marginRight: 8,
marginTop: 10,
},
roundedInput: {
...input,
borderRadius: 5,
},
input,
dropdownPlaceholder: {
top: 3,
marginLeft: 5,
color: changeOpacity(theme.centerChannelColor, 0.5),
},
dropdownSelected: {
top: 3,
marginLeft: 5,
color: theme.centerChannelColor,
},
icon: {
position: 'absolute',
top: 13,
right: 12,
},
labelContainer: {
flexDirection: 'row',
marginTop: 15,
marginBottom: 10,
},
label: {
fontSize: 14,
color: theme.centerChannelColor,
marginLeft: 15,
},
optional: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 14,
marginLeft: 5,
},
helpText: {
fontSize: 12,
color: changeOpacity(theme.centerChannelColor, 0.5),
marginHorizontal: 15,
marginVertical: 10,
},
errorText: {
fontSize: 12,
color: theme.errorTextColor,
marginHorizontal: 15,
marginVertical: 10,
},
asterisk: {
color: theme.errorTextColor,
fontSize: 14,
},
};
});

View file

@ -0,0 +1,28 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {setAutocompleteSelector} from 'app/actions/views/post';
import AutocompleteSelector from './autocomplete_selector';
function mapStateToProps(state) {
return {
teammateNameDisplay: getTeammateNameDisplaySetting(state),
theme: getTheme(state),
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
setAutocompleteSelector,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(AutocompleteSelector);

View file

@ -0,0 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import InteractiveDialogController from './interactive_dialog_controller';
function mapStateToProps(state) {
return {
triggerId: state.entities.integrations.dialogTriggerId,
dialog: state.entities.integrations.dialog || {},
};
}
export default connect(mapStateToProps)(InteractiveDialogController);

View file

@ -0,0 +1,76 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {PureComponent} from 'react';
import PropTypes from 'prop-types';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
export default class InteractiveDialogController extends PureComponent {
static propTypes = {
triggerId: PropTypes.string,
dialog: PropTypes.object,
navigator: PropTypes.object,
theme: PropTypes.object,
};
constructor(props) {
super(props);
MaterialIcon.getImageSource('close', 20, props.theme.sidebarHeaderTextColor).then((source) => {
this.closeButton = source;
});
}
componentDidUpdate(prevProps) {
const triggerId = this.props.triggerId;
if (!triggerId) {
return;
}
const dialogData = this.props.dialog || {};
const prevDialogData = prevProps.dialog || {};
if (prevProps.triggerId === triggerId && dialogData.trigger_id === prevDialogData.trigger_id) {
return;
}
if (dialogData.trigger_id !== triggerId) {
return;
}
if (!dialogData.trigger_id || !dialogData.dialog) {
return;
}
const theme = this.props.theme;
this.props.navigator.showModal({
backButtonTitle: '',
screen: 'InteractiveDialog',
title: dialogData.dialog.title,
animated: true,
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
},
navigatorButtons: {
leftButtons: [{
id: 'close-dialog',
icon: this.closeButton,
}],
rightButtons: [
{
id: 'submit-dialog',
showAsAction: 'always',
title: dialogData.dialog.submit_label,
},
],
},
});
}
render() {
return null;
}
}

View file

@ -2,23 +2,14 @@
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {Text, TouchableOpacity, View} from 'react-native';
import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import Icon from 'react-native-vector-icons/FontAwesome';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
import FormattedText from 'app/components/formatted_text';
import {preventDoubleTap} from 'app/utils/tap';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
import {ViewTypes} from 'app/constants';
import AutocompleteSelector from 'app/components/autocomplete_selector';
export default class ActionMenu extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
selectAttachmentMenuAction: PropTypes.func.isRequired,
setMenuActionSelector: PropTypes.func.isRequired,
}).isRequired,
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
@ -26,34 +17,9 @@ export default class ActionMenu extends PureComponent {
options: PropTypes.arrayOf(PropTypes.object),
postId: PropTypes.string.isRequired,
selected: PropTypes.object,
teammateNameDisplay: PropTypes.string,
theme: PropTypes.object.isRequired,
navigator: PropTypes.object,
};
static contextTypes = {
intl: intlShape,
};
constructor(props) {
super(props);
this.state = {
selectedText: null,
};
}
static getDerivedStateFromProps(props, state) {
if (props.selected && props.selected !== state.selected) {
return {
selectedText: props.selected.displayText,
selected: props.selected,
};
}
return null;
}
handleSelect = (selected) => {
if (!selected) {
return;
@ -61,157 +27,31 @@ export default class ActionMenu extends PureComponent {
const {
actions,
dataSource,
id,
postId,
teammateNameDisplay,
} = this.props;
let selectedText;
let selectedValue;
if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
selectedText = displayUsername(selected, teammateNameDisplay);
selectedValue = selected.id;
} else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
selectedText = selected.display_name;
selectedValue = selected.id;
} else {
selectedText = selected.text;
selectedValue = selected.value;
}
this.setState({selectedText});
actions.selectAttachmentMenuAction(postId, id, selectedText, selectedValue);
actions.selectAttachmentMenuAction(postId, id, selected.text, selected.value);
};
goToMenuActionSelector = preventDoubleTap(() => {
const {formatMessage} = this.context.intl;
const {navigator, theme, actions, dataSource, options, name} = this.props;
actions.setMenuActionSelector(dataSource, this.handleSelect, options);
navigator.push({
backButtonTitle: '',
screen: 'MenuActionSelector',
title: name || formatMessage({id: 'mobile.action_menu.select', defaultMessage: 'Select an option'}),
animated: true,
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
},
});
});
render() {
const {intl} = this.context;
const {name, theme, id} = this.props;
const {selectedText} = this.state;
const style = getStyleSheet(theme);
let text = name || intl.formatMessage({id: 'mobile.action_menu.select', defaultMessage: 'Select an option'});
let selectedStyle = style.dropdownPlaceholder;
let submitted;
if (selectedText) {
text = selectedText;
selectedStyle = style.dropdownSelected;
submitted = (
<View style={style.submittedContainer}>
<Icon
key={id + 'check'}
name='check'
color={'#287B39'}
/>
<FormattedText
key={id + 'submitted'}
id='mobile.action_menu.submitted'
defaultMessage='Submitted'
style={style.submittedText}
/>
</View>
);
} else {
submitted = <View style={style.blankSubmittedContainer}/>;
}
const {
name,
dataSource,
selected,
options,
navigator,
} = this.props;
return (
<View style={style.container}>
<TouchableOpacity
style={style.flex}
onPress={this.goToMenuActionSelector}
>
<View style={style.input}>
<Text
style={selectedStyle}
numberOfLines={1}
>
{text}
</Text>
<Icon
name='chevron-down'
color={changeOpacity(theme.centerChannelColor, 0.5)}
style={style.icon}
/>
</View>
</TouchableOpacity>
{submitted}
</View>
<AutocompleteSelector
placeholder={name}
dataSource={dataSource}
options={options}
selected={selected}
navigator={navigator}
onSelected={this.handleSelect}
/>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
flex: {
flex: 1,
},
container: {
width: '100%',
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
input: {
flex: 1,
position: 'relative',
borderWidth: 1,
borderRadius: 5,
borderColor: changeOpacity(theme.centerChannelColor, 0.1),
backgroundColor: changeOpacity(theme.centerChannelBg, 0.9),
marginBottom: 2,
marginRight: 8,
marginTop: 10,
paddingLeft: 10,
paddingRight: 30,
paddingVertical: 7,
},
dropdownPlaceholder: {
color: changeOpacity(theme.centerChannelColor, 0.5),
},
dropdownSelected: {
color: theme.centerChannelColor,
},
icon: {
position: 'absolute',
top: 10,
right: 12,
},
blankSubmittedContainer: {
width: 80,
height: 23,
},
submittedContainer: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 10,
marginBottom: 2,
},
submittedText: {
marginLeft: 5,
color: '#287B39',
},
};
});
}

View file

@ -4,9 +4,7 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {setMenuActionSelector, selectAttachmentMenuAction} from 'app/actions/views/post';
import {selectAttachmentMenuAction} from 'app/actions/views/post';
import ActionMenu from './action_menu';
@ -16,8 +14,6 @@ function mapStateToProps(state, ownProps) {
return {
selected,
teammateNameDisplay: getTeammateNameDisplaySetting(state),
theme: getTheme(state),
};
}
@ -25,7 +21,6 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
selectAttachmentMenuAction,
setMenuActionSelector,
}, dispatch),
};
}

View file

@ -2,7 +2,6 @@
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {StyleSheet, View} from 'react-native';
import PropTypes from 'prop-types';
import ActionMenu from './action_menu';
@ -61,16 +60,6 @@ export default class AttachmentActions extends PureComponent {
}
});
return (
<View style={style.container}>
{content}
</View>
);
return content;
}
}
const style = StyleSheet.create({
container: {
flex: 1,
},
});
}

View file

@ -0,0 +1,235 @@
// 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 {
View,
Text,
TextInput,
Platform,
} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class TextSetting extends PureComponent {
static propTypes = {
id: PropTypes.string.isRequired,
label: PropTypes.oneOfType([
PropTypes.shape({
id: PropTypes.string.isRequired,
defaultMessage: PropTypes.string.isRequired,
}),
PropTypes.string,
]),
placeholder: PropTypes.string,
helpText: PropTypes.node,
errorText: PropTypes.node,
disabled: PropTypes.bool,
disabledText: PropTypes.string,
maxLength: PropTypes.number,
optional: PropTypes.bool,
theme: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
value: PropTypes.string.isRequired,
multiline: PropTypes.bool,
showRequiredAsterisk: PropTypes.bool,
keyboardType: PropTypes.oneOf([
'default',
'number-pad',
'decimal-pad',
'numeric',
'email-address',
'phone-pad',
'url',
]),
};
static defaultProps = {
optional: false,
disabled: false,
multiline: false,
showRequiredAsterisk: false,
keyboardType: 'default',
};
onChangeText = (value) => {
const {id, onChange} = this.props;
onChange(id, value);
};
render() {
const {
theme,
label,
placeholder,
helpText,
optional,
disabled,
disabledText,
errorText,
value,
multiline,
showRequiredAsterisk,
} = this.props;
const style = getStyleSheet(theme);
let labelContent = label;
if (label && label.defaultMessage) {
labelContent = (
<FormattedText
style={style.title}
id={label.id}
defaultMessage={label.defaultMessage}
/>
);
} else if (typeof label === 'string') {
labelContent = <Text style={style.title}>{label}</Text>;
}
let optionalContent;
let asterisk;
if (optional) {
optionalContent = (
<FormattedText
style={style.optional}
id='channel_modal.optional'
defaultMessage='(optional)'
/>
);
} else if (showRequiredAsterisk) {
asterisk = <Text style={style.asterisk}>{' *'}</Text>;
}
let {keyboardType} = this.props;
if (Platform.OS === 'android' && keyboardType === 'url') {
keyboardType = 'default';
}
let inputStyle = style.input;
if (multiline) {
inputStyle = style.multiline;
}
let helpTextContent;
if (helpText) {
helpTextContent = (
<Text style={style.helpText}>
{helpText}
</Text>
);
}
let errorTextContent;
if (errorText) {
errorTextContent = (
<Text style={style.errorText}>
{errorText}
</Text>
);
}
let disabledTextContent;
if (disabled && disabledText) {
disabledTextContent = (
<Text style={style.helpText}>
{disabledText}
</Text>
);
}
return (
<View>
<View style={style.titleContainer}>
{labelContent}
{asterisk}
{optionalContent}
</View>
<View style={style.inputContainer}>
<View style={disabled ? style.disabled : null}>
<TextInput
value={value}
placeholder={placeholder}
onChangeText={this.onChangeText}
style={inputStyle}
autoCapitalize='none'
autoCorrect={false}
maxLength={this.props.maxLength}
editable={!disabled}
underlineColorAndroid='transparent'
disableFullscreenUI={true}
multiline={multiline}
keyboardType={keyboardType}
/>
</View>
</View>
{disabledTextContent}
{helpTextContent}
{errorTextContent}
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
const input = {
color: theme.centerChannelColor,
fontSize: 14,
paddingHorizontal: 15,
};
return {
inputContainer: {
borderTopWidth: 1,
borderBottomWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1),
backgroundColor: '#fff',
},
input: {
...input,
height: 40,
},
multiline: {
...input,
paddingTop: 10,
paddingBottom: 13,
height: 125,
},
disabled: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
},
title: {
fontSize: 14,
color: theme.centerChannelColor,
marginLeft: 15,
},
titleContainer: {
flexDirection: 'row',
marginTop: 15,
marginBottom: 10,
},
optional: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 14,
marginLeft: 5,
},
helpText: {
fontSize: 12,
color: changeOpacity(theme.centerChannelColor, 0.5),
marginHorizontal: 15,
marginTop: 10,
},
errorText: {
fontSize: 12,
color: theme.errorTextColor,
marginHorizontal: 15,
marginTop: 10,
},
asterisk: {
color: theme.errorTextColor,
fontSize: 14,
},
};
});

View file

@ -0,0 +1,29 @@
// 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 TextSetting from './text_setting.js';
describe('components/widgets/settings/TextSetting', () => {
const theme = Preferences.THEMES.default;
test('onChange', () => {
const onChange = jest.fn();
const wrapper = shallow(
<TextSetting
id='string.id'
label='some label'
value='some value'
onChange={onChange}
theme={theme}
/>
);
wrapper.instance().onChangeText('somenewvalue');
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith('string.id', 'somenewvalue');
});
});

View file

@ -14,6 +14,7 @@ import {
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import InteractiveDialogController from 'app/components/interactive_dialog_controller';
import EmptyToolbar from 'app/components/start/empty_toolbar';
import ChannelLoader from 'app/components/channel_loader';
import MainSidebar from 'app/components/sidebars/main';
@ -336,6 +337,10 @@ export default class Channel extends PureComponent {
{LocalConfig.EnableMobileClientUpgrade && <ClientUpgradeListener navigator={navigator}/>}
</SafeAreaView>
</SettingsSidebar>
<InteractiveDialogController
navigator={navigator}
theme={theme}
/>
</MainSidebar>
);
}

View file

@ -16,6 +16,7 @@ import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
import TextSetting from 'app/components/widgets/settings/text_setting';
import Loading from 'app/components/loading';
import ErrorText from 'app/components/error_text';
import StatusBar from 'app/components/status_bar/index';
@ -24,8 +25,6 @@ import AttachmentButton from 'app/components/attachment_button';
import mattermostBucket from 'app/mattermost_bucket';
import LocalConfig from 'assets/config';
import EditProfileItem from './edit_profile_item';
const holders = {
firstName: {
id: t('user.settings.general.firstName'),
@ -221,7 +220,8 @@ export default class EditProfile extends PureComponent {
return RNFetchBlob.config(options).fetch('POST', `${Client4.getUserRoute(currentUser.id)}/image`, headers, [fileInfo]);
};
updateField = (field) => {
updateField = (id, name) => {
const field = {[id]: name};
this.setState(field, () => {
this.emitCanUpdateAccount(this.canUpdate(field));
});
@ -250,15 +250,15 @@ export default class EditProfile extends PureComponent {
(service === 'saml' && config.SamlFirstNameAttributeSet === 'true');
return (
<EditProfileItem
<TextSetting
disabled={disabled}
field='firstName'
format={holders.firstName}
helpText={formatMessage({
id='firstName'
label={holders.firstName}
disabledText={formatMessage({
id: 'user.settings.general.field_handled_externally',
defaultMessage: 'This field is handled through your login provider. If you want to change it, you need to do so through your login provider.',
})}
updateValue={this.updateField}
onChange={this.updateField}
theme={theme}
value={firstName}
/>
@ -276,15 +276,15 @@ export default class EditProfile extends PureComponent {
return (
<View>
<EditProfileItem
<TextSetting
disabled={disabled}
field='lastName'
format={holders.lastName}
helpText={formatMessage({
id='lastName'
label={holders.lastName}
disabledText={formatMessage({
id: 'user.settings.general.field_handled_externally',
defaultMessage: 'This field is handled through your login provider. If you want to change it, you need to do so through your login provider.',
})}
updateValue={this.updateField}
onChange={this.updateField}
theme={theme}
value={lastName}
/>
@ -299,16 +299,16 @@ export default class EditProfile extends PureComponent {
const disabled = currentUser.auth_service !== '';
return (
<EditProfileItem
<TextSetting
disabled={disabled}
field='username'
format={holders.username}
helpText={formatMessage({
id='username'
label={holders.username}
disabledText={formatMessage({
id: 'user.settings.general.field_handled_externally',
defaultMessage: 'This field is handled through your login provider. If you want to change it, you need to do so through your login provider.',
})}
maxLength={22}
updateValue={this.updateField}
onChange={this.updateField}
theme={theme}
value={username}
/>
@ -368,12 +368,12 @@ export default class EditProfile extends PureComponent {
return (
<View>
<EditProfileItem
<TextSetting
disabled={disabled}
field='email'
format={holders.email}
helpText={helpText}
updateValue={this.updateField}
id='email'
label={holders.email}
disabledText={helpText}
onChange={this.updateField}
theme={theme}
value={email}
/>
@ -391,16 +391,16 @@ export default class EditProfile extends PureComponent {
(service === 'saml' && config.SamlNicknameAttributeSet === 'true');
return (
<EditProfileItem
<TextSetting
disabled={disabled}
field='nickname'
format={holders.nickname}
helpText={formatMessage({
id='nickname'
label={holders.nickname}
disabledText={formatMessage({
id: 'user.settings.general.field_handled_externally',
defaultMessage: 'This field is handled through your login provider. If you want to change it, you need to do so through your login provider.',
})}
maxLength={22}
updateValue={this.updateField}
onChange={this.updateField}
theme={theme}
value={nickname}
/>
@ -416,16 +416,16 @@ export default class EditProfile extends PureComponent {
const disabled = (service === 'ldap' || service === 'saml') && config.PositionAttribute === 'true';
return (
<EditProfileItem
<TextSetting
disabled={disabled}
field='position'
format={holders.position}
helpText={formatMessage({
id='position'
label={holders.position}
disabledText={formatMessage({
id: 'user.settings.general.field_handled_externally',
defaultMessage: 'This field is handled through your login provider. If you want to change it, you need to do so through your login provider.',
})}
maxLength={128}
updateValue={this.updateField}
onChange={this.updateField}
theme={theme}
value={position}
/>

View file

@ -1,133 +0,0 @@
// 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 {
View,
Text,
TextInput,
} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class AccountSettingsItem extends PureComponent {
static propTypes = {
disabled: PropTypes.bool,
field: PropTypes.string.isRequired,
format: PropTypes.shape({
id: PropTypes.string.isRequired,
defaultMessage: PropTypes.string.isRequired,
}),
helpText: PropTypes.string,
maxLength: PropTypes.number,
optional: PropTypes.bool,
theme: PropTypes.object.isRequired,
updateValue: PropTypes.func.isRequired,
value: PropTypes.string.isRequired,
};
static defaultProps = {
optional: false,
disabled: false,
};
onChangeText = (value) => {
const {field, updateValue} = this.props;
updateValue({[field]: value});
};
render() {
const {
theme,
format,
helpText,
optional,
disabled,
value,
} = this.props;
const style = getStyleSheet(theme);
return (
<View>
<View style={style.titleContainer15}>
<FormattedText
style={style.title}
id={format.id}
defaultMessage={format.defaultMessage}
/>
{optional && (
<FormattedText
style={style.optional}
id='channel_modal.optional'
defaultMessage='(optional)'
/>
)}
</View>
<View style={style.inputContainer}>
<View style={disabled ? style.disabled : null}>
<TextInput
ref={this.channelNameRef}
value={value}
onChangeText={this.onChangeText}
style={style.input}
autoCapitalize='none'
autoCorrect={false}
maxLength={this.props.maxLength}
editable={!disabled}
underlineColorAndroid='transparent'
disableFullscreenUI={true}
/>
</View>
{disabled &&
<Text style={style.helpText}>
{helpText}
</Text>
}
</View>
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
inputContainer: {
borderTopWidth: 1,
borderBottomWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1),
backgroundColor: theme.centerChannelBg,
},
input: {
color: theme.centerChannelColor,
fontSize: 14,
height: 40,
paddingHorizontal: 15,
},
disabled: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
},
title: {
fontSize: 14,
color: theme.centerChannelColor,
marginLeft: 15,
},
titleContainer15: {
flexDirection: 'row',
marginTop: 15,
},
optional: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 14,
marginLeft: 5,
},
helpText: {
fontSize: 12,
color: changeOpacity(theme.centerChannelColor, 0.5),
marginHorizontal: 15,
marginVertical: 10,
},
};
});

View file

@ -30,10 +30,10 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('FlaggedPosts', () => wrapWithContextProvider(require('app/screens/flagged_posts').default), store, Provider);
Navigation.registerComponent('ForgotPassword', () => wrapWithContextProvider(require('app/screens/forgot_password').default), store, Provider);
Navigation.registerComponent('ImagePreview', () => wrapWithContextProvider(require('app/screens/image_preview').default), store, Provider);
Navigation.registerComponent('InteractiveDialog', () => wrapWithContextProvider(require('app/screens/interactive_dialog').default), store, Provider);
Navigation.registerComponent('Login', () => wrapWithContextProvider(require('app/screens/login').default), store, Provider);
Navigation.registerComponent('LoginOptions', () => wrapWithContextProvider(require('app/screens/login_options').default), store, Provider);
Navigation.registerComponent('LongPost', () => wrapWithContextProvider(require('app/screens/long_post').default), store, Provider);
Navigation.registerComponent('MenuActionSelector', () => wrapWithContextProvider(require('app/screens/menu_action_selector').default), store, Provider);
Navigation.registerComponent('MFA', () => wrapWithContextProvider(require('app/screens/mfa').default), store, Provider);
Navigation.registerComponent('MoreChannels', () => wrapWithContextProvider(require('app/screens/more_channels').default), store, Provider);
Navigation.registerComponent('MoreDirectMessages', () => wrapWithContextProvider(require('app/screens/more_dms').default), store, Provider);
@ -52,6 +52,7 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('RecentMentions', () => wrapWithContextProvider(require('app/screens/recent_mentions').default), store, Provider);
Navigation.registerComponent('Root', () => require('app/screens/root').default, store, Provider);
Navigation.registerComponent('Search', () => wrapWithContextProvider(require('app/screens/search').default), store, Provider);
Navigation.registerComponent('SelectorScreen', () => wrapWithContextProvider(require('app/screens/selector_screen').default), store, Provider);
Navigation.registerComponent('SelectServer', () => wrapWithContextProvider(SelectServer), store, Provider);
Navigation.registerComponent('SelectTeam', () => wrapWithContextProvider(require('app/screens/select_team').default), store, Provider);
Navigation.registerComponent('SelectTimezone', () => wrapWithContextProvider(require('app/screens/timezone/select_timezone').default), store, Provider);

View file

@ -0,0 +1,139 @@
// 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 TextSetting from 'app/components/widgets/settings/text_setting';
import AutocompleteSelector from 'app/components/autocomplete_selector';
const TEXT_DEFAULT_MAX_LENGTH = 150;
const TEXTAREA_DEFAULT_MAX_LENGTH = 3000;
export default class DialogElement extends PureComponent {
static propTypes = {
displayName: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
subtype: PropTypes.string,
placeholder: PropTypes.string,
helpText: PropTypes.string,
errorText: PropTypes.node,
maxLength: PropTypes.number,
dataSource: PropTypes.string,
optional: PropTypes.bool,
options: PropTypes.arrayOf(PropTypes.object),
value: PropTypes.any,
onChange: PropTypes.func,
navigator: PropTypes.object,
theme: PropTypes.object,
};
constructor(props) {
super(props);
this.state = {
selected: null,
};
}
onChange = (name, value) => {
const {type, subtype, onChange} = this.props;
let newValue = value;
if (type === 'text' && subtype === 'number') {
newValue = parseInt(value, 10);
}
onChange(name, newValue);
}
handleAutocompleteSelect = (selected) => {
if (!selected) {
return;
}
this.setState({selected});
const {name, onChange} = this.props;
onChange(name, selected.value);
}
render() {
const {
name,
type,
subtype,
displayName,
value,
placeholder,
helpText,
errorText,
optional,
theme,
dataSource,
options,
navigator,
} = this.props;
let {maxLength} = this.props;
if (type === 'text' || type === 'textarea') {
let keyboardType = 'default';
let multiline = false;
if (type === 'text') {
maxLength = maxLength || TEXT_DEFAULT_MAX_LENGTH;
if (subtype === 'email') {
keyboardType = 'email-address';
} else if (subtype === 'number') {
keyboardType = 'numeric';
} else if (subtype === 'tel') {
keyboardType = 'phone-pad';
} else if (subtype === 'url') {
keyboardType = 'url';
}
} else {
multiline = true;
maxLength = maxLength || TEXTAREA_DEFAULT_MAX_LENGTH;
}
return (
<TextSetting
id={name}
label={displayName}
maxLength={maxLength}
value={String(value || '')}
placeholder={placeholder}
helpText={helpText}
errorText={errorText}
onChange={this.onChange}
optional={optional}
showRequiredAsterisk={true}
resizable={false}
theme={theme}
multiline={multiline}
keyboardType={keyboardType}
/>
);
} else if (type === 'select') {
return (
<AutocompleteSelector
id={name}
label={displayName}
dataSource={dataSource}
options={options}
optional={optional}
onSelected={this.handleAutocompleteSelect}
helpText={helpText}
errorText={errorText}
placeholder={placeholder}
showRequiredAsterisk={true}
selected={this.state.selected}
navigator={navigator}
roundedBorders={false}
/>
);
}
return null;
}
}

View file

@ -0,0 +1,36 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {submitInteractiveDialog} from 'mattermost-redux/actions/integrations';
import InteractiveDialog from './interactive_dialog';
function mapStateToProps(state) {
const data = state.entities.integrations.dialog || {dialog: {}};
return {
url: data.url,
callbackId: data.dialog.callback_id,
elements: data.dialog.elements,
title: data.dialog.title,
iconUrl: data.dialog.icon_url,
submitLabel: data.dialog.submit_label,
notifyOnCancel: data.dialog.notify_on_cancel,
state: data.dialog.state,
theme: getTheme(state),
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
submitInteractiveDialog,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(InteractiveDialog);

View file

@ -0,0 +1,195 @@
// 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 {ScrollView, View} from 'react-native';
import {checkDialogElementForError, checkIfErrorsMatchElements} from 'mattermost-redux/utils/integration_utils';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import StatusBar from 'app/components/status_bar';
import FormattedText from 'app/components/formatted_text';
import DialogElement from './dialog_element.js';
export default class InteractiveDialog extends PureComponent {
static propTypes = {
url: PropTypes.string.isRequired,
callbackId: PropTypes.string,
elements: PropTypes.arrayOf(PropTypes.object).isRequired,
notifyOnCancel: PropTypes.bool,
state: PropTypes.string,
navigator: PropTypes.object,
theme: PropTypes.object,
actions: PropTypes.shape({
submitInteractiveDialog: PropTypes.func.isRequired,
}).isRequired,
};
static defaultProps = {
url: '',
elements: [],
};
constructor(props) {
super(props);
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
const values = {};
props.elements.forEach((e) => {
values[e.name] = e.default || null;
});
this.state = {
values,
errors: {},
submitting: false,
};
}
onNavigatorEvent = (event) => {
if (event.type === 'NavBarButtonPress') {
switch (event.id) {
case 'submit-dialog':
this.handleSubmit();
break;
case 'close-dialog':
this.notifyOnCancelIfNeeded();
this.handleHide();
break;
}
}
};
handleSubmit = async () => {
const {elements} = this.props;
const values = this.state.values;
const errors = {};
elements.forEach((elem) => {
const error = checkDialogElementForError(elem, values[elem.name]);
if (error) {
errors[elem.name] = (
<FormattedText
id={error.id}
defaultMessage={error.defaultMessage}
values={error.values}
/>
);
}
});
this.setState({errors});
if (Object.keys(errors).length !== 0) {
return;
}
const {url, callbackId, state} = this.props;
const dialog = {
url,
callback_id: callbackId,
state,
submission: values,
};
const {data} = await this.props.actions.submitInteractiveDialog(dialog);
this.submitted = true;
if (!data || !data.errors || Object.keys(data.errors).length === 0) {
this.handleHide();
return;
}
if (checkIfErrorsMatchElements(data.errors, elements)) {
this.setState({errors: data.errors});
return;
}
this.handleHide();
}
notifyOnCancelIfNeeded = () => {
if (this.submitted) {
return;
}
const {url, callbackId, state, notifyOnCancel} = this.props;
if (!notifyOnCancel) {
return;
}
const dialog = {
url,
callback_id: callbackId,
state,
cancelled: true,
};
this.props.actions.submitInteractiveDialog(dialog);
}
handleHide = () => {
this.props.navigator.dismissModal({
animationType: 'slide-down',
});
}
onChange = (name, value) => {
const values = {...this.state.values, [name]: value};
this.setState({values});
}
render() {
const {elements, theme, navigator} = this.props;
const style = getStyleFromTheme(theme);
return (
<View style={style.container}>
<ScrollView style={style.scrollView}>
<StatusBar/>
{elements.map((e) => {
return (
<DialogElement
key={'dialogelement' + e.name}
displayName={e.display_name}
name={e.name}
type={e.type}
subtype={e.subtype}
helpText={e.help_text}
errorText={this.state.errors[e.name]}
placeholder={e.placeholder}
minLength={e.min_length}
maxLength={e.max_length}
dataSource={e.data_source}
optional={e.optional}
options={e.options}
value={this.state.values[e.name]}
onChange={this.onChange}
navigator={navigator}
theme={theme}
/>
);
})}
</ScrollView>
</View>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
container: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03),
},
scrollView: {
marginBottom: 20,
marginTop: 10,
},
};
});

View file

@ -0,0 +1,95 @@
// 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 InteractiveDialog from './interactive_dialog';
describe('InteractiveDialog', () => {
const baseProps = {
url: 'http://mattermost.com',
callbackId: 'someid',
elements: [
{type: 'text', name: 'name1', default: 'defaulttext', display_name: 'Name1'},
],
notifyOnCancel: false,
state: 'somestate',
theme: Preferences.THEMES.default,
actions: {
submitInteractiveDialog: jest.fn(),
},
navigator: {
setOnNavigatorEvent: jest.fn(),
dismissModal: jest.fn(),
},
};
test('should set default values', async () => {
const wrapper = shallow(
<InteractiveDialog
{...baseProps}
/>,
);
expect(wrapper.state().values.name1).toBe('defaulttext');
});
test('should submit dialog', async () => {
const submitInteractiveDialog = jest.fn();
const wrapper = shallow(
<InteractiveDialog
{...baseProps}
actions={{submitInteractiveDialog}}
/>,
);
const dialog = {
url: baseProps.url,
callback_id: baseProps.callbackId,
state: baseProps.state,
submission: {name1: 'defaulttext'},
};
wrapper.instance().handleSubmit();
expect(submitInteractiveDialog).toHaveBeenCalledTimes(1);
expect(submitInteractiveDialog).toHaveBeenCalledWith(dialog);
});
test('should submit dialog on cancel', async () => {
const submitInteractiveDialog = jest.fn();
const wrapper = shallow(
<InteractiveDialog
{...baseProps}
actions={{submitInteractiveDialog}}
notifyOnCancel={true}
/>,
);
const dialog = {
url: baseProps.url,
callback_id: baseProps.callbackId,
state: baseProps.state,
cancelled: true,
};
wrapper.instance().onNavigatorEvent({type: 'NavBarButtonPress', id: 'close-dialog'});
expect(submitInteractiveDialog).toHaveBeenCalledTimes(1);
expect(submitInteractiveDialog).toHaveBeenCalledWith(dialog);
});
test('should not submit dialog on cancel', async () => {
const submitInteractiveDialog = jest.fn();
const wrapper = shallow(
<InteractiveDialog
{...baseProps}
actions={{submitInteractiveDialog}}
notifyOnCancel={false}
/>,
);
wrapper.instance().onNavigatorEvent({type: 'NavBarButtonPress', id: 'close-dialog'});
expect(submitInteractiveDialog).not.toHaveBeenCalled();
});
});

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MenuActionSelector should match snapshot for channels 1`] = `
exports[`SelectorScreen should match snapshot for channels 1`] = `
<View
style={
Object {
@ -86,7 +86,7 @@ exports[`MenuActionSelector should match snapshot for channels 1`] = `
</View>
`;
exports[`MenuActionSelector should match snapshot for channels 2`] = `
exports[`SelectorScreen should match snapshot for channels 2`] = `
<View
style={
Object {
@ -172,7 +172,7 @@ exports[`MenuActionSelector should match snapshot for channels 2`] = `
</View>
`;
exports[`MenuActionSelector should match snapshot for explicit options 1`] = `
exports[`SelectorScreen should match snapshot for explicit options 1`] = `
<View
style={
Object {
@ -265,7 +265,7 @@ exports[`MenuActionSelector should match snapshot for explicit options 1`] = `
</View>
`;
exports[`MenuActionSelector should match snapshot for searching 1`] = `
exports[`SelectorScreen should match snapshot for searching 1`] = `
<View
style={
Object {
@ -351,7 +351,7 @@ exports[`MenuActionSelector should match snapshot for searching 1`] = `
</View>
`;
exports[`MenuActionSelector should match snapshot for users 1`] = `
exports[`SelectorScreen should match snapshot for users 1`] = `
<View
style={
Object {
@ -437,7 +437,7 @@ exports[`MenuActionSelector should match snapshot for users 1`] = `
</View>
`;
exports[`MenuActionSelector should match snapshot for users 2`] = `
exports[`SelectorScreen should match snapshot for users 2`] = `
<View
style={
Object {

View file

@ -9,7 +9,7 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getProfiles, searchProfiles} from 'mattermost-redux/actions/users';
import {getChannels, searchChannels} from 'mattermost-redux/actions/channels';
import MenuActionSelector from './menu_action_selector';
import SelectorScreen from './selector_screen';
function mapStateToProps(state) {
const menuAction = state.views.post.selectedMenuAction || {};
@ -36,4 +36,4 @@ function mapDispatchToProps(dispatch) {
};
}
export default connect(mapStateToProps, mapDispatchToProps)(MenuActionSelector);
export default connect(mapStateToProps, mapDispatchToProps)(SelectorScreen);

View file

@ -27,7 +27,7 @@ import {createProfilesSections, loadingText} from 'app/utils/member_list';
import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
export default class MenuActionSelector extends PureComponent {
export default class SelectorScreen extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
getProfiles: PropTypes.func.isRequired,

View file

@ -6,7 +6,7 @@ import {IntlProvider} from 'react-intl';
import Preferences from 'mattermost-redux/constants/preferences';
import MenuActionSelector from './menu_action_selector.js';
import SelectorScreen from './selector_screen.js';
jest.mock('rn-fetch-blob', () => ({
fs: {
@ -61,7 +61,7 @@ const searchChannels = async () => {
const intlProvider = new IntlProvider({locale: 'en'}, {});
const {intl} = intlProvider.getChildContext();
describe('MenuActionSelector', () => {
describe('SelectorScreen', () => {
const actions = {
getProfiles,
getChannels,
@ -83,7 +83,7 @@ describe('MenuActionSelector', () => {
test('should match snapshot for explicit options', async () => {
const wrapper = shallow(
<MenuActionSelector {...baseProps}/>,
<SelectorScreen {...baseProps}/>,
{context: {intl}},
);
expect(wrapper.getElement()).toMatchSnapshot();
@ -97,7 +97,7 @@ describe('MenuActionSelector', () => {
};
const wrapper = shallow(
<MenuActionSelector {...props}/>,
<SelectorScreen {...props}/>,
{context: {intl}},
);
expect(wrapper.getElement()).toMatchSnapshot();
@ -114,7 +114,7 @@ describe('MenuActionSelector', () => {
};
const wrapper = shallow(
<MenuActionSelector {...props}/>,
<SelectorScreen {...props}/>,
{context: {intl}},
);
expect(wrapper.getElement()).toMatchSnapshot();
@ -131,7 +131,7 @@ describe('MenuActionSelector', () => {
};
const wrapper = shallow(
<MenuActionSelector {...props}/>,
<SelectorScreen {...props}/>,
{context: {intl}},
);
wrapper.setState({isLoading: false, searching: true, term: 'name2'});

4
package-lock.json generated
View file

@ -2806,7 +2806,7 @@
},
"colors": {
"version": "0.5.1",
"resolved": "http://registry.npmjs.org/colors/-/colors-0.5.1.tgz",
"resolved": "https://registry.npmjs.org/colors/-/colors-0.5.1.tgz",
"integrity": "sha1-fQAj6usVTo7p/Oddy5I9DtFmd3Q=",
"dev": true
},
@ -12143,7 +12143,7 @@
"dependencies": {
"jsesc": {
"version": "0.5.0",
"resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
"integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0="
}
}