From c6ea5e6024aa681db160e03ab5f3fde2adc70800 Mon Sep 17 00:00:00 2001 From: Sudheer Date: Fri, 7 Sep 2018 05:05:50 +0530 Subject: [PATCH] MM-11603 Add a retry screen when fetching teams fails (#2085) --- app/actions/views/select_team.js | 3 + app/components/root/root.js | 32 +++- app/constants/navigation.js | 1 + .../error_teams_list.test.js.snap | 166 ++++++++++++++++++ .../error_teams_list/error_teams_list.js | 116 ++++++++++++ .../error_teams_list/error_teams_list.test.js | 65 +++++++ app/screens/error_teams_list/index.js | 24 +++ app/screens/index.js | 1 + 8 files changed, 401 insertions(+), 7 deletions(-) create mode 100644 app/screens/error_teams_list/__snapshots__/error_teams_list.test.js.snap create mode 100644 app/screens/error_teams_list/error_teams_list.js create mode 100644 app/screens/error_teams_list/error_teams_list.test.js create mode 100644 app/screens/error_teams_list/index.js diff --git a/app/actions/views/select_team.js b/app/actions/views/select_team.js index 3e7d69f71..23ba2e8d1 100644 --- a/app/actions/views/select_team.js +++ b/app/actions/views/select_team.js @@ -7,6 +7,7 @@ import {markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/c import {ChannelTypes, TeamTypes} from 'mattermost-redux/action_types'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; +import {RequestStatus} from 'mattermost-redux/constants'; import {NavigationTypes} from 'app/constants'; @@ -56,6 +57,8 @@ export function selectDefaultTeam() { if (defaultTeam) { handleTeamChange(defaultTeam.id)(dispatch, getState); + } else if (state.requests.teams.getTeams.status === RequestStatus.FAILURE || state.requests.teams.getMyTeams.status === RequestStatus.FAILURE) { + EventEmitter.emit(NavigationTypes.NAVIGATION_ERROR_TEAMS); } else { EventEmitter.emit(NavigationTypes.NAVIGATION_NO_TEAMS); } diff --git a/app/components/root/root.js b/app/components/root/root.js index a4438a0f9..5566e7335 100644 --- a/app/components/root/root.js +++ b/app/components/root/root.js @@ -30,6 +30,7 @@ export default class Root extends PureComponent { EventEmitter.on(ViewTypes.NOTIFICATION_IN_APP, this.handleInAppNotification); EventEmitter.on(ViewTypes.NOTIFICATION_TAPPED, this.handleNotificationTapped); EventEmitter.on(NavigationTypes.NAVIGATION_NO_TEAMS, this.handleNoTeams); + EventEmitter.on(NavigationTypes.NAVIGATION_ERROR_TEAMS, this.errorTeamsList); } } @@ -44,6 +45,7 @@ export default class Root extends PureComponent { EventEmitter.off(ViewTypes.NOTIFICATION_IN_APP, this.handleInAppNotification); EventEmitter.off(ViewTypes.NOTIFICATION_TAPPED, this.handleNotificationTapped); EventEmitter.off(NavigationTypes.NAVIGATION_NO_TEAMS, this.handleNoTeams); + EventEmitter.off(NavigationTypes.NAVIGATION_ERROR_TEAMS, this.errorTeamsList); } } @@ -69,11 +71,23 @@ export default class Root extends PureComponent { setTimeout(this.handleNoTeams, 200); return; } + this.navigateToTeamsPage('SelectTeam'); + }; + errorTeamsList = () => { + if (!this.refs.provider) { + setTimeout(this.errorTeamsList, 200); + return; + } + this.navigateToTeamsPage('ErrorTeamsList'); + } + + navigateToTeamsPage = (screen) => { const {currentUrl, navigator, theme} = this.props; const {intl} = this.refs.provider.getChildContext(); let navigatorButtons; + let passProps = {theme}; if (Platform.OS === 'android') { navigatorButtons = { rightButtons: [{ @@ -93,8 +107,16 @@ export default class Root extends PureComponent { }; } + if (screen === 'SelectTeam') { + passProps = { + ...passProps, + currentUrl, + userWithoutTeams: true, + }; + } + navigator.resetTo({ - screen: 'SelectTeam', + screen, title: intl.formatMessage({id: 'mobile.routes.selectTeam', defaultMessage: 'Select Team'}), animated: false, backButtonTitle: '', @@ -105,13 +127,9 @@ export default class Root extends PureComponent { screenBackgroundColor: theme.centerChannelBg, }, navigatorButtons, - passProps: { - currentUrl, - userWithoutTeams: true, - theme, - }, + passProps, }); - }; + } handleNotificationTapped = async () => { const {navigator} = this.props; diff --git a/app/constants/navigation.js b/app/constants/navigation.js index e781be3c3..809997da3 100644 --- a/app/constants/navigation.js +++ b/app/constants/navigation.js @@ -8,6 +8,7 @@ const NavigationTypes = keyMirror({ NAVIGATION_CLOSE_MODAL: null, NAVIGATION_NO_TEAMS: null, RESTART_APP: null, + NAVIGATION_ERROR_TEAMS: null, }); export default NavigationTypes; diff --git a/app/screens/error_teams_list/__snapshots__/error_teams_list.test.js.snap b/app/screens/error_teams_list/__snapshots__/error_teams_list.test.js.snap new file mode 100644 index 000000000..ddb3c9ca7 --- /dev/null +++ b/app/screens/error_teams_list/__snapshots__/error_teams_list.test.js.snap @@ -0,0 +1,166 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ErrorTeamsList should match snapshot 1`] = ` +ShallowWrapper { + "length": 1, + Symbol(enzyme.__root__): [Circular], + Symbol(enzyme.__unrendered__): , + Symbol(enzyme.__renderer__): Object { + "batchedUpdates": [Function], + "getNode": [Function], + "render": [Function], + "simulateEvent": [Function], + "unmount": [Function], + }, + Symbol(enzyme.__node__): Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": Array [ + , + , + ], + "style": Object { + "backgroundColor": undefined, + "flex": 1, + }, + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object {}, + "ref": null, + "rendered": null, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "errorDescription": Object { + "defaultMessage": "Make sure you have an active connection and try again.", + "id": "mobile.failed_network_action.shortDescription", + }, + "errorTitle": Object { + "defaultMessage": "Team Not Found", + "id": "error.team_not_found.title", + }, + "onRetry": [Function], + "theme": Object {}, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + Symbol(enzyme.__nodes__): Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": Array [ + , + , + ], + "style": Object { + "backgroundColor": undefined, + "flex": 1, + }, + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object {}, + "ref": null, + "rendered": null, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "errorDescription": Object { + "defaultMessage": "Make sure you have an active connection and try again.", + "id": "mobile.failed_network_action.shortDescription", + }, + "errorTitle": Object { + "defaultMessage": "Team Not Found", + "id": "error.team_not_found.title", + }, + "onRetry": [Function], + "theme": Object {}, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + ], + Symbol(enzyme.__options__): Object { + "adapter": ReactSixteenAdapter { + "options": Object { + "enableComponentDidUpdateOnSetState": true, + }, + }, + }, +} +`; diff --git a/app/screens/error_teams_list/error_teams_list.js b/app/screens/error_teams_list/error_teams_list.js new file mode 100644 index 000000000..a01257b06 --- /dev/null +++ b/app/screens/error_teams_list/error_teams_list.js @@ -0,0 +1,116 @@ +// 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 { + InteractionManager, + View, +} from 'react-native'; + +import FailedNetworkAction from 'app/components/failed_network_action'; +import Loading from 'app/components/loading'; +import StatusBar from 'app/components/status_bar'; +import {makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme'; + +const errorTitle = { + id: 'error.team_not_found.title', + defaultMessage: 'Team Not Found', +}; + +const errorDescription = { + id: 'mobile.failed_network_action.shortDescription', + defaultMessage: 'Make sure you have an active connection and try again.', +}; + +export default class ErrorTeamsList extends PureComponent { + static propTypes = { + actions: PropTypes.shape({ + loadMe: PropTypes.func.isRequired, + connection: PropTypes.func.isRequired, + logout: PropTypes.func.isRequired, + selectDefaultTeam: PropTypes.func.isRequired, + }).isRequired, + navigator: PropTypes.object, + theme: PropTypes.object, + }; + + constructor(props) { + super(props); + props.navigator.setOnNavigatorEvent(this.onNavigatorEvent); + + this.state = { + loading: false, + }; + } + + componentWillReceiveProps(nextProps) { + if (this.props.theme !== nextProps.theme) { + setNavigatorStyles(this.props.navigator, nextProps.theme); + } + } + + goToChannelView = () => { + const {navigator, theme} = this.props; + + navigator.resetTo({ + screen: 'Channel', + animated: false, + navigatorStyle: { + navBarHidden: true, + statusBarHidden: false, + statusBarHideWithNavBar: false, + screenBackgroundColor: theme.centerChannelBg, + }, + }); + }; + + getUserInfo = async () => { + this.setState({loading: true}); + this.props.actions.connection(true); + await this.props.actions.loadMe(); + this.props.actions.connection(false); + this.setState({loading: false}); + this.props.actions.selectDefaultTeam(); + this.goToChannelView(); + } + + onNavigatorEvent = (event) => { + if (event.type === 'NavBarButtonPress') { + const {logout} = this.props.actions; + if (event.id === 'logout') { + InteractionManager.runAfterInteractions(logout); + } + } + }; + + render() { + const {theme} = this.props; + const styles = getStyleSheet(theme); + + if (this.state.loading) { + return ; + } + + return ( + + + + + ); + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + container: { + backgroundColor: theme.centerChannelBg, + flex: 1, + }, + }; +}); diff --git a/app/screens/error_teams_list/error_teams_list.test.js b/app/screens/error_teams_list/error_teams_list.test.js new file mode 100644 index 000000000..11a2bfc49 --- /dev/null +++ b/app/screens/error_teams_list/error_teams_list.test.js @@ -0,0 +1,65 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {configure, shallow} from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import FailedNetworkAction from 'app/components/failed_network_action'; +import ErrorTeamsList from './error_teams_list'; + +configure({adapter: new Adapter()}); + +describe('ErrorTeamsList', () => { + const navigator = { + setOnNavigatorEvent: () => {}, // eslint-disable-line no-empty-function + }; + + const loadMe = async () => { + return { + data: {}, + }; + }; + + const baseProps = { + actions: { + loadMe: () => {}, // eslint-disable-line no-empty-function + connection: () => {}, // eslint-disable-line no-empty-function + logout: () => {}, // eslint-disable-line no-empty-function + selectDefaultTeam: () => {}, // eslint-disable-line no-empty-function + }, + theme: {}, + navigator, + }; + + test('should match snapshot', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('should call for userInfo on retry', async () => { + const connection = jest.fn(); + const selectDefaultTeam = jest.fn(); + const logout = jest.fn(); + const actions = { + loadMe, + logout, + selectDefaultTeam, + connection, + }; + + const newProps = { + ...baseProps, + actions, + }; + + const wrapper = shallow( + + ); + + wrapper.find(FailedNetworkAction).props().onRetry(); + await loadMe(); + expect(selectDefaultTeam).toHaveBeenCalledTimes(1); + }); +}); diff --git a/app/screens/error_teams_list/index.js b/app/screens/error_teams_list/index.js new file mode 100644 index 000000000..31424ed03 --- /dev/null +++ b/app/screens/error_teams_list/index.js @@ -0,0 +1,24 @@ +// 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 {logout, loadMe} from 'mattermost-redux/actions/users'; +import {connection} from 'app/actions/device'; +import {selectDefaultTeam} from 'app/actions/views/select_team'; + +import ErrorTeamsList from './error_teams_list.js'; + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + logout, + selectDefaultTeam, + connection, + loadMe, + }, dispatch), + }; +} + +export default connect(null, mapDispatchToProps)(ErrorTeamsList); diff --git a/app/screens/index.js b/app/screens/index.js index 958bc1b77..c6deec65f 100644 --- a/app/screens/index.js +++ b/app/screens/index.js @@ -58,5 +58,6 @@ export function registerScreens(store, Provider) { Navigation.registerComponent('TextPreview', () => wrapWithContextProvider(require('app/screens/text_preview').default), store, Provider); Navigation.registerComponent('Thread', () => wrapWithContextProvider(require('app/screens/thread').default), store, Provider); Navigation.registerComponent('TimezoneSettings', () => wrapWithContextProvider(require('app/screens/timezone').default), store, Provider); + Navigation.registerComponent('ErrorTeamsList', () => wrapWithContextProvider(require('app/screens/error_teams_list').default), store, Provider); Navigation.registerComponent('UserProfile', () => wrapWithContextProvider(require('app/screens/user_profile').default), store, Provider); }