From 5b79e0f1071db094fe69ba36b11a580dbda54e7f Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Tue, 12 Feb 2019 16:33:45 +0000 Subject: [PATCH] Implement journal update. --- src/App.tsx | 7 +- src/Journals/Journal.tsx | 27 +++--- src/Journals/JournalEdit.tsx | 160 +++++++++++++++++++++++++++++++++++ src/Journals/index.tsx | 74 ++++++++++++++-- src/helpers.tsx | 49 +++++++++++ src/store/actions.ts | 14 +++ src/store/reducers.ts | 30 +++++++ 7 files changed, 338 insertions(+), 23 deletions(-) create mode 100644 src/Journals/JournalEdit.tsx create mode 100644 src/helpers.tsx diff --git a/src/App.tsx b/src/App.tsx index a994bb0..d02c835 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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({ home: '', pim: { @@ -68,6 +63,7 @@ export const routeResolver = new RouteResolver({ journals: { _id: { _base: ':journalUid', + edit: 'edit', items: { _id: { _base: ':itemUid', @@ -79,6 +75,7 @@ export const routeResolver = new RouteResolver({ }, }, }, + new: 'new', }, }); diff --git a/src/Journals/Journal.tsx b/src/Journals/Journal.tsx index 7f028d0..f0168c5 100644 --- a/src/Journals/Journal.tsx +++ b/src/Journals/Journal.tsx @@ -1,7 +1,9 @@ import * as React from 'react'; +import IconButton from '@material-ui/core/IconButton'; import Tab from '@material-ui/core/Tab'; import Tabs from '@material-ui/core/Tabs'; import { Theme, withTheme } from '@material-ui/core/styles'; +import IconEdit from '@material-ui/icons/Edit'; import SearchableAddressBook from '../components/SearchableAddressBook'; import Contact from '../components/Contact'; @@ -16,15 +18,16 @@ import journalView from './journalView'; 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'; interface PropsType { syncInfo: SyncInfo; - match: match; + syncJournal: SyncInfoJournal; } interface PropsTypeInner extends PropsType { @@ -49,15 +52,9 @@ class Journal extends React.PureComponent { } render() { - const { theme } = this.props; + const { theme, syncJournal } = this.props; let currentTab = this.state.tab; let journalOnly = false; - const journalUid = this.props.match.params.journalUid; - - const syncJournal = this.props.syncInfo.get(journalUid); - if (!syncJournal) { - return (
Journal not found!
); - } const journal = syncJournal.journal; const collectionInfo = syncJournal.collection; @@ -87,7 +84,15 @@ class Journal extends React.PureComponent { return ( - + + + + + void; + onDelete: (info: EteSync.CollectionInfo) => void; + onCancel: () => void; +} + +interface PropsTypeInner extends PropsType { + theme: Theme; +} + +class JournalEdit extends React.PureComponent { + 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 ( + <> + + +
+ + + +
+ + + {this.props.item && + + } + + +
+ +
+ onDelete(this.props.item!)} + onCancel={() => this.setState({showDeleteDialog: false})} + > + Are you sure you would like to delete this journal? + + + ); + } + + private onSubmit(e: React.FormEvent) { + 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); diff --git a/src/Journals/index.tsx b/src/Journals/index.tsx index e441837..00a976a 100644 --- a/src/Journals/index.tsx +++ b/src/Journals/index.tsx @@ -3,14 +3,18 @@ import { Location, History } from 'history'; import { Route, Switch } from 'react-router'; import Journal from './Journal'; +import JournalEdit from './JournalEdit'; import JournalsList from './JournalsList'; import AppBarOverride from '../widgets/AppBarOverride'; 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 * as EteSync from '../api/EteSync'; + class Journals extends React.PureComponent { props: { etesync: CredentialsData; @@ -23,6 +27,9 @@ class Journals extends React.PureComponent { constructor(props: any) { super(props); + this.onCancel = this.onCancel.bind(this); + this.onItemDelete = this.onItemDelete.bind(this); + this.onItemSave = this.onItemSave.bind(this); } render() { @@ -45,16 +52,69 @@ class Journals extends React.PureComponent { /> ( - - )} + render={({match}) => { + const journalUid = match.params.journalUid; + + const syncJournal = this.props.syncInfo.get(journalUid); + if (!syncJournal) { + return (
Journal not found!
); + } + + const collectionInfo = syncJournal.collection; + return ( + + ( + + )} + /> + ( + + )} + /> + + ); + }} /> ); } + + 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(updateJournal(this.props.etesync, journal)).then(() => + this.props.history.goBack() + ); + } else { + store.dispatch(createJournal(this.props.etesync, journal)).then(() => + this.props.history.goBack() + ); + } + } + + onItemDelete(info: EteSync.CollectionInfo) { + return; + } + + onCancel() { + this.props.history.goBack(); + } } export default Journals; diff --git a/src/helpers.tsx b/src/helpers.tsx new file mode 100644 index 0000000..de456ee --- /dev/null +++ b/src/helpers.tsx @@ -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) => { + 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(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; +} diff --git a/src/store/actions.ts b/src/store/actions.ts index 44e2c31..1c12bba 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -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({ FETCH_ENTRIES: [ (etesync: CredentialsData, journalUid: string, prevUid: string | null) => { diff --git a/src/store/reducers.ts b/src/store/reducers.ts index 1971baa..d4a3dd8 100644 --- a/src/store/reducers.ts +++ b/src/store/reducers.ts @@ -185,6 +185,36 @@ const journals = handleActions( 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) => ( (journal.uid in oldJournalHash) && (journal.serialize().content === oldJournalHash[journal.uid].content)