summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Bruederli <bruederli@kolabsys.com>2015-03-25 10:59:10 (GMT)
committerThomas Bruederli <bruederli@kolabsys.com>2015-03-25 10:59:10 (GMT)
commit548d1d93b70ceb213d2693ab4520e861f9f5b0d9 (patch)
tree2935f13e05bafbebb332664ec0dd7249c90f1c42
parentdcb60dbee171a5b732bbdd289d3aa7f29f5c16ae (diff)
downloadroundcubemail-plugins-kolab-548d1d93b70ceb213d2693ab4520e861f9f5b0d9.tar.gz
Display object history for tasks (#3271)
-rw-r--r--plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php286
-rw-r--r--plugins/tasklist/drivers/tasklist_driver.php76
-rw-r--r--plugins/tasklist/localization/en_US.inc22
-rw-r--r--plugins/tasklist/skins/larry/tasklist.css59
-rw-r--r--plugins/tasklist/skins/larry/templates/mainview.html79
-rw-r--r--plugins/tasklist/tasklist.js219
-rw-r--r--plugins/tasklist/tasklist.php111
-rw-r--r--plugins/tasklist/tasklist_ui.php2
8 files changed, 827 insertions, 27 deletions
diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index 4fccf7e..4a192c6 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -38,6 +38,7 @@ class tasklist_kolab_driver extends tasklist_driver
private $folders = array();
private $tasks = array();
private $tags = array();
+ private $bonnie_api = false;
/**
@@ -55,6 +56,11 @@ class tasklist_kolab_driver extends tasklist_driver
// tasklist use fully encoded identifiers
kolab_storage::$encode_ids = true;
+ // get configuration for the Bonnie API
+ if ($bonnie_config = $this->rc->config->get('kolab_bonnie_api', false)) {
+ $this->bonnie_api = new kolab_bonnie_api($bonnie_config);
+ }
+
$this->_read_lists();
$this->plugin->register_action('folder-acl', array($this, 'folder_acl'));
@@ -152,6 +158,7 @@ class tasklist_kolab_driver extends tasklist_driver
'group' => $folder->default ? 'default' : $folder->get_namespace(),
'class' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')),
'caldavuid' => $folder->get_uid(),
+ 'history' => !empty($this->bonnie_api),
);
}
@@ -659,6 +666,250 @@ class tasklist_kolab_driver extends tasklist_driver
}
/**
+ * Provide a list of revisions for the given task
+ *
+ * @param array $task Hash array with task properties
+ * @return array List of changes, each as a hash array
+ * @see tasklist_driver::get_task_changelog()
+ */
+ public function get_task_changelog($prop)
+ {
+ if (empty($this->bonnie_api)) {
+ return false;
+ }
+
+ list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop);
+
+ $result = $uid && $mailbox ? $this->bonnie_api->changelog('task', $uid, $mailbox, $msguid) : null;
+ if (is_array($result) && $result['uid'] == $uid) {
+ return $result['changes'];
+ }
+
+ return false;
+ }
+
+ /**
+ * Return full data of a specific revision of an event
+ *
+ * @param mixed $task UID string or hash array with task properties
+ * @param mixed $rev Revision number
+ *
+ * @return array Task object as hash array
+ * @see tasklist_driver::get_task_revision()
+ */
+ public function get_task_revison($prop, $rev)
+ {
+ if (empty($this->bonnie_api)) {
+ return false;
+ }
+
+ $this->_parse_id($prop);
+ $uid = $prop['uid'];
+ $list_id = $prop['list'];
+ list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop);
+
+ // call Bonnie API
+ $result = $this->bonnie_api->get('task', $uid, $rev, $mailbox, $msguid);
+ if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) {
+ $format = kolab_format::factory('task');
+ $format->load($result['xml']);
+ $rec = $format->to_array();
+ $format->get_attachments($rec, true);
+
+ if ($format->is_valid()) {
+ $rec['rev'] = $result['rev'];
+ return self::_to_rcube_task($rec, $list_id, false);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Command the backend to restore a certain revision of a task.
+ * This shall replace the current object with an older version.
+ *
+ * @param mixed $task UID string or hash array with task properties
+ * @param mixed $rev Revision number
+ *
+ * @return boolean True on success, False on failure
+ * @see tasklist_driver::restore_task_revision()
+ */
+ public function restore_task_revision($prop, $rev)
+ {
+ if (empty($this->bonnie_api)) {
+ return false;
+ }
+
+ $this->_parse_id($prop);
+ $uid = $prop['uid'];
+ $list_id = $prop['list'];
+ list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop);
+
+ $folder = $this->get_folder($list_id);
+ $success = false;
+
+ if ($folder && ($raw_msg = $this->bonnie_api->rawdata('task', $uid, $rev, $mailbox))) {
+ $imap = $this->rc->get_storage();
+
+ // insert $raw_msg as new message
+ if ($imap->save_message($folder->name, $raw_msg, null, false)) {
+ $success = true;
+
+ // delete old revision from imap and cache
+ $imap->delete_message($msguid, $folder->name);
+ $folder->cache->set($msguid, false);
+ }
+ }
+
+ return $success;
+ }
+
+ /**
+ * Get a list of property changes beteen two revisions of a task object
+ *
+ * @param array $task Hash array with task properties
+ * @param mixed $rev Revisions: "from:to"
+ *
+ * @return array List of property changes, each as a hash array
+ * @see tasklist_driver::get_task_diff()
+ */
+ public function get_task_diff($prop, $rev1, $rev2)
+ {
+ $this->_parse_id($prop);
+ $uid = $prop['uid'];
+ $list_id = $prop['list'];
+ list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop);
+
+ // call Bonnie API
+ $result = $this->bonnie_api->diff('task', $uid, $rev1, $rev2, $mailbox, $msguid, $instance_id);
+ if (is_array($result) && $result['uid'] == $uid) {
+ $result['rev1'] = $rev1;
+ $result['rev2'] = $rev2;
+
+ $keymap = array(
+ 'start' => 'start',
+ 'due' => 'date',
+ 'dstamp' => 'changed',
+ 'summary' => 'title',
+ 'alarm' => 'alarms',
+ 'attendee' => 'attendees',
+ 'attach' => 'attachments',
+ 'rrule' => 'recurrence',
+ 'percent-complete' => 'complete',
+ 'lastmodified-date' => 'changed',
+ );
+ $prop_keymaps = array(
+ 'attachments' => array('fmttype' => 'mimetype', 'label' => 'name'),
+ 'attendees' => array('partstat' => 'status'),
+ );
+ $special_changes = array();
+
+ // map kolab event properties to keys the client expects
+ array_walk($result['changes'], function(&$change, $i) use ($keymap, $prop_keymaps, $special_changes) {
+ if (array_key_exists($change['property'], $keymap)) {
+ $change['property'] = $keymap[$change['property']];
+ }
+ if ($change['property'] == 'priority') {
+ $change['property'] = 'flagged';
+ $change['old'] = $change['old'] == 1 ? $this->plugin->gettext('yes') : null;
+ $change['new'] = $change['new'] == 1 ? $this->plugin->gettext('yes') : null;
+ }
+ // map alarms trigger value
+ if ($change['property'] == 'alarms') {
+ if (is_array($change['old']) && is_array($change['old']['trigger']))
+ $change['old']['trigger'] = $change['old']['trigger']['value'];
+ if (is_array($change['new']) && is_array($change['new']['trigger']))
+ $change['new']['trigger'] = $change['new']['trigger']['value'];
+ }
+ // make all property keys uppercase
+ if ($change['property'] == 'recurrence') {
+ $special_changes['recurrence'] = $i;
+ foreach (array('old','new') as $m) {
+ if (is_array($change[$m])) {
+ $props = array();
+ foreach ($change[$m] as $k => $v) {
+ $props[strtoupper($k)] = $v;
+ }
+ $change[$m] = $props;
+ }
+ }
+ }
+ // map property keys names
+ if (is_array($prop_keymaps[$change['property']])) {
+ foreach ($prop_keymaps[$change['property']] as $k => $dest) {
+ if (is_array($change['old']) && array_key_exists($k, $change['old'])) {
+ $change['old'][$dest] = $change['old'][$k];
+ unset($change['old'][$k]);
+ }
+ if (is_array($change['new']) && array_key_exists($k, $change['new'])) {
+ $change['new'][$dest] = $change['new'][$k];
+ unset($change['new'][$k]);
+ }
+ }
+ }
+
+ if ($change['property'] == 'exdate') {
+ $special_changes['exdate'] = $i;
+ }
+ else if ($change['property'] == 'rdate') {
+ $special_changes['rdate'] = $i;
+ }
+ });
+
+ // merge some recurrence changes
+ foreach (array('exdate','rdate') as $prop) {
+ if (array_key_exists($prop, $special_changes)) {
+ $exdate = $result['changes'][$special_changes[$prop]];
+ if (array_key_exists('recurrence', $special_changes)) {
+ $recurrence = &$result['changes'][$special_changes['recurrence']];
+ }
+ else {
+ $i = count($result['changes']);
+ $result['changes'][$i] = array('property' => 'recurrence', 'old' => array(), 'new' => array());
+ $recurrence = &$result['changes'][$i]['recurrence'];
+ }
+ $key = strtoupper($prop);
+ $recurrence['old'][$key] = $exdate['old'];
+ $recurrence['new'][$key] = $exdate['new'];
+ unset($result['changes'][$special_changes[$prop]]);
+ }
+ }
+
+ return $result;
+ }
+
+ return false;
+ }
+
+ /**
+ * Helper method to resolved the given task identifier into uid and folder
+ *
+ * @return array (uid,folder,msguid) tuple
+ */
+ private function _resolve_task_identity($prop)
+ {
+ $mailbox = $msguid = null;
+
+ $this->_parse_id($prop);
+ $uid = $prop['uid'];
+ $list_id = $prop['list'];
+
+ if ($folder = $this->get_folder($list_id)) {
+ $mailbox = $folder->get_mailbox_id();
+
+ // get task object from storage in order to get the real object uid an msguid
+ if ($rec = $folder->get_object($uid)) {
+ $msguid = $rec['_msguid'];
+ $uid = $rec['uid'];
+ }
+ }
+
+ return array($uid, $mailbox, $msguid);
+ }
+
+
+ /**
* Get a list of pending alarms to be displayed to the user
*
* @param integer Current time (unix timestamp)
@@ -1232,6 +1483,7 @@ class tasklist_kolab_driver extends tasklist_driver
* @param array $task Hash array with event properties:
* id: Task identifier
* list: List identifier
+ * rev: Revision (optional)
*
* @return array Hash array with attachment properties:
* id: Attachment identifier
@@ -1241,7 +1493,13 @@ class tasklist_kolab_driver extends tasklist_driver
*/
public function get_attachment($id, $task)
{
- $task = $this->get_task($task);
+ // get old revision of the object
+ if ($task['rev']) {
+ $task = $this->get_task_revison($task, $task['rev']);
+ }
+ else {
+ $task = $this->get_task($task);
+ }
if ($task && !empty($task['attachments'])) {
foreach ($task['attachments'] as $att) {
@@ -1260,12 +1518,38 @@ class tasklist_kolab_driver extends tasklist_driver
* @param array $task Hash array with event properties:
* id: Task identifier
* list: List identifier
+ * rev: Revision (optional)
*
* @return string Attachment body
*/
public function get_attachment_body($id, $task)
{
$this->_parse_id($task);
+
+ // get old revision of event
+ if ($task['rev']) {
+ if (empty($this->bonnie_api)) {
+ return false;
+ }
+
+ $cid = substr($id, 4);
+
+ // call Bonnie API and get the raw mime message
+ list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($task);
+ if ($msg_raw = $this->bonnie_api->rawdata('task', $uid, $task['rev'], $mailbox, $msguid)) {
+ // parse the message and find the part with the matching content-id
+ $message = rcube_mime::parse_message($msg_raw);
+ foreach ((array)$message->parts as $part) {
+ if ($part->headers['content-id'] && trim($part->headers['content-id'], '<>') == $cid) {
+ return $part->body;
+ }
+ }
+ }
+
+ return false;
+ }
+
+
if ($storage = $this->get_folder($task['list'])) {
return $storage->get_attachment($task['uid'], $id);
}
diff --git a/plugins/tasklist/drivers/tasklist_driver.php b/plugins/tasklist/drivers/tasklist_driver.php
index 3362e7f..be82344 100644
--- a/plugins/tasklist/drivers/tasklist_driver.php
+++ b/plugins/tasklist/drivers/tasklist_driver.php
@@ -242,6 +242,7 @@ abstract class tasklist_driver
*
* @param array Hash array with task properties:
* id: Task identifier
+ * list: Tasklist identifer
* @param boolean Remove record irreversible (mark as deleted otherwise, if supported by the backend)
* @return boolean True on success, False on error
*/
@@ -266,6 +267,7 @@ abstract class tasklist_driver
* @param array $task Hash array with event properties:
* id: Task identifier
* list: List identifier
+ * rev: Revision (optional)
*
* @return array Hash array with attachment properties:
* id: Attachment identifier
@@ -282,6 +284,7 @@ abstract class tasklist_driver
* @param array $task Hash array with event properties:
* id: Task identifier
* list: List identifier
+ * rev: Revision (optional)
*
* @return string Attachment body
*/
@@ -319,7 +322,7 @@ abstract class tasklist_driver
/**
* Helper method to determine whether the given task is considered "complete"
*
- * @param array $task Hash array with event properties:
+ * @param array $task Hash array with event properties
* @return boolean True if complete, False otherwiese
*/
public function is_complete($task)
@@ -328,13 +331,74 @@ abstract class tasklist_driver
}
/**
- * List availabale categories
- * The default implementation reads them from config/user prefs
+ * Provide a list of revisions for the given task
+ *
+ * @param array $task Hash array with task properties:
+ * id: Task identifier
+ * list: List identifier
+ *
+ * @return array List of changes, each as a hash array:
+ * rev: Revision number
+ * type: Type of the change (create, update, move, delete)
+ * date: Change date
+ * user: The user who executed the change
+ * ip: Client IP
+ * mailbox: Destination list for 'move' type
*/
- public function list_categories()
+ public function get_task_changelog($task)
{
- $rcmail = rcube::get_instance();
- return $rcmail->config->get('tasklist_categories', array());
+ return false;
+ }
+
+ /**
+ * Get a list of property changes beteen two revisions of a task object
+ *
+ * @param array $task Hash array with task properties:
+ * id: Task identifier
+ * list: List identifier
+ * @param mixed $rev1 Old Revision
+ * @param mixed $rev2 New Revision
+ *
+ * @return array List of property changes, each as a hash array:
+ * property: Revision number
+ * old: Old property value
+ * new: Updated property value
+ */
+ public function get_task_diff($task, $rev1, $rev2)
+ {
+ return false;
+ }
+
+ /**
+ * Return full data of a specific revision of an event
+ *
+ * @param mixed $task UID string or hash array with task properties:
+ * id: Task identifier
+ * list: List identifier
+ * @param mixed $rev Revision number
+ *
+ * @return array Task object as hash array
+ * @see self::get_task()
+ */
+ public function get_task_revison($task, $rev)
+ {
+ return false;
+ }
+
+ /**
+ * Command the backend to restore a certain revision of a task.
+ * This shall replace the current object with an older version.
+ *
+ * @param mixed $task UID string or hash array with task properties:
+ * id: Task identifier
+ * list: List identifier
+ * @param mixed $rev Revision number
+ *
+ * @return boolean True on success, False on failure
+ */
+ public function restore_task_revision($task, $rev)
+ {
+ return false;
}
/**
diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
index ee66759..8dc2929 100644
--- a/plugins/tasklist/localization/en_US.inc
+++ b/plugins/tasklist/localization/en_US.inc
@@ -50,6 +50,9 @@ $labels['status-cancelled'] = 'Cancelled';
$labels['assignedto'] = 'Assigned to';
$labels['created'] = 'Created';
$labels['changed'] = 'Last Modified';
+$labels['taskoptions'] = 'Options';
+$labels['taskhistory'] = 'History';
+$labels['compare'] = 'Compare';
$labels['all'] = 'All';
$labels['flagged'] = 'Flagged';
@@ -101,6 +104,7 @@ $labels['on'] = 'on';
$labels['at'] = 'at';
$labels['this'] = 'this';
$labels['next'] = 'next';
+$labels['yes'] = 'yes';
// messages
$labels['savingdata'] = 'Saving data...';
@@ -150,6 +154,24 @@ $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 ';
+// history dialog
+$labels['objectchangelog'] = 'Change History';
+$labels['objectdiff'] = 'Changes from $rev1 to $rev2';
+
+$labels['actionappend'] = 'Saved';
+$labels['actionmove'] = 'Moved';
+$labels['actiondelete'] = 'Deleted';
+$labels['compare'] = 'Compare';
+$labels['showrevision'] = 'Show this version';
+$labels['restore'] = 'Restore this version';
+
+$labels['objectnotfound'] = 'Failed to load task data';
+$labels['objectchangelognotavailable'] = 'Change history is not available for this task';
+$labels['objectdiffnotavailable'] = 'No comparison possible for the selected revisions';
+$labels['revisionrestoreconfirm'] = 'Do you really want to restore revision $rev of this task? This will replace the current task with the old version.';
+$labels['objectrestoresuccess'] = 'Revision $rev successfully restored';
+$labels['objectrestoreerror'] = 'Failed to restore the old revision';
+
// invitation handling (overrides labels from libcalendaring)
$labels['itipobjectnotfound'] = 'The task referred by this message was not found in your tasks list.';
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index bd5ec60..a39c1f2 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -642,7 +642,8 @@ ul.toolbarmenu li span.icon.taskadd,
font-size: 12px;
}
-.taskhead .flagged {
+.taskhead .flagged,
+.taskshow.status-flagged h2:after {
display: inline-block;
width: 16px;
height: 16px;
@@ -657,7 +658,8 @@ ul.toolbarmenu li span.icon.taskadd,
background-position: -2px -3px;
}
-.taskhead.flagged .flagged {
+.taskhead.flagged .flagged,
+.taskshow.status-flagged h2:after {
background-position: -2px -23px;
}
@@ -839,8 +841,9 @@ ul.toolbarmenu .sortcol.by-auto a {
/*** task edit form ***/
#taskedit,
-#taskshow {
- display:none;
+#taskshow,
+#taskdiff {
+ display: none;
}
#taskedit {
@@ -850,15 +853,32 @@ ul.toolbarmenu .sortcol.by-auto a {
margin: 0 -0.2em;
}
-#taskshow h2 {
+.taskshow h2 {
margin-top: -0.5em;
}
-#taskshow label {
+#taskdiff h2 {
+ font-size: 18px;
+ margin: -0.3em 0 0.4em 0;
+}
+
+.taskshow.status-completed h2 {
+ text-decoration: line-through;
+}
+
+.taskshow.status-flagged h2:after {
+ content: " ";
+ position: relative;
+ margin-left: 0.6em;
+ top: 1px;
+ cursor: default;
+}
+
+.taskshow label {
color: #999;
}
-#taskshow.status-cancelled {
+.taskshow.status-cancelled {
background: url(images/badge_cancelled.png) top right no-repeat;
}
@@ -1048,10 +1068,33 @@ label.block {
margin-bottom: 0.3em;
}
-#task-description {
+.task-description {
margin-bottom: 1em;
}
+.taskshow .task-text-old,
+.taskshow .task-text-new,
+.taskshow .task-text-diff {
+ padding: 2px;
+}
+
+.taskshow .task-text-diff del,
+.taskshow .task-text-diff ins {
+ text-decoration: none;
+ color: inherit;
+}
+
+.taskshow .task-text-old,
+.taskshow .task-text-diff del {
+ background-color: #fdd;
+ /* text-decoration: line-through; */
+}
+
+.taskshow .task-text-new,
+.taskshow .task-text-diff ins {
+ background-color: #dfd;
+}
+
#taskedit-completeness-slider {
display: inline-block;
margin-left: 2em;
diff --git a/plugins/tasklist/skins/larry/templates/mainview.html b/plugins/tasklist/skins/larry/templates/mainview.html
index f80b2f7..dd38409 100644
--- a/plugins/tasklist/skins/larry/templates/mainview.html
+++ b/plugins/tasklist/skins/larry/templates/mainview.html
@@ -149,6 +149,9 @@
<li role="menuitem"><roundcube:button name="edit" type="link" onclick="rctasks.edit_task(rctasks.selected_task.id, 'edit'); return false" label="edit" class="icon active" innerclass="icon edit" /></li>
<li role="menuitem"><roundcube:button name="delete" type="link" onclick="rctasks.delete_task(rctasks.selected_task.id); return false" label="delete" class="icon active" innerclass="icon delete" /></li>
<li role="menuitem"><roundcube:button name="addchild" type="link" onclick="rctasks.add_childtask(rctasks.selected_task.id); return false" label="tasklist.addsubtask" class="icon active" innerclass="icon add" /></li>
+ <roundcube:if condition="env:tasklist_driver == 'kolab' && config:kolab_bonnie_api" />
+ <li role="menuitem"><roundcube:button command="task-history" type="link" label="tasklist.taskhistory" class="icon" classAct="icon active" innerclass="icon history" /></li>
+ <roundcube:endif />
</ul>
</div>
@@ -159,12 +162,12 @@
<roundcube:object name="message" id="messagestack" />
-<div id="taskshow">
+<div id="taskshow" class="taskshow">
<div class="form-section" id="task-parent-title"></div>
<div class="form-section">
<h2 id="task-title"></h2>
</div>
- <div id="task-description" class="form-section">
+ <div id="task-description" class="form-section task-description">
</div>
<div id="task-tags" class="form-section">
<label><roundcube:label name="tasklist.tags" /></label>
@@ -239,6 +242,78 @@
<roundcube:object name="plugin.task_rsvp_buttons" id="task-rsvp" class="task-dialog-message" style="display:none" />
</div>
+<roundcube:if condition="env:tasklist_driver == 'kolab' && config:kolab_bonnie_api" />
+<div id="taskhistory" class="uidialog" aria-hidden="true">
+ <roundcube:object name="plugin.object_changelog_table" class="records-table changelog-table" domain="calendar" />
+ <div class="compare-button"><input type="button" class="button" value="↳ <roundcube:label name='tasklist.compare' />" /></div>
+</div>
+
+<div id="taskdiff" class="uidialog taskshow" aria-hidden="true">
+ <h2 class="task-title">Task Title</h2>
+ <h2 class="task-title-new task-text-new"></h2>
+ <div class="form-section task-description">
+ <label><roundcube:label name="calendar.description" /></label>
+ <div class="task-text-diff" style="white-space:pre-wrap"></div>
+ <div class="task-text-old"></div>
+ <div class="task-text-new"></div>
+ </div>
+ <div class="form-section task-flagged">
+ <label><roundcube:label name="tasklist.flagged" /></label>
+ <span class="task-text-old"></span> &#8674;
+ <span class="task-text-new"></span>
+ </div>
+ <div class="form-section task-start">
+ <label><roundcube:label name="tasklist.start" /></label>
+ <span class="task-text-old"></span> &#8674;
+ <span class="task-text-new"></span>
+ </div>
+ <div class="form-section task-date">
+ <label><roundcube:label name="tasklist.datetime" /></label>
+ <span class="task-text-old"></span> &#8674;
+ <span class="task-text-new"></span>
+ </div>
+ <div class="form-section task-recurrence">
+ <label><roundcube:label name="tasklist.repeat" /></label>
+ <span class="task-text-old"></span> &#8674;
+ <span class="task-text-new"></span>
+ </div>
+ <div class="form-section task-alarms">
+ <label><roundcube:label name="tasklist.alarms" /></label>
+ <span class="task-text-old"></span> &#8674;
+ <span class="task-text-new"></span>
+ </div>
+ <div class="form-section task-attendees">
+ <label><roundcube:label name="tasklist.assignedto" /></label>
+ <span class="task-text-old"></span> &#8674;
+ <span class="task-text-new"></span>
+ </div>
+ <div class="form-section task-organizer">
+ <label><roundcube:label name="tasklist.roleorganizer" /></label>
+ <span class="task-text-old"></span> &#8674;
+ <span class="task-text-new"></span>
+ </div>
+ <div class="form-section task-complete">
+ <label><roundcube:label name="tasklist.complete" /></label>
+ <span class="task-text-old"></span> &#8674;
+ <span class="task-text-new"></span>
+ </div>
+ <div class="form-section task-status">
+ <label><roundcube:label name="tasklist.status" /></label>
+ <span class="task-text-old"></span> &#8674;
+ <span class="task-text-new"></span>
+ </div>
+ <div class="form-section task-links">
+ <label><roundcube:label name="tasklist.links" /></label>
+ <span class="task-text"></span>
+ </div>
+ <div class="form-section task-attachments">
+ <label><roundcube:label name="attachments" /></label>
+ <div class="task-text-old"></div>
+ <div class="task-text-new"></div>
+ </div>
+</div>
+<roundcube:endif />
+
<roundcube:include file="/templates/taskedit.html" />
<div id="tasklistform" class="uidialog">
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index a01689f..514d060 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -282,8 +282,14 @@ function rcube_tasklist_ui(settings)
setTimeout(fetch_counts, 200);
});
+ rcmail.addEventListener('plugin.task_render_changelog', task_render_changelog);
+ rcmail.addEventListener('plugin.task_show_diff', task_show_diff);
+ rcmail.addEventListener('plugin.task_show_revision', function(data){ task_show_dialog(null, data, true); });
+ rcmail.addEventListener('plugin.close_history_dialog', close_history_dialog);
+
rcmail.register_command('list-sort', list_set_sort, true);
rcmail.register_command('list-order', list_set_order, (settings.sort_col || 'auto') != 'auto');
+ rcmail.register_command('task-history', task_history_dialog, false);
$('#taskviewsortmenu .by-' + (settings.sort_col || 'auto')).attr('aria-checked', 'true').addClass('selected');
$('#taskviewsortmenu .sortorder.' + (settings.sort_order || 'asc')).attr('aria-checked', 'true').addClass('selected');
@@ -459,6 +465,7 @@ function rcube_tasklist_ui(settings)
rcmail.command('menu-close', 'taskitemmenu');
}
else {
+ rcmail.enable_command('task-history', me.tasklists[rec.list] && !!me.tasklists[rec.list].history);
rcmail.command('menu-open', { menu: 'taskitemmenu', show: true }, e.target, e);
menu.data('refid', id);
me.selected_task = rec;
@@ -1835,7 +1842,7 @@ function rcube_tasklist_ui(settings)
/**
* Show task details in a dialog
*/
- function task_show_dialog(id)
+ function task_show_dialog(id, data, temp)
{
var $dialog = $('#taskshow'), rec, list;
@@ -1848,7 +1855,7 @@ function rcube_tasklist_ui(settings)
return $.grep(oldies, function(cls) { return cls.indexOf('status-') === 0 }).join(' ');
});
- if (!(rec = listdata[id]) || (rcmail.menu_stack && rcmail.menu_stack.length > 0))
+ if (!(rec = (data || listdata[id])) || (rcmail.menu_stack && rcmail.menu_stack.length > 0))
return;
me.selected_task = rec;
@@ -1892,6 +1899,10 @@ function rcube_tasklist_ui(settings)
$dialog.addClass('status-' + String(rec.status).toLowerCase());
}
+ if (rec.flagged) {
+ $dialog.addClass('status-flagged');
+ }
+
if (rec.recurrence && rec.recurrence_text) {
$('#task-recurrence').show().children('.task-text').html(Q(rec.recurrence_text));
}
@@ -1986,7 +1997,7 @@ function rcube_tasklist_ui(settings)
.html(Q(rcmail.gettext('itip' + mystatus, 'libcalendaring')));
}
*/
- var show_rsvp = rsvp && list.editable && !is_organizer(rec) && rec.status != 'CANCELLED';
+ var show_rsvp = !temp && rsvp && list.editable && !is_organizer(rec) && rec.status != 'CANCELLED';
$('#task-rsvp')[(show_rsvp ? 'show' : 'hide')]();
$('#task-rsvp .rsvp-buttons input').prop('disabled', false).filter('input[rel='+mystatus+']').prop('disabled', true);
@@ -2036,6 +2047,13 @@ function rcube_tasklist_ui(settings)
},
close: function() {
$dialog.dialog('destroy').appendTo(document.body);
+ $('.libcal-rsvp-replymode').hide();
+ },
+ dragStart: function() {
+ $('.libcal-rsvp-replymode').hide();
+ },
+ resizeStart: function() {
+ $('.libcal-rsvp-replymode').hide();
},
buttons: buttons,
minWidth: 500,
@@ -2065,6 +2083,190 @@ function rcube_tasklist_ui(settings)
}
/**
+ *
+ */
+ function task_history_dialog()
+ {
+ var dialog, rec = me.selected_task;
+ if (!rec || !rec.id || !window.libkolab_audittrail) {
+ return false;
+ }
+
+ // render dialog
+ $dialog = libkolab_audittrail.object_history_dialog({
+ module: 'tasklist',
+ container: '#taskhistory',
+ title: rcmail.gettext('objectchangelog','tasklist') + ' - ' + rec.title,
+
+ // callback function for list actions
+ listfunc: function(action, rev) {
+ var rec = $dialog.data('rec');
+ saving_lock = rcmail.set_busy(true, 'loading', saving_lock);
+ rcmail.http_post('task', { action: action, t: { id: rec.id, list:rec.list, rev: rev } }, saving_lock);
+ },
+
+ // callback function for comparing two object revisions
+ comparefunc: function(rev1, rev2) {
+ var rec = $dialog.data('rec');
+ saving_lock = rcmail.set_busy(true, 'loading', saving_lock);
+ rcmail.http_post('task', { action:'diff', t: { id: rec.id, list: rec.list, rev1: rev1, rev2: rev2 } }, saving_lock);
+ }
+ });
+
+ $dialog.data('rec', rec);
+
+ // fetch changelog data
+ saving_lock = rcmail.set_busy(true, 'loading', saving_lock);
+ rcmail.http_post('task', { action: 'changelog', t: { id: rec.id, list: rec.list } }, saving_lock);
+ }
+
+ /**
+ *
+ */
+ function task_render_changelog(data)
+ {
+ var $dialog = $('#taskhistory'),
+ rec = $dialog.data('rec');
+
+ if (data === false || !data.length || !event) {
+ // display 'unavailable' message
+ $('<div class="notfound-message task-dialog-message warning">' + rcmail.gettext('objectchangelognotavailable','tasklist') + '</div>')
+ .insertBefore($dialog.find('.changelog-table').hide());
+ return;
+ }
+
+ data.module = 'tasklist';
+ libkolab_audittrail.render_changelog(data, rec, me.tasklists[rec.list]);
+
+ // set dialog size according to content
+ me.dialog_resize($dialog.get(0), $dialog.height(), 600);
+ }
+
+ /**
+ *
+ */
+ function task_show_diff(data)
+ {
+ var rec = me.selected_task,
+ $dialog = $("#taskdiff");
+
+ $dialog.find('div.form-section, h2.task-title-new').hide().data('set', false).find('.index').html('');
+ $dialog.find('div.form-section.clone').remove();
+
+ // always show event title and date
+ $('.task-title', $dialog).text(rec.title).removeClass('task-text-old').show();
+
+ // show each property change
+ $.each(data.changes, function(i, change) {
+ var prop = change.property, r2, html = false,
+ row = $('div.task-' + prop, $dialog).first();
+
+ // special case: title
+ if (prop == 'title') {
+ $('.task-title', $dialog).addClass('task-text-old').text(change['old'] || '--');
+ $('.task-title-new', $dialog).text(change['new'] || '--').show();
+ }
+
+ // no display container for this property
+ if (!row.length) {
+ return true;
+ }
+
+ // clone row if already exists
+ if (row.data('set')) {
+ r2 = row.clone().addClass('clone').insertAfter(row);
+ row = r2;
+ }
+
+ // render description text
+ if (prop == 'description') {
+ if (!change.diff_ && change['old']) change.old_ = text2html(change['old']);
+ if (!change.diff_ && change['new']) change.new_ = text2html(change['new']);
+ html = true;
+ }
+ // format attendees struct
+ else if (prop == 'attendees') {
+ if (change['old']) change.old_ = task_attendee_html(change['old']);
+ if (change['new']) change.new_ = task_attendee_html($.extend({}, change['old'] || {}, change['new']));
+ html = true;
+ }
+ // localize status
+ else if (prop == 'status') {
+ if (change['old']) change.old_ = rcmail.gettext('status-'+String(change['old']).toLowerCase(), 'tasklist');
+ if (change['new']) change.new_ = rcmail.gettext('status-'+String(change['new']).toLowerCase(), 'tasklist');
+ }
+
+ // format attachments struct
+ if (prop == 'attachments') {
+ if (change['old']) task_show_attachments([change['old']], row.children('.task-text-old'), rec, false);
+ else row.children('.task-text-old').text('--');
+ if (change['new']) task_show_attachments([$.extend({}, change['old'] || {}, change['new'])], row.children('.task-text-new'), rec, false);
+ else row.children('.task-text-new').text('--');
+ // remove click handler in diff view
+ $('.attachmentslist li a', row).unbind('click').removeAttr('href');
+ }
+ else if (change.diff_) {
+ row.children('.task-text-diff').html(change.diff_);
+ row.children('.task-text-old, .task-text-new').hide();
+ }
+ else {
+ if (!html) {
+ // escape HTML characters
+ change.old_ = Q(change.old_ || change['old'] || '--')
+ change.new_ = Q(change.new_ || change['new'] || '--')
+ }
+ row.children('.task-text-old').html(change.old_ || change['old'] || '--').show();
+ row.children('.task-text-new').html(change.new_ || change['new'] || '--').show();
+ }
+
+ // display index number
+ if (typeof change.index != 'undefined') {
+ row.find('.index').html('(' + change.index + ')');
+ }
+
+ row.show().data('set', true);
+ });
+
+ var buttons = {};
+ buttons[rcmail.gettext('close')] = function() {
+ $dialog.dialog('close');
+ };
+
+ // open jquery UI dialog
+ $dialog.dialog({
+ modal: false,
+ resizable: true,
+ closeOnEscape: true,
+ title: rcmail.gettext('objectdiff','tasklist').replace('$rev1', data.rev1).replace('$rev2', data.rev2) + ' - ' + rec.title,
+ open: function() {
+ $dialog.attr('aria-hidden', 'false');
+ setTimeout(function(){
+ $dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
+ }, 5);
+ },
+ close: function() {
+ $dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
+ },
+ buttons: buttons,
+ minWidth: 320,
+ width: 450
+ }).show();
+
+ // set dialog size according to content
+ me.dialog_resize($dialog.get(0), $dialog.height(), 400);
+ }
+
+ // close the event history dialog
+ function close_history_dialog()
+ {
+ $('#taskhistory, #taskdiff').each(function(i, elem) {
+ var $dialog = $(elem);
+ if ($dialog.is(':ui-dialog'))
+ $dialog.dialog('close');
+ });
+ };
+
+ /**
* Opens the dialog to edit a task
*/
function task_edit_dialog(id, action, presets)
@@ -2371,17 +2573,22 @@ function rcube_tasklist_ui(settings)
if (!rec.id || rec.id < 0)
return false;
- var qstring = '_id='+urlencode(att.id)+'&_t='+urlencode(rec.recurrence_id||rec.id)+'&_list='+urlencode(rec.list);
+ var query = { _id: att.id, _t: rec.recurrence_id||rec.id, _list:rec.list, _frame: 1 };
+ if (rec.rev)
+ query._rev = event.rev;
+
// open attachment in frame if it's of a supported mimetype
// similar as in app.js and calendar_ui.js
if (att.id && att.mimetype && $.inArray(att.mimetype, settings.mimetypes)>=0) {
- if (rcmail.open_window(rcmail.env.comm_path+'&_action=get-attachment&'+qstring+'&_frame=1', true, true)) {
+ if (rcmail.open_window(rcmail.url('get-attachment', query), true, true)) {
return;
}
}
- rcmail.goto_url('get-attachment', qstring+'&_download=1', false);
+ query._frame = null;
+ query._download = 1;
+ rcmail.goto_url('get-attachment', query, false);
};
/**
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 780602c..449f43c 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -208,7 +208,7 @@ class tasklist extends rcube_plugin
$action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
$rec = rcube_utils::get_input_value('t', rcube_utils::INPUT_POST, true);
$oldrec = $rec;
- $success = $refresh = false;
+ $success = $refresh = $got_msg = false;
// force notify if hidden + active
$itip_send_option = (int)$this->rc->config->get('calendar_itip_send_option', 3);
@@ -385,13 +385,115 @@ class tasklist extends rcube_plugin
}
}
break;
+
+ case 'changelog':
+ $data = $this->driver->get_task_changelog($rec);
+ if (is_array($data) && !empty($data)) {
+ $lib = $this->lib;
+ $dtformat = $this->rc->config->get('date_format') . ' ' . $this->rc->config->get('time_format');
+ array_walk($data, function(&$change) use ($lib, $dtformat) {
+ if ($change['date']) {
+ $dt = $lib->adjust_timezone($change['date']);
+ if ($dt instanceof DateTime)
+ $change['date'] = $this->rc->format_date($dt, $dtformat, false);
+ }
+ });
+ $this->rc->output->command('plugin.task_render_changelog', $data);
+ }
+ else {
+ $this->rc->output->command('plugin.task_render_changelog', false);
+ }
+ $got_msg = true;
+ break;
+
+ case 'diff':
+ $data = $this->driver->get_task_diff($rec, $rec['rev1'], $rec['rev2']);
+ if (is_array($data)) {
+ // convert some properties, similar to self::_client_event()
+ $lib = $this->lib;
+ $date_format = $this->rc->config->get('date_format', 'Y-m-d');
+ $time_format = $this->rc->config->get('time_format', 'H:i');
+ array_walk($data['changes'], function(&$change, $i) use ($lib, $date_format, $time_format) {
+ // convert date cols
+ if (in_array($change['property'], array('date','start','created','changed'))) {
+ if (!empty($change['old'])) {
+ $dtformat = strlen($change['old']) == 10 ? $date_format : $date_format . ' ' . $time_format;
+ $change['old_'] = $lib->adjust_timezone($change['old'], strlen($change['old']) == 10)->format($dtformat);
+ }
+ if (!empty($change['new'])) {
+ $dtformat = strlen($change['new']) == 10 ? $date_format : $date_format . ' ' . $time_format;
+ $change['new_'] = $lib->adjust_timezone($change['new'], strlen($change['new']) == 10)->format($dtformat);
+ }
+ }
+ // create textual representation for alarms and recurrence
+ if ($change['property'] == 'alarms') {
+ if (is_array($change['old']))
+ $change['old_'] = libcalendaring::alarm_text($change['old']);
+ if (is_array($change['new']))
+ $change['new_'] = libcalendaring::alarm_text(array_merge((array)$change['old'], $change['new']));
+ }
+ if ($change['property'] == 'recurrence') {
+ if (is_array($change['old']))
+ $change['old_'] = $lib->recurrence_text($change['old']);
+ if (is_array($change['new']))
+ $change['new_'] = $lib->recurrence_text(array_merge((array)$change['old'], $change['new']));
+ }
+ if ($change['property'] == 'complete') {
+ $change['old_'] = intval($change['old']) . '%';
+ $change['new_'] = intval($change['new']) . '%';
+ }
+ if ($change['property'] == 'attachments') {
+ if (is_array($change['old']))
+ $change['old']['classname'] = rcube_utils::file2class($change['old']['mimetype'], $change['old']['name']);
+ if (is_array($change['new'])) {
+ $change['new'] = array_merge((array)$change['old'], $change['new']);
+ $change['new']['classname'] = rcube_utils::file2class($change['new']['mimetype'], $change['new']['name']);
+ }
+ }
+ // compute a nice diff of description texts
+ if ($change['property'] == 'description') {
+ $change['diff_'] = libkolab::html_diff($change['old'], $change['new']);
+ }
+ });
+ $this->rc->output->command('plugin.task_show_diff', $data);
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error');
+ }
+ $got_msg = true;
+ break;
+
+ case 'show':
+ if ($rec = $this->driver->get_task_revison($rec, $rec['rev'])) {
+ $this->encode_task($rec);
+ $rec['readonly'] = 1;
+ $this->rc->output->command('plugin.task_show_revision', $rec);
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('objectnotfound'), 'error');
+ }
+ $got_msg = true;
+ break;
+
+ case 'restore':
+ if ($success = $this->driver->restore_task_revision($rec, $rec['rev'])) {
+ $refresh = $this->driver->get_task($rec);
+ $this->rc->output->command('display_message', $this->gettext(array('name' => 'objectrestoresuccess', 'vars' => array('rev' => $rec['rev']))), 'confirmation');
+ $this->rc->output->command('plugin.close_history_dialog');
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error');
+ }
+ $got_msg = true;
+ break;
+
}
if ($success) {
$this->rc->output->show_message('successfullysaved', 'confirmation');
$this->update_counts($oldrec, $refresh);
}
- else {
+ else if (!$got_msg) {
$this->rc->output->show_message('tasklist.errorsaving', 'error');
}
@@ -1268,7 +1370,7 @@ class tasklist extends rcube_plugin
$this->rc->output->set_env('autocomplete_threads', (int)$this->rc->config->get('autocomplete_threads', 0));
$this->rc->output->set_env('autocomplete_max', (int)$this->rc->config->get('autocomplete_max', 15));
$this->rc->output->set_env('autocomplete_min_length', $this->rc->config->get('autocomplete_min_length'));
- $this->rc->output->add_label('autocompletechars', 'autocompletemore', 'delete', 'libcalendaring.expandattendeegroup', 'libcalendaring.expandattendeegroupnodata');
+ $this->rc->output->add_label('autocompletechars', 'autocompletemore', 'delete', 'close', 'libcalendaring.expandattendeegroup', 'libcalendaring.expandattendeegroupnodata');
$this->rc->output->set_pagetitle($this->gettext('navtitle'));
$this->rc->output->send('tasklist.mainview');
@@ -1396,8 +1498,9 @@ class tasklist extends rcube_plugin
$task = rcube_utils::get_input_value('_t', rcube_utils::INPUT_GPC);
$list = rcube_utils::get_input_value('_list', rcube_utils::INPUT_GPC);
$id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
+ $rev = rcube_utils::get_input_value('_rev', rcube_utils::INPUT_GPC);
- $task = array('id' => $task, 'list' => $list);
+ $task = array('id' => $task, 'list' => $list, 'rev' => $rev);
$attachment = $this->driver->get_attachment($id, $task);
// show part page
diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php
index a46aa1e..97b1633 100644
--- a/plugins/tasklist/tasklist_ui.php
+++ b/plugins/tasklist/tasklist_ui.php
@@ -156,6 +156,7 @@ class tasklist_ui
$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->register_handler('plugin.task_rsvp_buttons', array($this->plugin->itip, 'itip_rsvp_buttons'));
+ $this->plugin->register_handler('plugin.object_changelog_table', array('libkolab', 'object_changelog_table'));
jqueryui::tagedit();
@@ -165,6 +166,7 @@ class tasklist_ui
// include kolab folderlist widget if available
if (in_array('libkolab', $this->plugin->api->loaded_plugins())) {
$this->plugin->api->include_script('libkolab/js/folderlist.js');
+ $this->plugin->api->include_script('libkolab/js/audittrail.js');
}
}