From bfd70f742cf1eb95dd18723e4505bd6bb1528df4 Mon Sep 17 00:00:00 2001 From: enahum Date: Mon, 30 Jan 2017 10:41:14 -0300 Subject: [PATCH] Modal with options (#179) * modal options and channel long press * PureComponent optimization (#183) * Bug fix when leaving the current channel * Rebased with new navigation * Add disabled drawer to unit test * re-organize modal up in the stack and controlled by redux * renaming modalOptions to optionsModal --- app/actions/views/channel.js | 57 ++++- app/actions/views/options_modal.js | 30 +++ .../{channel_drawer => channel_list}/badge.js | 0 .../channel_item.js | 8 +- .../channel_list.js | 144 +++++++++-- .../channel_list/channel_list_container.js | 33 +++ app/components/channel_list/index.js | 6 + .../unread_indicator.js | 0 app/components/options_modal/index.js | 6 + app/components/options_modal/options_modal.js | 223 ++++++++++++++++++ .../options_modal/options_modal_container.js | 25 ++ app/constants/view.js | 5 +- app/initial_state.js | 5 + app/navigation/router.js | 20 +- app/reducers/views/index.js | 2 + app/reducers/views/options_modal.js | 46 ++++ app/scenes/channel_drawer/channel_drawer.js | 12 +- .../channel_drawer_container.js | 7 +- assets/base/i18n/en.json | 11 +- test/app/actions/options_modal.js | 30 +++ 20 files changed, 627 insertions(+), 43 deletions(-) create mode 100644 app/actions/views/options_modal.js rename app/components/{channel_drawer => channel_list}/badge.js (100%) rename app/components/{channel_drawer => channel_list}/channel_item.js (95%) rename app/components/{channel_drawer => channel_list}/channel_list.js (67%) create mode 100644 app/components/channel_list/channel_list_container.js create mode 100644 app/components/channel_list/index.js rename app/components/{channel_drawer => channel_list}/unread_indicator.js (100%) create mode 100644 app/components/options_modal/index.js create mode 100644 app/components/options_modal/options_modal.js create mode 100644 app/components/options_modal/options_modal_container.js create mode 100644 app/reducers/views/options_modal.js create mode 100644 test/app/actions/options_modal.js diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js index 54ef78804..a9c4962d1 100644 --- a/app/actions/views/channel.js +++ b/app/actions/views/channel.js @@ -6,7 +6,11 @@ import {batchActions} from 'redux-batched-actions'; import {ViewTypes} from 'app/constants'; import {updateStorage} from 'app/actions/storage'; -import {fetchMyChannelsAndMembers, getMyChannelMembers, selectChannel} from 'service/actions/channels'; +import { + fetchMyChannelsAndMembers, + getMyChannelMembers, + selectChannel, + leaveChannel as serviceLeaveChannel} from 'service/actions/channels'; import {getPosts} from 'service/actions/posts'; import {savePreferences, deletePreferences} from 'service/actions/preferences'; import {getTeamMembersByIds} from 'service/actions/teams'; @@ -96,10 +100,10 @@ export function loadPostsIfNecessary(channel) { export function selectInitialChannel(teamId) { return async (dispatch, getState) => { const state = getState(); - const channels = state.entities.channels.channels; + const {channels, myMembers} = state.entities.channels; const currentChannelId = state.entities.channels.currentId; - if (channels[currentChannelId] && channels[currentChannelId].team_id === teamId) { + if (channels[currentChannelId] && myMembers[currentChannelId] && channels[currentChannelId].team_id === teamId) { await selectChannel(currentChannelId)(dispatch, getState); return; } @@ -109,7 +113,9 @@ export function selectInitialChannel(teamId) { await selectChannel(channel.id)(dispatch, getState); } else { // Handle case when the default channel cannot be found - const firstChannel = Object.values(channels)[0]; + // so we need to get the first available channel of the team + const channelsInTeam = Object.values(channels).filter((c) => c.team_id === teamId); + const firstChannel = channelsInTeam[0].id; await selectChannel(firstChannel.id)(dispatch, getState); } }; @@ -145,12 +151,7 @@ export function closeDMChannel(channel) { }]; if (channel.isFavorite) { - const fav = [{ - user_id: userId, - category: Constants.CATEGORY_FAVORITE_CHANNEL, - name: channel.id - }]; - deletePreferences(fav)(dispatch, getState); + unmarkFavorite(channel.id)(dispatch, getState); } savePreferences(dm)(dispatch, getState).then(() => { if (channel.isCurrent) { @@ -159,3 +160,39 @@ export function closeDMChannel(channel) { }); }; } + +export function markFavorite(channelId) { + return async (dispatch, getState) => { + const userId = getState().entities.users.currentId; + const fav = [{ + user_id: userId, + category: Constants.CATEGORY_FAVORITE_CHANNEL, + name: channelId, + value: 'true' + }]; + savePreferences(fav)(dispatch, getState); + }; +} + +export function unmarkFavorite(channelId) { + return async (dispatch, getState) => { + const userId = getState().entities.users.currentId; + const fav = [{ + user_id: userId, + category: Constants.CATEGORY_FAVORITE_CHANNEL, + name: channelId + }]; + deletePreferences(fav)(dispatch, getState); + }; +} + +export function leaveChannel(channel) { + return async (dispatch, getState) => { + const {currentId: teamId} = getState().entities.teams; + serviceLeaveChannel(teamId, channel.id)(dispatch, getState).then(() => { + if (channel.isCurrent) { + selectInitialChannel(teamId)(dispatch, getState); + } + }); + }; +} diff --git a/app/actions/views/options_modal.js b/app/actions/views/options_modal.js new file mode 100644 index 000000000..a60c5e065 --- /dev/null +++ b/app/actions/views/options_modal.js @@ -0,0 +1,30 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {ViewTypes} from 'app/constants'; + +export function openModal(title, options) { + return async (dispatch, getState) => { + dispatch({ + type: ViewTypes.OPTIONS_MODAL_CHANGED, + data: { + title, + options, + visible: true + } + }, getState); + }; +} + +export function closeModal() { + return async (dispatch, getState) => { + dispatch({ + type: ViewTypes.OPTIONS_MODAL_CHANGED, + data: { + title: '', + options: [], + visible: false + } + }, getState); + }; +} diff --git a/app/components/channel_drawer/badge.js b/app/components/channel_list/badge.js similarity index 100% rename from app/components/channel_drawer/badge.js rename to app/components/channel_list/badge.js diff --git a/app/components/channel_drawer/channel_item.js b/app/components/channel_list/channel_item.js similarity index 95% rename from app/components/channel_drawer/channel_item.js rename to app/components/channel_list/channel_item.js index 95642dc99..130ad5f42 100644 --- a/app/components/channel_drawer/channel_item.js +++ b/app/components/channel_list/channel_item.js @@ -17,6 +17,7 @@ export default class ChannelItem extends React.Component { channel: React.PropTypes.object.isRequired, onSelectChannel: React.PropTypes.func.isRequired, handleClose: React.PropTypes.func, + onLongPress: React.PropTypes.func, isActive: React.PropTypes.bool.isRequired, hasUnread: React.PropTypes.bool.isRequired, mentions: React.PropTypes.number.isRequired, @@ -34,6 +35,7 @@ export default class ChannelItem extends React.Component { } = this.props; let iconColor = changeOpacity(theme.centerChannelColor, 0.7); + const isDirectMessage = channel.type === Constants.DM_CHANNEL; let icon; let activeBorder; let badge; @@ -153,7 +155,7 @@ export default class ChannelItem extends React.Component { } let closeButton = null; - if (handleClose && !badge) { + if (isDirectMessage && !badge) { const closeStyle = { position: 'absolute', justifyContent: 'center', @@ -183,6 +185,10 @@ export default class ChannelItem extends React.Component { this.props.onSelectChannel(channel)} + delayLongPress={1000} + onLongPress={() => { + this.props.onLongPress(channel); + }} > {activeBorder} diff --git a/app/components/channel_drawer/channel_list.js b/app/components/channel_list/channel_list.js similarity index 67% rename from app/components/channel_drawer/channel_list.js rename to app/components/channel_list/channel_list.js index c020ed97b..4f74b8664 100644 --- a/app/components/channel_drawer/channel_list.js +++ b/app/components/channel_list/channel_list.js @@ -59,8 +59,15 @@ class ChannelList extends React.Component { channelMembers: React.PropTypes.object, theme: React.PropTypes.object.isRequired, onSelectChannel: React.PropTypes.func.isRequired, - onViewChannel: React.PropTypes.func.isRequired, - handleCloseDM: React.PropTypes.func.isRequired + actions: React.PropTypes.shape({ + viewChannel: React.PropTypes.func.isRequired, + closeDMChannel: React.PropTypes.func.isRequired, + leaveChannel: React.PropTypes.func.isRequired, + markFavorite: React.PropTypes.func.isRequired, + unmarkFavorite: React.PropTypes.func.isRequired, + openModal: React.PropTypes.func.isRequired, + closeModal: React.PropTypes.func.isRequired + }).isRequired }; constructor(props) { @@ -131,36 +138,139 @@ class ChannelList extends React.Component { }; onSelectChannel = (channel) => { - console.log('clicked channel ' + channel.name); // eslint-disable-line no-console - const { currentChannel, currentTeam } = this.props; this.props.onSelectChannel(channel.id); - this.props.onViewChannel(currentTeam.id, channel.id, currentChannel.id); + this.props.actions.viewChannel(currentTeam.id, channel.id, currentChannel.id); }; handleClose = (channel) => { + this.setState({showOptions: false}); + this.props.actions.closeDMChannel(channel); + }; + + handleLeave = (channel, term) => { const {formatMessage} = this.props.intl; Alert.alert( - formatMessage({id: 'mobile.channel_list.alertTitleCloseDM', defaultMessage: 'Close Direct Message'}), + formatMessage({id: 'mobile.channel_list.alertTitleLeaveChannel', defaultMessage: 'Leave {term}'}, {term}), formatMessage({ - id: 'mobile.channel_list.alertMessageCloseDM', - defaultMessage: 'Are you sure you want to close the DM with {username}' + id: 'mobile.channel_list.alertMessageLeaveChannel', + defaultMessage: 'Are you sure you want to leave the {term} with {name}?' }, { - username: channel.display_name + term: term.toLowerCase(), + name: channel.display_name }), - [ - {text: formatMessage({id: 'mobile.channel_list.alertNo', defaultMessage: 'No'})}, - { - text: formatMessage({id: 'mobile.channel_list.alertYes', defaultMessage: 'Yes'}), - onPress: () => this.props.handleCloseDM(channel) - }] + [{ + text: formatMessage({id: 'mobile.channel_list.alertNo', defaultMessage: 'No'}) + }, { + text: formatMessage({id: 'mobile.channel_list.alertYes', defaultMessage: 'Yes'}), + onPress: () => { + this.setState({showOptions: false}); + this.props.actions.leaveChannel(channel); + } + }] ); }; + onShowModal = (channel) => { + const {formatMessage} = this.props.intl; + let open; + let close; + let favorite; + let title; + + if (channel.type === Constants.DM_CHANNEL) { + title = formatMessage({ + id: 'mobile.channel_list.modalTitle', + defaultMessage: 'Select an action for the {term} {name}'}, + { + name: channel.display_name, + term: formatMessage({id: 'mobile.channel_list.dm', defaultMessage: 'Direct Message'}).toLowerCase() + }); + + open = { + action: () => { + this.props.actions.closeModal(); + this.onSelectChannel(channel); + }, + text: formatMessage({id: 'mobile.channel_list.openDM', defaultMessage: 'Open Direct Message'}) + }; + + close = { + action: () => { + this.handleClose(channel); + }, + text: formatMessage({id: 'sidebar.removeList', defaultMessage: 'Remove from list'}), + textStyle: { + color: '#CC3239' + } + }; + } else { + const term = channel.type === Constants.OPEN_CHANNEL ? + formatMessage({id: 'mobile.channel_list.publicChannel', defaultMessage: 'Public Channel'}) : + formatMessage({id: 'mobile.channel_list.privateChannel', defaultMessage: 'Private Channel'}); + + title = formatMessage({ + id: 'mobile.channel_list.modalTitle', + defaultMessage: 'Select an action for the {term} {name}'}, + { + name: channel.display_name, + term: term.toLowerCase() + }); + + open = { + action: () => { + this.props.actions.closeModal(); + this.onSelectChannel(channel); + }, + text: formatMessage({id: 'mobile.channel_list.openChannel', defaultMessage: 'Open {term}'}, { + term + }) + }; + + if (channel.name !== Constants.DEFAULT_CHANNEL) { + close = { + action: () => { + this.handleLeave(channel, term); + }, + text: formatMessage({id: 'channel_header.leave', defaultMessage: 'Leave {term}'}, { + term + }), + textStyle: { + color: '#CC3239' + } + }; + } + } + + if (channel.isFavorite) { + favorite = { + action: () => { + this.props.actions.closeModal(); + this.props.actions.unmarkFavorite(channel.id); + }, + text: formatMessage({id: 'channelHeader.removeFromFavorites', defaultMessage: 'Remove from Favorites'}) + }; + } else { + favorite = { + action: () => { + this.props.actions.closeModal(); + this.props.actions.markFavorite(channel.id); + }, + text: formatMessage({id: 'channelHeader.addToFavorites', defaultMessage: 'Add to Favorites'}) + }; + } + + const options = [open, favorite]; + if (close) { + options.push(close); + } + this.props.actions.openModal(title, options); + }; + getUnreadMessages = (channel) => { const member = this.props.channelMembers[channel.id]; let mentions = 0; @@ -207,7 +317,8 @@ class ChannelList extends React.Component { hasUnread={unread} mentions={mentions} onSelectChannel={this.onSelectChannel} - handleClose={channel.type === Constants.DM_CHANNEL ? this.handleClose : null} + onLongPress={this.onShowModal} + handleClose={this.handleClose} isActive={channel.isCurrent} theme={this.props.theme} /> @@ -319,6 +430,7 @@ class ChannelList extends React.Component { /> ); } + if (this.state.showBelow) { below = ( { + // do not change the way this is event handler as its necessary to properly display any alert boxes + // that needs to be shown by selecting an option from the modal. + // By hiding the modal previous to that will cause a rendering bug. + if (option) { + option.action(); + } else { + this.props.actions.closeModal(); + } + }; + + renderOption = (option, index) => { + const optionStyle = [styles.optionStyle]; + const optionTextStyle = [styles.optionTextStyle]; + + if (option.style) { + optionStyle.push(option.style); + } + + if (option.textStyle) { + optionTextStyle.push(option.textStyle); + } + + let text = option.text; + if (!(text instanceof FormattedText)) { + text = ( + {option.text} + ); + } + return ( + this.onOptionSelected(option)} + > + + {text} + + + ); + }; + + renderOptionList = () => { + const options = this.props.options.map((option, index) => { + return this.renderOption(option, index); + }); + + let title; + if (this.props.title) { + title = ( + + + {this.props.title} + + + ); + } + + return ( + + + + + {title} + {options} + + + + + this.onOptionSelected(null)}> + + + {this.props.cancelText} + + + + + + ); + }; + + render() { + if (this.props.options.length) { + return ( + + this.onOptionSelected(null)} + animationType={this.state.animationType} + > + this.onOptionSelected(null)}> + {this.renderOptionList()} + + + + ); + } + + return null; + } +} diff --git a/app/components/options_modal/options_modal_container.js b/app/components/options_modal/options_modal_container.js new file mode 100644 index 000000000..06b567d3a --- /dev/null +++ b/app/components/options_modal/options_modal_container.js @@ -0,0 +1,25 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; + +import {closeModal} from 'app/actions/views/options_modal'; +import OptionsModal from './options_modal'; + +function mapStateToProps(state, ownProps) { + return { + ...ownProps, + ...state.views.optionsModal + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + closeModal + }, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(OptionsModal); diff --git a/app/constants/view.js b/app/constants/view.js index fd5962dcb..be74411aa 100644 --- a/app/constants/view.js +++ b/app/constants/view.js @@ -9,8 +9,9 @@ const ViewTypes = keyMirror({ LOGIN_ID_CHANGED: null, PASSWORD_CHANGED: null, - TOGGLE_CHANNEL_DRAWER: null, - POST_DRAFT_CHANGED: null + POST_DRAFT_CHANGED: null, + + OPTIONS_MODAL_CHANGED: null }); export default ViewTypes; diff --git a/app/initial_state.js b/app/initial_state.js index bcfde8ba6..baac3e574 100644 --- a/app/initial_state.js +++ b/app/initial_state.js @@ -271,6 +271,11 @@ const state = { loginId: '', password: '' }, + optionsModal: { + title: '', + options: [], + visible: false + }, selectServer: { serverUrl: Config.DefaultServerUrl } diff --git a/app/navigation/router.js b/app/navigation/router.js index 7c5c2d48e..9bab52154 100644 --- a/app/navigation/router.js +++ b/app/navigation/router.js @@ -13,12 +13,14 @@ import {bindActionCreators} from 'redux'; import {closeDrawers, goBack} from 'app/actions/navigation'; import Drawer from 'app/components/drawer'; import FormattedText from 'app/components/formatted_text'; +import OptionsModal from 'app/components/options_modal'; import {RouteTransitions} from 'app/navigation/routes'; import {getComponentForScene} from 'app/scenes'; class Router extends React.Component { static propTypes = { navigation: React.PropTypes.object, + modalVisible: React.PropTypes.bool.isRequired, actions: React.PropTypes.shape({ closeDrawers: React.PropTypes.func.isRequired, goBack: React.PropTypes.func.isRequired @@ -97,7 +99,7 @@ class Router extends React.Component { const SceneComponent = getComponentForScene(route.key); return ; - } + }; configureTransition = () => { return { @@ -116,6 +118,8 @@ class Router extends React.Component { let leftDrawerContent; if (leftDrawerRoute) { + // TODO: We should make it so that this is availabe once we login + // son when sliding it renders correctly without the neeed to open using the top-bar leftDrawerContent = this.renderRoute(leftDrawerRoute); } @@ -124,19 +128,28 @@ class Router extends React.Component { rightDrawerContent = this.renderRoute(rightDrawerRoute); } + const {modalVisible} = this.props; + return ( + ); @@ -155,8 +169,10 @@ class Router extends React.Component { } function mapStateToProps(state) { + const modalVisible = state.views.optionsModal.visible; return { - navigation: state.navigation + navigation: state.navigation, + modalVisible }; } diff --git a/app/reducers/views/index.js b/app/reducers/views/index.js index bb9fd29c2..7a58a7c69 100644 --- a/app/reducers/views/index.js +++ b/app/reducers/views/index.js @@ -6,11 +6,13 @@ import {combineReducers} from 'redux'; import channel from './channel'; import i18n from './i18n'; import login from './login'; +import optionsModal from './options_modal'; import selectServer from './select_server'; export default combineReducers({ channel, i18n, login, + optionsModal, selectServer }); diff --git a/app/reducers/views/options_modal.js b/app/reducers/views/options_modal.js new file mode 100644 index 000000000..2ef212b09 --- /dev/null +++ b/app/reducers/views/options_modal.js @@ -0,0 +1,46 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {combineReducers} from 'redux'; + +import {UsersTypes} from 'service/constants'; +import {ViewTypes} from 'app/constants'; + +function title(state = '', action) { + switch (action.type) { + case ViewTypes.OPTIONS_MODAL_CHANGED: + return action.data.title; + case UsersTypes.LOGOUT_SUCCESS: + return ''; + default: + return state; + } +} + +function options(state = [], action) { + switch (action.type) { + case ViewTypes.OPTIONS_MODAL_CHANGED: + return action.data.options; + case UsersTypes.LOGOUT_SUCCESS: + return []; + default: + return state; + } +} + +function visible(state = false, action) { + switch (action.type) { + case ViewTypes.OPTIONS_MODAL_CHANGED: + return action.data.visible; + case UsersTypes.LOGOUT_SUCCESS: + return false; + default: + return state; + } +} + +export default combineReducers({ + title, + options, + visible +}); diff --git a/app/scenes/channel_drawer/channel_drawer.js b/app/scenes/channel_drawer/channel_drawer.js index 3bbbdbcb8..aa895ae7f 100644 --- a/app/scenes/channel_drawer/channel_drawer.js +++ b/app/scenes/channel_drawer/channel_drawer.js @@ -3,15 +3,13 @@ import React from 'react'; -import ChannelList from 'app/components/channel_drawer/channel_list'; +import ChannelList from 'app/components/channel_list'; export default class ChannelDrawer extends React.PureComponent { static propTypes = { actions: React.PropTypes.shape({ - selectChannel: React.PropTypes.func.isRequired, - viewChannel: React.PropTypes.func.isRequired, - closeDMChannel: React.PropTypes.func.isRequired, - closeDrawers: React.PropTypes.func.isRequired + closeDrawers: React.PropTypes.func.isRequired, + selectChannel: React.PropTypes.func.isRequired }).isRequired, currentTeam: React.PropTypes.object, currentChannel: React.PropTypes.object, @@ -23,7 +21,7 @@ export default class ChannelDrawer extends React.PureComponent { selectChannel = (id) => { this.props.actions.selectChannel(id); this.props.actions.closeDrawers(); - } + }; render() { const { @@ -42,8 +40,6 @@ export default class ChannelDrawer extends React.PureComponent { channelMembers={channelMembers} theme={theme} onSelectChannel={this.selectChannel} - onViewChannel={this.props.actions.viewChannel} - handleCloseDM={this.props.actions.closeDMChannel} /> ); } diff --git a/app/scenes/channel_drawer/channel_drawer_container.js b/app/scenes/channel_drawer/channel_drawer_container.js index cd60ec371..032504df7 100644 --- a/app/scenes/channel_drawer/channel_drawer_container.js +++ b/app/scenes/channel_drawer/channel_drawer_container.js @@ -5,7 +5,7 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {closeDrawers} from 'app/actions/navigation'; -import {closeDMChannel} from 'app/actions/views/channel'; +import {closeDMChannel, leaveChannel, markFavorite, unmarkFavorite} from 'app/actions/views/channel'; import {selectChannel, viewChannel} from 'service/actions/channels'; import {getChannelsByCategory, getCurrentChannel} from 'service/selectors/entities/channels'; @@ -30,7 +30,10 @@ function mapDispatchToProps(dispatch) { selectChannel, viewChannel, closeDMChannel, - closeDrawers + closeDrawers, + leaveChannel, + markFavorite, + unmarkFavorite }, dispatch) }; } diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 41c722199..3d12a51c1 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -959,6 +959,8 @@ "calling_screen": "Calling", "center_panel.recent": "Click here to jump to recent messages. ", "channel_header.addMembers": "Add Members", + "channelHeader.addToFavorites": "Add to Favorites", + "channelHeader.removeFromFavorites": "Remove from Favorites", "change_url.close": "Close", "change_url.endWithLetter": "Must end with a letter or number", "change_url.invalidUrl": "Invalid URL", @@ -1480,10 +1482,15 @@ "member_list.noUsersAdd": "No users to add.", "members_popover.msg": "Message", "members_popover.title": "Members", - "mobile.channel_list.alertTitleCloseDM": "Close Direct Message", - "mobile.channel_list.alertMessageCloseDM": "Are you sure you want to close the DM with {username}", + "mobile.channel_list.alertTitleLeaveChannel": "Leave {term}", + "mobile.channel_list.alertMessageLeaveChannel": "Are you sure you want to leave the {term} with {name}?", "mobile.channel_list.alertNo": "No", "mobile.channel_list.alertYes": "Yes", + "mobile.channel_list.open": "Open {term}", + "mobile.channel_list.openDM": "Open Direct Message", + "mobile.channel_list.dm": "Direct Message", + "mobile.channel_list.privateChannel": "Private Channel", + "mobile.channel_list.publicChannel": "Public Channel", "mobile.components.channels_list_view.yourChannels": "Your channels:", "mobile.components.select_server_view.enterServerUrl": "Enter Server URL", "mobile.components.select_server_view.continue": "Continue", diff --git a/test/app/actions/options_modal.js b/test/app/actions/options_modal.js new file mode 100644 index 000000000..2e9339553 --- /dev/null +++ b/test/app/actions/options_modal.js @@ -0,0 +1,30 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import assert from 'assert'; + +import * as Actions from 'app/actions/views/options_modal'; +import configureStore from 'app/store'; + +describe('Actions.Views.OptionsModal', () => { + let store; + beforeEach(() => { + store = configureStore(); + }); + + it('openModal', async () => { + await Actions.openModal('title', [1, 2, 3])(store.dispatch, store.getState); + const {optionsModal} = store.getState().views; + assert.equal('title', optionsModal.title); + assert.deepEqual([1, 2, 3], optionsModal.options); + assert.ok(optionsModal.visible); + }); + + it('closeModal', async () => { + await Actions.closeModal()(store.dispatch, store.getState); + const {optionsModal} = store.getState().views; + assert.equal('', optionsModal.title); + assert.deepEqual([], optionsModal.options); + assert.ifError(optionsModal.visible); + }); +});