Commit 548d1d93 authored by Thomas Bruederli's avatar Thomas Bruederli

Display object history for tasks (#3271)

parent dcb60dbe
......@@ -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;
}
/**
......
......@@ -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.';
......
......@@ -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;
......
......@@ -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">
......
This diff is collapsed.
......@@ -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
......
......@@ -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');
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment