Add support for tasks.

master
Tom Hacohen 6 years ago
parent d11180fed6
commit 2fd674a456

@ -37,7 +37,10 @@ const muiTheme = createMuiTheme({
dark: lightBlue.A700, dark: lightBlue.A700,
contrastText: 'white', contrastText: 'white',
}, },
} },
typography: {
useNextVariants: true,
},
}); });
export const routeResolver = new RouteResolver({ export const routeResolver = new RouteResolver({
@ -59,6 +62,14 @@ export const routeResolver = new RouteResolver({
}, },
new: 'new', new: 'new',
}, },
tasks: {
_id: {
_base: ':itemUid',
edit: 'edit',
log: 'log',
},
new: 'new',
},
}, },
journals: { journals: {
_id: { _id: {

@ -10,6 +10,7 @@ import SearchableAddressBook from '../components/SearchableAddressBook';
import Contact from '../components/Contact'; import Contact from '../components/Contact';
import Calendar from '../components/Calendar'; import Calendar from '../components/Calendar';
import Event from '../components/Event'; import Event from '../components/Event';
import TaskList from '../components/TaskList';
import AppBarOverride from '../widgets/AppBarOverride'; import AppBarOverride from '../widgets/AppBarOverride';
import Container from '../widgets/Container'; import Container from '../widgets/Container';
@ -17,7 +18,7 @@ import Container from '../widgets/Container';
import JournalEntries from '../components/JournalEntries'; import JournalEntries from '../components/JournalEntries';
import journalView from './journalView'; import journalView from './journalView';
import { syncEntriesToItemMap, syncEntriesToCalendarItemMap } from '../journal-processors'; import { syncEntriesToItemMap, syncEntriesToEventItemMap, syncEntriesToTaskItemMap } from '../journal-processors';
import { SyncInfo, SyncInfoJournal } from '../SyncGate'; import { SyncInfo, SyncInfoJournal } from '../SyncGate';
@ -39,6 +40,7 @@ interface PropsTypeInner extends PropsType {
const JournalAddressBook = journalView(SearchableAddressBook, Contact); const JournalAddressBook = journalView(SearchableAddressBook, Contact);
const PersistCalendar = historyPersistor('Calendar')(Calendar); const PersistCalendar = historyPersistor('Calendar')(Calendar);
const JournalCalendar = journalView(PersistCalendar, Event); const JournalCalendar = journalView(PersistCalendar, Event);
const JournalTaskList = journalView(TaskList, Event);
class Journal extends React.Component<PropsTypeInner> { class Journal extends React.Component<PropsTypeInner> {
state: { state: {
@ -65,17 +67,23 @@ class Journal extends React.Component<PropsTypeInner> {
let itemsTitle: string; let itemsTitle: string;
let itemsView: JSX.Element; let itemsView: JSX.Element;
if (collectionInfo.type === 'CALENDAR') { if (collectionInfo.type === 'CALENDAR') {
itemsView = itemsView = (
<JournalCalendar journal={journal} entries={syncEntriesToCalendarItemMap(collectionInfo, syncEntries)} />; <JournalCalendar
journal={journal}
entries={syncEntriesToEventItemMap(collectionInfo, syncEntries)}
/>);
itemsTitle = 'Events'; itemsTitle = 'Events';
} else if (collectionInfo.type === 'ADDRESS_BOOK') { } else if (collectionInfo.type === 'ADDRESS_BOOK') {
itemsView = itemsView =
<JournalAddressBook journal={journal} entries={syncEntriesToItemMap(collectionInfo, syncEntries)} />; <JournalAddressBook journal={journal} entries={syncEntriesToItemMap(collectionInfo, syncEntries)} />;
itemsTitle = 'Contacts'; itemsTitle = 'Contacts';
} else if (collectionInfo.type === 'TASKS') { } else if (collectionInfo.type === 'TASKS') {
itemsView = <div>Task preview is not yet supported</div>; itemsView = (
<JournalTaskList
journal={journal}
entries={syncEntriesToTaskItemMap(collectionInfo, syncEntries)}
/>);
itemsTitle = 'Tasks'; itemsTitle = 'Tasks';
journalOnly = true;
} else { } else {
itemsView = <div>Preview is not supported for this journal type</div>; itemsView = <div>Preview is not supported for this journal type</div>;
itemsTitle = 'Items'; itemsTitle = 'Items';

@ -13,8 +13,9 @@ import Container from '../widgets/Container';
import SearchableAddressBook from '../components/SearchableAddressBook'; import SearchableAddressBook from '../components/SearchableAddressBook';
import Calendar from '../components/Calendar'; import Calendar from '../components/Calendar';
import TaskList from '../components/TaskList';
import { EventType, ContactType } from '../pim-types'; import { EventType, ContactType, TaskType } from '../pim-types';
import { routeResolver } from '../App'; import { routeResolver } from '../App';
@ -22,12 +23,14 @@ import { historyPersistor } from '../persist-state-history';
const addressBookTitle = 'Address Book'; const addressBookTitle = 'Address Book';
const calendarTitle = 'Calendar'; const calendarTitle = 'Calendar';
const tasksTitle = 'Tasks';
const PersistCalendar = historyPersistor('Calendar')(Calendar); const PersistCalendar = historyPersistor('Calendar')(Calendar);
interface PropsType { interface PropsType {
contacts: Array<ContactType>; contacts: Array<ContactType>;
events: Array<EventType>; events: Array<EventType>;
tasks: Array<TaskType>;
location?: Location; location?: Location;
history?: History; history?: History;
theme: Theme; theme: Theme;
@ -42,6 +45,7 @@ class PimMain extends React.PureComponent<PropsType> {
super(props); super(props);
this.state = {tab: 1}; this.state = {tab: 1};
this.eventClicked = this.eventClicked.bind(this); this.eventClicked = this.eventClicked.bind(this);
this.taskClicked = this.taskClicked.bind(this);
this.contactClicked = this.contactClicked.bind(this); this.contactClicked = this.contactClicked.bind(this);
this.floatingButtonClicked = this.floatingButtonClicked.bind(this); this.floatingButtonClicked = this.floatingButtonClicked.bind(this);
this.newEvent = this.newEvent.bind(this); this.newEvent = this.newEvent.bind(this);
@ -54,6 +58,13 @@ class PimMain extends React.PureComponent<PropsType> {
routeResolver.getRoute('pim.events._id', { itemUid: uid })); routeResolver.getRoute('pim.events._id', { itemUid: uid }));
} }
taskClicked(event: ICAL.Event) {
const uid = event.uid;
this.props.history!.push(
routeResolver.getRoute('pim.tasks._id', { itemUid: uid }));
}
contactClicked(contact: ContactType) { contactClicked(contact: ContactType) {
const uid = contact.uid; const uid = contact.uid;
@ -106,24 +117,33 @@ class PimMain extends React.PureComponent<PropsType> {
<Tab <Tab
label={calendarTitle} label={calendarTitle}
/> />
<Tab
label={tasksTitle}
/>
</Tabs> </Tabs>
{ tab === 0 &&
<Container> <Container>
{ tab === 0 &&
<SearchableAddressBook entries={this.props.contacts} onItemClick={this.contactClicked} /> <SearchableAddressBook entries={this.props.contacts} onItemClick={this.contactClicked} />
</Container>
} }
{ tab === 1 && { tab === 1 &&
<Container>
<PersistCalendar <PersistCalendar
entries={this.props.events} entries={this.props.events}
onItemClick={this.eventClicked} onItemClick={this.eventClicked}
onSlotClick={this.newEvent} onSlotClick={this.newEvent}
/> />
</Container>
} }
{ tab === 2 &&
<TaskList
entries={this.props.tasks}
onItemClick={this.taskClicked}
/>
}
</Container>
<Fab <Fab
color="primary" color="primary"
disabled={tab === 2}
style={style.floatingButton} style={style.floatingButton}
onClick={this.floatingButtonClicked} onClick={this.floatingButtonClicked}
> >

@ -16,7 +16,7 @@ import { pure } from 'recompose';
import { History } from 'history'; import { History } from 'history';
import { PimType, ContactType, EventType } from '../pim-types'; import { PimType, ContactType, EventType, TaskType } from '../pim-types';
import Container from '../widgets/Container'; import Container from '../widgets/Container';
@ -36,7 +36,7 @@ import { SyncInfo } from '../SyncGate';
import { createJournalEntry } from '../etesync-helpers'; import { createJournalEntry } from '../etesync-helpers';
import { syncEntriesToItemMap, syncEntriesToCalendarItemMap } from '../journal-processors'; import { syncEntriesToItemMap, syncEntriesToEventItemMap, syncEntriesToTaskItemMap } from '../journal-processors';
function objValues(obj: any) { function objValues(obj: any) {
return Object.keys(obj).map((x) => obj[x]); return Object.keys(obj).map((x) => obj[x]);
@ -47,8 +47,10 @@ const itemsSelector = createSelector(
(syncInfo) => { (syncInfo) => {
let collectionsAddressBook: Array<EteSync.CollectionInfo> = []; let collectionsAddressBook: Array<EteSync.CollectionInfo> = [];
let collectionsCalendar: Array<EteSync.CollectionInfo> = []; let collectionsCalendar: Array<EteSync.CollectionInfo> = [];
let collectionsTaskList: Array<EteSync.CollectionInfo> = [];
let addressBookItems: {[key: string]: ContactType} = {}; let addressBookItems: {[key: string]: ContactType} = {};
let calendarItems: {[key: string]: EventType} = {}; let calendarItems: {[key: string]: EventType} = {};
let taskListItems: {[key: string]: TaskType} = {};
syncInfo.forEach( syncInfo.forEach(
(syncJournal) => { (syncJournal) => {
const syncEntries = syncJournal.entries; const syncEntries = syncJournal.entries;
@ -59,13 +61,18 @@ const itemsSelector = createSelector(
addressBookItems = syncEntriesToItemMap(collectionInfo, syncEntries, addressBookItems); addressBookItems = syncEntriesToItemMap(collectionInfo, syncEntries, addressBookItems);
collectionsAddressBook.push(collectionInfo); collectionsAddressBook.push(collectionInfo);
} else if (collectionInfo.type === 'CALENDAR') { } else if (collectionInfo.type === 'CALENDAR') {
calendarItems = syncEntriesToCalendarItemMap(collectionInfo, syncEntries, calendarItems); calendarItems = syncEntriesToEventItemMap(collectionInfo, syncEntries, calendarItems);
collectionsCalendar.push(collectionInfo); collectionsCalendar.push(collectionInfo);
} else if (collectionInfo.type === 'TASKS') {
taskListItems = syncEntriesToTaskItemMap(collectionInfo, syncEntries, taskListItems);
collectionsTaskList.push(collectionInfo);
} }
} }
); );
return { collectionsAddressBook, collectionsCalendar, addressBookItems, calendarItems }; return {
collectionsAddressBook, collectionsCalendar, collectionsTaskList, addressBookItems, calendarItems, taskListItems
};
}, },
); );
@ -188,6 +195,7 @@ const CollectionRoutes = withStyles(styles)(withRouter(
<Button <Button
color="secondary" color="secondary"
variant="contained" variant="contained"
disabled={!props.componentEdit}
className={classes.button} className={classes.button}
style={{marginLeft: 15}} style={{marginLeft: 15}}
onClick={() => onClick={() =>
@ -300,7 +308,7 @@ class Pim extends React.PureComponent {
} }
render() { render() {
const { collectionsAddressBook, collectionsCalendar, addressBookItems, calendarItems } = itemsSelector(this.props); const { collectionsAddressBook, collectionsCalendar, collectionsTaskList, addressBookItems, calendarItems, taskListItems } = itemsSelector(this.props);
return ( return (
<Switch> <Switch>
@ -311,6 +319,7 @@ class Pim extends React.PureComponent {
<PimMain <PimMain
contacts={objValues(addressBookItems)} contacts={objValues(addressBookItems)}
events={objValues(calendarItems)} events={objValues(calendarItems)}
tasks={objValues(taskListItems)}
history={history} history={history}
/> />
)} )}
@ -347,6 +356,22 @@ class Pim extends React.PureComponent {
/> />
)} )}
/> />
<Route
path={routeResolver.getRoute('pim.tasks')}
render={() => (
<CollectionRoutes
syncInfo={this.props.syncInfo}
routePrefix="pim.tasks"
collections={collectionsTaskList}
items={taskListItems}
componentEdit={undefined}
componentView={Event}
onItemSave={this.onItemSave}
onItemDelete={this.onItemDelete}
onItemCancel={this.onCancel}
/>
)}
/>
</Switch> </Switch>
); );
} }

@ -0,0 +1,73 @@
import * as React from 'react';
import { pure } from 'recompose';
import { createSelector } from 'reselect';
import { List, ListItem } from '../widgets/List';
import { TaskType } from '../pim-types';
const TaskListItem = pure((_props: any) => {
const {
entry,
onClick,
} = _props;
const title = entry.title;
return (
<ListItem
primaryText={title}
onClick={() => onClick(entry)}
/>
);
});
const sortSelector = createSelector(
(entries: Array<TaskType>) => entries,
(entries) => {
return entries.sort((_a, _b) => {
const a = _a.title;
const b = _b.title;
if (a < b) {
return -1;
} else if (a > b) {
return 1;
} else {
return 0;
}
});
},
);
class TaskList extends React.PureComponent {
props: {
entries: Array<TaskType>,
onItemClick: (contact: TaskType) => void,
};
render() {
const entries = this.props.entries.filter((x) => !x.completed);
const sortedEntries = sortSelector(entries);
let itemList = sortedEntries.map((entry, idx, array) => {
const uid = entry.uid;
return (
<TaskListItem
key={uid}
entry={entry}
onClick={this.props.onItemClick}
/>
);
});
return (
<List>
{itemList}
</List>
);
}
}
export default TaskList;

@ -2,7 +2,7 @@ import { List } from 'immutable';
import * as ICAL from 'ical.js'; import * as ICAL from 'ical.js';
import { EventType, ContactType } from './pim-types'; import { EventType, ContactType, TaskType } from './pim-types';
import * as EteSync from './api/EteSync'; import * as EteSync from './api/EteSync';
@ -50,14 +50,15 @@ function colorIntToHtml(color: number) {
((alpha > 0) ? toHex(alpha) : ''); ((alpha > 0) ? toHex(alpha) : '');
} }
export function syncEntriesToCalendarItemMap( function syncEntriesToCalendarItemMap<T extends EventType>(
collection: EteSync.CollectionInfo, entries: List<EteSync.SyncEntry>, base: {[key: string]: EventType} = {}) { ItemType: any,
collection: EteSync.CollectionInfo, entries: List<EteSync.SyncEntry>, base: {[key: string]: T} = {}) {
let items = base; let items = base;
const color = colorIntToHtml(collection.color); const color = colorIntToHtml(collection.color);
entries.forEach((syncEntry) => { entries.forEach((syncEntry) => {
let comp = EventType.fromVCalendar(new ICAL.Component(ICAL.parse(syncEntry.content))); let comp = ItemType.fromVCalendar(new ICAL.Component(ICAL.parse(syncEntry.content)));
if (comp === null) { if (comp === null) {
return; return;
@ -80,3 +81,13 @@ export function syncEntriesToCalendarItemMap(
return items; return items;
} }
export function syncEntriesToEventItemMap(
collection: EteSync.CollectionInfo, entries: List<EteSync.SyncEntry>, base: {[key: string]: EventType} = {}) {
return syncEntriesToCalendarItemMap<EventType>(EventType, collection, entries, base);
}
export function syncEntriesToTaskItemMap(
collection: EteSync.CollectionInfo, entries: List<EteSync.SyncEntry>, base: {[key: string]: TaskType} = {}) {
return syncEntriesToCalendarItemMap<TaskType>(TaskType, collection, entries, base);
}

@ -53,20 +53,16 @@ export class EventType extends ICAL.Event implements PimType {
} }
} }
export class TaskType extends ICAL.Event implements PimType { export class TaskType extends EventType {
color: string; color: string;
static fromVCalendar(comp: ICAL.Component) { static fromVCalendar(comp: ICAL.Component) {
return new EventType(comp.getFirstSubcomponent('vtodo')); return new TaskType(comp.getFirstSubcomponent('vtodo'));
} }
toIcal() { get completed() {
let comp = new ICAL.Component(['vcalendar', [], []]); const status = this.component.getFirstPropertyValue('status');
comp.updatePropertyWithValue('prodid', '-//iCal.js EteSync Web'); return status === 'COMPLETED';
comp.updatePropertyWithValue('version', '4.0');
comp.addSubcomponent(this.component);
return comp.toString();
} }
clone() { clone() {

Loading…
Cancel
Save