Implement client-side and user-adjustable sorting of tasks (#3259)

This commit is contained in:
Thomas Bruederli 2014-08-13 11:07:06 +02:00
parent aa63f121c8
commit b6b12069df
9 changed files with 174 additions and 37 deletions

View file

@ -1,4 +1,11 @@
<?php <?php
$rcmail_config['tasklist_driver'] = 'kolab'; // backend type (database, kolab)
$config['tasklist_driver'] = 'kolab';
// default sorting order of tasks listing (auto, datetime, startdatetime, flagged, complete, changed)
$config['tasklist_sort_col'] = '';
// default sorting order for tasks listing (asc or desc)
$config['tasklist_sort_order'] = 'asc';

View file

@ -794,8 +794,11 @@ class tasklist_kolab_driver extends tasklist_driver
if (!$record['start']->_dateonly) if (!$record['start']->_dateonly)
$task['starttime'] = $start->format('H:i'); $task['starttime'] = $start->format('H:i');
} }
if (is_a($record['dtstamp'], 'DateTime')) { if (is_a($record['changed'], 'DateTime')) {
$task['changed'] = $record['dtstamp']; $task['changed'] = $record['changed'];
}
if (is_a($record['created'], 'DateTime')) {
$task['created'] = $record['created'];
} }
if ($record['valarms']) { if ($record['valarms']) {
@ -912,7 +915,7 @@ class tasklist_kolab_driver extends tasklist_driver
$object['sequence'] = $old['sequence']; $object['sequence'] = $old['sequence'];
} }
unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags']); unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags'], $object['created']);
return $object; return $object;
} }

View file

@ -34,10 +34,13 @@ $labels['status-in-process'] = 'In process';
$labels['status-completed'] = 'Completed'; $labels['status-completed'] = 'Completed';
$labels['status-cancelled'] = 'Cancelled'; $labels['status-cancelled'] = 'Cancelled';
$labels['assignedto'] = 'Assigned to'; $labels['assignedto'] = 'Assigned to';
$labels['created'] = 'Created';
$labels['changed'] = 'Last Modified';
$labels['all'] = 'All'; $labels['all'] = 'All';
$labels['flagged'] = 'Flagged'; $labels['flagged'] = 'Flagged';
$labels['complete'] = 'Complete'; $labels['complete'] = 'Complete';
$labels['completeness'] = 'Progress';
$labels['overdue'] = 'Overdue'; $labels['overdue'] = 'Overdue';
$labels['today'] = 'Today'; $labels['today'] = 'Today';
$labels['tomorrow'] = 'Tomorrow'; $labels['tomorrow'] = 'Tomorrow';
@ -49,6 +52,7 @@ $labels['mytasks'] = 'My tasks';
$labels['mytaskstitle'] = 'Tasks assigned to you'; $labels['mytaskstitle'] = 'Tasks assigned to you';
$labels['nodate'] = 'no date'; $labels['nodate'] = 'no date';
$labels['removetag'] = 'Remove'; $labels['removetag'] = 'Remove';
$labels['auto'] = 'Auto';
$labels['taskdetails'] = 'Details'; $labels['taskdetails'] = 'Details';
$labels['newtask'] = 'New Task'; $labels['newtask'] = 'New Task';
@ -74,7 +78,7 @@ $labels['listactions'] = 'List options...';
$labels['listname'] = 'Name'; $labels['listname'] = 'Name';
$labels['showalarms'] = 'Show reminders'; $labels['showalarms'] = 'Show reminders';
$labels['import'] = 'Import'; $labels['import'] = 'Import';
$labels['viewoptions'] = 'View options'; $labels['viewactions'] = 'View actions';
$labels['focusview'] = 'View only this list'; $labels['focusview'] = 'View only this list';
// date words // date words
@ -172,3 +176,5 @@ $labels['itipresponseerror'] = 'Failed to send the response to this task assignm
$labels['itipinvalidrequest'] = 'This invitation is no longer valid'; $labels['itipinvalidrequest'] = 'This invitation is no longer valid';
$labels['sentresponseto'] = 'Successfully sent assignment response to $mailto'; $labels['sentresponseto'] = 'Successfully sent assignment response to $mailto';
$labels['successremoval'] = 'The task has been deleted successfully.'; $labels['successremoval'] = 'The task has been deleted successfully.';
$labels['arialabelsortmenu'] = 'Tasks sorting options';

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View file

