Create tasks route.

master
Tom Hacohen 4 years ago
parent 32426b2460
commit b39f100bdb

@ -1,413 +0,0 @@
// SPDX-FileCopyrightText: © 2017 EteSync Authors
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from "react";
import { Route, Switch } from "react-router";
import Button from "@material-ui/core/Button";
import IconEdit from "@material-ui/icons/Edit";
import IconDuplicate from "@material-ui/icons/FileCopy";
import IconChangeHistory from "@material-ui/icons/ChangeHistory";
import { withStyles } from "@material-ui/core/styles";
import { RouteComponentProps, withRouter } from "react-router";
import { Action } from "redux-actions";
import * as EteSync from "etesync";
import { createSelector } from "reselect";
import { History } from "history";
import { PimType, ContactType, EventType, TaskType } from "../pim-types";
import Container from "../widgets/Container";
import JournalEntries from "../components/JournalEntries";
import TaskEdit from "../components/Tasks/TaskEdit";
import Task from "../components/Tasks/Task";
import { routeResolver } from "../App";
import { store, CredentialsData, UserInfoData } from "../store";
import { fetchEntries } from "../store/actions";
import { SyncInfo } from "../SyncGate";
import { addJournalEntry } from "../etesync-helpers";
import { syncEntriesToItemMap, syncEntriesToEventItemMap, syncEntriesToTaskItemMap } from "../journal-processors";
const itemsSelector = createSelector(
(props: {syncInfo: SyncInfo}) => props.syncInfo,
(syncInfo) => {
const collectionsAddressBook: EteSync.CollectionInfo[] = [];
const collectionsCalendar: EteSync.CollectionInfo[] = [];
const collectionsTaskList: EteSync.CollectionInfo[] = [];
let addressBookItems: {[key: string]: ContactType} = {};
let calendarItems: {[key: string]: EventType} = {};
let taskListItems: {[key: string]: TaskType} = {};
syncInfo.forEach(
(syncJournal) => {
const syncEntries = syncJournal.entries;
const collectionInfo = syncJournal.collection;
if (collectionInfo.type === "ADDRESS_BOOK") {
addressBookItems = syncEntriesToItemMap(collectionInfo, syncEntries, addressBookItems);
collectionsAddressBook.push(collectionInfo);
} else if (collectionInfo.type === "CALENDAR") {
calendarItems = syncEntriesToEventItemMap(collectionInfo, syncEntries, calendarItems);
collectionsCalendar.push(collectionInfo);
} else if (collectionInfo.type === "TASKS") {
taskListItems = syncEntriesToTaskItemMap(collectionInfo, syncEntries, taskListItems);
collectionsTaskList.push(collectionInfo);
}
}
);
return {
collectionsAddressBook, collectionsCalendar, collectionsTaskList, addressBookItems, calendarItems, taskListItems,
};
}
);
const ItemChangeLog = React.memo((props: any) => {
const {
syncInfo,
paramItemUid,
} = props;
const tmp = paramItemUid.split("|");
const journalUid = tmp.shift();
const uid = tmp.join("|");
const journalItem = syncInfo.get(journalUid);
return (
<React.Fragment>
<h2>Item Change History</h2>
<JournalEntries
journal={journalItem.journal}
entries={journalItem.entries}
uid={uid}
/>
</React.Fragment>
);
});
type CollectionRoutesPropsType = RouteComponentProps<{}> & {
syncInfo: SyncInfo;
routePrefix: string;
collections: EteSync.CollectionInfo[];
componentEdit: any;
componentView: any;
items: {[key: string]: PimType};
onItemSave: (item: PimType, journalUid: string, originalItem?: PimType) => Promise<void>;
onItemDelete: (item: PimType, journalUid: string) => void;
onItemCancel: () => void;
classes: any;
};
const styles = (theme: any) => ({
button: {
marginLeft: theme.spacing(1),
},
leftIcon: {
marginRight: theme.spacing(1),
},
});
function PageNotFound() {
return (
<Container>
<h1>404 Page Not Found</h1>
</Container>
);
}
const CollectionRoutes = withStyles(styles)(withRouter(
class CollectionRoutesInner extends React.PureComponent<CollectionRoutesPropsType> {
private itemUndefined(itemUid: string) {
return this.props.items[itemUid] === undefined;
}
public render() {
const props = this.props;
const { classes } = this.props;
const ComponentEdit = props.componentEdit;
const ComponentView = props.componentView;
return (
<Switch>
<Route
path={routeResolver.getRoute(props.routePrefix + ".new")}
exact
render={() => (
<Container style={{ maxWidth: "30rem" }}>
<ComponentEdit
key={props.routePrefix}
collections={props.collections}
onSave={props.onItemSave}
onCancel={props.onItemCancel}
history={props.history}
/>
</Container>
)}
/>
<Route
path={routeResolver.getRoute(props.routePrefix + "._id.edit")}
exact
render={({ match }) => {
const itemUid = decodeURIComponent(match.params.itemUid);
if (this.itemUndefined(itemUid)) {
return PageNotFound();
}
return (
<Container style={{ maxWidth: "30rem" }}>
{(itemUid in props.items) &&
<ComponentEdit
key={(props.items[itemUid] as any).journalUid}
initialCollection={(props.items[itemUid] as any).journalUid}
item={props.items[itemUid]}
collections={props.collections}
onSave={props.onItemSave}
onDelete={props.onItemDelete}
onCancel={props.onItemCancel}
history={props.history}
/>
}
</Container>
);
}}
/>
{props.routePrefix === "pim.events" &&
<Route
path={routeResolver.getRoute(props.routePrefix + "._id.duplicate")}
exact
render={({ match }) => {
const itemUid = decodeURIComponent(match.params.itemUid);
if (this.itemUndefined(itemUid)) {
return PageNotFound();
}
return (
<Container style={{ maxWidth: "30rem" }}>
{(itemUid in props.items) &&
<ComponentEdit
key={(props.items[itemUid] as any).journalUid}
initialCollection={(props.items[itemUid] as any).journalUid}
item={props.items[itemUid]}
collections={props.collections}
onSave={props.onItemSave}
onDelete={props.onItemDelete}
onCancel={props.onItemCancel}
history={props.history}
duplicate
/>
}
</Container>
);
}}
/>
}
<Route
path={routeResolver.getRoute(props.routePrefix + "._id.log")}
exact
render={({ match }) => {
const paramItemUid = decodeURIComponent(match.params.itemUid);
if (this.itemUndefined(paramItemUid)) {
return PageNotFound();
}
return (
<Container>
<ItemChangeLog
syncInfo={props.syncInfo}
paramItemUid={paramItemUid}
/>
</Container>
);
}}
/>
<Route
path={routeResolver.getRoute(props.routePrefix + "._id")}
exact
render={({ match, history }) => {
const itemUid = decodeURIComponent(match.params.itemUid);
if (this.itemUndefined(itemUid)) {
return PageNotFound();
}
return (
<Container>
<div style={{ textAlign: "right", marginBottom: 15 }}>
<Button
variant="contained"
className={classes.button}
onClick={() =>
history.push(routeResolver.getRoute(
props.routePrefix + "._id.log",
{ itemUid: match.params.itemUid }))
}
>
<IconChangeHistory className={classes.leftIcon} />
Change History
</Button>
<Button
color="secondary"
variant="contained"
disabled={!props.componentEdit}
className={classes.button}
style={{ marginLeft: 15 }}
onClick={() =>
history.push(routeResolver.getRoute(
props.routePrefix + "._id.edit",
{ itemUid: match.params.itemUid }))
}
>
<IconEdit className={classes.leftIcon} />
Edit
</Button>
{props.routePrefix === "pim.events" &&
<Button
color="secondary"
variant="contained"
disabled={!props.componentEdit}
className={classes.button}
style={{ marginLeft: 15 }}
onClick={() =>
history.push(routeResolver.getRoute(
props.routePrefix + "._id.duplicate",
{ itemUid: match.params.itemUid }))
}
>
<IconDuplicate className={classes.leftIcon} />
Duplicate
</Button>
}
</div>
<ComponentView item={props.items[decodeURIComponent(match.params.itemUid)]} />
</Container>
);
}}
/>
</Switch>
);
}
}
));
class Pim extends React.PureComponent {
public props: {
etesync: CredentialsData;
userInfo: UserInfoData;
syncInfo: SyncInfo;
history: History;
};
constructor(props: any) {
super(props);
this.onCancel = this.onCancel.bind(this);
this.onItemDelete = this.onItemDelete.bind(this);
this.onItemSave = this.onItemSave.bind(this);
}
public onItemSave(item: PimType, journalUid: string, originalEvent?: PimType): Promise<void> {
const syncJournal = this.props.syncInfo.get(journalUid);
if (syncJournal === undefined) {
return Promise.reject();
}
const journal = syncJournal.journal;
const action = (originalEvent === undefined) ? EteSync.SyncEntryAction.Add : EteSync.SyncEntryAction.Change;
let prevUid: string | null = null;
let last = syncJournal.journalEntries.last() as EteSync.Entry;
if (last) {
prevUid = last.uid;
}
return store.dispatch<any>(fetchEntries(this.props.etesync, journal.uid, prevUid))
.then((entriesAction: Action<EteSync.Entry[]>) => {
last = entriesAction.payload!.slice(-1).pop() as EteSync.Entry;
if (last) {
prevUid = last.uid;
}
return store.dispatch(
addJournalEntry(
this.props.etesync, this.props.userInfo, journal,
prevUid, action, item.toIcal()));
});
}
public onItemDelete(item: PimType, journalUid: string) {
const syncJournal = this.props.syncInfo.get(journalUid);
if (syncJournal === undefined) {
return;
}
const journal = syncJournal.journal;
const action = EteSync.SyncEntryAction.Delete;
let prevUid: string | null = null;
let last = syncJournal.journalEntries.last() as EteSync.Entry;
if (last) {
prevUid = last.uid;
}
store.dispatch<any>(fetchEntries(this.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 deleteItem = store.dispatch(
addJournalEntry(
this.props.etesync, this.props.userInfo, journal,
prevUid, action, item.toIcal()));
(deleteItem as any).then(() => {
this.props.history.push(routeResolver.getRoute("pim"));
});
});
}
public onCancel() {
this.props.history.goBack();
}
public render() {
const { collectionsTaskList, taskListItems } = itemsSelector(this.props);
return (
<Switch>
<Route
path={routeResolver.getRoute("pim.tasks")}
render={() => (
<CollectionRoutes
syncInfo={this.props.syncInfo}
routePrefix="pim.tasks"
collections={collectionsTaskList}
items={taskListItems}
componentEdit={TaskEdit}
componentView={Task}
onItemSave={this.onItemSave}
onItemDelete={this.onItemDelete}
onItemCancel={this.onCancel}
/>
)}
/>
</Switch>
);
}
}
export default Pim;

