Implement journal update.

master
Tom Hacohen 6 years ago
parent b583347473
commit 5b79e0f107

@ -40,11 +40,6 @@ const muiTheme = createMuiTheme({
} }
}); });
export let appBarPortals = {
'title': null as Element | null,
'buttons': null as Element | null,
};
export const routeResolver = new RouteResolver({ export const routeResolver = new RouteResolver({
home: '', home: '',
pim: { pim: {
@ -68,6 +63,7 @@ export const routeResolver = new RouteResolver({
journals: { journals: {
_id: { _id: {
_base: ':journalUid', _base: ':journalUid',
edit: 'edit',
items: { items: {
_id: { _id: {
_base: ':itemUid', _base: ':itemUid',
@ -79,6 +75,7 @@ export const routeResolver = new RouteResolver({
}, },
}, },
}, },
new: 'new',
}, },
}); });

@ -1,7 +1,9 @@
import * as React from 'react'; import * as React from 'react';
import IconButton from '@material-ui/core/IconButton';
import Tab from '@material-ui/core/Tab'; import Tab from '@material-ui/core/Tab';
import Tabs from '@material-ui/core/Tabs'; import Tabs from '@material-ui/core/Tabs';
import { Theme, withTheme } from '@material-ui/core/styles'; import { Theme, withTheme } from '@material-ui/core/styles';
import IconEdit from '@material-ui/icons/Edit';
import SearchableAddressBook from '../components/SearchableAddressBook'; import SearchableAddressBook from '../components/SearchableAddressBook';
import Contact from '../components/Contact'; import Contact from '../components/Contact';
@ -16,15 +18,16 @@ import journalView from './journalView';
import { syncEntriesToItemMap, syncEntriesToCalendarItemMap } from '../journal-processors'; import { syncEntriesToItemMap, syncEntriesToCalendarItemMap } from '../journal-processors';
import { SyncInfo } from '../SyncGate'; import { SyncInfo, SyncInfoJournal } from '../SyncGate';
import { match } from 'react-router'; import { Link } from 'react-router-dom';
import { routeResolver } from '../App';
import { historyPersistor } from '../persist-state-history'; import { historyPersistor } from '../persist-state-history';
interface PropsType { interface PropsType {
syncInfo: SyncInfo; syncInfo: SyncInfo;
match: match<any>; syncJournal: SyncInfoJournal;
} }
interface PropsTypeInner extends PropsType { interface PropsTypeInner extends PropsType {
@ -49,15 +52,9 @@ class Journal extends React.PureComponent<PropsTypeInner> {
} }
render() { render() {
const { theme } = this.props; const { theme, syncJournal } = this.props;
let currentTab = this.state.tab; let currentTab = this.state.tab;
let journalOnly = false; let journalOnly = false;
const journalUid = this.props.match.params.journalUid;
const syncJournal = this.props.syncInfo.get(journalUid);
if (!syncJournal) {
return (<div>Journal not found!</div>);
}
const journal = syncJournal.journal; const journal = syncJournal.journal;
const collectionInfo = syncJournal.collection; const collectionInfo = syncJournal.collection;
@ -87,7 +84,15 @@ class Journal extends React.PureComponent<PropsTypeInner> {
return ( return (
<React.Fragment> <React.Fragment>
<AppBarOverride title={collectionInfo.displayName} /> <AppBarOverride title={collectionInfo.displayName}>
<IconButton
component={Link}
title="Edit"
{...{to: routeResolver.getRoute('journals._id.edit', { journalUid: journal.uid })}}
>
<IconEdit />
</IconButton>
</AppBarOverride>
<Tabs <Tabs
fullWidth={true} fullWidth={true}
style={{ backgroundColor: theme.palette.primary.main }} style={{ backgroundColor: theme.palette.primary.main }}

@ -0,0 +1,160 @@
import * as React from 'react';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
import { Theme, withTheme } from '@material-ui/core/styles';
import IconDelete from '@material-ui/icons/Delete';
import IconCancel from '@material-ui/icons/Clear';
import IconSave from '@material-ui/icons/Save';
import * as colors from '@material-ui/core/colors';
import AppBarOverride from '../widgets/AppBarOverride';
import Container from '../widgets/Container';
import ConfirmationDialog from '../widgets/ConfirmationDialog';
import * as EteSync from '../api/EteSync';
import { SyncInfo } from '../SyncGate';
import { handleInputChange } from '../helpers';
interface PropsType {
syncInfo: SyncInfo;
item?: EteSync.CollectionInfo;
onSave: (info: EteSync.CollectionInfo, originalInfo?: EteSync.CollectionInfo) => void;
onDelete: (info: EteSync.CollectionInfo) => void;
onCancel: () => void;
}
interface PropsTypeInner extends PropsType {
theme: Theme;
}
class JournalEdit extends React.PureComponent<PropsTypeInner> {
state = {
info: {
uid: '',
type: '',
displayName: '',
description: '',
} as EteSync.CollectionInfo,
showDeleteDialog: false,
};
private handleInputChange: any;
constructor(props: PropsTypeInner) {
super(props);
this.handleInputChange = handleInputChange(this, 'info');
this.onSubmit = this.onSubmit.bind(this);
this.onDeleteRequest = this.onDeleteRequest.bind(this);
if (this.props.item !== undefined) {
const collection = this.props.item;
this.state.info = {...collection};
} else {
this.state.info.uid = EteSync.genUid();
// FIXME: set the type
}
}
render() {
const { item, onDelete, onCancel } = this.props;
const pageTitle = (item !== undefined) ? item.displayName : 'New Journal';
const styles = {
fullWidth: {
width: '100%',
},
submit: {
marginTop: 40,
marginBottom: 20,
textAlign: 'right' as any,
},
};
return (
<>
<AppBarOverride title={pageTitle} />
<Container style={{maxWidth: 400}}>
<form onSubmit={this.onSubmit}>
<TextField
name="displayName"
label="Display name of this collection"
value={this.state.info.displayName}
onChange={this.handleInputChange}
style={styles.fullWidth}
margin="normal"
/>
<TextField
name="description"
label="Description (optional)"
value={this.state.info.description}
onChange={this.handleInputChange}
style={styles.fullWidth}
margin="normal"
/>
<div style={styles.submit}>
<Button
variant="raised"
onClick={onCancel}
>
<IconCancel style={{marginRight: 8}} />
Cancel
</Button>
{this.props.item &&
<Button
variant="raised"
style={{marginLeft: 15, backgroundColor: colors.red[500], color: 'white'}}
onClick={this.onDeleteRequest}
>
<IconDelete style={{marginRight: 8}} />
Delete
</Button>
}
<Button
type="submit"
variant="raised"
color="secondary"
style={{marginLeft: 15}}
>
<IconSave style={{marginRight: 8}} />
Save
</Button>
</div>
</form>
</Container>
<ConfirmationDialog
title="Delete Confirmation"
labelOk="Delete"
open={this.state.showDeleteDialog}
onOk={() => onDelete(this.props.item!)}
onCancel={() => this.setState({showDeleteDialog: false})}
>
Are you sure you would like to delete this journal?
</ConfirmationDialog>
</>
);
}
private onSubmit(e: React.FormEvent<any>) {
e.preventDefault();
const { onSave } = this.props;
const item = new EteSync.CollectionInfo(this.state.info);
onSave(item, this.props.item);
}
private onDeleteRequest() {
this.setState({
showDeleteDialog: true
});
}
}
export default withTheme()(JournalEdit);

@ -3,14 +3,18 @@ import { Location, History } from 'history';
import { Route, Switch } from 'react-router'; import { Route, Switch } from 'react-router';
import Journal from './Journal'; import Journal from './Journal';
import JournalEdit from './JournalEdit';
import JournalsList from './JournalsList'; import JournalsList from './JournalsList';
import AppBarOverride from '../widgets/AppBarOverride'; import AppBarOverride from '../widgets/AppBarOverride';
import { routeResolver } from '../App'; import { routeResolver } from '../App';
import { JournalsData, UserInfoData, CredentialsData } from '../store'; import { store, JournalsData, UserInfoData, CredentialsData } from '../store';
import { createJournal, updateJournal } from '../store/actions';
import { SyncInfo } from '../SyncGate'; import { SyncInfo } from '../SyncGate';
import * as EteSync from '../api/EteSync';
class Journals extends React.PureComponent { class Journals extends React.PureComponent {
props: { props: {
etesync: CredentialsData; etesync: CredentialsData;
@ -23,6 +27,9 @@ class Journals extends React.PureComponent {
constructor(props: any) { constructor(props: any) {
super(props); super(props);
this.onCancel = this.onCancel.bind(this);
this.onItemDelete = this.onItemDelete.bind(this);
this.onItemSave = this.onItemSave.bind(this);
} }
render() { render() {
@ -45,15 +52,68 @@ class Journals extends React.PureComponent {
/> />
<Route <Route
path={routeResolver.getRoute('journals._id')} path={routeResolver.getRoute('journals._id')}
render={({match}) => ( render={({match}) => {
const journalUid = match.params.journalUid;
const syncJournal = this.props.syncInfo.get(journalUid);
if (!syncJournal) {
return (<div>Journal not found!</div>);
}
const collectionInfo = syncJournal.collection;
return (
<Switch>
<Route
path={routeResolver.getRoute('journals._id.edit')}
render={() => (
<JournalEdit
syncInfo={this.props.syncInfo}
item={collectionInfo}
onSave={this.onItemSave}
onDelete={this.onItemDelete}
onCancel={this.onCancel}
/>
)}
/>
<Route
path={routeResolver.getRoute('journals._id')}
render={() => (
<Journal <Journal
syncInfo={this.props.syncInfo} syncInfo={this.props.syncInfo}
match={match} syncJournal={syncJournal}
/> />
)} )}
/> />
</Switch> </Switch>
); );
}}
/>
</Switch>
);
}
onItemSave(info: EteSync.CollectionInfo, originalInfo?: EteSync.CollectionInfo) {
const journal = new EteSync.Journal();
const cryptoManager = new EteSync.CryptoManager(this.props.etesync.encryptionKey, info.uid);
journal.setInfo(cryptoManager, info);
if (originalInfo) {
store.dispatch<any>(updateJournal(this.props.etesync, journal)).then(() =>
this.props.history.goBack()
);
} else {
store.dispatch<any>(createJournal(this.props.etesync, journal)).then(() =>
this.props.history.goBack()
);
}
}
onItemDelete(info: EteSync.CollectionInfo) {
return;
}
onCancel() {
this.props.history.goBack();
} }
} }

@ -0,0 +1,49 @@
import * as React from 'react';
// Generic handling of input changes
export function handleInputChange(self: React.Component, part?: string) {
return (event: React.ChangeEvent<any>) => {
const name = event.target.name;
const value = event.target.value;
let newState;
if (event.target.type === 'checkbox') {
newState = {
[name]: event.target.checked,
};
} else {
newState = {
[name]: value,
};
}
if (part === undefined) {
self.setState(newState);
} else {
self.setState({
[part]: {
...self.state[part],
...newState,
},
});
}
};
}
export function insertSorted<T>(array: T[] = [], newItem: T, key: string) {
if (array.length === 0) {
return [newItem];
}
for (let i = 0, len = array.length; i < len; i++) {
if (newItem[key] < array[i][key]) {
array.splice(i, 0, newItem);
return array;
}
}
array.push(newItem);
return array;
}

@ -68,6 +68,20 @@ export const createJournal = createAction(
}, },
); );
export const updateJournal = createAction(
'UPDATE_JOURNAL',
(etesync: CredentialsData, journal: EteSync.Journal) => {
const creds = etesync.credentials;
const apiBase = etesync.serviceApiUrl;
let journalManager = new EteSync.JournalManager(creds, apiBase);
return journalManager.update(journal);
},
(etesync: CredentialsData, journal: EteSync.Journal) => {
return { journal };
},
);
export const { fetchEntries, createEntries } = createActions({ export const { fetchEntries, createEntries } = createActions({
FETCH_ENTRIES: [ FETCH_ENTRIES: [
(etesync: CredentialsData, journalUid: string, prevUid: string | null) => { (etesync: CredentialsData, journalUid: string, prevUid: string | null) => {

@ -185,6 +185,36 @@ const journals = handleActions(
oldJournalHash[x.uid] = x.serialize(); oldJournalHash[x.uid] = x.serialize();
}); });
if (newJournals.every((journal: EteSync.Journal) => (
(journal.uid in oldJournalHash) &&
(journal.serialize().content === oldJournalHash[journal.uid].content)
))) {
return state;
} else {
return newState;
}
},
[actions.updateJournal.toString()]: (state: JournalsTypeImmutable, _action: any) => {
const action = { ..._action };
if (action.payload) {
action.payload = (action.meta === undefined) ? action.payload : action.meta.journal;
action.payload = [ action.payload ];
}
const newState = fetchTypeIdentityReducer(state, action, true);
// Compare the states and see if they are really different
const oldJournals = state.get('value', null);
const newJournals = newState.get('value', null);
if (!oldJournals || !newJournals || (oldJournals.size !== newJournals.size)) {
return newState;
}
let oldJournalHash = {};
oldJournals.forEach((x) => {
oldJournalHash[x.uid] = x.serialize();
});
if (newJournals.every((journal: EteSync.Journal) => ( if (newJournals.every((journal: EteSync.Journal) => (
(journal.uid in oldJournalHash) && (journal.uid in oldJournalHash) &&
(journal.serialize().content === oldJournalHash[journal.uid].content) (journal.serialize().content === oldJournalHash[journal.uid].content)

Loading…
Cancel
Save