diff --git a/src/Pim/PimMain.tsx b/src/Pim/PimMain.tsx index e5cdf91..6067001 100644 --- a/src/Pim/PimMain.tsx +++ b/src/Pim/PimMain.tsx @@ -25,6 +25,8 @@ import { EventType, ContactType, TaskType, PimType } from '../pim-types'; import { routeResolver } from '../App'; import { historyPersistor } from '../persist-state-history'; +import { SyncInfo } from '../SyncGate'; +import { UserInfoData, CredentialsData } from '../store'; const addressBookTitle = 'Address Book'; const calendarTitle = 'Calendar'; @@ -41,6 +43,9 @@ interface PropsType { theme: Theme; collectionsTaskList: EteSync.CollectionInfo[]; onItemSave: (item: PimType, journalUid: string, originalItem?: PimType) => Promise; + syncInfo: SyncInfo; + userInfo: UserInfoData; + etesync: CredentialsData; } class PimMain extends React.PureComponent { @@ -153,6 +158,9 @@ class PimMain extends React.PureComponent { collections={this.props.collectionsTaskList} onItemClick={this.taskClicked} onItemSave={this.props.onItemSave} + syncInfo={this.props.syncInfo} + userInfo={this.props.userInfo} + etesync={this.props.etesync} /> } diff --git a/src/Pim/index.tsx b/src/Pim/index.tsx index 7734879..026b33f 100644 --- a/src/Pim/index.tsx +++ b/src/Pim/index.tsx @@ -329,6 +329,9 @@ class Pim extends React.PureComponent { history={history} onItemSave={this.onItemSave} collectionsTaskList={collectionsTaskList} + syncInfo={this.props.syncInfo} + userInfo={this.props.userInfo} + etesync={this.props.etesync} /> )} /> diff --git a/src/components/Tasks/TaskList.tsx b/src/components/Tasks/TaskList.tsx index c3e1d9d..feb742d 100644 --- a/src/components/Tasks/TaskList.tsx +++ b/src/components/Tasks/TaskList.tsx @@ -6,14 +6,14 @@ import * as React from 'react'; import * as EteSync from 'etesync'; import { List } from '../../widgets/List'; -import Toast from '../../widgets/Toast'; +import Toast, { PropsType as ToastProps } from '../../widgets/Toast'; import { TaskType, PimType, TaskStatusType } from '../../pim-types'; import Divider from '@material-ui/core/Divider'; import Grid from '@material-ui/core/Grid'; import { useTheme, makeStyles } from '@material-ui/core/styles'; -import { useSelector } from 'react-redux'; +import { useSelector, useDispatch } from 'react-redux'; import Fuse from 'fuse.js'; @@ -22,8 +22,12 @@ import Sidebar from './Sidebar'; import Toolbar from './Toolbar'; import QuickAdd from './QuickAdd'; -import { StoreState } from '../../store'; +import { StoreState, UserInfoData, CredentialsData } from '../../store'; import { formatDate } from '../../helpers'; +import { SyncInfo } from '../../SyncGate'; +import { fetchEntries } from '../../store/actions'; +import { Action } from 'redux-actions'; +import { addJournalEntries } from '../../etesync-helpers'; function sortCompleted(a: TaskType, b: TaskType) { return (!!a.finished === !!b.finished) ? 0 : (a.finished) ? 1 : -1; @@ -104,30 +108,71 @@ interface PropsType { collections: EteSync.CollectionInfo[]; onItemClick: (entry: TaskType) => void; onItemSave: (item: PimType, journalUid: string, originalItem?: PimType) => Promise; + syncInfo: SyncInfo; + userInfo: UserInfoData; + etesync: CredentialsData; } export default function TaskList(props: PropsType) { const [showCompleted, setShowCompleted] = React.useState(false); const [showHidden, setShowHidden] = React.useState(false); const [searchTerm, setSearchTerm] = React.useState(''); - const [info, setInfo] = React.useState(''); + const [toast, setToast] = React.useState<{ message: string, severity: ToastProps['severity'] }>({ message: '', severity: undefined }); const settings = useSelector((state: StoreState) => state.settings.taskSettings); const { filterBy, sortBy } = settings; const theme = useTheme(); const classes = useStyles(); + const dispatch = useDispatch(); - const handleToggleComplete = async (task: TaskType, completed: boolean) => { + const handleToggleComplete = (task: TaskType, completed: boolean) => { const clonedTask = task.clone(); clonedTask.status = completed ? TaskStatusType.Completed : TaskStatusType.NeedsAction; const nextTask = completed ? task.getNextOccurence() : null; - await props.onItemSave(clonedTask, (task as any).journalUid, task); + const syncJournal = props.syncInfo.get((task as any).journalUid); - if (nextTask) { - await props.onItemSave(nextTask, (task as any).journalUid); - setInfo(`${nextTask.title} rescheduled for ${formatDate(nextTask.startDate ?? nextTask.dueDate)}`); + if (syncJournal === undefined) { + setToast({ message: 'Could not sync.', severity: 'error' }); + return; } + + const journal = syncJournal.journal; + + let prevUid: string | null = null; + let last = syncJournal.journalEntries.last() as EteSync.Entry; + if (last) { + prevUid = last.uid; + } + + dispatch(fetchEntries(props.etesync, journal.uid, prevUid)) + .then((entriesAction: Action) => { + last = entriesAction.payload!.slice(-1).pop() as EteSync.Entry; + + if (last) { + prevUid = last.uid; + } + + const changeTask = [EteSync.SyncEntryAction.Change, clonedTask.toIcal()]; + + const updates = []; + updates.push(changeTask as [EteSync.SyncEntryAction, string]); + + if (nextTask) { + const addNextTask = [EteSync.SyncEntryAction.Add, nextTask.toIcal()]; + updates.push(addNextTask as [EteSync.SyncEntryAction, string]); + } + + return dispatch(addJournalEntries(props.etesync, props.userInfo, journal, prevUid, updates)); + }) + .then(() => { + if (nextTask) { + setToast({ message: `${nextTask.title} rescheduled for ${formatDate(nextTask.startDate ?? nextTask.dueDate)}`, severity: 'success' }); + } + }) + .catch(() => { + setToast({ message: 'Failed to save changes. This may be due to a network error.', severity: 'error' }); + }); }; const potentialEntries = React.useMemo( @@ -212,8 +257,8 @@ export default function TaskList(props: PropsType) { - setInfo('')} autoHideDuration={3000}> - {info} + setToast({ message: '', severity: undefined })} autoHideDuration={3000}> + {toast.message} ); diff --git a/src/etesync-helpers.ts b/src/etesync-helpers.ts index 9658ee5..993398c 100644 --- a/src/etesync-helpers.ts +++ b/src/etesync-helpers.ts @@ -49,3 +49,25 @@ export function addJournalEntry( const entry = createJournalEntry(etesync, userInfo, journal, prevUid, action, content); return addEntries(etesync, journal.uid, [entry], prevUid); } + +/** + * Adds multiple journal entries and uploads them all at once + * @param updates list of tuples with shape (action, content) + */ +export function addJournalEntries( + etesync: CredentialsData, + userInfo: UserInfoData, + journal: EteSync.Journal, + lastUid: string | null, + updates: [EteSync.SyncEntryAction, string][]) { + + let prevUid = lastUid; + + const entries = updates.map(([action, content]) => { + const entry = createJournalEntry(etesync, userInfo, journal, prevUid, action, content); + prevUid = entry.uid; + return entry; + }); + + return addEntries(etesync, journal.uid, entries, lastUid); +} diff --git a/src/widgets/Toast.tsx b/src/widgets/Toast.tsx index 9b65060..9894070 100644 --- a/src/widgets/Toast.tsx +++ b/src/widgets/Toast.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import Snackbar from '@material-ui/core/Snackbar'; import Alert from '@material-ui/lab/Alert'; -interface PropsType { +export interface PropsType { open: boolean; children: React.ReactNode; onClose?: (event?: React.SyntheticEvent, reason?: string) => void; @@ -19,7 +19,7 @@ export default function Toast(props: PropsType) { return ( - + {children}