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:
parent
8097764d87
commit
e431bd36c4
24 changed files with 1226 additions and 385 deletions
|
|
@ -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};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
289
app/components/autocomplete_selector/autocomplete_selector.js
Normal file
289
app/components/autocomplete_selector/autocomplete_selector.js
Normal 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,
|
||||
},
|
||||
};
|
||||
});
|
||||
28
app/components/autocomplete_selector/index.js
Normal file
28
app/components/autocomplete_selector/index.js
Normal 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);
|
||||
15
app/components/interactive_dialog_controller/index.js
Normal file
15
app/components/interactive_dialog_controller/index.js
Normal 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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
235
app/components/widgets/settings/text_setting.js
Normal file
235
app/components/widgets/settings/text_setting.js
Normal 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,
|
||||
},
|
||||
};
|
||||
});
|
||||
29
app/components/widgets/settings/text_setting.test.js
Normal file
29
app/components/widgets/settings/text_setting.test.js
Normal 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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
139
app/screens/interactive_dialog/dialog_element.js
Normal file
139
app/screens/interactive_dialog/dialog_element.js
Normal 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;
|
||||
}
|
||||
}
|
||||
36
app/screens/interactive_dialog/index.js
Normal file
36
app/screens/interactive_dialog/index.js
Normal 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);
|
||||
195
app/screens/interactive_dialog/interactive_dialog.js
Normal file
195
app/screens/interactive_dialog/interactive_dialog.js
Normal 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,
|
||||
},
|
||||
};
|
||||
});
|
||||
95
app/screens/interactive_dialog/interactive_dialog.test.js
Normal file
95
app/screens/interactive_dialog/interactive_dialog.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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);
|
||||
|
|
@ -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,
|
||||
|
|
@ -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
4
package-lock.json
generated
|
|
@ -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="
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue