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
parent
c9d9802a3b
commit
9abb3dcad9
52
src/App.tsx
52
src/App.tsx
|
@ -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…
Reference in New Issue