Merge branch 'dev/task-attendees'
This commit is contained in:
commit
65989e7783
23 changed files with 1969 additions and 120 deletions
|
@ -2341,6 +2341,7 @@ class calendar extends rcube_plugin
|
|||
}
|
||||
|
||||
$html = '';
|
||||
$has_events = false;
|
||||
foreach ($this->ics_parts as $mime_id) {
|
||||
$part = $this->message->mime_parts[$mime_id];
|
||||
$charset = $part->ctype_parameters['charset'] ? $part->ctype_parameters['charset'] : RCMAIL_CHARSET;
|
||||
|
@ -2356,16 +2357,20 @@ class calendar extends rcube_plugin
|
|||
if ($event['_type'] != 'event') // skip non-event objects (#2928)
|
||||
continue;
|
||||
|
||||
$has_events = true;
|
||||
|
||||
// get prepared inline UI for this event object
|
||||
$html .= html::div('calendar-invitebox',
|
||||
$this->itip->mail_itip_inline_ui(
|
||||
$event,
|
||||
$this->ical->method,
|
||||
$mime_id.':'.$idx,
|
||||
'calendar',
|
||||
rcube_utils::anytodatetime($this->message->headers->date)
|
||||
)
|
||||
);
|
||||
if ($this->ical->method) {
|
||||
$html .= html::div('calendar-invitebox',
|
||||
$this->itip->mail_itip_inline_ui(
|
||||
$event,
|
||||
$this->ical->method,
|
||||
$mime_id.':'.$idx,
|
||||
'calendar',
|
||||
rcube_utils::anytodatetime($this->message->headers->date)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// limit listing
|
||||
if ($idx >= 3)
|
||||
|
@ -2381,7 +2386,7 @@ class calendar extends rcube_plugin
|
|||
}
|
||||
|
||||
// add "Save to calendar" button into attachment menu
|
||||
if (!empty($this->ics_parts)) {
|
||||
if ($has_events) {
|
||||
$this->add_button(array(
|
||||
'id' => 'attachmentsavecal',
|
||||
'name' => 'attachmentsavecal',
|
||||
|
|
|
@ -60,6 +60,11 @@ class libcalendaring_itip
|
|||
$this->rsvp_status = array_merge($this->rsvp_actions, array('delegated'));
|
||||
}
|
||||
|
||||
public function set_rsvp_status($status)
|
||||
{
|
||||
$this->rsvp_status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for rcube_plugin::gettext()
|
||||
* Checking for a label in different domains
|
||||
|
@ -213,6 +218,14 @@ class libcalendaring_itip
|
|||
$event['attendees'] = $reply_attendees;
|
||||
}
|
||||
}
|
||||
// set RSVP=TRUE for every attendee if not set
|
||||
else if ($method == 'REQUEST') {
|
||||
foreach ($event['attendees'] as $i => $attendee) {
|
||||
if (!isset($attendee['rsvp'])) {
|
||||
$event['attendees'][$i]['rsvp']= true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// compose multipart message using PEAR:Mail_Mime
|
||||
$message = new Mail_mime("\r\n");
|
||||
|
@ -236,9 +249,10 @@ class libcalendaring_itip
|
|||
$message->headers($headers);
|
||||
|
||||
// attach ics file for this event
|
||||
$ical = $this->plugin->get_ical();
|
||||
$ical = libcalendaring::get_ical();
|
||||
$ics = $ical->export(array($event), $method, false, $method == 'REQUEST' && $this->plugin->driver ? array($this->plugin->driver, 'get_attachment_body') : false);
|
||||
$message->addAttachment($ics, 'text/calendar', 'event.ics', false, '8bit', '', RCMAIL_CHARSET . "; method=" . $method);
|
||||
$filename = $event['_type'] == 'task' ? 'todo.ics' : 'event.ics';
|
||||
$message->addAttachment($ics, 'text/calendar', $filename, false, '8bit', '', RCMAIL_CHARSET . "; method=" . $method);
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
@ -313,9 +327,10 @@ class libcalendaring_itip
|
|||
$listed = false;
|
||||
foreach ($existing['attendees'] as $attendee) {
|
||||
if ($attendee['role'] != 'ORGANIZER' && strcasecmp($attendee['email'], $event['attendee']) == 0) {
|
||||
if (in_array($status, array('ACCEPTED','TENTATIVE','DECLINED','DELEGATED'))) {
|
||||
$html = html::div('rsvp-status ' . strtolower($status), $this->gettext(array(
|
||||
'name' => 'attendee'.strtolower($status),
|
||||
$status_lc = strtolower($status);
|
||||
if (in_array($status_lc, $this->rsvp_status)) {
|
||||
$html = html::div('rsvp-status ' . $status_lc, $this->gettext(array(
|
||||
'name' => 'attendee' . $status_lc,
|
||||
'vars' => array(
|
||||
'delegatedto' => Q($attendee['delegated-to'] ?: '?'),
|
||||
)
|
||||
|
@ -532,15 +547,21 @@ class libcalendaring_itip
|
|||
}
|
||||
|
||||
/**
|
||||
* Render event details in a table
|
||||
* Render event/task details in a table
|
||||
*/
|
||||
function itip_object_details_table($event, $title)
|
||||
{
|
||||
$table = new html_table(array('cols' => 2, 'border' => 0, 'class' => 'calendar-eventdetails'));
|
||||
$table->add('ititle', $title);
|
||||
$table->add('title', Q($event['title']));
|
||||
$table->add('label', $this->plugin->gettext('date'), $this->domain);
|
||||
$table->add('date', Q($this->lib->event_date_text($event)));
|
||||
if ($event['start'] && $event['end']) {
|
||||
$table->add('label', $this->plugin->gettext('date'), $this->domain);
|
||||
$table->add('date', Q($this->lib->event_date_text($event)));
|
||||
}
|
||||
else if ($event['due'] && $event['_type'] == 'task') {
|
||||
$table->add('label', $this->plugin->gettext('date'), $this->domain);
|
||||
$table->add('date', Q($this->lib->event_date_text($event)));
|
||||
}
|
||||
if ($event['location']) {
|
||||
$table->add('label', $this->plugin->gettext('location'), $this->domain);
|
||||
$table->add('location', Q($event['location']));
|
||||
|
|
|
@ -247,11 +247,25 @@ class libcalendaring extends rcube_plugin
|
|||
*/
|
||||
public function event_date_text($event, $tzinfo = false)
|
||||
{
|
||||
$fromto = '';
|
||||
$fromto = '--';
|
||||
|
||||
// handle task objects
|
||||
if ($event['_type'] == 'task' && is_object($event['due'])) {
|
||||
$date_format = $event['due']->_dateonly ? self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])) : null;
|
||||
$fromto = $this->rc->format_date($event['due'], $date_format, false);
|
||||
|
||||
// add timezone information
|
||||
if ($fromto && $tzinfo && ($tzname = $this->timezone->getName())) {
|
||||
$fromto .= ' (' . strtr($tzname, '_', ' ') . ')';
|
||||
}
|
||||
|
||||
return $fromto;
|
||||
}
|
||||
|
||||
// abort if no valid event dates are given
|
||||
if (!is_object($event['start']) || !is_a($event['start'], 'DateTime') || !is_object($event['end']) || !is_a($event['end'], 'DateTime'))
|
||||
if (!is_object($event['start']) || !is_a($event['start'], 'DateTime') || !is_object($event['end']) || !is_a($event['end'], 'DateTime')) {
|
||||
return $fromto;
|
||||
}
|
||||
|
||||
$duration = $event['start']->diff($event['end'])->format('s');
|
||||
|
||||
|
|
|
@ -567,7 +567,7 @@ class libvcalendar implements Iterator
|
|||
}
|
||||
|
||||
// make organizer part of the attendees list for compatibility reasons
|
||||
if (!empty($event['organizer']) && is_array($event['attendees'])) {
|
||||
if (!empty($event['organizer']) && is_array($event['attendees']) && $event['_type'] == 'event') {
|
||||
array_unshift($event['attendees'], $event['organizer']);
|
||||
}
|
||||
|
||||
|
|
|
@ -87,6 +87,8 @@ $labels['itipobjectnotfound'] = 'The object referred by this message was not fou
|
|||
$labels['itipsubjectaccepted'] = '"$title" has been accepted by $name';
|
||||
$labels['itipsubjecttentative'] = '"$title" has been tentatively accepted by $name';
|
||||
$labels['itipsubjectdeclined'] = '"$title" has been declined by $name';
|
||||
$labels['itipsubjectin-process'] = '"$title" is in-process by $name';
|
||||
$labels['itipsubjectcompleted'] = '"$title" was completed by $name';
|
||||
$labels['itipsubjectcancel'] = 'Your participation in "$title" has been cancelled';
|
||||
|
||||
$labels['itipnewattendee'] = 'This is a reply from a new participant';
|
||||
|
@ -100,18 +102,24 @@ $labels['youhaveaccepted'] = 'You have accepted this invitation';
|
|||
$labels['youhavetentative'] = 'You have tentatively accepted this invitation';
|
||||
$labels['youhavedeclined'] = 'You have declined this invitation';
|
||||
$labels['youhavedelegated'] = 'You have delegated this invitation';
|
||||
$labels['youhavein-process'] = 'You are working on this assignment';
|
||||
$labels['youhavecompleted'] = 'You have completed this assignment';
|
||||
$labels['youhaveneeds-action'] = 'Your response to this invitation is still pending';
|
||||
|
||||
$labels['youhavepreviouslyaccepted'] = 'You have previously accepted this invitation';
|
||||
$labels['youhavepreviouslytentative'] = 'You have previously accepted this invitation tentatively';
|
||||
$labels['youhavepreviouslydeclined'] = 'You have previously declined this invitation';
|
||||
$labels['youhavepreviouslydelegated'] = 'You have previously delegated this invitation';
|
||||
$labels['youhavepreviouslyin-process'] = 'You have previously reported to work on this assignment';
|
||||
$labels['youhavepreviouslycompleted'] = 'You have previously completed this assignment';
|
||||
$labels['youhavepreviouslyneeds-action'] = 'Your response to this invitation is still pending';
|
||||
|
||||
$labels['attendeeaccepted'] = 'Participant has accepted';
|
||||
$labels['attendeetentative'] = 'Participant has tentatively accepted';
|
||||
$labels['attendeedeclined'] = 'Participant has declined';
|
||||
$labels['attendeedelegated'] = 'Participant has delegated to $delegatedto';
|
||||
$labels['attendeein-process'] = 'Participant is in-process';
|
||||
$labels['attendeecompleted'] = 'Participant has completed';
|
||||
$labels['notanattendee'] = 'You\'re not listed as an attendee of this object';
|
||||
$labels['outdatedinvitation'] = 'This invitation has been replaced by a newer version';
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ class kolab_format_event extends kolab_format_xcal
|
|||
{
|
||||
public $CTYPEv2 = 'application/x-vnd.kolab.event';
|
||||
|
||||
public static $scheduling_properties = array('start', 'end', 'allday', 'location', 'status', 'cancelled');
|
||||
public $scheduling_properties = array('start', 'end', 'allday', 'location', 'status', 'cancelled');
|
||||
|
||||
protected $objclass = 'Event';
|
||||
protected $read_func = 'readEvent';
|
||||
|
|
|
@ -26,7 +26,7 @@ class kolab_format_task extends kolab_format_xcal
|
|||
{
|
||||
public $CTYPEv2 = 'application/x-vnd.kolab.task';
|
||||
|
||||
public static $scheduling_properties = array('start', 'due', 'summary', 'status');
|
||||
public $scheduling_properties = array('start', 'due', 'summary', 'status');
|
||||
|
||||
protected $objclass = 'Todo';
|
||||
protected $read_func = 'readTodo';
|
||||
|
|
|
@ -29,7 +29,8 @@ abstract class kolab_format_xcal extends kolab_format
|
|||
public $CTYPE = 'application/calendar+xml';
|
||||
|
||||
public static $fulltext_cols = array('title', 'description', 'location', 'attendees:name', 'attendees:email', 'categories');
|
||||
public static $scheduling_properties = array('start', 'end', 'location');
|
||||
|
||||
public $scheduling_properties = array('start', 'end', 'location');
|
||||
|
||||
protected $sensitivity_map = array(
|
||||
'public' => kolabformat::ClassPublic,
|
||||
|
@ -315,7 +316,7 @@ abstract class kolab_format_xcal extends kolab_format
|
|||
// increment sequence when updating properties relevant for scheduling.
|
||||
// RFC 5545: "It is incremented [...] each time the Organizer makes a significant revision to the calendar component."
|
||||
// TODO: make the list of properties considered 'significant' for scheduling configurable
|
||||
foreach (self::$scheduling_properties as $prop) {
|
||||
foreach ($this->scheduling_properties as $prop) {
|
||||
$a = $old[$prop];
|
||||
$b = $object[$prop];
|
||||
if ($object['allday'] && ($prop == 'start' || $prop == 'end') && $a instanceof DateTime && $b instanceof DateTime) {
|
||||
|
|
|
@ -25,16 +25,17 @@
|
|||
class tasklist_kolab_driver extends tasklist_driver
|
||||
{
|
||||
// features supported by the backend
|
||||
public $alarms = false;
|
||||
public $alarms = false;
|
||||
public $attachments = true;
|
||||
public $undelete = false; // task undelete action
|
||||
public $attendees = true;
|
||||
public $undelete = false; // task undelete action
|
||||
public $alarm_types = array('DISPLAY','AUDIO');
|
||||
|
||||
private $rc;
|
||||
private $plugin;
|
||||
private $lists;
|
||||
private $folders = array();
|
||||
private $tasks = array();
|
||||
private $tasks = array();
|
||||
|
||||
|
||||
/**
|
||||
|
@ -772,6 +773,9 @@ class tasklist_kolab_driver extends tasklist_driver
|
|||
'status' => $record['status'],
|
||||
'parent_id' => $record['parent_id'],
|
||||
'recurrence' => $record['recurrence'],
|
||||
'attendees' => $record['attendees'],
|
||||
'organizer' => $record['organizer'],
|
||||
'sequence' => $record['sequence'],
|
||||
);
|
||||
|
||||
// convert from DateTime to internal date format
|
||||
|
@ -815,8 +819,8 @@ class tasklist_kolab_driver extends tasklist_driver
|
|||
}
|
||||
|
||||
/**
|
||||
* Convert the given task record into a data structure that can be passed to kolab_storage backend for saving
|
||||
* (opposite of self::_to_rcube_event())
|
||||
* Convert the given task record into a data structure that can be passed to kolab_storage backend for saving
|
||||
* (opposite of self::_to_rcube_event())
|
||||
*/
|
||||
private function _from_rcube_task($task, $old = array())
|
||||
{
|
||||
|
@ -824,14 +828,14 @@ class tasklist_kolab_driver extends tasklist_driver
|
|||
$object['categories'] = (array)$task['tags'];
|
||||
|
||||
if (!empty($task['date'])) {
|
||||
$object['due'] = new DateTime($task['date'].' '.$task['time'], $this->plugin->timezone);
|
||||
$object['due'] = rcube_utils::anytodatetime($task['date'].' '.$task['time'], $this->plugin->timezone);
|
||||
if (empty($task['time']))
|
||||
$object['due']->_dateonly = true;
|
||||
unset($object['date']);
|
||||
}
|
||||
|
||||
if (!empty($task['startdate'])) {
|
||||
$object['start'] = new DateTime($task['startdate'].' '.$task['starttime'], $this->plugin->timezone);
|
||||
$object['start'] = rcube_utils::anytodatetime($task['startdate'].' '.$task['starttime'], $this->plugin->timezone);
|
||||
if (empty($task['starttime']))
|
||||
$object['start']->_dateonly = true;
|
||||
unset($object['startdate']);
|
||||
|
@ -898,6 +902,14 @@ class tasklist_kolab_driver extends tasklist_driver
|
|||
unset($object['attachments']);
|
||||
}
|
||||
|
||||
// allow sequence increments if I'm the organizer
|
||||
if ($this->plugin->is_organizer($object)) {
|
||||
unset($object['sequence']);
|
||||
}
|
||||
else if (isset($old['sequence'])) {
|
||||
$object['sequence'] = $old['sequence'];
|
||||
}
|
||||
|
||||
unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags']);
|
||||
return $object;
|
||||
}
|
||||
|
|
|
@ -72,6 +72,7 @@ abstract class tasklist_driver
|
|||
// features supported by the backend
|
||||
public $alarms = false;
|
||||
public $attachments = false;
|
||||
public $attendees = false;
|
||||
public $undelete = false; // task undelete action
|
||||
public $sortable = false;
|
||||
public $alarm_types = array('DISPLAY');
|
||||
|
|
|
@ -33,6 +33,7 @@ $labels['status-needs-action'] = 'Needs action';
|
|||
$labels['status-in-process'] = 'In process';
|
||||
$labels['status-completed'] = 'Completed';
|
||||
$labels['status-cancelled'] = 'Cancelled';
|
||||
$labels['assignedto'] = 'Assigned to';
|
||||
|
||||
$labels['all'] = 'All';
|
||||
$labels['flagged'] = 'Flagged';
|
||||
|
@ -50,6 +51,7 @@ $labels['newtask'] = 'New Task';
|
|||
$labels['edittask'] = 'Edit Task';
|
||||
$labels['save'] = 'Save';
|
||||
$labels['cancel'] = 'Cancel';
|
||||
$labels['saveandnotify'] = 'Save and Notify';
|
||||
$labels['addsubtask'] = 'Add subtask';
|
||||
$labels['deletetask'] = 'Delete task';
|
||||
$labels['deletethisonly'] = 'Delete this task only';
|
||||
|
@ -58,6 +60,7 @@ $labels['taskactions'] = 'Task options...';
|
|||
|
||||
$labels['tabsummary'] = 'Summary';
|
||||
$labels['tabrecurrence'] = 'Recurrence';
|
||||
$labels['tabassignments'] = 'Assignments';
|
||||
$labels['tabattachments'] = 'Attachments';
|
||||
$labels['tabsharing'] = 'Sharing';
|
||||
|
||||
|
@ -76,7 +79,7 @@ $labels['at'] = 'at';
|
|||
$labels['this'] = 'this';
|
||||
$labels['next'] = 'next';
|
||||
|
||||
// mesages
|
||||
// messages
|
||||
$labels['savingdata'] = 'Saving data...';
|
||||
$labels['errorsaving'] = 'Failed to save data.';
|
||||
$labels['notasksfound'] = 'No tasks found for the given criteria';
|
||||
|
@ -86,6 +89,9 @@ $labels['deleteparenttasktconfirm'] = 'Do you really want to delete this task an
|
|||
$labels['deletelistconfirm'] = 'Do you really want to delete this list with all its tasks?';
|
||||
$labels['deletelistconfirmrecursive'] = 'Do you really want to delete this list with all its sub-lists and tasks?';
|
||||
$labels['aclnorights'] = 'You do not have administrator rights on this task list.';
|
||||
$labels['changetaskconfirm'] = 'Update task';
|
||||
$labels['changeconfirmnotifications'] = 'Do you want to notify the attendees about the modification?';
|
||||
$labels['partstatupdatenotification'] = 'Do you want to notify the organizer about the status change?';
|
||||
|
||||
// (hidden) titles and labels for accessibility annotations
|
||||
$labels['quickaddinput'] = 'New task date and title';
|
||||
|
@ -96,3 +102,68 @@ $labels['arialabellistsearchform'] = 'Tasklists search form';
|
|||
$labels['arialabeltaskselector'] = 'List mode';
|
||||
$labels['arialabeltasklisting'] = 'Tasks listing';
|
||||
|
||||
// attendees
|
||||
$labels['attendee'] = 'Assignee';
|
||||
$labels['role'] = 'Role';
|
||||
$labels['availability'] = 'Avail.';
|
||||
$labels['confirmstate'] = 'Status';
|
||||
$labels['addattendee'] = 'Add assignee';
|
||||
$labels['roleorganizer'] = 'Organizer';
|
||||
$labels['rolerequired'] = 'Required';
|
||||
$labels['roleoptional'] = 'Optional';
|
||||
$labels['rolechair'] = 'Chair';
|
||||
$labels['rolenonparticipant'] = 'Observer';
|
||||
$labels['sendinvitations'] = 'Send invitations';
|
||||
$labels['sendnotifications'] = 'Notify assignees about modifications';
|
||||
$labels['sendcancellation'] = 'Notify assignees about task cancellation';
|
||||
$labels['invitationsubject'] = 'You\'ve been assigned to "$title"';
|
||||
$labels['invitationmailbody'] = "*\$title*\n\nDue: \$date\n\nAssignees: \$attendees\n\nPlease find attached an iCalendar file with all the task details which you can import to your tasks application.";
|
||||
$labels['itipupdatesubject'] = '"$title" has been updated';
|
||||
$labels['itipupdatesubjectempty'] = 'A task that concerns you has been updated';
|
||||
$labels['itipupdatemailbody'] = "*\$title*\n\nDue: \$date\n\nAssignees: \$attendees\n\nPlease find attached an iCalendar file with the updated task details which you can import to your tasks application.";
|
||||
$labels['itipcancelsubject'] = '"$title" has been canceled';
|
||||
$labels['itipcancelmailbody'] = "*\$title*\n\nDue: \$date\n\nAssignees: \$attendees\n\nThe task has been cancelled by \$organizer.\n\nPlease find attached an iCalendar file with the updated task details.";
|
||||
$labels['saveintasklist'] = 'save in ';
|
||||
|
||||
// invitation handling (overrides labels from libcalendaring)
|
||||
$labels['itipobjectnotfound'] = 'The task referred by this message was not found in your tasks list.';
|
||||
|
||||
$labels['itipmailbodyaccepted'] = "\$sender has accepted the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nAssignees: \$attendees";
|
||||
$labels['itipmailbodytentative'] = "\$sender has tentatively accepted the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nAssignees: \$attendees";
|
||||
$labels['itipmailbodydeclined'] = "\$sender has declined the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nAssignees: \$attendees";
|
||||
$labels['itipmailbodycancel'] = "\$sender has rejected your assignment to the following task:\n\n*\$title*\n\nDue: \$date";
|
||||
$labels['itipmailbodyin-process'] = "\$sender has set the status of the following task to in-process:\n\n*\$title*\n\nDue: \$date";
|
||||
$labels['itipmailbodycompleted'] = "\$sender has completed the following task:\n\n*\$title*\n\nDue: \$date";
|
||||
|
||||
$labels['attendeeaccepted'] = 'Assignee has accepted';
|
||||
$labels['attendeetentative'] = 'Assignee has tentatively accepted';
|
||||
$labels['attendeedeclined'] = 'Assignee has declined';
|
||||
$labels['attendeedelegated'] = 'Assignee has delegated to $delegatedto';
|
||||
$labels['attendeein-process'] = 'Assignee is in-process';
|
||||
$labels['attendeecompleted'] = 'Assignee has completed';
|
||||
|
||||
$labels['itipdeclinetask'] = 'Decline your assignment to this task to the organizer';
|
||||
$labels['declinedeleteconfirm'] = 'Do you also want to delete this declined task from your tasks list?';
|
||||
$labels['itipcomment'] = 'Invitation/notification comment';
|
||||
$labels['itipcommenttitle'] = 'This comment will be attached to the invitation/notification message sent to assignees';
|
||||
$labels['itipsendsuccess'] = 'Invitation sent to assignees';
|
||||
$labels['errornotifying'] = 'Failed to send notifications to task assignees';
|
||||
$labels['removefromcalendar'] = 'Remove from my tasks';
|
||||
|
||||
$labels['andnmore'] = '$nr more...';
|
||||
$labels['delegatedto'] = 'Delegated to: ';
|
||||
$labels['delegatedfrom'] = 'Delegated from: ';
|
||||
$labels['savetotasklist'] = 'Save to tasks';
|
||||
$labels['comment'] = 'Comment';
|
||||
$labels['errorimportingtask'] = 'Failed to import task(s)';
|
||||
$labels['importwarningexists'] = 'A copy of this task already exists in your tasklist.';
|
||||
$labels['importsuccess'] = 'Successfully imported $nr tasks';
|
||||
$labels['newerversionexists'] = 'A newer version of this task already exists! Aborted.';
|
||||
$labels['nowritetasklistfound'] = 'No tasklist found to save the task';
|
||||
$labels['importedsuccessfully'] = 'The task was successfully added to \'$list\'';
|
||||
$labels['updatedsuccessfully'] = 'The task was successfully updated in \'$list\'';
|
||||
$labels['attendeupdateesuccess'] = 'Successfully updated the participant\'s status';
|
||||
$labels['itipresponseerror'] = 'Failed to send the response to this task assignment';
|
||||
$labels['itipinvalidrequest'] = 'This invitation is no longer valid';
|
||||
$labels['sentresponseto'] = 'Successfully sent assignment response to $mailto';
|
||||
$labels['successremoval'] = 'The task has been deleted successfully.';
|
||||
|
|
BIN
plugins/tasklist/skins/larry/images/attendee-status.png
Normal file
BIN
plugins/tasklist/skins/larry/images/attendee-status.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.1 KiB |
BIN
plugins/tasklist/skins/larry/images/badge_cancelled.png
Normal file
BIN
plugins/tasklist/skins/larry/images/badge_cancelled.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
BIN
plugins/tasklist/skins/larry/images/loading_blue.gif
Normal file
BIN
plugins/tasklist/skins/larry/images/loading_blue.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 847 B |
BIN
plugins/tasklist/skins/larry/images/sendinvitation.png
Normal file
BIN
plugins/tasklist/skins/larry/images/sendinvitation.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
BIN
plugins/tasklist/skins/larry/images/tasklist.png
Normal file
BIN
plugins/tasklist/skins/larry/images/tasklist.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
|
@ -20,7 +20,8 @@
|
|||
background-position: 0 -26px;
|
||||
}
|
||||
|
||||
ul.toolbarmenu li span.icon.taskadd {
|
||||
ul.toolbarmenu li span.icon.taskadd,
|
||||
#attachmentmenu li a.tasklistlink span.icon.taskadd {
|
||||
background-image: url(buttons.png);
|
||||
background-position: -4px -90px;
|
||||
}
|
||||
|
@ -814,6 +815,10 @@ ul.toolbarmenu li span.delete {
|
|||
color: #999;
|
||||
}
|
||||
|
||||
#taskshow.status-cancelled {
|
||||
background: url(images/badge_cancelled.png) top right no-repeat;
|
||||
}
|
||||
|
||||
#task-parent-title {
|
||||
position: relative;
|
||||
top: -0.6em;
|
||||
|
@ -849,6 +854,93 @@ a.morelink:hover {
|
|||
border-bottom: 2px solid #fafafa;
|
||||
}
|
||||
|
||||
.edit-attendees-table {
|
||||
width: 100%;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.edit-attendees-table tbody td {
|
||||
padding: 4px 7px;
|
||||
}
|
||||
|
||||
.edit-attendees-table tbody tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.edit-attendees-table th.role,
|
||||
.edit-attendees-table td.role {
|
||||
width: 9em;
|
||||
}
|
||||
|
||||
.edit-attendees-table th.availability,
|
||||
.edit-attendees-table td.availability,
|
||||
.edit-attendees-table th.confirmstate,
|
||||
.edit-attendees-table td.confirmstate {
|
||||
width: 6em;
|
||||
}
|
||||
|
||||
.edit-attendees-table th.options,
|
||||
.edit-attendees-table td.options {
|
||||
width: 24px;
|
||||
padding: 2px 4px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.edit-attendees-table th.sendmail,
|
||||
.edit-attendees-table td.sendmail {
|
||||
width: 48px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.edit-attendees-table th.sendmail label {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: 4px;
|
||||
width: 24px;
|
||||
height: 18px;
|
||||
min-width: 24px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
text-indent: -5000px;
|
||||
white-space: nowrap;
|
||||
background: url(images/sendinvitation.png) 1px 0 no-repeat;
|
||||
}
|
||||
|
||||
.edit-attendees-table th.name,
|
||||
.edit-attendees-table td.name {
|
||||
width: auto;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.edit-attendees-table td.name select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.edit-attendees-table a.deletelink {
|
||||
display: inline-block;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
text-indent: 1000px;
|
||||
}
|
||||
|
||||
#edit-attendees-form {
|
||||
position: relative;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#edit-attendees-form .attendees-invitebox {
|
||||
text-align: right;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#edit-attendees-form .attendees-invitebox label {
|
||||
padding-right: 3px;
|
||||
}
|
||||
|
||||
#taskedit-attachments {
|
||||
margin: 0.6em 0;
|
||||
}
|
||||
|
@ -857,10 +949,11 @@ a.morelink:hover {
|
|||
display: block;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
padding: 8px 4px 3px 30px;
|
||||
padding: 3px 4px 3px 30px;
|
||||
text-shadow: 0px 1px 1px #fff;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
#taskedit-attachments ul li a.file {
|
||||
|
@ -876,7 +969,7 @@ a.morelink:hover {
|
|||
div.form-section {
|
||||
position: relative;
|
||||
margin-top: 0.2em;
|
||||
margin-bottom: 0.8em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.form-section label {
|
||||
|
@ -891,6 +984,10 @@ label.block {
|
|||
margin-bottom: 0.3em;
|
||||
}
|
||||
|
||||
#task-description {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
#taskedit-completeness-slider {
|
||||
display: inline-block;
|
||||
margin-left: 2em;
|
||||
|
@ -935,6 +1032,57 @@ label.block {
|
|||
outline: none;
|
||||
}
|
||||
|
||||
.task-attendees span.attendee {
|
||||
padding-right: 18px;
|
||||
margin-right: 0.5em;
|
||||
background: url(images/attendee-status.png) right 0 no-repeat;
|
||||
}
|
||||
|
||||
.task-attendees span.attendee a.mailtolink {
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.task-attendees span.attendee a.mailtolink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.task-attendees span.completed {
|
||||
background-position: right -20px;
|
||||
}
|
||||
|
||||
.task-attendees span.declined {
|
||||
background-position: right -40px;
|
||||
}
|
||||
|
||||
.task-attendees span.tentative {
|
||||
background-position: right -60px;
|
||||
}
|
||||
|
||||
.task-attendees span.delegated {
|
||||
background-position: right -180px;
|
||||
}
|
||||
|
||||
.task-attendees span.in-process {
|
||||
background-position: right -200px;
|
||||
}
|
||||
|
||||
.task-attendees span.accepted {
|
||||
background-position: right -220px;
|
||||
}
|
||||
|
||||
.task-attendees span.organizer {
|
||||
background-position: right 100px;
|
||||
}
|
||||
|
||||
#all-task-attendees span.attendee {
|
||||
display: block;
|
||||
margin-bottom: 0.4em;
|
||||
padding-bottom: 0.3em;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.tasklistview .uidialog .tabbed {
|
||||
min-width: 600px;
|
||||
}
|
||||
|
@ -947,6 +1095,128 @@ label.block {
|
|||
width: 20em;
|
||||
}
|
||||
|
||||
.task-dialog-message {
|
||||
margin-top: 0.5em;
|
||||
padding: 0.8em;
|
||||
border: 1px solid #ffdf0e;
|
||||
background-color: #fef893;
|
||||
}
|
||||
|
||||
.task-dialog-message .message,
|
||||
.task-update-confirm .message {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
/* Invitation UI in mail */
|
||||
|
||||
.messagelist tbody .attachment span.ical {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
height: 18px;
|
||||
width: 20px;
|
||||
padding: 0;
|
||||
background: url(images/ical-attachment.png) 2px 1px no-repeat;
|
||||
}
|
||||
|
||||
div.tasklist-invitebox {
|
||||
min-height: 20px;
|
||||
margin: 5px 8px;
|
||||
padding: 3px 6px 6px 34px;
|
||||
border: 1px solid #ffdf0e;
|
||||
background: url(images/tasklist.png) 6px 5px no-repeat #fef893;
|
||||
}
|
||||
|
||||
div.tasklist-invitebox td.ititle {
|
||||
font-weight: bold;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
div.tasklist-invitebox td.label {
|
||||
color: #666;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
#event-rsvp .rsvp-buttons,
|
||||
div.tasklist-invitebox .itip-buttons div {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
#event-rsvp input.button,
|
||||
div.tasklist-invitebox input.button {
|
||||
font-weight: bold;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
div.tasklist-invitebox .folder-select {
|
||||
font-weight: 10px;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
div.tasklist-invitebox .rsvp-status {
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
div.tasklist-invitebox .rsvp-status.loading {
|
||||
color: #666;
|
||||
padding: 1px 0 2px 24px;
|
||||
background: url(images/loading_blue.gif) top left no-repeat;
|
||||
}
|
||||
|
||||
div.tasklist-invitebox .rsvp-status.hint {
|
||||
color: #666;
|
||||
text-shadow: none;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#event-partstat .changersvp,
|
||||
.edit-attendees-table td.confirmstate span,
|
||||
div.tasklist-invitebox .rsvp-status.declined,
|
||||
div.tasklist-invitebox .rsvp-status.tentative,
|
||||
div.tasklist-invitebox .rsvp-status.accepted,
|
||||
div.tasklist-invitebox .rsvp-status.delegated,
|
||||
div.tasklist-invitebox .rsvp-status.in-process,
|
||||
div.tasklist-invitebox .rsvp-status.completed,
|
||||
div.tasklist-invitebox .rsvp-status.needs-action {
|
||||
padding: 0 0 1px 22px;
|
||||
background: url(images/attendee-status.png) 2px -20px no-repeat;
|
||||
}
|
||||
|
||||
#event-partstat .changersvp.declined,
|
||||
div.tasklist-invitebox .rsvp-status.declined,
|
||||
.edit-attendees-table td.confirmstate span.declined {
|
||||
background-position: 2px -40px;
|
||||
}
|
||||
|
||||
#event-partstat .changersvp.tentative,
|
||||
div.tasklist-invitebox .rsvp-status.tentative,
|
||||
.edit-attendees-table td.confirmstate span.tentative {
|
||||
background-position: 2px -60px;
|
||||
}
|
||||
|
||||
#event-partstat .changersvp.delegated,
|
||||
div.tasklist-invitebox .rsvp-status.delegated,
|
||||
.edit-attendees-table td.confirmstate span.delegated {
|
||||
background-position: 2px -180px;
|
||||
}
|
||||
|
||||
#event-partstat .changersvp.needs-action,
|
||||
div.tasklist-invitebox .rsvp-status.needs-action,
|
||||
.edit-attendees-table td.confirmstate span.needs-action {
|
||||
background-position: 2px 0;
|
||||
}
|
||||
|
||||
#event-partstat .changersvp.in-process,
|
||||
div.tasklist-invitebox .rsvp-status.in-process,
|
||||
.edit-attendees-table td.confirmstate span.in-process {
|
||||
background-position: 2px -200px;
|
||||
}
|
||||
|
||||
#event-partstat .changersvp.accepted,
|
||||
div.tasklist-invitebox .rsvp-status.accepted,
|
||||
.edit-attendees-table td.confirmstate span.accepted {
|
||||
background-position: 2px -220px;
|
||||
}
|
||||
|
||||
|
||||
/** Special hacks for IE7 **/
|
||||
/** They need to be in this file to also affect the task-create dialog embedded in mail view **/
|
||||
|
@ -954,4 +1224,3 @@ label.block {
|
|||
html.ie7 #taskedit-completeness-slider {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
|
|
|
@ -156,6 +156,23 @@
|
|||
<label><roundcube:label name="tasklist.alarms" /></label>
|
||||
<span class="task-text"></span>
|
||||
</div>
|
||||
<div id="task-attendees" class="form-section task-attendees">
|
||||
<label><roundcube:label name="tasklist.assignedto" /></label>
|
||||
<span class="task-text"></span>
|
||||
</div>
|
||||
<div id="task-organizer" class="form-section task-attendees">
|
||||
<label><roundcube:label name="tasklist.roleorganizer" /></label>
|
||||
<span class="task-text"></span>
|
||||
</div>
|
||||
<!--
|
||||
<div id="task-partstat" class="form-section">
|
||||
<label><roundcube:label name="tasklist.mystatus" /></label>
|
||||
<span class="changersvp" role="button" tabindex="0" title="<roundcube:label name='tasklist.changepartstat' />">
|
||||
<span class="task-text"></span>
|
||||
<a class="iconbutton edit"><roundcube:label name='tasklist.changepartstat' /></a>
|
||||
</span>
|
||||
</div>
|
||||
-->
|
||||
<div id="task-list" class="form-section">
|
||||
<label><roundcube:label name="tasklist.list" /></label>
|
||||
<span class="task-text"></span>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div id="taskedit" class="uidialog uidialog-tabbed">
|
||||
<form id="taskeditform" action="#" method="post" enctype="multipart/form-data">
|
||||
<ul>
|
||||
<li><a href="#taskedit-panel-main"><roundcube:label name="tasklist.tabsummary" /></a></li><li><a href="#taskedit-panel-recurrence"><roundcube:label name="tasklist.tabrecurrence" /></a></li><li id="taskedit-tab-attachments"><a href="#taskedit-panel-attachments"><roundcube:label name="tasklist.tabattachments" /></a></li>
|
||||
<li><a href="#taskedit-panel-main"><roundcube:label name="tasklist.tabsummary" /></a></li><li><a href="#taskedit-panel-recurrence"><roundcube:label name="tasklist.tabrecurrence" /></a></li><li id="edit-tab-attendees"><a href="#taskedit-panel-attendees"><roundcube:label name="tasklist.tabassignments" /></a></li><li id="taskedit-tab-attachments"><a href="#taskedit-panel-attachments"><roundcube:label name="tasklist.tabattachments" /></a></li>
|
||||
</ul>
|
||||
<!-- basic info -->
|
||||
<div id="taskedit-panel-main">
|
||||
|
@ -79,6 +79,17 @@
|
|||
<roundcube:object name="plugin.recurrence_form" part="rdate" class="form-section" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- attendees list (assignments) -->
|
||||
<div id="taskedit-panel-attendees">
|
||||
<div class="form-section" id="taskedit-organizer">
|
||||
<label for="edit-identities-list"><roundcube:label name="tasklist.roleorganizer" /></label>
|
||||
<roundcube:object name="plugin.identity_select" id="edit-identities-list" />
|
||||
</div>
|
||||
<h3 id="aria-label-attendeestable" class="voice"><roundcube:label name="tasklist.arialabeleventassignments" /></h3>
|
||||
<roundcube:object name="plugin.attendees_list" id="edit-attendees-table" class="records-table edit-attendees-table" coltitle="attendee" aria-labelledby="aria-label-attendeestable" />
|
||||
<roundcube:object name="plugin.attendees_form" id="edit-attendees-form" />
|
||||
<roundcube:include file="/templates/freebusylegend.html" />
|
||||
</div>
|
||||
<!-- attachments list (with upload form) -->
|
||||
<div id="taskedit-panel-attachments">
|
||||
<div id="taskedit-attachments">
|
||||
|
@ -91,4 +102,5 @@
|
|||
<roundcube:object name="plugin.filedroparea" id="taskedit-tab-2" />
|
||||
</div>
|
||||
</form>
|
||||
<roundcube:object name="plugin.edit_attendees_notify" id="edit-attendees-notify" class="task-dialog-message" style="display:none" />
|
||||
</div>
|
||||
|
|
|
@ -82,6 +82,8 @@ function rcube_tasklist_ui(settings)
|
|||
var tasklists_widget;
|
||||
var focused_task;
|
||||
var focused_subclass;
|
||||
var task_attendees = [];
|
||||
var attendees_list;
|
||||
var me = this;
|
||||
|
||||
// general datepicker settings
|
||||
|
@ -351,9 +353,8 @@ function rcube_tasklist_ui(settings)
|
|||
if (rcmail.busy)
|
||||
return false;
|
||||
|
||||
rec.status = e.target.checked ? 'COMPLETED' : (rec.complete == 1 ? 'NEEDS-ACTION' : '');
|
||||
li.toggleClass('complete');
|
||||
save_task(rec, 'edit');
|
||||
save_task_confirm(rec, 'edit', { _status_before:rec.status + '', status:e.target.checked ? 'COMPLETED' : (rec.complete > 0 ? 'IN-PROCESS' : 'NEEDS-ACTION') });
|
||||
item.toggleClass('complete');
|
||||
return true;
|
||||
|
||||
case 'flagged':
|
||||
|
@ -361,7 +362,7 @@ function rcube_tasklist_ui(settings)
|
|||
return false;
|
||||
|
||||
rec.flagged = rec.flagged ? 0 : 1;
|
||||
li.toggleClass('flagged').find('.flagged:first').attr('aria-checked', (rec.flagged ? 'true' : 'false'));
|
||||
item.toggleClass('flagged').find('.flagged:first').attr('aria-checked', (rec.flagged ? 'true' : 'false'));
|
||||
save_task(rec, 'edit');
|
||||
break;
|
||||
|
||||
|
@ -375,8 +376,7 @@ function rcube_tasklist_ui(settings)
|
|||
input.datepicker($.extend({
|
||||
onClose: function(dateText, inst) {
|
||||
if (dateText != (rec.date || '')) {
|
||||
rec.date = dateText;
|
||||
save_task(rec, 'edit');
|
||||
save_task_confirm(rec, 'edit', { date:dateText });
|
||||
}
|
||||
input.datepicker('destroy').remove();
|
||||
link.html(dateText || rcmail.gettext('nodate','tasklist'));
|
||||
|
@ -541,6 +541,47 @@ function rcube_tasklist_ui(settings)
|
|||
if (sel) $(sel).val('');
|
||||
return false;
|
||||
});
|
||||
|
||||
// init attendees autocompletion
|
||||
var ac_props;
|
||||
// parallel autocompletion
|
||||
if (rcmail.env.autocomplete_threads > 0) {
|
||||
ac_props = {
|
||||
threads: rcmail.env.autocomplete_threads,
|
||||
sources: rcmail.env.autocomplete_sources
|
||||
};
|
||||
}
|
||||
rcmail.init_address_input_events($('#edit-attendee-name'), ac_props);
|
||||
rcmail.addEventListener('autocomplete_insert', function(e){
|
||||
if (e.field.name == 'participant') {
|
||||
$('#edit-attendee-add').click();
|
||||
}
|
||||
// else if (e.field.name == 'resource' && e.data && e.data.email) {
|
||||
// add_attendee($.extend(e.data, { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'RESOURCE' }));
|
||||
// e.field.value = '';
|
||||
// }
|
||||
});
|
||||
|
||||
$('#edit-attendee-add').click(function(){
|
||||
var input = $('#edit-attendee-name');
|
||||
rcmail.ksearch_blur();
|
||||
if (add_attendees(input.val(), { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'INDIVIDUAL' })) {
|
||||
input.val('');
|
||||
}
|
||||
});
|
||||
|
||||
// handle change of "send invitations" checkbox
|
||||
$('#edit-attendees-invite').change(function() {
|
||||
$('#edit-attendees-donotify,input.edit-attendee-reply').prop('checked', this.checked);
|
||||
// hide/show comment field
|
||||
$('.attendees-commentbox')[this.checked ? 'show' : 'hide']();
|
||||
});
|
||||
|
||||
// delegate change task to "send invitations" checkbox
|
||||
$('#edit-attendees-donotify').change(function() {
|
||||
$('#edit-attendees-invite').click();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -928,6 +969,10 @@ function rcube_tasklist_ui(settings)
|
|||
*/
|
||||
function save_task(rec, action)
|
||||
{
|
||||
// show confirmation dialog when status of an assigned task has changed
|
||||
if (rec._status_before !== undefined && is_attendee(rec))
|
||||
return save_task_confirm(rec, action);
|
||||
|
||||
if (!rcmail.busy) {
|
||||
saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
|
||||
rcmail.http_post('tasks/task', { action:action, t:rec, filter:filtermask });
|
||||
|
@ -938,6 +983,82 @@ function rcube_tasklist_ui(settings)
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display confirm dialog when modifying/deleting a task record
|
||||
*/
|
||||
var save_task_confirm = function(rec, action, updates)
|
||||
{
|
||||
var data = $.extend({}, rec, updates || {}),
|
||||
notify = false, partstat = false, html = '';
|
||||
|
||||
// task has attendees, ask whether to notify them
|
||||
if (has_attendees(rec) && is_organizer(rec)) {
|
||||
notify = true;
|
||||
html = rcmail.gettext('changeconfirmnotifications', 'tasklist');
|
||||
}
|
||||
// ask whether to change my partstat and notify organizer
|
||||
else if (data._status_before !== undefined && data.status && data._status_before != data.status && is_attendee(rec)) {
|
||||
partstat = true;
|
||||
html = rcmail.gettext('partstatupdatenotification', 'tasklist');
|
||||
}
|
||||
|
||||
// remove to avoid endless recursion
|
||||
delete data._status_before;
|
||||
|
||||
// show dialog
|
||||
if (html) {
|
||||
var $dialog = $('<div>').html(html);
|
||||
|
||||
var buttons = [];
|
||||
buttons.push({
|
||||
text: rcmail.gettext('saveandnotify', 'tasklist'),
|
||||
click: function() {
|
||||
if (notify) data._notify = 1;
|
||||
if (partstat) data._reportpartstat = data.status == 'CANCELLED' ? 'DECLINED' : data.status;
|
||||
save_task(data, action);
|
||||
$(this).dialog('close');
|
||||
}
|
||||
});
|
||||
buttons.push({
|
||||
text: rcmail.gettext('save', 'tasklist'),
|
||||
click: function() {
|
||||
save_task(data, action);
|
||||
$(this).dialog('close');
|
||||
}
|
||||
});
|
||||
buttons.push({
|
||||
text: rcmail.gettext('cancel', 'tasklist'),
|
||||
click: function() {
|
||||
$(this).dialog('close');
|
||||
if (updates)
|
||||
render_task(rec, rec.id); // restore previous state
|
||||
}
|
||||
});
|
||||
|
||||
$dialog.dialog({
|
||||
modal: true,
|
||||
width: 460,
|
||||
closeOnEscapeType: false,
|
||||
dialogClass: 'warning no-close',
|
||||
title: rcmail.gettext('changetaskconfirm', 'tasklist'),
|
||||
buttons: buttons,
|
||||
open: function() {
|
||||
setTimeout(function(){
|
||||
$dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
|
||||
}, 5);
|
||||
},
|
||||
close: function(){
|
||||
$dialog.dialog('destroy').remove();
|
||||
}
|
||||
}).addClass('task-update-confirm').show();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// do update
|
||||
return save_task(data, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove saving lock and free the UI for new input
|
||||
*/
|
||||
|
@ -1298,6 +1419,143 @@ function rcube_tasklist_ui(settings)
|
|||
scroll_timer = window.setTimeout(function(){ tasklist_drag_scroll(container, dir); }, scroll_speed);
|
||||
}
|
||||
|
||||
// check if the task has 'real' attendees, excluding the current user
|
||||
var has_attendees = function(task)
|
||||
{
|
||||
return !!(task.attendees && task.attendees.length && (task.attendees.length > 1 || String(task.attendees[0].email).toLowerCase() != settings.identity.email));
|
||||
};
|
||||
|
||||
// check if the current user is an attendee of this task
|
||||
var is_attendee = function(task, email, role)
|
||||
{
|
||||
var i, attendee, emails = email ? ';' + email.toLowerCase() : settings.identity.emails;
|
||||
|
||||
for (i=0; task.attendees && i < task.attendees.length; i++) {
|
||||
attendee = task.attendees[i];
|
||||
if ((!role || attendee.role == role) && attendee.email && emails.indexOf(';'+attendee.email.toLowerCase()) >= 0) {
|
||||
return attendee;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// check if the current user is the organizer
|
||||
var is_organizer = function(task, email)
|
||||
{
|
||||
if (!email) email = task.organizer ? task.organizer.email : null;
|
||||
if (email)
|
||||
return settings.identity.emails.indexOf(';'+email) >= 0;
|
||||
return true;
|
||||
};
|
||||
|
||||
// add the given list of participants
|
||||
var add_attendees = function(names, params)
|
||||
{
|
||||
names = explode_quoted_string(names.replace(/,\s*$/, ''), ',');
|
||||
|
||||
// parse name/email pairs
|
||||
var i, item, email, name, success = false;
|
||||
for (i=0; i < names.length; i++) {
|
||||
email = name = '';
|
||||
item = $.trim(names[i]);
|
||||
|
||||
if (!item.length) {
|
||||
continue;
|
||||
}
|
||||
// address in brackets without name (do nothing)
|
||||
else if (item.match(/^<[^@]+@[^>]+>$/)) {
|
||||
email = item.replace(/[<>]/g, '');
|
||||
}
|
||||
// address without brackets and without name (add brackets)
|
||||
else if (rcube_check_email(item)) {
|
||||
email = item;
|
||||
}
|
||||
// address with name
|
||||
else if (item.match(/([^\s<@]+@[^>]+)>*$/)) {
|
||||
email = RegExp.$1;
|
||||
name = item.replace(email, '').replace(/^["\s<>]+/, '').replace(/["\s<>]+$/, '');
|
||||
}
|
||||
|
||||
if (email) {
|
||||
add_attendee($.extend({ email:email, name:name }, params));
|
||||
success = true;
|
||||
}
|
||||
else {
|
||||
alert(rcmail.gettext('noemailwarning'));
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
};
|
||||
|
||||
// add the given attendee to the list
|
||||
var add_attendee = function(data, readonly)
|
||||
{
|
||||
if (!me.selected_task)
|
||||
return false;
|
||||
|
||||
// check for dupes...
|
||||
var exists = false;
|
||||
$.each(task_attendees, function(i, v) { exists |= (v.email == data.email); });
|
||||
if (exists)
|
||||
return false;
|
||||
|
||||
var dispname = Q(data.name || data.email);
|
||||
if (data.email)
|
||||
dispname = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink" data-cutype="' + data.cutype + '">' + dispname + '</a>';
|
||||
|
||||
// delete icon
|
||||
var icon = rcmail.env.deleteicon ? '<img src="' + rcmail.env.deleteicon + '" alt="" />' : rcmail.gettext('delete');
|
||||
var dellink = '<a href="#delete" class="iconlink delete deletelink" title="' + Q(rcmail.gettext('delete')) + '">' + icon + '</a>';
|
||||
var tooltip = data.status || '';
|
||||
|
||||
// send invitation checkbox
|
||||
var invbox = '<input type="checkbox" class="edit-attendee-reply" value="' + Q(data.email) +'" title="' + Q(rcmail.gettext('tasklist.sendinvitations')) + '" '
|
||||
+ (!data.noreply ? 'checked="checked" ' : '') + '/>';
|
||||
|
||||
if (data['delegated-to'])
|
||||
tooltip = rcmail.gettext('delegatedto', 'tasklist') + data['delegated-to'];
|
||||
else if (data['delegated-from'])
|
||||
tooltip = rcmail.gettext('delegatedfrom', 'tasklist') + data['delegated-from'];
|
||||
|
||||
var html = '<td class="name">' + dispname + '</td>' +
|
||||
'<td class="confirmstate"><span class="' + String(data.status).toLowerCase() + '" title="' + Q(tooltip) + '">' + Q(data.status || '') + '</span></td>' +
|
||||
(data.cutype != 'RESOURCE' ? '<td class="sendmail">' + (readonly || !invbox ? '' : invbox) + '</td>' : '') +
|
||||
'<td class="options">' + (readonly ? '' : dellink) + '</td>';
|
||||
|
||||
var tr = $('<tr>')
|
||||
.addClass(String(data.role).toLowerCase())
|
||||
.html(html)
|
||||
.appendTo(attendees_list);
|
||||
|
||||
tr.find('a.deletelink').click({ id:(data.email || data.name) }, function(e) { remove_attendee(this, e.data.id); return false; });
|
||||
tr.find('a.mailtolink').click(task_attendee_click);
|
||||
tr.find('input.edit-attendee-reply').click(function() {
|
||||
var enabled = $('#edit-attendees-invite:checked').length || $('input.edit-attendee-reply:checked').length;
|
||||
$('p.attendees-commentbox')[enabled ? 'show' : 'hide']();
|
||||
});
|
||||
|
||||
task_attendees.push(data);
|
||||
return true;
|
||||
};
|
||||
|
||||
// event handler for clicks on an attendee link
|
||||
var task_attendee_click = function(e)
|
||||
{
|
||||
var mailto = this.href.substr(7);
|
||||
rcmail.command('compose', mailto);
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// remove an attendee from the list
|
||||
var remove_attendee = function(elem, id)
|
||||
{
|
||||
$(elem).closest('tr').remove();
|
||||
task_attendees = $.grep(task_attendees, function(data) { return (data.name != id && data.email != id) });
|
||||
};
|
||||
|
||||
/**
|
||||
* Show task details in a dialog
|
||||
*/
|
||||
|
@ -1308,6 +1566,12 @@ function rcube_tasklist_ui(settings)
|
|||
if ($dialog.is(':ui-dialog'))
|
||||
$dialog.dialog('close');
|
||||
|
||||
// remove status-* classes
|
||||
$dialog.removeClass(function(i, oldclass) {
|
||||
var oldies = String(oldclass).split(' ');
|
||||
return $.grep(oldies, function(cls) { return cls.indexOf('status-') === 0 }).join(' ');
|
||||
});
|
||||
|
||||
if (!(rec = listdata[id]) || clear_popups({}))
|
||||
return;
|
||||
|
||||
|
@ -1326,6 +1590,7 @@ function rcube_tasklist_ui(settings)
|
|||
$('#task-completeness .task-text').html(((rec.complete || 0) * 100) + '%');
|
||||
$('#task-status')[(rec.status ? 'show' : 'hide')]().children('.task-text').html(rcmail.gettext('status-'+String(rec.status).toLowerCase(),'tasklist'));
|
||||
$('#task-list .task-text').html(Q(me.tasklists[rec.list] ? me.tasklists[rec.list].name : ''));
|
||||
$('#task-attendees, #task-organizer').hide();
|
||||
|
||||
var itags = get_inherited_tags(rec);
|
||||
var taglist = $('#task-tags')[(rec.tags && rec.tags.length || itags.length ? 'show' : 'hide')]().children('.task-text').empty();
|
||||
|
@ -1347,6 +1612,10 @@ function rcube_tasklist_ui(settings)
|
|||
});
|
||||
}
|
||||
|
||||
if (rec.status) {
|
||||
$dialog.addClass('status-' + String(rec.status).toLowerCase());
|
||||
}
|
||||
|
||||
if (rec.recurrence && rec.recurrence_text) {
|
||||
$('#task-recurrence').show().children('.task-text').html(Q(rec.recurrence_text));
|
||||
}
|
||||
|
@ -1363,6 +1632,84 @@ function rcube_tasklist_ui(settings)
|
|||
}
|
||||
}
|
||||
|
||||
// list task attendees
|
||||
if (list.attendees && rec.attendees) {
|
||||
console.log(rec.attendees)
|
||||
/*
|
||||
// sort resources to the end
|
||||
rec.attendees.sort(function(a,b) {
|
||||
var j = a.cutype == 'RESOURCE' ? 1 : 0,
|
||||
k = b.cutype == 'RESOURCE' ? 1 : 0;
|
||||
return (j - k);
|
||||
});
|
||||
*/
|
||||
var j, data, rsvp = false, mystatus = null, line, morelink, html = '', overflow = '',
|
||||
organizer = is_organizer(rec);
|
||||
|
||||
for (j=0; j < rec.attendees.length; j++) {
|
||||
data = rec.attendees[j];
|
||||
|
||||
if (data.email && settings.identity.emails.indexOf(';'+data.email) >= 0) {
|
||||
mystatus = data.status.toLowerCase();
|
||||
if (data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE' || data.rsvp)
|
||||
rsvp = mystatus;
|
||||
}
|
||||
|
||||
line = task_attendee_html(data);
|
||||
|
||||
if (morelink)
|
||||
overflow += line;
|
||||
else
|
||||
html += line;
|
||||
|
||||
// stop listing attendees
|
||||
if (j == 7 && rec.attendees.length >= 7) {
|
||||
morelink = $('<a href="#more" class="morelink"></a>').html(rcmail.gettext('andnmore', 'tasklist').replace('$nr', rec.attendees.length - j - 1));
|
||||
}
|
||||
}
|
||||
|
||||
if (html) {
|
||||
$('#task-attendees').show()
|
||||
.children('.task-text')
|
||||
.html(html)
|
||||
.find('a.mailtolink').click(task_attendee_click);
|
||||
|
||||
// display all attendees in a popup when clicking the "more" link
|
||||
if (morelink) {
|
||||
$('#task-attendees .task-text').append(morelink);
|
||||
morelink.click(function(e) {
|
||||
rcmail.show_popup_dialog(
|
||||
'<div id="all-task-attendees" class="task-attendees">' + html + overflow + '</div>',
|
||||
rcmail.gettext('tabattendees', 'tasklist'),
|
||||
null,
|
||||
{width: 450, modal: false}
|
||||
);
|
||||
$('#all-task-attendees a.mailtolink').click(task_attendee_click);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
/*
|
||||
if (mystatus && !rsvp) {
|
||||
$('#task-partstat').show().children('.changersvp')
|
||||
.removeClass('accepted tentative declined delegated needs-action')
|
||||
.addClass(mystatus)
|
||||
.children('.task-text')
|
||||
.html(Q(rcmail.gettext('itip' + mystatus, 'libcalendaring')));
|
||||
}
|
||||
|
||||
$('#task-rsvp')[(rsvp && !is_organizer(event) && rec.status != 'CANCELLED' ? 'show' : 'hide')]();
|
||||
$('#task-rsvp .rsvp-buttons input').prop('disabled', false).filter('input[rel='+mystatus+']').prop('disabled', true);
|
||||
|
||||
$('#task-rsvp a.reply-comment-toggle').show();
|
||||
$('#task-rsvp .itip-reply-comment textarea').hide().val('');
|
||||
*/
|
||||
|
||||
if (rec.organizer && !organizer) {
|
||||
$('#task-organizer').show().children('.task-text').html(task_attendee_html(rec.organizer));
|
||||
}
|
||||
}
|
||||
|
||||
// define dialog buttons
|
||||
var buttons = [];
|
||||
if (list.editable && !rec.readonly) {
|
||||
|
@ -1391,7 +1738,7 @@ function rcube_tasklist_ui(settings)
|
|||
closeOnEscape: true,
|
||||
title: rcmail.gettext('taskdetails', 'tasklist'),
|
||||
open: function() {
|
||||
$dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
|
||||
$dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
|
||||
},
|
||||
close: function() {
|
||||
$dialog.dialog('destroy').appendTo(document.body);
|
||||
|
@ -1405,6 +1752,24 @@ function rcube_tasklist_ui(settings)
|
|||
me.dialog_resize($dialog.get(0), $dialog.height(), 580);
|
||||
}
|
||||
|
||||
// render HTML code for displaying an attendee record
|
||||
function task_attendee_html(data)
|
||||
{
|
||||
var dispname = Q(data.name || data.email), tooltip = '';
|
||||
|
||||
if (data.email) {
|
||||
tooltip = data.email;
|
||||
dispname = '<a href="mailto:' + data.email + '" class="mailtolink" data-cutype="' + data.cutype + '">' + dispname + '</a>';
|
||||
}
|
||||
|
||||
if (data['delegated-to'])
|
||||
tooltip = rcmail.gettext('delegatedto', 'tasklist') + data['delegated-to'];
|
||||
else if (data['delegated-from'])
|
||||
tooltip = rcmail.gettext('delegatedfrom', 'tasklist') + data['delegated-from'];
|
||||
|
||||
return '<span class="attendee ' + String(data.role == 'ORGANIZER' ? 'organizer' : data.status).toLowerCase() + '" title="' + Q(tooltip) + '">' + dispname + '</span> ';
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the dialog to edit a task
|
||||
*/
|
||||
|
@ -1422,7 +1787,7 @@ function rcube_tasklist_ui(settings)
|
|||
return false;
|
||||
|
||||
me.selected_task = $.extend({ valarms:[] }, rec); // clone task object
|
||||
rec = me.selected_task;
|
||||
rec = me.selected_task;
|
||||
|
||||
// assign temporary id
|
||||
if (!me.selected_task.id)
|
||||
|
@ -1442,6 +1807,12 @@ function rcube_tasklist_ui(settings)
|
|||
completeness_slider.slider('value', complete.val());
|
||||
var taskstatus = $('#taskedit-status').val(rec.status || '');
|
||||
var tasklist = $('#taskedit-tasklist').val(rec.list || me.selected_list).prop('disabled', rec.parent_id ? true : false);
|
||||
var notify = $('#edit-attendees-donotify').get(0);
|
||||
var invite = $('#edit-attendees-invite').get(0);
|
||||
var comment = $('#edit-attendees-comment');
|
||||
|
||||
notify.checked = has_attendees(rec);
|
||||
invite.checked = true;
|
||||
|
||||
// tag-edit line
|
||||
var tagline = $(rcmail.gui_objects.edittagline).empty();
|
||||
|
@ -1468,6 +1839,48 @@ function rcube_tasklist_ui(settings)
|
|||
// set recurrence
|
||||
me.set_recurrence_edit(rec);
|
||||
|
||||
// init attendees tab
|
||||
var organizer = !rec.attendees || is_organizer(rec),
|
||||
allow_invitations = organizer || (rec.owner && rec.owner == 'anonymous') || settings.invite_shared;
|
||||
|
||||
task_attendees = [];
|
||||
attendees_list = $('#edit-attendees-table > tbody').html('');
|
||||
$('#edit-attendees-notify')[(notify.checked && allow_invitations ? 'show' : 'hide')]();
|
||||
$('#edit-localchanges-warning')[(has_attendees(rec) && !(allow_invitations || (rec.owner && is_organizer(rec, rec.owner))) ? 'show' : 'hide')]();
|
||||
|
||||
// attendees (aka assignees)
|
||||
if (list.attendees) {
|
||||
var j, data, reply_selected = 0;
|
||||
if (rec.attendees) {
|
||||
for (j=0; j < rec.attendees.length; j++) {
|
||||
data = rec.attendees[j];
|
||||
add_attendee(data, !allow_invitations);
|
||||
if (allow_invitations && !data.noreply) {
|
||||
reply_selected++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// make sure comment box is visible if at least one attendee has reply enabled
|
||||
// or global "send invitations" checkbox is checked
|
||||
if (reply_selected || $('#edit-attendees-invite:checked').length) {
|
||||
$('p.attendees-commentbox').show();
|
||||
}
|
||||
|
||||
// select the correct organizer identity
|
||||
var identity_id = 0;
|
||||
$.each(settings.identities, function(i,v) {
|
||||
if (rec.organizer && v == rec.organizer.email) {
|
||||
identity_id = i;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
$('#edit-attendees-form')[(allow_invitations?'show':'hide')]();
|
||||
$('#edit-identities-list').val(identity_id);
|
||||
$('#taskedit-organizer')[(organizer ? 'show' : 'hide')]();
|
||||
}
|
||||
|
||||
// attachments
|
||||
rcmail.enable_command('remove-attachment', list.editable);
|
||||
me.selected_task.deleted_attachments = [];
|
||||
|
@ -1491,23 +1904,27 @@ function rcube_tasklist_ui(settings)
|
|||
// define dialog buttons
|
||||
var buttons = {};
|
||||
buttons[rcmail.gettext('save', 'tasklist')] = function() {
|
||||
var data = me.selected_task;
|
||||
data._status_before = me.selected_task.status + '';
|
||||
|
||||
// copy form field contents into task object to save
|
||||
$.each({ title:title, description:description, date:recdate, time:rectime, startdate:recstartdate, starttime:recstarttime, status:taskstatus, list:tasklist }, function(key,input){
|
||||
me.selected_task[key] = input.val();
|
||||
data[key] = input.val();
|
||||
});
|
||||
me.selected_task.tags = [];
|
||||
me.selected_task.attachments = [];
|
||||
me.selected_task.valarms = me.serialize_alarms('#taskedit-alarms');
|
||||
me.selected_task.recurrence = me.serialize_recurrence(rectime.val());
|
||||
data.tags = [];
|
||||
data.attachments = [];
|
||||
data.attendees = task_attendees;
|
||||
data.valarms = me.serialize_alarms('#taskedit-alarms');
|
||||
data.recurrence = me.serialize_recurrence(rectime.val());
|
||||
|
||||
// do some basic input validation
|
||||
if (!me.selected_task.title || !me.selected_task.title.length) {
|
||||
if (!data.title || !data.title.length) {
|
||||
title.focus();
|
||||
return false;
|
||||
}
|
||||
else if (me.selected_task.startdate && me.selected_task.date) {
|
||||
var startdate = $.datepicker.parseDate(datepicker_settings.dateFormat, me.selected_task.startdate, datepicker_settings);
|
||||
var duedate = $.datepicker.parseDate(datepicker_settings.dateFormat, me.selected_task.date, datepicker_settings);
|
||||
else if (data.startdate && data.date) {
|
||||
var startdate = $.datepicker.parseDate(datepicker_settings.dateFormat, data.startdate, datepicker_settings);
|
||||
var duedate = $.datepicker.parseDate(datepicker_settings.dateFormat, data.date, datepicker_settings);
|
||||
if (startdate > duedate) {
|
||||
alert(rcmail.gettext('invalidstartduedates', 'tasklist'));
|
||||
return false;
|
||||
|
@ -1515,38 +1932,67 @@ function rcube_tasklist_ui(settings)
|
|||
}
|
||||
|
||||
// collect tags
|
||||
$('input[type="hidden"]', rcmail.gui_objects.edittagline).each(function(i,elem){
|
||||
$('input[type="hidden"]', rcmail.gui_objects.edittagline).each(function(i,elem) {
|
||||
if (elem.value)
|
||||
me.selected_task.tags.push(elem.value);
|
||||
data.tags.push(elem.value);
|
||||
});
|
||||
// including the "pending" one in the text box
|
||||
var newtag = $('#tagedit-input').val();
|
||||
if (newtag != '') {
|
||||
me.selected_task.tags.push(newtag);
|
||||
data.tags.push(newtag);
|
||||
}
|
||||
|
||||
// uploaded attachments list
|
||||
for (var i in rcmail.env.attachments) {
|
||||
if (i.match(/^rcmfile(.+)/))
|
||||
me.selected_task.attachments.push(RegExp.$1);
|
||||
data.attachments.push(RegExp.$1);
|
||||
}
|
||||
|
||||
// task assigned to a new list
|
||||
if (me.selected_task.list && listdata[id] && me.selected_task.list != listdata[id].list) {
|
||||
me.selected_task._fromlist = list.id;
|
||||
if (data.list && listdata[id] && data.list != listdata[id].list) {
|
||||
data._fromlist = list.id;
|
||||
}
|
||||
|
||||
me.selected_task.complete = complete.val() / 100;
|
||||
if (isNaN(me.selected_task.complete))
|
||||
me.selected_task.complete = null;
|
||||
data.complete = complete.val() / 100;
|
||||
if (isNaN(data.complete))
|
||||
data.complete = null;
|
||||
else if (data.complete == 1.0 && rec.status === '')
|
||||
data.status = 'COMPLETED';
|
||||
|
||||
if (!me.selected_task.list && list.id)
|
||||
me.selected_task.list = list.id;
|
||||
if (!data.list && list.id)
|
||||
data.list = list.id;
|
||||
|
||||
if (!me.selected_task.tags.length)
|
||||
me.selected_task.tags = '';
|
||||
if (!data.tags.length)
|
||||
data.tags = '';
|
||||
|
||||
if (save_task(me.selected_task, action))
|
||||
if (organizer) {
|
||||
data._identity = $('#edit-identities-list option:selected').val();
|
||||
delete data.organizer;
|
||||
}
|
||||
|
||||
// per-attendee notification suppression
|
||||
var need_invitation = false;
|
||||
if (allow_invitations) {
|
||||
$.each(data.attendees, function (i, v) {
|
||||
if (v.role != 'ORGANIZER') {
|
||||
if ($('input.edit-attendee-reply[value="' + v.email + '"]').prop('checked')) {
|
||||
need_invitation = true;
|
||||
delete data.attendees[i]['noreply'];
|
||||
}
|
||||
else {
|
||||
data.attendees[i].noreply = 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// tell server to send notifications
|
||||
if ((data.attendees.length || (rec.id && rec.attendees.length)) && allow_invitations && (notify.checked || invite.checked || need_invitation)) {
|
||||
data._notify = 1;
|
||||
data._comment = comment.val();
|
||||
}
|
||||
|
||||
if (save_task(data, action))
|
||||
$dialog.dialog('close');
|
||||
};
|
||||
|
||||
|
@ -1583,7 +2029,6 @@ function rcube_tasklist_ui(settings)
|
|||
me.dialog_resize($dialog.get(0), $dialog.height(), 580);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Open a task attachment either in a browser window for inline view or download it
|
||||
*/
|
||||
|
@ -1691,7 +2136,34 @@ function rcube_tasklist_ui(settings)
|
|||
if (!rec || rec.readonly || rcmail.busy)
|
||||
return false;
|
||||
|
||||
var html, buttons = [];
|
||||
var html, buttons = [], $dialog = $('<div>');
|
||||
|
||||
// Subfunction to submit the delete command after confirm
|
||||
var _delete_task = function(id, mode) {
|
||||
var rec = listdata[id],
|
||||
li = $('li[rel="'+id+'"]', rcmail.gui_objects.resultlist).hide(),
|
||||
decline = $dialog.find('input.confirm-attendees-decline:checked').length,
|
||||
notify = $dialog.find('input.confirm-attendees-notify:checked').length;
|
||||
|
||||
saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
|
||||
rcmail.http_post('task', { action:'delete', t:{ id:rec.id, list:rec.list, _decline:decline, _notify:notify }, mode:mode, filter:filtermask });
|
||||
|
||||
// move childs to parent/root
|
||||
if (mode != 1 && rec.children !== undefined) {
|
||||
var parent_node = rec.parent_id ? $('li[rel="'+rec.parent_id+'"] > .childtasks', rcmail.gui_objects.resultlist) : null;
|
||||
if (!parent_node || !parent_node.length)
|
||||
parent_node = rcmail.gui_objects.resultlist;
|
||||
|
||||
$.each(rec.children, function(i,cid) {
|
||||
var child = listdata[cid];
|
||||
child.parent_id = rec.parent_id;
|
||||
resort_task(child, $('li[rel="'+cid+'"]').appendTo(parent_node), true);
|
||||
});
|
||||
}
|
||||
|
||||
li.remove();
|
||||
delete listdata[id];
|
||||
}
|
||||
|
||||
if (rec.children && rec.children.length) {
|
||||
html = rcmail.gettext('deleteparenttasktconfirm','tasklist');
|
||||
|
@ -1721,6 +2193,19 @@ function rcube_tasklist_ui(settings)
|
|||
});
|
||||
}
|
||||
|
||||
if (is_attendee(rec)) {
|
||||
html += '<div class="task-dialog-message">' +
|
||||
'<label><input class="confirm-attendees-decline" type="checkbox" checked="checked" value="1" name="_decline" /> ' +
|
||||
rcmail.gettext('itipdeclinetask', 'tasklist') +
|
||||
'</label></div>';
|
||||
}
|
||||
else if (has_attendees(rec) && is_organizer(rec)) {
|
||||
html += '<div class="task-dialog-message">' +
|
||||
'<label><input class="confirm-attendees-notify" type="checkbox" checked="checked" value="1" name="_notify" /> ' +
|
||||
rcmail.gettext('sendcancellation', 'tasklist') +
|
||||
'</label></div>';
|
||||
}
|
||||
|
||||
buttons.push({
|
||||
text: rcmail.gettext('cancel', 'tasklist'),
|
||||
click: function() {
|
||||
|
@ -1728,11 +2213,11 @@ function rcube_tasklist_ui(settings)
|
|||
}
|
||||
});
|
||||
|
||||
var $dialog = $('<div>').html(html);
|
||||
$dialog.html(html);
|
||||
$dialog.dialog({
|
||||
modal: true,
|
||||
width: 520,
|
||||
dialogClass: 'warning',
|
||||
dialogClass: 'warning no-close',
|
||||
title: rcmail.gettext('deletetask', 'tasklist'),
|
||||
buttons: buttons,
|
||||
close: function(){
|
||||
|
@ -1743,34 +2228,6 @@ function rcube_tasklist_ui(settings)
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subfunction to submit the delete command after confirm
|
||||
*/
|
||||
function _delete_task(id, mode)
|
||||
{
|
||||
var rec = listdata[id],
|
||||
li = $('li[rel="'+id+'"]', rcmail.gui_objects.resultlist).hide();
|
||||
|
||||
saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
|
||||
rcmail.http_post('task', { action:'delete', t:{ id:rec.id, list:rec.list }, mode:mode, filter:filtermask });
|
||||
|
||||
// move childs to parent/root
|
||||
if (mode != 1 && rec.children !== undefined) {
|
||||
var parent_node = rec.parent_id ? $('li[rel="'+rec.parent_id+'"] > .childtasks', rcmail.gui_objects.resultlist) : null;
|
||||
if (!parent_node || !parent_node.length)
|
||||
parent_node = rcmail.gui_objects.resultlist;
|
||||
|
||||
$.each(rec.children, function(i,cid) {
|
||||
var child = listdata[cid];
|
||||
child.parent_id = rec.parent_id;
|
||||
resort_task(child, $('li[rel="'+cid+'"]').appendTo(parent_node), true);
|
||||
});
|
||||
}
|
||||
|
||||
li.remove();
|
||||
delete listdata[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given task matches the current filtermask and tag selection
|
||||
*/
|
||||
|
@ -2060,6 +2517,29 @@ function rcube_tasklist_ui(settings)
|
|||
|
||||
/**** Utility functions ****/
|
||||
|
||||
// same as str.split(delimiter) but it ignores delimiters within quoted strings
|
||||
var explode_quoted_string = function(str, delimiter)
|
||||
{
|
||||
var result = [],
|
||||
strlen = str.length,
|
||||
q, p, i, char, last;
|
||||
|
||||
for (q = p = i = 0; i < strlen; i++) {
|
||||
char = str.charAt(i);
|
||||
if (char == '"' && last != '\\') {
|
||||
q = !q;
|
||||
}
|
||||
else if (!q && char == delimiter) {
|
||||
result.push(str.substring(p, i));
|
||||
p = i + 1;
|
||||
}
|
||||
last = char;
|
||||
}
|
||||
|
||||
result.push(str.substr(p));
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear any text selection
|
||||
* (text is probably selected when double-clicking somewhere)
|
||||
|
@ -2205,7 +2685,7 @@ jQuery.unqiqueStrings = (function() {
|
|||
var rctasks;
|
||||
window.rcmail && rcmail.addEventListener('init', function(evt) {
|
||||
|
||||
rctasks = new rcube_tasklist_ui(rcmail.env.libcal_settings);
|
||||
rctasks = new rcube_tasklist_ui($.extend(rcmail.env.tasklist_settings, rcmail.env.libcal_settings));
|
||||
|
||||
// register button commands
|
||||
rcmail.register_command('newtask', function(){ rctasks.edit_task(null, 'new', {}); }, true);
|
||||
|
|
|
@ -55,6 +55,8 @@ class tasklist extends rcube_plugin
|
|||
public $home; // declare public to be used in other classes
|
||||
|
||||
private $collapsed_tasks = array();
|
||||
private $itip;
|
||||
private $ical;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -64,7 +66,7 @@ class tasklist extends rcube_plugin
|
|||
{
|
||||
$this->require_plugin('libcalendaring');
|
||||
|
||||
$this->rc = rcube::get_instance();
|
||||
$this->rc = rcube::get_instance();
|
||||
$this->lib = libcalendaring::get_instance();
|
||||
|
||||
$this->register_task('tasks', 'tasklist');
|
||||
|
@ -106,15 +108,19 @@ class tasklist extends rcube_plugin
|
|||
$this->register_action('mail2task', array($this, 'mail_message2task'));
|
||||
$this->register_action('get-attachment', array($this, 'attachment_get'));
|
||||
$this->register_action('upload', array($this, 'attachment_upload'));
|
||||
$this->register_action('mailimportitip', array($this, 'mail_import_itip'));
|
||||
$this->register_action('mailimportattach', array($this, 'mail_import_attachment'));
|
||||
$this->register_action('itip-status', array($this, 'task_itip_status'));
|
||||
$this->register_action('itip-remove', array($this, 'task_itip_remove'));
|
||||
$this->register_action('itip-decline-reply', array($this, 'mail_itip_decline_reply'));
|
||||
$this->add_hook('refresh', array($this, 'refresh'));
|
||||
|
||||
$this->collapsed_tasks = array_filter(explode(',', $this->rc->config->get('tasklist_collapsed_tasks', '')));
|
||||
}
|
||||
else if ($args['task'] == 'mail') {
|
||||
// TODO: register hooks to catch ical/vtodo email attachments
|
||||
if ($args['action'] == 'show' || $args['action'] == 'preview') {
|
||||
// $this->add_hook('message_load', array($this, 'mail_message_load'));
|
||||
// $this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html'));
|
||||
$this->add_hook('message_load', array($this, 'mail_message_load'));
|
||||
$this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html'));
|
||||
}
|
||||
|
||||
// add 'Create event' item to message menu
|
||||
|
@ -188,7 +194,7 @@ class tasklist extends rcube_plugin
|
|||
{
|
||||
$filter = intval(get_input_value('filter', RCUBE_INPUT_GPC));
|
||||
$action = get_input_value('action', RCUBE_INPUT_GPC);
|
||||
$rec = get_input_value('t', RCUBE_INPUT_POST, true);
|
||||
$rec = get_input_value('t', RCUBE_INPUT_POST, true);
|
||||
$oldrec = $rec;
|
||||
$success = $refresh = false;
|
||||
|
||||
|
@ -318,8 +324,42 @@ class tasklist extends rcube_plugin
|
|||
$this->rc->output->show_message('successfullysaved', 'confirmation');
|
||||
$this->update_counts($oldrec, $refresh);
|
||||
}
|
||||
else
|
||||
else {
|
||||
$this->rc->output->show_message('tasklist.errorsaving', 'error');
|
||||
}
|
||||
|
||||
// send out notifications
|
||||
if ($success && $rec['_notify'] && ($rec['attendees'] || $oldrec['attendees'])) {
|
||||
// make sure we have the complete record
|
||||
$task = $action == 'delete' ? $oldrec : $this->driver->get_task($rec);
|
||||
|
||||
// only notify if data really changed (TODO: do diff check on client already)
|
||||
if (!$oldrec || $action == 'delete' || self::task_diff($task, $oldrec)) {
|
||||
$sent = $this->notify_attendees($task, $oldrec, $action, $rec['_comment']);
|
||||
if ($sent > 0)
|
||||
$this->rc->output->show_message('tasklist.itipsendsuccess', 'confirmation');
|
||||
else if ($sent < 0)
|
||||
$this->rc->output->show_message('tasklist.errornotifying', 'error');
|
||||
}
|
||||
}
|
||||
else if ($success && $rec['_reportpartstat']) {
|
||||
// get the full record after update
|
||||
$task = $this->driver->get_task($rec);
|
||||
|
||||
// send iTip REPLY with the updated partstat
|
||||
if ($task['organizer'] && ($idx = $this->is_attendee($task)) !== false) {
|
||||
$sender = $task['attendees'][$idx];
|
||||
$status = strtolower($sender['status']);
|
||||
|
||||
$itip = $this->load_itip();
|
||||
$itip->set_sender_email($sender['email']);
|
||||
|
||||
if ($itip->send_itip_message($this->to_libcal($task), 'REPLY', $task['organizer'], 'itipsubject' . $status, 'itipmailbody' . $status))
|
||||
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $task['organizer']['name'] ?: $task['organizer']['email']))), 'confirmation');
|
||||
else
|
||||
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// unlock client
|
||||
$this->rc->output->command('plugin.unlock_saving');
|
||||
|
@ -336,6 +376,21 @@ class tasklist extends rcube_plugin
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load iTIP functions
|
||||
*/
|
||||
private function load_itip()
|
||||
{
|
||||
if (!$this->itip) {
|
||||
require_once realpath(__DIR__ . '/../libcalendaring/lib/libcalendaring_itip.php');
|
||||
$this->itip = new libcalendaring_itip($this, 'tasklist');
|
||||
$this->itip->set_rsvp_actions(array('accepted','declined'));
|
||||
$this->itip->set_rsvp_status(array('accepted','tentative','declined','delegated','in-process','completed'));
|
||||
}
|
||||
|
||||
return $this->itip;
|
||||
}
|
||||
|
||||
/**
|
||||
* repares new/edited task properties before save
|
||||
*/
|
||||
|
@ -481,6 +536,25 @@ class tasklist extends rcube_plugin
|
|||
|
||||
$rec['attachments'] = $attachments;
|
||||
|
||||
// convert invalid data
|
||||
if (isset($rec['attendees']) && !is_array($rec['attendees']))
|
||||
$rec['attendees'] = array();
|
||||
|
||||
// copy the task status to my attendee partstat
|
||||
if (!empty($rec['_reportpartstat'])) {
|
||||
if (($idx = $this->is_attendee($rec)) !== false) {
|
||||
if (!($rec['_reportpartstat'] == 'NEEDS-ACTION' && $rec['attendees'][$idx]['status'] == 'ACCEPTED'))
|
||||
$rec['attendees'][$idx]['status'] = $rec['_reportpartstat'];
|
||||
else
|
||||
unset($rec['_reportpartstat']);
|
||||
}
|
||||
}
|
||||
|
||||
// set organizer from identity selector
|
||||
if (isset($rec['_identity']) && ($identity = $this->rc->user->get_identity($rec['_identity']))) {
|
||||
$rec['organizer'] = array('name' => $identity['name'], 'email' => $identity['email']);
|
||||
}
|
||||
|
||||
if (is_numeric($rec['id']) && $rec['id'] < 0)
|
||||
unset($rec['id']);
|
||||
|
||||
|
@ -586,6 +660,107 @@ class tasklist extends rcube_plugin
|
|||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send out an invitation/notification to all task attendees
|
||||
*/
|
||||
private function notify_attendees($task, $old, $action = 'edit', $comment = null)
|
||||
{
|
||||
if ($action == 'delete' || ($task['status'] == 'CANCELLED' && $old['status'] != $task['status'])) {
|
||||
$task['cancelled'] = true;
|
||||
$is_cancelled = true;
|
||||
}
|
||||
|
||||
$itip = $this->load_itip();
|
||||
$emails = $this->lib->get_user_emails();
|
||||
|
||||
// add comment to the iTip attachment
|
||||
$task['comment'] = $comment;
|
||||
|
||||
// needed to generate VTODO instead of VEVENT entry
|
||||
$task['_type'] = 'task';
|
||||
|
||||
// compose multipart message using PEAR:Mail_Mime
|
||||
$method = $action == 'delete' ? 'CANCEL' : 'REQUEST';
|
||||
$object = $this->to_libcal($task);
|
||||
$message = $itip->compose_itip_message($object, $method);
|
||||
|
||||
// list existing attendees from the $old task
|
||||
$old_attendees = array();
|
||||
foreach ((array)$old['attendees'] as $attendee) {
|
||||
$old_attendees[] = $attendee['email'];
|
||||
}
|
||||
|
||||
// send to every attendee
|
||||
$sent = 0; $current = array();
|
||||
foreach ((array)$task['attendees'] as $attendee) {
|
||||
$current[] = strtolower($attendee['email']);
|
||||
|
||||
// skip myself for obvious reasons
|
||||
if (!$attendee['email'] || in_array(strtolower($attendee['email']), $emails)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// skip if notification is disabled for this attendee
|
||||
if ($attendee['noreply']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// which template to use for mail text
|
||||
$is_new = !in_array($attendee['email'], $old_attendees);
|
||||
$bodytext = $is_cancelled ? 'itipcancelmailbody' : ($is_new ? 'invitationmailbody' : 'itipupdatemailbody');
|
||||
$subject = $is_cancelled ? 'itipcancelsubject' : ($is_new ? 'invitationsubject' : ($task['title'] ? 'itipupdatesubject' : 'itipupdatesubjectempty'));
|
||||
|
||||
// finally send the message
|
||||
if ($itip->send_itip_message($object, $method, $attendee, $subject, $bodytext, $message))
|
||||
$sent++;
|
||||
else
|
||||
$sent = -100;
|
||||
}
|
||||
|
||||
// send CANCEL message to removed attendees
|
||||
foreach ((array)$old['attendees'] as $attendee) {
|
||||
if (!$attendee['email'] || in_array(strtolower($attendee['email']), $current)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$vtodo = $this->to_libcal($old);
|
||||
$vtodo['cancelled'] = $is_cancelled;
|
||||
$vtodo['attendees'] = array($attendee);
|
||||
$vtodo['comment'] = $comment;
|
||||
|
||||
if ($itip->send_itip_message($vtodo, 'CANCEL', $attendee, 'itipcancelsubject', 'itipcancelmailbody'))
|
||||
$sent++;
|
||||
else
|
||||
$sent = -100;
|
||||
}
|
||||
|
||||
return $sent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two task objects and return differing properties
|
||||
*
|
||||
* @param array Event A
|
||||
* @param array Event B
|
||||
* @return array List of differing task properties
|
||||
*/
|
||||
public static function task_diff($a, $b)
|
||||
{
|
||||
$diff = array();
|
||||
$ignore = array('changed' => 1, 'attachments' => 1);
|
||||
|
||||
foreach (array_unique(array_merge(array_keys($a), array_keys($b))) as $key) {
|
||||
if (!$ignore[$key] && $a[$key] != $b[$key])
|
||||
$diff[] = $key;
|
||||
}
|
||||
|
||||
// only compare number of attachments
|
||||
if (count($a['attachments']) != count($b['attachments']))
|
||||
$diff[] = 'attachments';
|
||||
|
||||
return $diff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatcher for tasklist actions initiated by the client
|
||||
*/
|
||||
|
@ -902,9 +1077,35 @@ class tasklist extends rcube_plugin
|
|||
else if ($start > $weeklimit || ($rec['date'] && $duedate > $weeklimit))
|
||||
$mask |= self::FILTER_MASK_LATER;
|
||||
|
||||
// TODO: add mask for "assigned to me"
|
||||
|
||||
return $mask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the current user is an attendee of the given task
|
||||
*/
|
||||
public function is_attendee($task)
|
||||
{
|
||||
$emails = $this->lib->get_user_emails();
|
||||
foreach ((array)$task['attendees'] as $i => $attendee) {
|
||||
if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
|
||||
return $i;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the current user is the organizer of the given task
|
||||
*/
|
||||
public function is_organizer($task)
|
||||
{
|
||||
$emails = $this->lib->get_user_emails();
|
||||
return (empty($task['organizer']) || in_array(strtolower($task['organizer']['email']), $emails));
|
||||
}
|
||||
|
||||
|
||||
/******* UI functions ********/
|
||||
|
||||
|
@ -937,11 +1138,17 @@ class tasklist extends rcube_plugin
|
|||
|
||||
$texts['tasklist.newtask'] = $this->gettext('createfrommail');
|
||||
|
||||
// collect env variables
|
||||
$env = array(
|
||||
'tasklists' => array(),
|
||||
'tasklist_settings' => $this->ui->load_settings(),
|
||||
);
|
||||
|
||||
$this->ui->init_templates();
|
||||
echo $this->api->output->parse('tasklist.taskedit', false, false);
|
||||
echo html::tag('link', array('rel' => 'stylesheet', 'type' => 'text/css', 'href' => $this->url($this->local_skin_path() . '/tagedit.css'), 'nl' => true));
|
||||
echo html::tag('script', array('type' => 'text/javascript'),
|
||||
"rcmail.set_env('tasklists', " . json_encode($this->api->output->env['tasklists']) . ");\n".
|
||||
"rcmail.set_env(" . json_encode($env) . ");\n".
|
||||
"rcmail.add_label(" . json_encode($texts) . ");\n"
|
||||
);
|
||||
exit;
|
||||
|
@ -1121,6 +1328,539 @@ class tasklist extends rcube_plugin
|
|||
$this->rc->output->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check mail message structure of there are .ics files attached
|
||||
*
|
||||
* @todo move to libcalendaring
|
||||
*/
|
||||
public function mail_message_load($p)
|
||||
{
|
||||
$this->message = $p['object'];
|
||||
$itip_part = null;
|
||||
|
||||
// check all message parts for .ics files
|
||||
foreach ((array)$this->message->mime_parts as $part) {
|
||||
if ($this->is_vcalendar($part)) {
|
||||
if ($part->ctype_parameters['method'])
|
||||
$itip_part = $part->mime_id;
|
||||
else
|
||||
$this->ics_parts[] = $part->mime_id;
|
||||
}
|
||||
}
|
||||
|
||||
// priorize part with method parameter
|
||||
if ($itip_part) {
|
||||
$this->ics_parts = array($itip_part);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add UI element to copy event invitations or updates to the calendar
|
||||
*
|
||||
* @todo move to libcalendaring
|
||||
*/
|
||||
public function mail_messagebody_html($p)
|
||||
{
|
||||
// load iCalendar functions (if necessary)
|
||||
if (!empty($this->ics_parts)) {
|
||||
$this->get_ical();
|
||||
$this->load_itip();
|
||||
}
|
||||
|
||||
// @todo: Calendar plugin does the same, which means the
|
||||
// attachment body is fetched twice, this is not optimal
|
||||
$html = '';
|
||||
$has_tasks = false;
|
||||
foreach ($this->ics_parts as $mime_id) {
|
||||
$part = $this->message->mime_parts[$mime_id];
|
||||
$charset = $part->ctype_parameters['charset'] ? $part->ctype_parameters['charset'] : RCMAIL_CHARSET;
|
||||
$objects = $this->ical->import($this->message->get_part_content($mime_id), $charset);
|
||||
$title = $this->gettext('title');
|
||||
|
||||
// successfully parsed events?
|
||||
if (empty($objects)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// show a box for every task in the file
|
||||
foreach ($objects as $idx => $task) {
|
||||
if ($task['_type'] != 'task') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$has_tasks = true;
|
||||
|
||||
// get prepared inline UI for this event object
|
||||
if ($this->ical->method) {
|
||||
$html .= html::div('tasklist-invitebox',
|
||||
$this->itip->mail_itip_inline_ui(
|
||||
$task,
|
||||
$this->ical->method,
|
||||
$mime_id . ':' . $idx,
|
||||
'tasks',
|
||||
rcube_utils::anytodatetime($this->message->headers->date)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// limit listing
|
||||
if ($idx >= 3) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// prepend event boxes to message body
|
||||
if ($html) {
|
||||
$this->load_ui();
|
||||
$this->ui->init();
|
||||
|
||||
$p['content'] = $html . $p['content'];
|
||||
|
||||
$this->rc->output->add_label('tasklist.savingdata','tasklist.deletetaskconfirm','tasklist.declinedeleteconfirm');
|
||||
}
|
||||
|
||||
// add "Save to tasks" button into attachment menu
|
||||
if ($has_tasks) {
|
||||
$this->add_button(array(
|
||||
'id' => 'attachmentsavetask',
|
||||
'name' => 'attachmentsavetask',
|
||||
'type' => 'link',
|
||||
'wrapper' => 'li',
|
||||
'command' => 'attachment-save-task',
|
||||
'class' => 'icon tasklistlink',
|
||||
'classact' => 'icon tasklistlink active',
|
||||
'innerclass' => 'icon taskadd',
|
||||
'label' => 'tasklist.savetotasklist',
|
||||
), 'attachmentmenu');
|
||||
}
|
||||
|
||||
return $p;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the given mime message from IMAP and parse ical data
|
||||
*
|
||||
* @todo move to libcalendaring
|
||||
*/
|
||||
private function mail_get_itip_task($mbox, $uid, $mime_id)
|
||||
{
|
||||
$charset = RCMAIL_CHARSET;
|
||||
|
||||
// establish imap connection
|
||||
$imap = $this->rc->get_storage();
|
||||
$imap->set_mailbox($mbox);
|
||||
|
||||
if ($uid && $mime_id) {
|
||||
list($mime_id, $index) = explode(':', $mime_id);
|
||||
|
||||
$part = $imap->get_message_part($uid, $mime_id);
|
||||
$headers = $imap->get_message_headers($uid);
|
||||
|
||||
if ($part->ctype_parameters['charset']) {
|
||||
$charset = $part->ctype_parameters['charset'];
|
||||
}
|
||||
|
||||
if ($part) {
|
||||
$tasks = $this->get_ical()->import($part, $charset);
|
||||
}
|
||||
}
|
||||
|
||||
// successfully parsed events?
|
||||
if (!empty($tasks) && ($task = $tasks[$index])) {
|
||||
$task = $this->from_ical($task);
|
||||
|
||||
// store the message's sender address for comparisons
|
||||
$task['_sender'] = preg_match('/([a-z0-9][a-z0-9\-\.\+\_]*@[^&@"\'.][^@&"\']*\\.([^\\x00-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-z0-9]{2,}))/', $headers->from, $m) ? $m[1] : '';
|
||||
$askt['_sender_utf'] = rcube_idn_to_utf8($task['_sender']);
|
||||
|
||||
return $task;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if specified message part is a vcalendar data
|
||||
*
|
||||
* @param rcube_message_part Part object
|
||||
*
|
||||
* @return boolean True if part is of type vcard
|
||||
*
|
||||
* @todo move to libcalendaring
|
||||
*/
|
||||
private function is_vcalendar($part)
|
||||
{
|
||||
return (
|
||||
in_array($part->mimetype, array('text/calendar', 'text/x-vcalendar', 'application/ics')) ||
|
||||
// Apple sends files as application/x-any (!?)
|
||||
($part->mimetype == 'application/x-any' && $part->filename && preg_match('/\.ics$/i', $part->filename))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load iCalendar functions
|
||||
*/
|
||||
public function get_ical()
|
||||
{
|
||||
if (!$this->ical) {
|
||||
$this->ical = libcalendaring::get_ical();
|
||||
}
|
||||
|
||||
return $this->ical;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get properties of the tasklist this user has specified as default
|
||||
*/
|
||||
public function get_default_tasklist($writeable = false)
|
||||
{
|
||||
// $default_id = $this->rc->config->get('tasklist_default_list');
|
||||
$lists = $this->driver->get_lists();
|
||||
// $list = $calendars[$default_id] ?: null;
|
||||
|
||||
if (!$list || ($writeable && !$list['editable'])) {
|
||||
foreach ($lists as $l) {
|
||||
if ($l['default']) {
|
||||
$list = $l;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$writeable || $l['editable']) {
|
||||
$first = $l;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $list ?: $first;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import the full payload from a mail message attachment
|
||||
*/
|
||||
public function mail_import_attachment()
|
||||
{
|
||||
$uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
|
||||
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
|
||||
$mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);
|
||||
$charset = RCMAIL_CHARSET;
|
||||
|
||||
// establish imap connection
|
||||
$imap = $this->rc->get_storage();
|
||||
$imap->set_mailbox($mbox);
|
||||
|
||||
if ($uid && $mime_id) {
|
||||
$part = $imap->get_message_part($uid, $mime_id);
|
||||
$headers = $imap->get_message_headers($uid);
|
||||
|
||||
if ($part->ctype_parameters['charset']) {
|
||||
$charset = $part->ctype_parameters['charset'];
|
||||
}
|
||||
|
||||
if ($part) {
|
||||
$tasks = $this->get_ical()->import($part, $charset);
|
||||
}
|
||||
}
|
||||
|
||||
$success = $existing = 0;
|
||||
|
||||
if (!empty($tasks)) {
|
||||
// find writeable tasklist to store task
|
||||
$cal_id = !empty($_REQUEST['_list']) ? rcube_utils::get_input_value('_list', rcube_utils::INPUT_POST) : null;
|
||||
$lists = $this->driver->get_lists();
|
||||
$list = $lists[$cal_id] ?: $this->get_default_tasklist(true);
|
||||
|
||||
foreach ($tasks as $task) {
|
||||
// save to tasklist
|
||||
if ($list && $list['editable'] && $task['_type'] == 'task') {
|
||||
$task = $this->from_ical($task);
|
||||
$task['list'] = $list['id'];
|
||||
|
||||
if (!$this->driver->get_task($task['uid'])) {
|
||||
$success += (bool) $this->driver->create_task($task);
|
||||
}
|
||||
else {
|
||||
$existing++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
$this->rc->output->command('display_message', $this->gettext(array(
|
||||
'name' => 'importsuccess',
|
||||
'vars' => array('nr' => $success),
|
||||
)), 'confirmation');
|
||||
}
|
||||
else if ($existing) {
|
||||
$this->rc->output->command('display_message', $this->gettext('importwarningexists'), 'warning');
|
||||
}
|
||||
else {
|
||||
$this->rc->output->command('display_message', $this->gettext('errorimportingtask'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for POST request to import an event attached to a mail message
|
||||
*/
|
||||
public function mail_import_itip()
|
||||
{
|
||||
$uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
|
||||
$mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
|
||||
$mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);
|
||||
$status = rcube_utils::get_input_value('_status', rcube_utils::INPUT_POST);
|
||||
$delete = intval(rcube_utils::get_input_value('_del', rcube_utils::INPUT_POST));
|
||||
$noreply = intval(rcube_utils::get_input_value('_noreply', rcube_utils::INPUT_POST)) || $status == 'needs-action';
|
||||
|
||||
$error_msg = $this->gettext('errorimportingtask');
|
||||
$success = false;
|
||||
|
||||
// successfully parsed tasks?
|
||||
if ($task = $this->mail_get_itip_task($mbox, $uid, $mime_id)) {
|
||||
// find writeable list to store the task
|
||||
$list_id = !empty($_REQUEST['_list']) ? rcube_utils::get_input_value('_list', rcube_utils::INPUT_POST) : null;
|
||||
$lists = $this->driver->get_lists();
|
||||
$list = $lists[$list_id] ?: $this->get_default_tasklist(true);
|
||||
|
||||
$metadata = array(
|
||||
'uid' => $task['uid'],
|
||||
'changed' => is_object($task['changed']) ? $task['changed']->format('U') : 0,
|
||||
'sequence' => intval($task['sequence']),
|
||||
'fallback' => strtoupper($status),
|
||||
'method' => $this->ical->method,
|
||||
'task' => 'tasks',
|
||||
);
|
||||
|
||||
// update my attendee status according to submitted method
|
||||
if (!empty($status)) {
|
||||
$organizer = $task['organizer'];
|
||||
$emails = $this->lib->get_user_emails();
|
||||
|
||||
foreach ($task['attendees'] as $i => $attendee) {
|
||||
if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
|
||||
$metadata['attendee'] = $attendee['email'];
|
||||
$metadata['rsvp'] = $attendee['role'] != 'NON-PARTICIPANT';
|
||||
$reply_sender = $attendee['email'];
|
||||
|
||||
$task['attendees'][$i]['status'] = strtoupper($status);
|
||||
if ($task['attendees'][$i]['status'] != 'NEEDS-ACTION') {
|
||||
unset($task['attendees'][$i]['rsvp']); // remove RSVP attribute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add attendee with this user's default identity if not listed
|
||||
if (!$reply_sender) {
|
||||
$sender_identity = $this->rc->user->get_identity();
|
||||
$task['attendees'][] = array(
|
||||
'name' => $sender_identity['name'],
|
||||
'email' => $sender_identity['email'],
|
||||
'role' => 'OPT-PARTICIPANT',
|
||||
'status' => strtoupper($status),
|
||||
);
|
||||
$metadata['attendee'] = $sender_identity['email'];
|
||||
}
|
||||
}
|
||||
|
||||
// save to tasklist
|
||||
if ($list && $list['editable']) {
|
||||
$task['list'] = $list['id'];
|
||||
|
||||
// check for existing task with the same UID
|
||||
$existing = $this->driver->get_task($task['uid']);
|
||||
|
||||
if ($existing) {
|
||||
// only update attendee status
|
||||
if ($this->ical->method == 'REPLY') {
|
||||
// try to identify the attendee using the email sender address
|
||||
$existing_attendee = -1;
|
||||
foreach ($existing['attendees'] as $i => $attendee) {
|
||||
if ($task['_sender'] && ($attendee['email'] == $task['_sender'] || $attendee['email'] == $task['_sender_utf'])) {
|
||||
$existing_attendee = $i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$task_attendee = null;
|
||||
foreach ($task['attendees'] as $attendee) {
|
||||
if ($task['_sender'] && ($attendee['email'] == $task['_sender'] || $attendee['email'] == $task['_sender_utf'])) {
|
||||
$task_attendee = $attendee;
|
||||
$metadata['fallback'] = $attendee['status'];
|
||||
$metadata['attendee'] = $attendee['email'];
|
||||
$metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// found matching attendee entry in both existing and new events
|
||||
if ($existing_attendee >= 0 && $task_attendee) {
|
||||
$existing['attendees'][$existing_attendee] = $task_attendee;
|
||||
$success = $this->driver->edit_task($existing);
|
||||
}
|
||||
// update the entire attendees block
|
||||
else if (($task['sequence'] >= $existing['sequence'] || $task['changed'] >= $existing['changed']) && $task_attendee) {
|
||||
$existing['attendees'][] = $task_attendee;
|
||||
$success = $this->driver->edit_task($existing);
|
||||
}
|
||||
else {
|
||||
$error_msg = $this->gettext('newerversionexists');
|
||||
}
|
||||
}
|
||||
// delete the task when declined
|
||||
else if ($status == 'declined' && $delete) {
|
||||
$deleted = $this->driver->delete_task($existing, true);
|
||||
$success = true;
|
||||
}
|
||||
// import the (newer) task
|
||||
else if ($task['sequence'] >= $existing['sequence'] || $task['changed'] >= $existing['changed']) {
|
||||
$task['id'] = $existing['id'];
|
||||
$task['list'] = $existing['list'];
|
||||
|
||||
// preserve my participant status for regular updates
|
||||
if (empty($status)) {
|
||||
$emails = $this->lib->get_user_emails();
|
||||
foreach ($task['attendees'] as $i => $attendee) {
|
||||
if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
|
||||
foreach ($existing['attendees'] as $j => $_attendee) {
|
||||
if ($attendee['email'] == $_attendee['email']) {
|
||||
$task['attendees'][$i] = $existing['attendees'][$j];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set status=CANCELLED on CANCEL messages
|
||||
if ($this->ical->method == 'CANCEL') {
|
||||
$task['status'] = 'CANCELLED';
|
||||
}
|
||||
// show me as free when declined (#1670)
|
||||
if ($status == 'declined' || $task['status'] == 'CANCELLED') {
|
||||
$task['free_busy'] = 'free';
|
||||
}
|
||||
|
||||
$success = $this->driver->edit_task($task);
|
||||
}
|
||||
else if (!empty($status)) {
|
||||
$existing['attendees'] = $task['attendees'];
|
||||
if ($status == 'declined') { // show me as free when declined (#1670)
|
||||
$existing['free_busy'] = 'free';
|
||||
}
|
||||
|
||||
$success = $this->driver->edit_event($existing);
|
||||
}
|
||||
else {
|
||||
$error_msg = $this->gettext('newerversionexists');
|
||||
}
|
||||
}
|
||||
else if (!$existing && ($status != 'declined' || $this->rc->config->get('kolab_invitation_tasklists'))) {
|
||||
$success = $this->driver->create_task($task);
|
||||
}
|
||||
else if ($status == 'declined') {
|
||||
$error_msg = null;
|
||||
}
|
||||
}
|
||||
else if ($status == 'declined') {
|
||||
$error_msg = null;
|
||||
}
|
||||
else {
|
||||
$error_msg = $this->gettext('nowritetasklistfound');
|
||||
}
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
$message = $this->ical->method == 'REPLY' ? 'attendeupdateesuccess' : ($deleted ? 'successremoval' : ($existing ? 'updatedsuccessfully' : 'importedsuccessfully'));
|
||||
$this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('list' => $list['name']))), 'confirmation');
|
||||
|
||||
$metadata['rsvp'] = intval($metadata['rsvp']);
|
||||
$metadata['after_action'] = $this->rc->config->get('calendar_itip_after_action', 0);
|
||||
|
||||
$this->rc->output->command('plugin.itip_message_processed', $metadata);
|
||||
$error_msg = null;
|
||||
}
|
||||
else if ($error_msg) {
|
||||
$this->rc->output->command('display_message', $error_msg, 'error');
|
||||
}
|
||||
|
||||
// send iTip reply
|
||||
if ($this->ical->method == 'REQUEST' && $organizer && !$noreply && !in_array(strtolower($organizer['email']), $emails) && !$error_msg) {
|
||||
$task['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST);
|
||||
$itip = $this->load_itip();
|
||||
$itip->set_sender_email($reply_sender);
|
||||
|
||||
if ($itip->send_itip_message($this->to_libcal($task), 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
|
||||
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ?: $organizer['email']))), 'confirmation');
|
||||
else
|
||||
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
|
||||
}
|
||||
|
||||
$this->rc->output->send();
|
||||
}
|
||||
|
||||
|
||||
/**** Task invitation plugin hooks ****/
|
||||
|
||||
/**
|
||||
* Handler for calendar/itip-status requests
|
||||
*/
|
||||
public function task_itip_status()
|
||||
{
|
||||
$data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);
|
||||
|
||||
// find local copy of the referenced task
|
||||
$existing = $this->driver->get_task($data);
|
||||
$itip = $this->load_itip();
|
||||
$response = $itip->get_itip_status($data, $existing);
|
||||
|
||||
// get a list of writeable lists to save new tasks to
|
||||
if (!$existing && $response['action'] == 'rsvp' || $response['action'] == 'import') {
|
||||
$lists = $this->driver->get_lists();
|
||||
$select = new html_select(array('name' => 'tasklist', 'id' => 'itip-saveto', 'is_escaped' => true));
|
||||
$num = 0;
|
||||
|
||||
foreach ($lists as $list) {
|
||||
if ($list['editable']) {
|
||||
$select->add($list['name'], $list['id']);
|
||||
$num++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($num <= 1) {
|
||||
$select = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($select) {
|
||||
$default_list = $this->get_default_tasklist(true);
|
||||
$response['select'] = html::span('folder-select', $this->gettext('saveintasklist') . ' ' .
|
||||
$select->show($this->rc->config->get('tasklist_default_list', $default_list['id'])));
|
||||
}
|
||||
|
||||
$this->rc->output->command('plugin.update_itip_object_status', $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for calendar/itip-remove requests
|
||||
*/
|
||||
public function task_itip_remove()
|
||||
{
|
||||
$success = false;
|
||||
$uid = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST);
|
||||
|
||||
// search for event if only UID is given
|
||||
if ($task = $this->driver->get_task($uid)) {
|
||||
$success = $this->driver->delete_task($task, true);
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
$this->rc->output->show_message('tasklist.successremoval', 'confirmation');
|
||||
}
|
||||
else {
|
||||
$this->rc->output->show_message('tasklist.errorsaving', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/******* Utility functions *******/
|
||||
|
||||
|
@ -1132,6 +1872,79 @@ class tasklist extends rcube_plugin
|
|||
return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16));
|
||||
}
|
||||
|
||||
/**
|
||||
* Map task properties for ical exprort using libcalendaring
|
||||
*/
|
||||
public function to_libcal($task)
|
||||
{
|
||||
$object = $task;
|
||||
$object['_type'] = 'task';
|
||||
$object['categories'] = (array)$task['tags'];
|
||||
|
||||
// convert to datetime objects
|
||||
if (!empty($task['date'])) {
|
||||
$object['due'] = rcube_utils::anytodatetime($task['date'].' '.$task['time'], $this->timezone);
|
||||
if (empty($task['time']))
|
||||
$object['due']->_dateonly = true;
|
||||
unset($object['date']);
|
||||
}
|
||||
|
||||
if (!empty($task['startdate'])) {
|
||||
$object['start'] = rcube_utils::anytodatetime($task['startdate'].' '.$task['starttime'], $this->timezone);
|
||||
if (empty($task['starttime']))
|
||||
$object['start']->_dateonly = true;
|
||||
unset($object['startdate']);
|
||||
}
|
||||
|
||||
$object['complete'] = $task['complete'] * 100;
|
||||
if ($task['complete'] == 1.0 && empty($task['complete'])) {
|
||||
$object['status'] = 'COMPLETED';
|
||||
}
|
||||
|
||||
if ($task['flagged']) {
|
||||
$object['priority'] = 1;
|
||||
}
|
||||
else if (!$task['priority']) {
|
||||
$object['priority'] = 0;
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert task properties from ical parser to the internal format
|
||||
*/
|
||||
public function from_ical($vtodo)
|
||||
{
|
||||
$task = $vtodo;
|
||||
|
||||
$task['tags'] = array_filter((array)$vtodo['categories']);
|
||||
$task['flagged'] = $vtodo['priority'] == 1;
|
||||
$task['complete'] = floatval($vtodo['complete'] / 100);
|
||||
|
||||
// convert from DateTime to internal date format
|
||||
if (is_a($vtodo['due'], 'DateTime')) {
|
||||
$due = $this->lib->adjust_timezone($vtodo['due']);
|
||||
$task['date'] = $due->format('Y-m-d');
|
||||
if (!$vtodo['due']->_dateonly)
|
||||
$task['time'] = $due->format('H:i');
|
||||
}
|
||||
// convert from DateTime to internal date format
|
||||
if (is_a($vtodo['start'], 'DateTime')) {
|
||||
$start = $this->lib->adjust_timezone($vtodo['start']);
|
||||
$task['startdate'] = $start->format('Y-m-d');
|
||||
if (!$vtodo['start']->_dateonly)
|
||||
$task['starttime'] = $start->format('H:i');
|
||||
}
|
||||
if (is_a($vtodo['dtstamp'], 'DateTime')) {
|
||||
$task['changed'] = $vtodo['dtstamp'];
|
||||
}
|
||||
|
||||
unset($task['categories'], $task['due'], $task['start'], $task['dtstamp']);
|
||||
|
||||
return $task;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for user_delete plugin hook
|
||||
*/
|
||||
|
@ -1141,4 +1954,3 @@ class tasklist extends rcube_plugin
|
|||
return $this->driver->user_delete($args);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ function rcube_tasklist(settings)
|
|||
/* public methods */
|
||||
this.create_from_mail = create_from_mail;
|
||||
this.mail2taskdialog = mail2task_dialog;
|
||||
this.save_to_tasklist = save_to_tasklist;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -56,7 +57,7 @@ function rcube_tasklist(settings)
|
|||
// rcmail.gui_object('attachmentlist', 'attachmentlist');
|
||||
|
||||
ui_loaded = true;
|
||||
me.ui = new rcube_tasklist_ui(settings);
|
||||
me.ui = new rcube_tasklist_ui($.extend(rcmail.env.tasklist_settings, settings));
|
||||
create_from_mail(uid); // start over
|
||||
});
|
||||
return;
|
||||
|
@ -80,21 +81,46 @@ function rcube_tasklist(settings)
|
|||
this.ui.edit_task(null, 'new', prop);
|
||||
}
|
||||
|
||||
// handler for attachment-save-tasklist commands
|
||||
function save_to_tasklist()
|
||||
{
|
||||
// TODO: show dialog to select the tasklist for importing
|
||||
if (this.selected_attachment && window.rcube_libcalendaring) {
|
||||
rcmail.http_post('tasks/mailimportattach', {
|
||||
_uid: rcmail.env.uid,
|
||||
_mbox: rcmail.env.mailbox,
|
||||
_part: this.selected_attachment,
|
||||
// _list: $('#tasklist-attachment-saveto').val(),
|
||||
}, rcmail.set_busy(true, 'itip.savingdata'));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* tasklist plugin initialization (for email task) */
|
||||
window.rcmail && rcmail.env.task == 'mail' && rcmail.addEventListener('init', function(evt) {
|
||||
var tasks = new rcube_tasklist(rcmail.env.libcal_settings);
|
||||
|
||||
rcmail.register_command('tasklist-create-from-mail', function() { tasks.create_from_mail() });
|
||||
rcmail.addEventListener('plugin.mail2taskdialog', function(p){ tasks.mail2taskdialog(p) });
|
||||
rcmail.addEventListener('plugin.unlock_saving', function(p){ tasks.ui && tasks.ui.unlock_saving(); });
|
||||
rcmail.register_command('tasklist-create-from-mail', function() { tasks.create_from_mail(); });
|
||||
rcmail.register_command('attachment-save-task', function() { tasks.save_to_tasklist(); });
|
||||
rcmail.addEventListener('plugin.mail2taskdialog', function(p) { tasks.mail2taskdialog(p); });
|
||||
rcmail.addEventListener('plugin.unlock_saving', function(p) { tasks.ui && tasks.ui.unlock_saving(); });
|
||||
|
||||
if (rcmail.env.action != 'show')
|
||||
rcmail.env.message_commands.push('tasklist-create-from-mail');
|
||||
else
|
||||
rcmail.enable_command('tasklist-create-from-mail', true);
|
||||
|
||||
rcmail.addEventListener('beforemenu-open', function(p) {
|
||||
if (p.menu == 'attachmentmenu') {
|
||||
tasks.selected_attachment = p.id;
|
||||
var mimetype = rcmail.env.attachments[p.id],
|
||||
is_ics = mimetype == 'text/calendar' || mimetype == 'text/x-vcalendar' || mimetype == 'application/ics';
|
||||
|
||||
rcmail.enable_command('attachment-save-task', is_ics);
|
||||
}
|
||||
});
|
||||
|
||||
// add contextmenu item
|
||||
if (window.rcm_contextmenu_register_command) {
|
||||
rcm_contextmenu_register_command(
|
||||
|
@ -104,4 +130,3 @@ window.rcmail && rcmail.env.task == 'mail' && rcmail.addEventListener('init', fu
|
|||
'moveto');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -55,10 +55,57 @@ class tasklist_ui
|
|||
$this->plugin->include_script('tasklist_base.js');
|
||||
|
||||
// copy config to client
|
||||
// $this->rc->output->set_env('tasklist_settings', $settings);
|
||||
$this->rc->output->set_env('tasklist_settings', $this->load_settings());
|
||||
|
||||
// initialize attendees autocompletion
|
||||
$this->rc->autocomplete_init();
|
||||
|
||||
$this->ready = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function load_settings()
|
||||
{
|
||||
$settings = array();
|
||||
|
||||
$settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', 0);
|
||||
|
||||
// get user identity to create default attendee
|
||||
foreach ($this->rc->user->list_identities() as $rec) {
|
||||
if (!$identity)
|
||||
$identity = $rec;
|
||||
|
||||
$identity['emails'][] = $rec['email'];
|
||||
$settings['identities'][$rec['identity_id']] = $rec['email'];
|
||||
}
|
||||
|
||||
$identity['emails'][] = $this->rc->user->get_username();
|
||||
$settings['identity'] = array(
|
||||
'name' => $identity['name'],
|
||||
'email' => strtolower($identity['email']),
|
||||
'emails' => ';' . strtolower(join(';', $identity['emails']))
|
||||
);
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a HTML select box for user identity selection
|
||||
*/
|
||||
function identity_select($attrib = array())
|
||||
{
|
||||
$attrib['name'] = 'identity';
|
||||
$select = new html_select($attrib);
|
||||
$identities = $this->rc->user->list_identities();
|
||||
|
||||
foreach ($identities as $ident) {
|
||||
$select->add(format_email_recipient($ident['email'], $ident['name']), $ident['identity_id']);
|
||||
}
|
||||
|
||||
return $select->show(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register handler methods for the template engine
|
||||
|
@ -78,6 +125,10 @@ class tasklist_ui
|
|||
$this->plugin->register_handler('plugin.attachments_form', array($this, 'attachments_form'));
|
||||
$this->plugin->register_handler('plugin.attachments_list', array($this, 'attachments_list'));
|
||||
$this->plugin->register_handler('plugin.filedroparea', array($this, 'file_drop_area'));
|
||||
$this->plugin->register_handler('plugin.attendees_list', array($this, 'attendees_list'));
|
||||
$this->plugin->register_handler('plugin.attendees_form', array($this, 'attendees_form'));
|
||||
$this->plugin->register_handler('plugin.identity_select', array($this, 'identity_select'));
|
||||
$this->plugin->register_handler('plugin.edit_attendees_notify', array($this, 'edit_attendees_notify'));
|
||||
|
||||
$this->plugin->include_script('jquery.tagedit.js');
|
||||
$this->plugin->include_script('tasklist.js');
|
||||
|
@ -166,10 +217,11 @@ class tasklist_ui
|
|||
// enrich list properties with settings from the driver
|
||||
if (!$prop['virtual']) {
|
||||
unset($prop['user_id']);
|
||||
$prop['alarms'] = $this->plugin->driver->alarms;
|
||||
$prop['undelete'] = $this->plugin->driver->undelete;
|
||||
$prop['sortable'] = $this->plugin->driver->sortable;
|
||||
$prop['alarms'] = $this->plugin->driver->alarms;
|
||||
$prop['undelete'] = $this->plugin->driver->undelete;
|
||||
$prop['sortable'] = $this->plugin->driver->sortable;
|
||||
$prop['attachments'] = $this->plugin->driver->attachments;
|
||||
$prop['attendees'] = $this->plugin->driver->attendees;
|
||||
$jsenv[$id] = $prop;
|
||||
}
|
||||
|
||||
|
@ -375,4 +427,53 @@ class tasklist_ui
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function attendees_list($attrib = array())
|
||||
{
|
||||
// add "noreply" checkbox to attendees table only
|
||||
$invitations = strpos($attrib['id'], 'attend') !== false;
|
||||
|
||||
$invite = new html_checkbox(array('value' => 1, 'id' => 'edit-attendees-invite'));
|
||||
$table = new html_table(array('cols' => 4 + intval($invitations), 'border' => 0, 'cellpadding' => 0, 'class' => 'rectable'));
|
||||
|
||||
// $table->add_header('role', $this->plugin->gettext('role'));
|
||||
$table->add_header('name', $this->plugin->gettext($attrib['coltitle'] ?: 'attendee'));
|
||||
$table->add_header('confirmstate', $this->plugin->gettext('confirmstate'));
|
||||
if ($invitations) {
|
||||
$table->add_header(array('class' => 'sendmail', 'title' => $this->plugin->gettext('sendinvitations')),
|
||||
$invite->show(1) . html::label('edit-attendees-invite', $this->plugin->gettext('sendinvitations')));
|
||||
}
|
||||
$table->add_header('options', '');
|
||||
|
||||
return $table->show($attrib);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function attendees_form($attrib = array())
|
||||
{
|
||||
$input = new html_inputfield(array('name' => 'participant', 'id' => 'edit-attendee-name', 'size' => 30));
|
||||
$textarea = new html_textarea(array('name' => 'comment', 'id' => 'edit-attendees-comment',
|
||||
'rows' => 4, 'cols' => 55, 'title' => $this->plugin->gettext('itipcommenttitle')));
|
||||
|
||||
return html::div($attrib,
|
||||
html::div(null, $input->show() . " " .
|
||||
html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-add', 'value' => $this->plugin->gettext('addattendee')))
|
||||
// . " " . html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-schedule', 'value' => $this->plugin->gettext('scheduletime').'...'))
|
||||
) .
|
||||
html::p('attendees-commentbox', html::label(null, $this->plugin->gettext('itipcomment') . $textarea->show()))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function edit_attendees_notify($attrib = array())
|
||||
{
|
||||
$checkbox = new html_checkbox(array('name' => '_notify', 'id' => 'edit-attendees-donotify', 'value' => 1));
|
||||
return html::div($attrib, html::label(null, $checkbox->show(1) . ' ' . $this->plugin->gettext('sendnotifications')));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue