Tasks: batched uploads for recurring task completion
parent
de94a02025
commit
508d02a0ea
|
@ -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<void>;
|
||||
syncInfo: SyncInfo;
|
||||
userInfo: UserInfoData;
|
||||
etesync: CredentialsData;
|
||||
}
|
||||
|
||||
class PimMain extends React.PureComponent<PropsType> {
|
||||
|
@ -153,6 +158,9 @@ class PimMain extends React.PureComponent<PropsType> {
|
|||
collections={this.props.collectionsTaskList}
|
||||
onItemClick={this.taskClicked}
|
||||
onItemSave={this.props.onItemSave}
|
||||
syncInfo={this.props.syncInfo}
|
||||
userInfo={this.props.userInfo}
|
||||
etesync={this.props.etesync}
|
||||
/>
|
||||
}
|
||||
</Container>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -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<void>;
|
||||
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<any>(fetchEntries(props.etesync, journal.uid, prevUid))
|
||||
.then((entriesAction: Action<EteSync.Entry[]>) => {
|
||||
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) {
|
|||
</List>
|
||||
</Grid>
|
||||
|
||||
<Toast open={!!info} severity="info" onClose={() => setInfo('')} autoHideDuration={3000}>
|
||||
{info}
|
||||
<Toast open={!!toast.message} severity={toast.severity} onClose={() => setToast({ message: '', severity: undefined })} autoHideDuration={3000}>
|
||||
{toast.message}
|
||||
</Toast>
|
||||
</Grid>
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<Snackbar open={open} onClose={onClose} autoHideDuration={autoHideDuration}>
|
||||
<Alert severity={severity ?? 'error'} variant="filled" elevation={6} onClose={onClose}>
|
||||
<Alert severity={severity} variant="filled" elevation={6} onClose={onClose}>
|
||||
{children}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
|
|
Loading…
Reference in New Issue