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 { List as ImmutableList } from 'immutable';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
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 Drawer from '@material-ui/core/Drawer';
import IconButton from '@material-ui/core/IconButton';
import Badge from '@material-ui/core/Badge';
import NavigationMenu from '@material-ui/icons/Menu';
import NavigationBack from '@material-ui/icons/ArrowBack';
import NavigationRefresh from '@material-ui/icons/Refresh';
import ErrorsIcon from '@material-ui/icons/Error';
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 ErrorBoundary from './components/ErrorBoundary';
import SideMenu from './SideMenu';
@ -160,17 +166,19 @@ const IconRefreshWithSpin = withSpin(NavigationRefresh);
class App extends React.PureComponent {
public state: {
drawerOpen: boolean;
errorsDialog: boolean;
};
public props: {
credentials: store.CredentialsType;
entries: store.EntriesType;
fetchCount: number;
errors: ImmutableList<Error>;
};
constructor(props: any) {
super(props);
this.state = { drawerOpen: false };
this.state = { drawerOpen: false, errorsDialog: false };
this.toggleDrawer = this.toggleDrawer.bind(this);
this.closeDrawer = this.closeDrawer.bind(this);
@ -180,6 +188,7 @@ class App extends React.PureComponent {
public render() {
const credentials = (this.props.credentials) ? this.props.credentials.value : null;
const errors = this.props.errors;
const fetching = this.props.fetchCount > 0;
const styles = {
@ -197,11 +206,43 @@ class App extends React.PureComponent {
<AppBarWitHistory
toggleDrawerIcon={<IconButton onClick={this.toggleDrawer}><NavigationMenu /></IconButton>}
iconElementRight={
<IconButton disabled={!credentials || fetching} onClick={this.refresh} title="Refresh">
<IconRefreshWithSpin spin={fetching} />
</IconButton>}
<>
{(errors.size > 0) && (
<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
open={this.state.drawerOpen}
onClose={this.toggleDrawer}
@ -257,6 +298,7 @@ const mapStateToProps = (state: store.StoreState) => {
credentials: credentialsSelector(state),
entries: state.cache.entries,
fetchCount: state.fetchCount,
errors: state.errors,
};
};

@ -3,6 +3,8 @@ import { List } from 'immutable';
import * as ICAL from 'ical.js';
import { EventType, ContactType, TaskType } from './pim-types';
import { store } from './store';
import { addError } from './store/actions';
import * as EteSync from 'etesync';
@ -11,7 +13,16 @@ export function syncEntriesToItemMap(
const items = base;
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
(comp as any).journalUid = collection.uid;
@ -57,7 +68,16 @@ function syncEntriesToCalendarItemMap<T extends EventType>(
const color = colorIntToHtml(collection.color);
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) {
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
export const setSettings = createAction(
'SET_SETTINGS',

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

@ -277,6 +277,19 @@ export const fetchCount = handleAction(
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
export interface SettingsType {
locale: string;

Loading…
Cancel
Save