@ -507,13 +507,14 @@ body.tasklist.attachmentwin #mainscreen {
cursor: pointer; cursor: pointer;
} }
.buttonbar-right .listmenu .inner { .buttonbar-right a.iconbutton {
display: inline-block;
height: 18px;
width: 20px;
padding: 0; padding: 0;
background: url(sprites.png) 0 -237px no-repeat; background-image: url(sprites.png);
text-indent: -5000px; background-position: 0 -238px;
}
.buttonbar-right a.iconbutton.sorting {
background-position: -18px -347px;
} }
#thelist { #thelist {
@ -728,7 +729,8 @@ body.tasklist.attachmentwin #mainscreen {
ul.toolbarmenu li span.add, ul.toolbarmenu li span.add,
ul.toolbarmenu li span.expand, ul.toolbarmenu li span.expand,
ul.toolbarmenu li span.collapse { ul.toolbarmenu li span.collapse,
ul.toolbarmenu.iconized .selected span.icon {
background-image: url(sprites.png); background-image: url(sprites.png);
} }
@ -748,6 +750,14 @@ ul.toolbarmenu li span.delete {
background-position: 0 -1508px; background-position: 0 -1508px;
} }
ul.toolbarmenu.iconized .selected span.icon {
background-position: 0 -324px;
}
ul.toolbarmenu .sortcol.by-auto a {
font-style: italic;
}
.taskitem-draghelper { .taskitem-draghelper {
/* /*
width: 32px; width: 32px;
@ -975,6 +985,10 @@ div.form-section {
margin-bottom: 0.3em; margin-bottom: 0.3em;
} }
.tasklistview div.form-section span.task-text + label {
margin-left: 2em;
}
label.block { label.block {
display: block; display: block;
margin-bottom: 0.3em; margin-bottom: 0.3em;

View file

@ -97,12 +97,31 @@
</ul> </ul>
<div class="buttonbar-right"> <div class="buttonbar-right">
<roundcube:button name="taskviewmenulink" id="taskviewmenulink" type="link" title="tasklist.viewoptions" class="listmenu viewoptions" onclick="return UI.toggle_popup('taskviewmenu',event)" innerClass="inner" label="tasklist.viewoptions" aria-haspopup="true" aria-expanded="false" aria-owns="taskviewmenu-menu" /> <roundcube:button name="taskviewactionslink" id="taskviewactionslink" type="link" title="tasklist.viewactions" class="iconbutton viewactions" onclick="return UI.toggle_popup('taskviewactions',event)" label="tasklist.viewactions" aria-haspopup="true" aria-expanded="false" aria-owns="taskviewactions-menu" />
<roundcube:button name="taskviewsortmenulink" id="taskviewsortmenulink" type="link" title="sortby" class="iconbutton sorting" onclick="return UI.toggle_popup('taskviewsortmenu',event)" label="sortby" aria-haspopup="true" aria-expanded="false" aria-owns="taskviewsortmenu-menu" />
</div> </div>
<div id="taskviewmenu" class="popupmenu" aria-hidden="true"> <div id="taskviewsortmenu" class="popupmenu" aria-hidden="true" data-align="right">
<h3 id="aria-label-taskviewmenu" class="voice"><roundcube:label name="tasklist.viewoptions" /></h3> <h3 id="aria-label-taskviewsortmenu" class="voice"><roundcube:label name="tasklist.arialabelsortmenu" /></h3>
<ul class="toolbarmenu" id="taskviewmenu-menu" role="menu" aria-labelledby="aria-label-taskviewmenu"> <ul class="toolbarmenu iconized" id="taskviewsortmenu-menu" role="menu" aria-labelledby="aria-label-taskviewsortmenu">
<ul role="radiogroup" aria-label="<roundcube:label name='sortby' />">
<li><roundcube:button command="list-sort" prop="auto" type="link" label="tasklist.auto" role="radio" aria-checked="false" class="sortcol by-auto icon active" innerclass="icon" /></li>
<li><roundcube:button command="list-sort" prop="datetime" type="link" label="tasklist.datetime" role="radio" aria-checked="false" class="sortcol by-datetime icon active" innerclass="icon" /></li>
<li><roundcube:button command="list-sort" prop="startdatetime" type="link" label="tasklist.start" role="radio" aria-checked="false" class="sortcol by-startdatetime icon active" innerclass="icon" /></li>
<li><roundcube:button command="list-sort" prop="flagged" type="link" label="tasklist.flagged" role="radio" aria-checked="false" class="sortcol by-flagged icon active" innerclass="icon" /></li>
<li><roundcube:button command="list-sort" prop="complete" type="link" label="tasklist.completeness" role="radio" aria-checked="false" class="sortcol by-complete icon active" innerclass="icon" /></li>
<li><roundcube:button command="list-sort" prop="changed" type="link" label="tasklist.changed" role="radio" aria-checked="false" class="sortcol by-changed icon active" innerclass="icon" /></li>
</ul>
<li role="separator" class="separator"><label id="aria-label-taskviewsortorder"><roundcube:label name="listorder" /></label></li>
<ul role="radiogroup" aria-labelledby="aria-label-taskviewsortorder">
<li><roundcube:button command="list-order" prop="asc" type="link" label="sortasc" role="radio" aria-checked="false" class="sortorder asc icon" classAct="icon sortorder asc active" innerclass="icon" /></li>
<li><roundcube:button command="list-order" prop="desc" type="link" label="sortdesc" role="radio" aria-checked="false" class="sortorder desc icon" classAct="icon sortorder desc active" innerclass="icon" /></li>
</ul>
</ul>
</div>
<div id="taskviewactions" class="popupmenu" aria-hidden="true" data-align="right">
<h3 id="aria-label-taskviewactions" class="voice"><roundcube:label name="tasklist.viewactions" /></h3>
<ul class="toolbarmenu" id="taskviewactions-menu" role="menu" aria-labelledby="aria-label-taskviewactions">
<li role="menuitem"><roundcube:button command="expand-all" label="expand-all" class="icon" classAct="icon active" innerclass="icon expand" /></li> <li role="menuitem"><roundcube:button command="expand-all" label="expand-all" class="icon" classAct="icon active" innerclass="icon expand" /></li>
<li role="menuitem"><roundcube:button command="collapse-all" label="collapse-all" class="icon" classAct="icon active" innerclass="icon collapse" /></li> <li role="menuitem"><roundcube:button command="collapse-all" label="collapse-all" class="icon" classAct="icon active" innerclass="icon collapse" /></li>
</ul> </ul>

View file

@ -256,6 +256,13 @@ function rcube_tasklist_ui(settings)
setTimeout(fetch_counts, 200); setTimeout(fetch_counts, 200);
}); });
rcmail.register_command('list-sort', list_set_sort, true);
rcmail.register_command('list-order', list_set_order, (settings.sort_col || 'auto') != 'auto');
$('#taskviewsortmenu .by-' + (settings.sort_col || 'auto')).attr('aria-checked', 'true').addClass('selected');
$('#taskviewsortmenu .sortorder.' + (settings.sort_order || 'asc')).attr('aria-checked', 'true').addClass('selected');
// start loading tasks // start loading tasks
fetch_counts(); fetch_counts();
list_tasks(); list_tasks();
@ -743,6 +750,9 @@ function rcube_tasklist_ui(settings)
listdata[listdata[id].parent_id].children.push(id); listdata[listdata[id].parent_id].children.push(id);
} }
// sort index before rendering
listindex.sort(function(a, b) { return task_cmp(listdata[a], listdata[b]); });
append_tags(response.tags || []); append_tags(response.tags || []);
render_tasklist(); render_tasklist();
@ -1016,6 +1026,14 @@ function rcube_tasklist_ui(settings)
} }
} }
// copy _depth property from old rec or derive from parent
if (rec.parent_id && listdata[rec.parent_id]) {
rec._depth = (listdata[rec.parent_id]._depth || 0) + 1;
}
else if (oldrec) {
rec._depth = oldrec._depth || 0;
}
if (list.active || rec.tempid) { if (list.active || rec.tempid) {
if (!filter || match_filter(rec, {})) if (!filter || match_filter(rec, {}))
render_task(rec, oldid); render_task(rec, oldid);
@ -1294,10 +1312,33 @@ function rcube_tasklist_ui(settings)
*/ */
function task_cmp(a, b) function task_cmp(a, b)
{ {
var d = is_complete(a) - is_complete(b); // sort by hierarchy level first
if ((a._depth || 0) != (b._depth || 0))
return a._depth - b._depth;
var p, alt, inv = 1, c = is_complete(a) - is_complete(b), d = c;
// completed tasks always move to the end
if (c != 0)
return c;
// custom sorting
if (settings.sort_col && settings.sort_col != 'auto') {
alt = settings.sort_col == 'datetime' || settings.sort_col == 'startdatetime' ? 99999999999 : 0
d = (a[settings.sort_col]||alt) - (b[settings.sort_col]||alt);
inv = settings.sort_order == 'desc' ? -1 : 1;
}
// default sorting (auto)
else {
if (!d) d = (b._hasdate-0) - (a._hasdate-0); if (!d) d = (b._hasdate-0) - (a._hasdate-0);
if (!d) d = (a.datetime||99999999999) - (b.datetime||99999999999); if (!d) d = (a.datetime||99999999999) - (b.datetime||99999999999);
return d; }
// fall-back to created/changed date
if (!d) d = (a.created||0) - (b.created||0);
if (!d) d = (a.changed||0) - (b.changed||0);
return d * inv;
} }
/** /**
@ -1688,6 +1729,12 @@ function rcube_tasklist_ui(settings)
$('#task-recurrence').hide(); $('#task-recurrence').hide();
} }
if (rec.created || rec.changed) {
$('#task-created-changed .task-created').html(Q(rec.created_ || rcmail.gettext('unknown','tasklist')))
$('#task-created-changed .task-changed').html(Q(rec.changed_ || rcmail.gettext('unknown','tasklist')))
$('#task-created-changed').show()
}
// build attachments list // build attachments list
$('#task-attachments').hide(); $('#task-attachments').hide();
if ($.isArray(rec.attachments)) { if ($.isArray(rec.attachments)) {
@ -2179,12 +2226,12 @@ function rcube_tasklist_ui(settings)
/** /**
* *
*/ */
var remove_attachment = function(elem, id) function remove_attachment(elem, id)
{ {
$(elem.parentNode).hide(); $(elem.parentNode).hide();
me.selected_task.deleted_attachments.push(id); me.selected_task.deleted_attachments.push(id);
delete rcmail.env.attachments[id]; delete rcmail.env.attachments[id];
}; }
/** /**
* *
@ -2363,6 +2410,45 @@ function rcube_tasklist_ui(settings)
return $.unqiqueStrings(itags); return $.unqiqueStrings(itags);
} }
/**
* Change tasks list sorting
*/
function list_set_sort(col)
{
if (settings.sort_col != col) {
settings.sort_col = col;
$('#taskviewsortmenu .sortcol').attr('aria-checked', 'false').removeClass('selected')
.filter('.by-' + col).attr('aria-checked', 'true').addClass('selected');
// re-sort list index and re-render list
listindex.sort(function(a, b) { return task_cmp(listdata[a], listdata[b]); });
render_tasklist();
rcmail.enable_command('list-order', settings.sort_col != 'auto');
$('#taskviewsortmenu .sortorder').removeClass('selected').filter('[aria-checked=true]').addClass('selected');
rcmail.save_pref({ name: 'tasklist_sort_col', value: (col == 'auto' ? '' : col) });
}
}
/**
* Change tasks list sort order
*/
function list_set_order(order)
{
if (settings.sort_order != order) {
settings.sort_order = order;
$('#taskviewsortmenu .sortorder').attr('aria-checked', 'false').removeClass('selected')
.filter('.' + order).attr('aria-checked', 'true').addClass('selected');
// re-sort list index and re-render list
listindex.sort(function(a, b) { return task_cmp(listdata[a], listdata[b]); });
render_tasklist();
rcmail.save_pref({ name: 'tasklist_sort_order', value: order });
}
}
/** /**
* *
*/ */

View file

@ -51,6 +51,8 @@ class tasklist extends rcube_plugin
); );
public $task = '?(?!login|logout).*'; public $task = '?(?!login|logout).*';
public $allowed_prefs = array('tasklist_sort_col','tasklist_sort_order');
public $rc; public $rc;
public $lib; public $lib;
public $driver; public $driver;
@ -959,9 +961,8 @@ class tasklist extends rcube_plugin
$data[] = $rec; $data[] = $rec;
} }
// sort tasks according to their hierarchy level and due date // assign hierarchy level indicators for later sorting
array_walk($data, array($this, 'task_walk_tree')); array_walk($data, array($this, 'task_walk_tree'));
usort($data, array($this, 'task_sort_cmp'));
return $data; return $data;
} }
@ -974,7 +975,18 @@ class tasklist extends rcube_plugin
$rec['mask'] = $this->filter_mask($rec); $rec['mask'] = $this->filter_mask($rec);
$rec['flagged'] = intval($rec['flagged']); $rec['flagged'] = intval($rec['flagged']);
$rec['complete'] = floatval($rec['complete']); $rec['complete'] = floatval($rec['complete']);
$rec['changed'] = is_object($rec['changed']) ? $rec['changed']->format('U') : null;
if (is_object($rec['created'])) {
$rec['created_'] = $this->rc->format_date($rec['created']);
$rec['created'] = $rec['created']->format('U');
}
if (is_object($rec['changed'])) {
$rec['changed_'] = $this->rc->format_date($rec['changed']);
$rec['changed'] = $rec['changed']->format('U');
}
else {
$rec['changed'] = null;
}
if ($rec['date']) { if ($rec['date']) {
try { try {
@ -1045,18 +1057,6 @@ class tasklist extends rcube_plugin
} }
} }
/**
* Compare function for task list sorting.
* Nested tasks need to be sorted to the end.
*/
private function task_sort_cmp($a, $b)
{
$d = $a['_depth'] - $b['_depth'];
if (!$d) $d = $b['_hasdate'] - $a['_hasdate'];
if (!$d) $d = $a['datetime'] - $b['datetime'];
return $d;
}
/** /**
* Compute the filter mask of the given task * Compute the filter mask of the given task
* *

View file

@ -72,6 +72,8 @@ class tasklist_ui
$settings = array(); $settings = array();
$settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', 0); $settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', 0);
$settings['sort_col'] = $this->rc->config->get('tasklist_sort_col', '');
$settings['sort_order'] = $this->rc->config->get('tasklist_sort_order', 'asc');
// get user identity to create default attendee // get user identity to create default attendee
foreach ($this->rc->user->list_identities() as $rec) { foreach ($this->rc->user->list_identities() as $rec) {