Make parse errors non-fatal

Before this patch, parse errors would make the whole app stop and show
an error. Now we handle them more gracefully by showing the parsing
errors in a non-fatal way.

This was implemented in a hacky way, and will be changed once the web
app is refactored to better resemble the iOS app.

Fixes #48.
master
Tom Hacohen 5 years ago
parent c9d9802a3b
commit 9abb3dcad9

@ -1,4 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { List as ImmutableList } from 'immutable';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
@ -10,13 +11,18 @@ import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar'; import Toolbar from '@material-ui/core/Toolbar';
import Drawer from '@material-ui/core/Drawer'; import Drawer from '@material-ui/core/Drawer';
import IconButton from '@material-ui/core/IconButton'; import IconButton from '@material-ui/core/IconButton';
import Badge from '@material-ui/core/Badge';
import NavigationMenu from '@material-ui/icons/Menu'; import NavigationMenu from '@material-ui/icons/Menu';
import NavigationBack from '@material-ui/icons/ArrowBack'; import NavigationBack from '@material-ui/icons/ArrowBack';
import NavigationRefresh from '@material-ui/icons/Refresh'; import NavigationRefresh from '@material-ui/icons/Refresh';
import ErrorsIcon from '@material-ui/icons/Error';
import './App.css'; import './App.css';
import ConfirmationDialog from './widgets/ConfirmationDialog';
import PrettyError from './widgets/PrettyError';
import { List, ListItem } from './widgets/List';
import withSpin from './widgets/withSpin'; import withSpin from './widgets/withSpin';
import ErrorBoundary from './components/ErrorBoundary'; import ErrorBoundary from './components/ErrorBoundary';
import SideMenu from './SideMenu'; import SideMenu from './SideMenu';
@ -160,17 +166,19 @@ const IconRefreshWithSpin = withSpin(NavigationRefresh);
class App extends React.PureComponent { class App extends React.PureComponent {
public state: { public state: {
drawerOpen: boolean; drawerOpen: boolean;
errorsDialog: boolean;
}; };
public props: { public props: {
credentials: store.CredentialsType; credentials: store.CredentialsType;
entries: store.EntriesType; entries: store.EntriesType;
fetchCount: number; fetchCount: number;
errors: ImmutableList<Error>;
}; };
constructor(props: any) { constructor(props: any) {
super(props); super(props);
this.state = { drawerOpen: false }; this.state = { drawerOpen: false, errorsDialog: false };
this.toggleDrawer = this.toggleDrawer.bind(this); this.toggleDrawer = this.toggleDrawer.bind(this);
this.closeDrawer = this.closeDrawer.bind(this); this.closeDrawer = this.closeDrawer.bind(this);
@ -180,6 +188,7 @@ class App extends React.PureComponent {
public render() { public render() {
const credentials = (this.props.credentials) ? this.props.credentials.value : null; const credentials = (this.props.credentials) ? this.props.credentials.value : null;
const errors = this.props.errors;
const fetching = this.props.fetchCount > 0; const fetching = this.props.fetchCount > 0;
const styles = { const styles = {
@ -197,11 +206,43 @@ class App extends React.PureComponent {
<AppBarWitHistory <AppBarWitHistory
toggleDrawerIcon={<IconButton onClick={this.toggleDrawer}><NavigationMenu /></IconButton>} toggleDrawerIcon={<IconButton onClick={this.toggleDrawer}><NavigationMenu /></IconButton>}
iconElementRight={ iconElementRight={
<IconButton disabled={!credentials || fetching} onClick={this.refresh} title="Refresh"> <>
<IconRefreshWithSpin spin={fetching} /> {(errors.size > 0) && (
</IconButton>} <IconButton onClick={() => this.setState({ errorsDialog: true })} title="Parse Errors">
<Badge badgeContent={errors.size} color="error">
<ErrorsIcon />
</Badge>
</IconButton>
)}
<IconButton disabled={!credentials || fetching} onClick={this.refresh} title="Refresh">
<IconRefreshWithSpin spin={fetching} />
</IconButton>
</>
}
/> />
<ConfirmationDialog
title="Parse Errors"
open={this.state.errorsDialog}
labelOk="OK"
onCancel={() => this.setState({ errorsDialog: false })}
onOk={() => this.setState({ errorsDialog: false })}
>
<h4>
This should not happen, please contact developers!
</h4>
<List>
{errors.map((error, index) => (
<ListItem
key={index}
style={{ height: 'unset' }}
onClick={() => (window as any).navigator.clipboard.writeText(`${error.message}\n\n${error.stack}`)}
>
<PrettyError error={error} />
</ListItem>
))}
</List>
</ConfirmationDialog>
<Drawer <Drawer
open={this.state.drawerOpen} open={this.state.drawerOpen}
onClose={this.toggleDrawer} onClose={this.toggleDrawer}
@ -257,6 +298,7 @@ const mapStateToProps = (state: store.StoreState) => {
credentials: credentialsSelector(state), credentials: credentialsSelector(state),
entries: state.cache.entries, entries: state.cache.entries,
fetchCount: state.fetchCount, fetchCount: state.fetchCount,
errors: state.errors,
}; };
}; };

@ -3,6 +3,8 @@ import { List } from 'immutable';
import * as ICAL from 'ical.js'; import * as ICAL from 'ical.js';
import { EventType, ContactType, TaskType } from './pim-types'; import { EventType, ContactType, TaskType } from './pim-types';
import { store } from './store';
import { addError } from './store/actions';
import * as EteSync from 'etesync'; import * as EteSync from 'etesync';
@ -11,7 +13,16 @@ export function syncEntriesToItemMap(
const items = base; const items = base;
entries.forEach((syncEntry) => { entries.forEach((syncEntry) => {
const comp = new ContactType(new ICAL.Component(ICAL.parse(syncEntry.content))); // FIXME: this is a terrible hack to handle parsing errors
let parsed;
try {
parsed = ICAL.parse(syncEntry.content);
} catch (e) {
e.message = `${e.message}\nWhile processing: ${syncEntry.content}`;
store.dispatch(addError(undefined as any, e));
return;
}
const comp = new ContactType(new ICAL.Component(parsed));
// FIXME:Hack // FIXME:Hack
(comp as any).journalUid = collection.uid; (comp as any).journalUid = collection.uid;
@ -57,7 +68,16 @@ function syncEntriesToCalendarItemMap<T extends EventType>(
const color = colorIntToHtml(collection.color); const color = colorIntToHtml(collection.color);
entries.forEach((syncEntry) => { entries.forEach((syncEntry) => {
const comp = ItemType.fromVCalendar(new ICAL.Component(ICAL.parse(syncEntry.content))); // FIXME: this is a terrible hack to handle parsing errors
let parsed;
try {
parsed = ICAL.parse(syncEntry.content);
} catch (e) {
e.message = `${e.message}\nWhile processing: ${syncEntry.content}`;
store.dispatch(addError(undefined as any, e));
return;
}
const comp = ItemType.fromVCalendar(new ICAL.Component(parsed));
if (comp === null) { if (comp === null) {
return; return;

@ -185,6 +185,20 @@ export function fetchAll(etesync: CredentialsData, currentEntries: EntriesType)
}; };
} }
export const addError = createAction(
'ADD_ERRORS',
(_etesync: CredentialsData, error: Error) => {
return error;
}
);
export const clearErros = createAction(
'CLEAR_ERRORS',
(_etesync: CredentialsData) => {
return true;
}
);
// FIXME: Move the rest to their own file // FIXME: Move the rest to their own file
export const setSettings = createAction( export const setSettings = createAction(
'SET_SETTINGS', 'SET_SETTINGS',

@ -9,7 +9,7 @@ import * as EteSync from 'etesync';
import { import {
JournalsData, FetchType, EntriesData, EntriesFetchRecord, UserInfoData, JournalsFetchRecord, UserInfoFetchRecord, JournalsData, FetchType, EntriesData, EntriesFetchRecord, UserInfoData, JournalsFetchRecord, UserInfoFetchRecord,
CredentialsTypeRemote, JournalsType, EntriesType, UserInfoType, SettingsType, CredentialsTypeRemote, JournalsType, EntriesType, UserInfoType, SettingsType,
fetchCount, journals, entries, credentials, userInfo, settingsReducer, encryptionKeyReducer, fetchCount, journals, entries, credentials, userInfo, settingsReducer, encryptionKeyReducer, errorsReducer,
} from './reducers'; } from './reducers';
export interface StoreState { export interface StoreState {
@ -22,6 +22,7 @@ export interface StoreState {
entries: EntriesType; entries: EntriesType;
userInfo: UserInfoType; userInfo: UserInfoType;
}; };
errors: List<Error>;
} }
const settingsPersistConfig = { const settingsPersistConfig = {
@ -160,6 +161,7 @@ const reducers = combineReducers({
journals, journals,
userInfo, userInfo,
})), })),
errors: errorsReducer,
}); });
export default reducers; export default reducers;

@ -277,6 +277,19 @@ export const fetchCount = handleAction(
0 0
); );
export const errorsReducer = handleActions(
{
[actions.addError.toString()]: (state: List<Error>, action: Action<any>) => {
return state.push(action.payload);
},
[actions.clearErros.toString()]: (state: List<Error>, _action: Action<any>) => {
return state.clear();
},
},
List([])
);
// FIXME Move all the below (potentially the fetchCount ones too) to their own file // FIXME Move all the below (potentially the fetchCount ones too) to their own file
export interface SettingsType { export interface SettingsType {
locale: string; locale: string;

Loading…
Cancel
Save