MM-11603 Add a retry screen when fetching teams fails (#2085)

This commit is contained in:
Sudheer 2018-09-07 05:05:50 +05:30 committed by Elias Nahum
parent b1dff8aa04
commit c6ea5e6024
8 changed files with 401 additions and 7 deletions

View file

@ -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);
}

View file

@ -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;

View file

@ -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;

View file

@ -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__): <ErrorTeamsList
actions={
Object {
"connection": [Function],
"loadMe": [Function],
"logout": [Function],
"selectDefaultTeam": [Function],
}
}
navigator={
Object {
"setOnNavigatorEvent": [Function],
}
}
theme={Object {}}
/>,
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 [
<Connect(StatusBar) />,
<FailedNetworkAction
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 {}}
/>,
],
"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 [
<Connect(StatusBar) />,
<FailedNetworkAction
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 {}}
/>,
],
"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,
},
},
},
}
`;

View file

@ -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 <Loading/>;
}
return (
<View style={styles.container}>
<StatusBar/>
<FailedNetworkAction
onRetry={this.getUserInfo}
theme={theme}
errorTitle={errorTitle}
errorDescription={errorDescription}
/>
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
backgroundColor: theme.centerChannelBg,
flex: 1,
},
};
});

View file

@ -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(
<ErrorTeamsList {...baseProps}/>
);
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(
<ErrorTeamsList {...newProps}/>
);
wrapper.find(FailedNetworkAction).props().onRetry();
await loadMe();
expect(selectDefaultTeam).toHaveBeenCalledTimes(1);
});
});

View file

@ -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);

View file

@ -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);
}