@ -16,6 +16,7 @@ import AppBarOverride from "./widgets/AppBarOverride";
import LoadingIndicator from "./widgets/LoadingIndicator";
import ContactsMain from "./Contacts/Main";
import CalendarsMain from "./Calendars/Main";
import TasksMain from "./Tasks/Main";
import Journals from "./Journals";
import Settings from "./Settings";
@ -102,7 +103,7 @@ export default function SyncGate() {
<Route
path={routeResolver.getRoute("pim.tasks")}
>
tasks
<TasksMain />
</Route>
</Switch>
</Route>

@ -0,0 +1,232 @@
// SPDX-FileCopyrightText: © 2020 EteSync Authors
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from "react";
import { Switch, Route, useHistory } from "react-router";
import * as Etebase from "etebase";
import { Button, useTheme } from "@material-ui/core";
import IconEdit from "@material-ui/icons/Edit";
import IconChangeHistory from "@material-ui/icons/ChangeHistory";
import { TaskType, PimType } from "../pim-types";
import { useCredentials } from "../credentials";
import { useItems, useCollections, getCollectionManager } from "../etebase-helpers";
import { routeResolver } from "../App";
import TaskList from "./TaskList";
import Task from "./Task";
import LoadingIndicator from "../widgets/LoadingIndicator";
import TaskEdit from "./TaskEdit";
import PageNotFound from "../PageNotFound";
import { CachedCollection, getItemNavigationUid, getDecryptCollectionsFunction, getDecryptItemsFunction, PimFab } from "../Pim/helpers";
const colType = "etebase.vtodo";
const decryptCollections = getDecryptCollectionsFunction(colType);
const decryptItems = getDecryptItemsFunction(colType, TaskType.parse);
export default function TasksMain() {
const [entries, setEntries] = React.useState<Map<string, Map<string, TaskType>>>();
const [cachedCollections, setCachedCollections] = React.useState<CachedCollection[]>();
const theme = useTheme();
const history = useHistory();
const etebase = useCredentials()!;
const collections = useCollections(etebase, colType);
const items = useItems(etebase, colType);
React.useEffect(() => {
if (items) {
decryptItems(items)
.then((entries) => setEntries(entries));
// FIXME: handle failure to decrypt items
}
if (collections) {
decryptCollections(collections)
.then((entries) => setCachedCollections(entries));
// FIXME: handle failure to decrypt collections
}
}, [items, collections]);
if (!entries || !cachedCollections) {
return (
<LoadingIndicator />
);
}
async function onItemSave(item: PimType, collectionUid: string, originalItem?: PimType): Promise<void> {
const itemUid = originalItem?.itemUid;
const colMgr = getCollectionManager(etebase);
const collection = collections!.find((x) => x.uid === collectionUid)!;
const itemMgr = colMgr.getItemManager(collection);
const mtime = (new Date()).getUTCMilliseconds();
const content = item.toIcal();
let eteItem;
if (itemUid) {
// Existing item
eteItem = items!.get(collectionUid)?.get(itemUid)!;
await eteItem.setContent(content);
const meta = await eteItem.getMeta();
meta.mtime = mtime;
await eteItem.setMeta(meta);
} else {
// New
const meta: Etebase.CollectionItemMetadata = {
mtime,
name: item.uid,
};
eteItem = await itemMgr.create(meta, content);
}
await itemMgr.batch([eteItem]);
}
async function onItemDelete(item: PimType, collectionUid: string) {
const itemUid = item.itemUid!;
const colMgr = getCollectionManager(etebase);
const collection = collections!.find((x) => x.uid === collectionUid)!;
const itemMgr = colMgr.getItemManager(collection);
const eteItem = items!.get(collectionUid)?.get(itemUid)!;
const mtime = (new Date()).getUTCMilliseconds();
const meta = await eteItem.getMeta();
meta.mtime = mtime;
await eteItem.setMeta(meta);
await eteItem.delete();
await itemMgr.batch([eteItem]);
history.push(routeResolver.getRoute("pim.tasks"));
}
function onCancel() {
history.goBack();
}
const flatEntries = [];
for (const col of entries.values()) {
for (const item of col.values()) {
flatEntries.push(item);
}
}
const styles = {
button: {
marginLeft: theme.spacing(1),
},
leftIcon: {
marginRight: theme.spacing(1),
},
};
return (
<Switch>
<Route
path={routeResolver.getRoute("pim.tasks")}
exact
>
<TaskList
entries={flatEntries}
collections={cachedCollections}
onItemClick={(item: TaskType) => history.push(
routeResolver.getRoute("pim.tasks._id", { itemUid: getItemNavigationUid(item) })
)}
onItemSave={onItemSave}
/>
<PimFab
onClick={() => history.push(
routeResolver.getRoute("pim.tasks.new")
)}
/>
</Route>
<Route
path={routeResolver.getRoute("pim.tasks.new")}
exact
>
<TaskEdit
collections={cachedCollections}
onSave={onItemSave}
onDelete={onItemDelete}
onCancel={onCancel}
history={history}
/>
</Route>
<Route
path={routeResolver.getRoute("pim.tasks._id")}
render={({ match }) => {
const [colUid, itemUid] = match.params.itemUid.split("|");
const item = entries.get(colUid)?.get(itemUid);
if (!item) {
return (<PageNotFound />);
}
/* FIXME:
const collection = collections!.find((x) => x.uid === colUid)!;
const readOnly = collection.accessLevel;
*/
const readOnly = false;
return (
<Switch>
<Route
path={routeResolver.getRoute("pim.tasks._id.edit")}
exact
>
<TaskEdit
key={itemUid}
initialCollection={item.collectionUid}
item={item}
collections={cachedCollections}
onSave={onItemSave}
onDelete={onItemDelete}
onCancel={onCancel}
history={history}
/>
</Route>
<Route
path={routeResolver.getRoute("pim.tasks._id.log")}
>
<h1>Not currently implemented.</h1>
</Route>
<Route
path={routeResolver.getRoute("pim.tasks._id")}
exact
>
<div style={{ textAlign: "right", marginBottom: 15 }}>
<Button
variant="contained"
style={styles.button}
onClick={() =>
history.push(routeResolver.getRoute("pim.tasks._id.log", { itemUid: getItemNavigationUid(item) }))
}
>
<IconChangeHistory style={styles.leftIcon} />
Change History
</Button>
<Button
color="secondary"
variant="contained"
disabled={readOnly}
style={{ ...styles.button, marginLeft: 15 }}
onClick={() =>
history.push(routeResolver.getRoute("pim.tasks._id.edit", { itemUid: getItemNavigationUid(item) }))
}
>
<IconEdit style={styles.leftIcon} />
Edit
</Button>
</div>
<Task item={item} />
</Route>
</Switch>
);
}}
/>
</Switch>
);
}

@ -3,20 +3,19 @@
import * as React from "react";
import * as EteSync from "etesync";
import ICAL from "ical.js";
import uuid from "uuid";
import TextField from "@material-ui/core/TextField";
import { TaskType, PimType, TaskStatusType } from "../../pim-types";
import { TaskType, PimType, TaskStatusType } from "../pim-types";
import { CachedCollection } from "../Pim/helpers";
interface PropsType {
style: React.CSSProperties;
onSubmit: (item: PimType, journalUid: string, originalItem?: PimType) => void;
defaultCollection: EteSync.CollectionInfo;
defaultCollection: CachedCollection;
}
function QuickAdd(props: PropsType) {
@ -37,7 +36,7 @@ function QuickAdd(props: PropsType) {
task.status = TaskStatusType.NeedsAction;
task.lastModified = ICAL.Time.now();
save(task, defaultCollection.uid, undefined);
save(task, defaultCollection.collection.uid, undefined);
setTitle("");
}

@ -6,11 +6,11 @@ import InboxIcon from "@material-ui/icons/Inbox";
import LabelIcon from "@material-ui/icons/LabelOutlined";
import TodayIcon from "@material-ui/icons/Today";
import { setSettings } from "../../store/actions";
import { StoreState } from "../../store";
import { setSettings } from "../store/actions";
import { StoreState } from "../store";
import { List, ListItem, ListSubheader } from "../../widgets/List";
import { TaskType } from "../../pim-types";
import { List, ListItem, ListSubheader } from "../widgets/List";
import { TaskType } from "../pim-types";
interface ListItemPropsType {
name: string | null;

@ -3,11 +3,11 @@
import * as React from "react";
import PimItemHeader from "../PimItemHeader";
import PimItemHeader from "../components/PimItemHeader";
import { formatDate, formatOurTimezoneOffset } from "../../helpers";
import { formatDate, formatOurTimezoneOffset } from "../helpers";
import { TaskType } from "../../pim-types";
import { TaskType } from "../pim-types";
class Task extends React.PureComponent {
public props: {

@ -24,11 +24,11 @@ import IconDelete from "@material-ui/icons/Delete";
import IconCancel from "@material-ui/icons/Clear";
import IconSave from "@material-ui/icons/Save";
import DateTimePicker from "../../widgets/DateTimePicker";
import DateTimePicker from "../widgets/DateTimePicker";
import ConfirmationDialog from "../../widgets/ConfirmationDialog";
import TimezonePicker from "../../widgets/TimezonePicker";
import Toast from "../../widgets/Toast";
import ConfirmationDialog from "../widgets/ConfirmationDialog";
import TimezonePicker from "../widgets/TimezonePicker";
import Toast from "../widgets/Toast";
import { Location } from "history";
import { withRouter } from "react-router";
@ -38,14 +38,14 @@ import * as ICAL from "ical.js";
import * as EteSync from "etesync";
import { getCurrentTimezone, mapPriority } from "../../helpers";
import { getCurrentTimezone, mapPriority } from "../helpers";
import { TaskType, TaskStatusType, timezoneLoadFromName, TaskPriorityType, TaskTags } from "../../pim-types";
import { TaskType, TaskStatusType, timezoneLoadFromName, TaskPriorityType, TaskTags } from "../pim-types";
import { History } from "history";
import ColoredRadio from "../../widgets/ColoredRadio";
import RRule, { RRuleOptions } from "../../widgets/RRule";
import ColoredRadio from "../widgets/ColoredRadio";
import RRule, { RRuleOptions } from "../widgets/RRule";
interface PropsType {
collections: EteSync.CollectionInfo[];

@ -3,17 +3,15 @@
import * as React from "react";
import * as EteSync from "etesync";
import { List } from "../widgets/List";
import Toast, { PropsType as ToastProps } from "../widgets/Toast";
import { List } from "../../widgets/List";
import Toast, { PropsType as ToastProps } from "../../widgets/Toast";
import { TaskType, PimType, TaskStatusType } from "../../pim-types";
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, useDispatch } from "react-redux";
import { useSelector } from "react-redux";
import Fuse from "fuse.js";
@ -22,13 +20,9 @@ import Sidebar from "./Sidebar";
import Toolbar from "./Toolbar";
import QuickAdd from "./QuickAdd";
import { StoreState, UserInfoData } 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";
import { useCredentials } from "../../login";
import { StoreState } from "../store";
import { formatDate } from "../helpers";
import { CachedCollection } from "../Pim/helpers";
function sortCompleted(a: TaskType, b: TaskType) {
return (!!a.finished === !!b.finished) ? 0 : (a.finished) ? 1 : -1;
@ -106,11 +100,9 @@ const useStyles = makeStyles((theme) => ({
interface PropsType {
entries: TaskType[];
collections: EteSync.CollectionInfo[];
collections: CachedCollection[];
onItemClick: (entry: TaskType) => void;
onItemSave: (item: PimType, journalUid: string, originalItem?: PimType) => Promise<void>;
syncInfo: SyncInfo;
userInfo: UserInfoData;
}
export default function TaskList(props: PropsType) {
@ -120,62 +112,25 @@ export default function TaskList(props: PropsType) {
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 etesync = useCredentials()!;
const theme = useTheme();
const classes = useStyles();
const dispatch = useDispatch();
const { onItemClick } = props;
const handleToggleComplete = (task: TaskType, completed: boolean) => {
const handleToggleComplete = async (task: TaskType, completed: boolean) => {
const clonedTask = task.clone();
clonedTask.status = completed ? TaskStatusType.Completed : TaskStatusType.NeedsAction;
const nextTask = completed ? task.getNextOccurence() : null;
const syncJournal = props.syncInfo.get((task as any).journalUid);
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;
try {
await props.onItemSave(clonedTask, task.collectionUid!, task);
if (nextTask) {
setToast({ message: `${nextTask.title} rescheduled for ${formatDate(nextTask.startDate ?? nextTask.dueDate)}`, severity: "success" });
}
} catch (_e) {
setToast({ message: "Failed to save changes. This may be due to a network error.", severity: "error" });
}
dispatch<any>(fetchEntries(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(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(

@ -3,15 +3,15 @@
import * as React from "react";
import { TaskType, TaskPriorityType } from "../../pim-types";
import { ListItem } from "../../widgets/List";
import { TaskType, TaskPriorityType } from "../pim-types";
import { ListItem } from "../widgets/List";
import Checkbox from "@material-ui/core/Checkbox";
import CheckBoxOutlineBlankIcon from "@material-ui/icons/CheckBoxOutlineBlank";
import * as colors from "@material-ui/core/colors";
import Chip from "@material-ui/core/Chip";
import { mapPriority, formatDate } from "../../helpers";
import { mapPriority, formatDate } from "../helpers";
const checkboxColor = {
[TaskPriorityType.Undefined]: colors.grey[600],

@ -1,7 +1,5 @@
import * as React from "react";
import * as EteSync from "etesync";
import Switch from "@material-ui/core/Switch";
import IconButton from "@material-ui/core/IconButton";
import MoreVertIcon from "@material-ui/icons/MoreVert";
@ -14,15 +12,16 @@ import { makeStyles } from "@material-ui/core/styles";
import { Transition } from "react-transition-group";
import InputAdornment from "@material-ui/core/InputAdornment";
import { PimType } from "../../pim-types";
import { PimType } from "../pim-types";
import { useSelector, useDispatch } from "react-redux";
import { setSettings } from "../../store/actions";
import { StoreState } from "../../store";
import { setSettings } from "../store/actions";
import { StoreState } from "../store";
import Menu from "../../widgets/Menu";
import Menu from "../widgets/Menu";
import { ListItemText, ListItemSecondaryAction } from "@material-ui/core";
import { CachedCollection } from "../Pim/helpers";
const transitionTimeout = 300;
@ -44,7 +43,7 @@ const useStyles = makeStyles((theme) => ({
}));
interface PropsType {
defaultCollection: EteSync.CollectionInfo;
defaultCollection: CachedCollection;
onItemSave: (item: PimType, journalUid: string, originalItem?: PimType) => Promise<void>;
showCompleted: boolean;
showHidden: boolean;
Loading…
Cancel
Save