From 4db3b2474330c7c7c2c8ead8f85f357a6bb0dfa9 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 14 Feb 2019 20:42:12 +0000 Subject: [PATCH] Add task edit support. --- src/Pim/PimMain.tsx | 5 +- src/Pim/index.tsx | 3 +- src/components/TaskEdit.tsx | 341 ++++++++++++++++++++++++++++++++++++ src/pim-types.ts | 16 ++ 4 files changed, 363 insertions(+), 2 deletions(-) create mode 100644 src/components/TaskEdit.tsx diff --git a/src/Pim/PimMain.tsx b/src/Pim/PimMain.tsx index 41f9b2d..9b26e4e 100644 --- a/src/Pim/PimMain.tsx +++ b/src/Pim/PimMain.tsx @@ -86,6 +86,10 @@ class PimMain extends React.PureComponent { ); } else if (this.state.tab === 1) { this.newEvent(); + } else if (this.state.tab === 2) { + this.props.history!.push( + routeResolver.getRoute('pim.tasks.new') + ); } } @@ -143,7 +147,6 @@ class PimMain extends React.PureComponent { diff --git a/src/Pim/index.tsx b/src/Pim/index.tsx index 3168e17..c222ddd 100644 --- a/src/Pim/index.tsx +++ b/src/Pim/index.tsx @@ -24,6 +24,7 @@ import JournalEntries from '../components/JournalEntries'; import ContactEdit from '../components/ContactEdit'; import Contact from '../components/Contact'; import EventEdit from '../components/EventEdit'; +import TaskEdit from '../components/TaskEdit'; import Event from '../components/Event'; import PimMain from './PimMain'; @@ -364,7 +365,7 @@ class Pim extends React.PureComponent { routePrefix="pim.tasks" collections={collectionsTaskList} items={taskListItems} - componentEdit={undefined} + componentEdit={TaskEdit} componentView={Event} onItemSave={this.onItemSave} onItemDelete={this.onItemDelete} diff --git a/src/components/TaskEdit.tsx b/src/components/TaskEdit.tsx new file mode 100644 index 0000000..fafa602 --- /dev/null +++ b/src/components/TaskEdit.tsx @@ -0,0 +1,341 @@ +import * as React from 'react'; + +import FormGroup from '@material-ui/core/FormGroup'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import Switch from '@material-ui/core/Switch'; + +import Button from '@material-ui/core/Button'; +import TextField from '@material-ui/core/TextField'; +import Select from '@material-ui/core/Select'; +import MenuItem from '@material-ui/core/MenuItem'; +import FormControl from '@material-ui/core/FormControl'; +import InputLabel from '@material-ui/core/InputLabel'; +import * as colors from '@material-ui/core/colors'; + +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 ConfirmationDialog from '../widgets/ConfirmationDialog'; + +import { Location } from 'history'; +import { withRouter } from 'react-router'; + +import * as uuid from 'uuid'; +import * as ICAL from 'ical.js'; + +import * as EteSync from '../api/EteSync'; + +import { TaskType } from '../pim-types'; + +interface PropsType { + collections: Array; + initialCollection?: string; + item?: TaskType; + onSave: (item: TaskType, journalUid: string, originalItem?: TaskType) => void; + onDelete: (item: TaskType, journalUid: string) => void; + onCancel: () => void; + location: Location; +}; + +class TaskEdit extends React.PureComponent { + state: { + uid: string, + title: string; + allDay: boolean; + start?: Date; + due?: Date; + location: string; + description: string; + journalUid: string; + + error?: string; + showDeleteDialog: boolean; + }; + + constructor(props: any) { + super(props); + this.state = { + uid: '', + title: '', + allDay: false, + location: '', + description: '', + + journalUid: '', + showDeleteDialog: false, + }; + + if (this.props.item !== undefined) { + const event = this.props.item; + + this.state.uid = event.uid; + this.state.title = event.title ? event.title : ''; + if (event.startDate) { + this.state.allDay = event.startDate.isDate; + this.state.start = event.startDate.toJSDate(); + } + if (event.dueDate) { + this.state.due = event.dueDate.toJSDate(); + } + this.state.location = event.location ? event.location : ''; + this.state.description = event.description ? event.description : ''; + } else { + this.state.uid = uuid.v4(); + } + + if (props.initialCollection) { + this.state.journalUid = props.initialCollection; + } else if (props.collections[0]) { + this.state.journalUid = props.collections[0].uid; + } + + this.onSubmit = this.onSubmit.bind(this); + this.handleChange = this.handleChange.bind(this); + this.handleInputChange = this.handleInputChange.bind(this); + this.toggleAllDay = this.toggleAllDay.bind(this); + this.onDeleteRequest = this.onDeleteRequest.bind(this); + } + + componentWillReceiveProps(nextProps: any) { + if ((this.props.collections !== nextProps.collections) || + (this.props.initialCollection !== nextProps.initialCollection)) { + if (nextProps.initialCollection) { + this.state.journalUid = nextProps.initialCollection; + } else if (nextProps.collections[0]) { + this.state.journalUid = nextProps.collections[0].uid; + } + } + } + + handleChange(name: string, value: string) { + this.setState({ + [name]: value + }); + + } + + handleInputChange(event: React.ChangeEvent) { + const name = event.target.name; + const value = event.target.value; + this.handleChange(name, value); + } + + toggleAllDay() { + this.setState({allDay: !this.state.allDay}); + } + + onSubmit(e: React.FormEvent) { + e.preventDefault(); + + function fromDate(date: Date | undefined, allDay: boolean) { + if (!date) { + return undefined; + } + const ret = ICAL.Time.fromJSDate(date, false); + if (!allDay) { + return ret; + } else { + let data = ret.toJSON(); + data.isDate = allDay; + return ICAL.Time.fromData(data); + } + } + + const startDate = fromDate(this.state.start, this.state.allDay); + const dueDate = fromDate(this.state.due, this.state.allDay); + + if (startDate && dueDate) { + if (startDate.compare(dueDate) >= 0) { + this.setState({error: 'End time must be later than start time!'}); + return; + } + } + + let event = (this.props.item) ? + this.props.item.clone() + : + new TaskType(null) + ; + + event.uid = this.state.uid; + event.summary = this.state.title; + if (startDate) { + event.startDate = startDate; + } + event.dueDate = dueDate; + event.location = this.state.location; + event.description = this.state.description; + + event.component.updatePropertyWithValue('last-modified', ICAL.Time.now()); + + this.props.onSave(event, this.state.journalUid, this.props.item); + } + + onDeleteRequest() { + this.setState({ + showDeleteDialog: true + }); + } + + render() { + const styles = { + form: { + }, + fullWidth: { + width: '100%', + boxSizing: 'border-box' as any, + marginTop: 16, + }, + submit: { + marginTop: 40, + marginBottom: 20, + textAlign: 'right' as any, + }, + }; + + const recurring = this.props.item && this.props.item.isRecurring(); + + return ( + +

+ {this.props.item ? 'Edit Task' : 'New Task'} +

+ {recurring && ( +
+ IMPORTANT: + This is a recurring event, for now, only editing the whole series + (by editing the first instance) is supported. +
+ )} + {this.state.error && ( +
ERROR! {this.state.error}
+ )} +
+ + + + + Saving to + + + + + + + } + label="All Day" + /> + + +
+ this.setState({start: date})} + /> +
+ +
+ this.setState({due: date})} + /> +
+ + + + + +
+ + + {this.props.item && + + } + + +
+ +
+ Not all types are supported at the moment. If you are editing a contact, + the unsupported types will be copied as is. +
+ + + this.props.onDelete(this.props.item!, this.props.initialCollection!)} + onCancel={() => this.setState({showDeleteDialog: false})} + > + Are you sure you would like to delete this event? + +
+ ); + } +} + +export default withRouter(TaskEdit); diff --git a/src/pim-types.ts b/src/pim-types.ts index 1bb57ce..902c11c 100644 --- a/src/pim-types.ts +++ b/src/pim-types.ts @@ -60,11 +60,27 @@ export class TaskType extends EventType { return new TaskType(comp.getFirstSubcomponent('vtodo')); } + constructor(comp: ICAL.Component | null) { + super(comp ? comp : new ICAL.Component('vtodo')); + } + get completed() { const status = this.component.getFirstPropertyValue('status'); return status === 'COMPLETED'; } + set dueDate(date: ICAL.Time | undefined) { + if (date) { + this.component.updatePropertyWithValue('due', date); + } else { + this.component.removeAllProperties('due'); + } + } + + get dueDate() { + return this.component.getFirstPropertyValue('due'); + } + clone() { const ret = new TaskType(new ICAL.Component(this.component.toJSON())); ret.color = this.color;