MM-11934 Add interactive action menus (#2099)

* Add interactive action menus

* Add snapshot tests

* Updates per feedback

* Update styling of menu action

* Updates per feedback

* Update submitted message color

* Update snapshots
This commit is contained in:
Joram Wilander 2018-09-20 14:01:51 -04:00 committed by GitHub
parent 46b6687d1f
commit ef3ba7eb00
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 2120 additions and 17 deletions

View file

@ -4,6 +4,8 @@
import {Posts} from 'mattermost-redux/constants';
import {PostTypes} from 'mattermost-redux/action_types';
import {ViewTypes} from 'app/constants';
import {generateId} from 'app/utils/file';
export function sendAddToChannelEphemeralPost(user, addedUsername, message, channelId, postRootId = '') {
@ -37,3 +39,14 @@ export function sendAddToChannelEphemeralPost(user, addedUsername, message, chan
});
};
}
export function setMenuActionSelector(dataSource, onSelect, options) {
return {
type: ViewTypes.SELECTED_ACTION_MENU,
data: {
dataSource,
onSelect,
options,
},
};
}

View file

@ -21,7 +21,7 @@ export default class ChannelListRow extends React.PureComponent {
};
onPress = () => {
this.props.onPress(this.props.id);
this.props.onPress(this.props.id, this.props.item);
};
render() {

View file

@ -20,6 +20,7 @@ export default class CustomListRow extends React.PureComponent {
selectable: PropTypes.bool,
selected: PropTypes.bool,
children: CustomPropTypes.Children,
item: PropTypes.object,
};
static defaultProps = {

View file

@ -0,0 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import OptionListRow from './option_list_row';
function mapStateToProps(state) {
return {
theme: getTheme(state),
};
}
export default connect(mapStateToProps)(OptionListRow);

View file

@ -0,0 +1,79 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import {
Text,
View,
} from 'react-native';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import CustomListRow from 'app/components/custom_list/custom_list_row';
export default class OptionListRow extends React.PureComponent {
static propTypes = {
id: PropTypes.string,
theme: PropTypes.object.isRequired,
...CustomListRow.propTypes,
};
static contextTypes = {
intl: intlShape,
};
onPress = () => {
if (this.props.onPress) {
this.props.onPress(this.props.id, this.props.item);
}
};
render() {
const {
enabled,
selectable,
selected,
theme,
item,
} = this.props;
const {text, value} = item;
const style = getStyleFromTheme(theme);
return (
<CustomListRow
id={value}
theme={theme}
onPress={this.onPress}
enabled={enabled}
selectable={selectable}
selected={selected}
>
<View style={style.textContainer}>
<View>
<Text style={style.optionText}>
{text}
</Text>
</View>
</View>
</CustomListRow>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
container: {
flexDirection: 'row',
height: 65,
paddingHorizontal: 15,
alignItems: 'center',
backgroundColor: theme.centerChannelBg,
},
optionText: {
fontSize: 15,
color: theme.centerChannelColor,
},
};
});

View file

@ -29,7 +29,7 @@ export default class UserListRow extends React.PureComponent {
onPress = () => {
if (this.props.onPress) {
this.props.onPress(this.props.id);
this.props.onPress(this.props.id, this.props.item);
}
};

View file

@ -9,7 +9,7 @@ import Button from 'react-native-button';
import {preventDoubleTap} from 'app/utils/tap';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
export default class InteractiveAction extends PureComponent {
export default class ActionButton extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
doPostAction: PropTypes.func.isRequired,

View file

@ -7,7 +7,7 @@ import {connect} from 'react-redux';
import {doPostAction} from 'mattermost-redux/actions/posts';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import InteractiveAction from './interactive_action';
import ActionButton from './action_button';
function mapStateToProps(state) {
return {
@ -23,4 +23,4 @@ function mapDispatchToProps(dispatch) {
};
}
export default connect(mapStateToProps, mapDispatchToProps)(InteractiveAction);
export default connect(mapStateToProps, mapDispatchToProps)(ActionButton);

View file

@ -0,0 +1,190 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {Text, View} from 'react-native';
import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import Icon from 'react-native-vector-icons/FontAwesome';
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 ActionMenu extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
doPostAction: PropTypes.func.isRequired,
setMenuActionSelector: PropTypes.func.isRequired,
}).isRequired,
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
dataSource: PropTypes.string,
options: PropTypes.arrayOf(PropTypes.object),
postId: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
navigator: PropTypes.object,
};
static contextTypes = {
intl: intlShape,
};
constructor(props) {
super(props);
this.state = {
selectedText: null,
};
}
handleSelect = (selected) => {
if (!selected) {
return;
}
const {dataSource, actions, postId, id} = this.props;
let selectedText;
let selectedValue;
if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
selectedText = selected.username;
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.doPostAction(postId, id, selectedValue);
}
goToMenuActionSelector = preventDoubleTap(() => {
const {intl} = this.context;
const {navigator, theme, actions, dataSource, options, name} = this.props;
actions.setMenuActionSelector(dataSource, this.handleSelect, options);
navigator.push({
backButtonTitle: '',
screen: 'MenuActionSelector',
title: name || intl.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}/>;
}
return (
<View style={style.container}>
<View style={style.input}>
<Text
style={selectedStyle}
onPress={this.goToMenuActionSelector}
numberOfLines={1}
>
{text}
</Text>
<Icon
name='chevron-down'
onPress={this.goToMenuActionSelector}
color={changeOpacity(theme.centerChannelColor, 0.5)}
style={style.icon}
/>
</View>
{submitted}
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
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

@ -0,0 +1,29 @@
// 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 {doPostAction} from 'mattermost-redux/actions/posts';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {setMenuActionSelector} from 'app/actions/views/post';
import ActionMenu from './action_menu';
function mapStateToProps(state) {
return {
theme: getTheme(state),
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
doPostAction,
setMenuActionSelector,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ActionMenu);

View file

@ -22,7 +22,8 @@ import ImageCacheManager from 'app/utils/image_cache_manager';
import {previewImageAtIndex, calculateDimensions} from 'app/utils/images';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import InteractiveAction from './interactive_action';
import ActionButton from './action_button';
import ActionMenu from './action_menu';
const VIEWPORT_IMAGE_CONTAINER_OFFSET = 10;
const VIEWPORT_IMAGE_OFFSET = 32;
@ -70,32 +71,51 @@ export default class MessageAttachment extends PureComponent {
}
getActionView = (style) => {
const {attachment, postId} = this.props;
const {attachment, postId, navigator} = this.props;
const {actions} = attachment;
if (!actions || !actions.length) {
return null;
}
const buttons = [];
const content = [];
actions.forEach((action) => {
if (!action.id || !action.name) {
return;
}
buttons.push(
<InteractiveAction
key={action.id}
id={action.id}
name={action.name}
postId={postId}
/>
);
switch (action.type) {
case 'select':
content.push(
<ActionMenu
key={action.id}
id={action.id}
name={action.name}
dataSource={action.data_source}
options={action.options}
postId={postId}
navigator={navigator}
/>
);
break;
case 'button':
default:
content.push(
<ActionButton
key={action.id}
id={action.id}
name={action.name}
postId={postId}
/>
);
break;
}
});
return (
<View style={style.actionsContainer}>
{buttons}
{content}
</View>
);
};

View file

@ -71,6 +71,8 @@ const ViewTypes = keyMirror({
SET_DEEP_LINK_URL: null,
SET_PROFILE_IMAGE_URI: null,
SELECTED_ACTION_MENU: null,
});
export default {
@ -88,4 +90,6 @@ export default {
IOSX_TOP_PORTRAIT: 88,
STATUS_BAR_HEIGHT: 20,
PROFILE_PICTURE_SIZE: 32,
DATA_SOURCE_USERS: 'users',
DATA_SOURCE_CHANNELS: 'channels',
};

View file

@ -17,6 +17,7 @@ import team from './team';
import thread from './thread';
import user from './user';
import emoji from './emoji';
import post from './post';
export default combineReducers({
announcement,
@ -33,4 +34,5 @@ export default combineReducers({
thread,
user,
emoji,
post,
});

View file

@ -0,0 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {combineReducers} from 'redux';
import {ViewTypes} from 'app/constants';
function menuAction(state = {}, action) {
switch (action.type) {
case ViewTypes.SELECTED_ACTION_MENU:
return action.data;
default:
return state;
}
}
export default combineReducers({
menuAction,
});

View file

@ -33,6 +33,7 @@ export function registerScreens(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);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,58 @@
// 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 {getChannelsInCurrentTeam} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getProfiles} from 'mattermost-redux/selectors/entities/users';
import {getProfiles as getProfilesAction, searchProfiles} from 'mattermost-redux/actions/users';
import {getChannels, searchChannels} from 'mattermost-redux/actions/channels';
import {ViewTypes} from 'app/constants';
import MenuActionSelector from './menu_action_selector';
function mapStateToProps(state) {
const menuAction = state.views.post.menuAction || {};
let data;
let loadMoreRequestStatus;
let searchRequestStatus;
if (menuAction.dataSource === ViewTypes.DATA_SOURCE_USERS) {
data = getProfiles(state);
loadMoreRequestStatus = state.requests.users.getProfiles.status;
searchRequestStatus = state.requests.users.searchProfiles.status;
} else if (menuAction.dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
data = getChannelsInCurrentTeam(state);
loadMoreRequestStatus = state.requests.channels.getChannels.status;
searchRequestStatus = state.requests.channels.getChannels.status;
} else {
data = menuAction.options || [];
}
return {
data,
dataSource: menuAction.dataSource,
onSelect: menuAction.onSelect,
theme: getTheme(state),
currentTeamId: getCurrentTeamId(state),
loadMoreRequestStatus,
searchRequestStatus,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getProfiles: getProfilesAction,
getChannels,
searchProfiles,
searchChannels,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(MenuActionSelector);

View file

@ -0,0 +1,286 @@
// 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 {injectIntl, intlShape} from 'react-intl';
import {
InteractionManager,
Platform,
View,
} from 'react-native';
import CustomList from 'app/components/custom_list';
import UserListRow from 'app/components/custom_list/user_list_row';
import ChannelListRow from 'app/components/custom_list/channel_list_row';
import OptionListRow from 'app/components/custom_list/option_list_row';
import SearchBar from 'app/components/search_bar';
import StatusBar from 'app/components/status_bar';
import {loadingText} from 'app/utils/member_list';
import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme';
import {ViewTypes} from 'app/constants';
import {General, RequestStatus} from 'mattermost-redux/constants';
import {filterProfilesMatchingTerm} from 'mattermost-redux/utils/user_utils';
import {filterChannelsMatchingTerm} from 'mattermost-redux/utils/channel_utils';
import {memoizeResult} from 'mattermost-redux/utils/helpers';
class MenuActionSelector extends PureComponent {
static propTypes = {
intl: intlShape.isRequired,
theme: PropTypes.object.isRequired,
navigator: PropTypes.object,
data: PropTypes.arrayOf(PropTypes.object),
dataSource: PropTypes.string,
onSelect: PropTypes.func.isRequired,
currentTeamId: PropTypes.string.isRequired,
loadMoreRequestStatus: PropTypes.string,
searchRequestStatus: PropTypes.string,
actions: PropTypes.shape({
getProfiles: PropTypes.func.isRequired,
getChannels: PropTypes.func.isRequired,
searchProfiles: PropTypes.func.isRequired,
searchChannels: PropTypes.func.isRequired,
}),
};
constructor(props) {
super(props);
this.searchTimeoutId = 0;
let data = [];
if (!props.dataSource) {
data = props.data;
}
const needsLoading = props.dataSource === ViewTypes.DATA_SOURCE_USERS || props.dataSource === ViewTypes.DATA_SOURCE_CHANNELS;
this.state = {
next: needsLoading,
page: 0,
data,
searching: false,
showNoResults: false,
term: '',
isLoading: needsLoading,
prevLoadMoreRequestStatus: RequestStatus.STARTED,
};
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
}
componentDidMount() {
this.mounted = true;
InteractionManager.runAfterInteractions(() => {
if (this.props.dataSource === ViewTypes.DATA_SOURCE_USERS) {
this.props.actions.getProfiles().then(() => this.setState({isLoading: false}));
} else if (this.props.dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
this.props.actions.getChannels(this.props.currentTeamId).then(() => this.setState({isLoading: false}));
}
});
}
componentWillUnmount() {
this.mounted = false;
}
componentDidUpdate(prevProps) {
if (this.props.theme !== prevProps.theme) {
setNavigatorStyles(this.props.navigator, this.props.theme);
}
}
cancelSearch = () => {
this.setState({
searching: false,
isLoading: false,
term: '',
page: 0,
data: filterPageData(this.props.data, 0),
});
};
close = () => {
this.props.navigator.pop({animated: true});
};
handleRowSelect = (id, selected) => {
this.props.onSelect(selected);
this.close();
};
loadMore = async () => {
const {actions, loadMoreRequestStatus, currentTeamId, dataSource} = this.props;
const {next, searching} = this.state;
let {page} = this.state;
if (loadMoreRequestStatus !== RequestStatus.STARTED && next && !searching) {
page = page + 1;
let results;
if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
results = await actions.getProfiles(page, General.PROFILE_CHUNK_SIZE);
} else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
results = await actions.getChannels(currentTeamId, page, General.PROFILE_CHUNK_SIZE);
} else {
return;
}
if (!this.mounted) {
return;
}
if (results.data && results.data.length) {
this.setState({
isLoading: false,
page,
});
} else {
this.setState({
isLoading: false,
next: false,
});
}
}
};
searchProfiles = (text) => {
const term = text;
const {actions, currentTeamId, dataSource} = this.props;
if (term) {
if (!dataSource) {
this.setState({data: filterSearchData(null, this.props.data, term.toLowerCase())});
return;
}
this.setState({searching: true, isLoading: true, term});
clearTimeout(this.searchTimeoutId);
this.searchTimeoutId = setTimeout(async () => {
if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
await actions.searchProfiles(term.toLowerCase());
} else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
await actions.searchChannels(currentTeamId, term.toLowerCase());
}
if (!this.mounted) {
return;
}
this.setState({isLoading: false});
}, General.SEARCH_TIMEOUT_MILLISECONDS);
} else {
this.cancelSearch();
}
};
render() {
const {intl, data, theme, dataSource} = this.props;
const {searching, term, page} = this.state;
const {formatMessage} = intl;
const style = getStyleFromTheme(theme);
const more = searching ? () => true : this.loadMore;
const searchBarInput = {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
color: theme.centerChannelColor,
fontSize: 15,
...Platform.select({
android: {
marginBottom: -5,
},
}),
};
let rowComponent;
if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
rowComponent = UserListRow;
} else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
rowComponent = ChannelListRow;
} else {
rowComponent = OptionListRow;
}
let filteredData;
if (searching) {
filteredData = filterSearchData(dataSource, data, term);
} else {
filteredData = filterPageData(data, page);
}
return (
<View style={style.container}>
<StatusBar/>
<View
style={style.searchContainer}
>
<SearchBar
ref='search_bar'
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={searchBarInput}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
titleCancelColor={theme.centerChannelColor}
onChangeText={this.searchProfiles}
onSearchButtonPress={this.searchProfiles}
onCancelButtonPress={this.cancelSearch}
autoCapitalize='none'
value={term}
/>
</View>
<CustomList
data={filteredData}
theme={theme}
searching={searching}
onListEndReached={more}
listScrollRenderAheadDistance={50}
loading={this.state.isLoading}
loadingText={loadingText}
selectable={false}
onRowPress={this.handleRowSelect}
renderRow={rowComponent}
showNoResults={this.state.showNoResults}
showSections={false}
/>
</View>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
backgroundColor: theme.centerChannelBg,
},
searchContainer: {
marginVertical: 5,
},
};
});
const filterPageData = memoizeResult((data, page) => {
return data.slice(0, (page + 1) * General.PROFILE_CHUNK_SIZE);
});
const filterSearchData = memoizeResult((dataSource, data, term) => {
if (!data) {
return [];
}
if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
return filterProfilesMatchingTerm(data, term);
} else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
return filterChannelsMatchingTerm(data, term);
}
return data.filter((option) => option.text && option.text.toLowerCase().startsWith(term));
});
export default injectIntl(MenuActionSelector);

View file

@ -0,0 +1,139 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import {IntlProvider} from 'react-intl';
import MenuActionSelector from './menu_action_selector.js';
jest.mock('rn-fetch-blob', () => ({
fs: {
dirs: {
DocumentDir: () => jest.fn(),
CacheDir: () => jest.fn(),
},
},
}));
jest.mock('rn-fetch-blob/fs', () => ({
dirs: {
DocumentDir: () => jest.fn(),
CacheDir: () => jest.fn(),
},
}));
const user1 = {id: 'id', username: 'username'};
const user2 = {id: 'id2', username: 'username2'};
const getProfiles = async () => {
return {
data: [user1, user2],
error: {},
};
};
const searchProfiles = async () => {
return {
data: [user2],
error: {},
};
};
const channel1 = {id: 'id', name: 'name', display_name: 'display_name'};
const channel2 = {id: 'id2', name: 'name2', display_name: 'display_name2'};
const getChannels = async () => {
return {
data: [channel1, channel2],
error: {},
};
};
const searchChannels = async () => {
return {
data: [channel2],
error: {},
};
};
const intlProvider = new IntlProvider({locale: 'en'}, {});
const {intl} = intlProvider.getChildContext();
describe('MenuActionSelector', () => {
const actions = {
getProfiles,
getChannels,
searchProfiles,
searchChannels,
};
const baseProps = {
actions,
currentTeamId: 'someId',
navigator: {
setOnNavigatorEvent: jest.fn(),
},
onSelect: jest.fn(),
data: [{text: 'text', value: 'value'}],
dataSource: null,
theme: {},
};
test('should match snapshot for explicit options', async () => {
const wrapper = shallow(
<MenuActionSelector {...baseProps}/>,
{context: {intl}},
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot for users', async () => {
const props = {
...baseProps,
dataSource: 'users',
data: [user1, user2],
};
const wrapper = shallow(
<MenuActionSelector {...props}/>,
{context: {intl}},
);
expect(wrapper).toMatchSnapshot();
wrapper.setState({isLoading: false});
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot for channels', async () => {
const props = {
...baseProps,
dataSource: 'channels',
data: [channel1, channel2],
};
const wrapper = shallow(
<MenuActionSelector {...props}/>,
{context: {intl}},
);
expect(wrapper).toMatchSnapshot();
wrapper.setState({isLoading: false});
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot for searching', async () => {
const props = {
...baseProps,
dataSource: 'channels',
data: [channel1, channel2],
};
const wrapper = shallow(
<MenuActionSelector {...props}/>,
{context: {intl}},
);
wrapper.setState({isLoading: false, searching: true, term: 'name2'});
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -21,6 +21,8 @@
"about.title": "About Mattermost",
"about.version": "Mattermost Version:",
"access_history.title": "Access History",
"mobile.action_menu.select": "Select an option",
"mobile.action_menu.submitted": "Submitted",
"activity_log.activeSessions": "Active Sessions",
"activity_log.browser": "Browser: {browser}",
"activity_log.firstTime": "First time active: {date}, {time}",