Branch 'roundcubemail-plugins-kolab-3.1' - 22 commits - plugins/calendar plugins/tasklist

Jeroen van Meeuwen vanmeeuwen at kolabsys.com
Thu Oct 31 13:20:39 CET 2013


 plugins/calendar/calendar.php                                  |   56 +
 plugins/calendar/calendar_base.js                              |   21 
 plugins/calendar/calendar_ui.js                                |   32 
 plugins/calendar/drivers/calendar_driver.php                   |    4 
 plugins/calendar/drivers/database/database_driver.php          |    8 
 plugins/calendar/drivers/kolab/kolab_driver.php                |   11 
 plugins/tasklist/drivers/database/tasklist_database_driver.php |    6 
 plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php       |    6 
 plugins/tasklist/localization/en_US.inc                        |    2 
 plugins/tasklist/skins/larry/sprites.png                       |binary
 plugins/tasklist/skins/larry/tasklist.css                      |  111 ++-
 plugins/tasklist/skins/larry/templates/mainview.html           |   13 
 plugins/tasklist/tasklist.js                                   |  358 ++++++++--
 plugins/tasklist/tasklist.php                                  |   66 +
 plugins/tasklist/tasklist_base.js                              |   18 
 plugins/tasklist/tasklist_ui.php                               |    2 
 16 files changed, 623 insertions(+), 91 deletions(-)

New commits:
commit d55977fee33b4e84b1af0991482ced917a61e927
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Oct 30 16:05:07 2013 +0100

    Add mail actions to contextmenu if loaded

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index ced9b20..685745d 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -163,6 +163,8 @@ class calendar extends rcube_plugin
             'innerclass' => 'icon calendar',
           ))),
           'messagemenu');
+
+        $this->api->output->add_label('calendar.createfrommail');
       }
     }
     
diff --git a/plugins/calendar/calendar_base.js b/plugins/calendar/calendar_base.js
index c60c89a..33fe9e4 100644
--- a/plugins/calendar/calendar_base.js
+++ b/plugins/calendar/calendar_base.js
@@ -6,7 +6,7 @@
  * @author Thomas Bruederli <bruederli at kolabsys.com>
  *
  * Copyright (C) 2010, Lazlo Westerhof <hello at lazlo.me>
- * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ * Copyright (C) 2013, Kolab Systems AG <contact at kolabsys.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -36,10 +36,9 @@ function rcube_calendar(settings)
     var me = this;
 
     // create new event from current mail message
-    this.create_from_mail = function()
+    this.create_from_mail = function(uid)
     {
-      var uid;
-      if ((uid = rcmail.get_single_uid())) {
+      if (uid || (uid = rcmail.get_single_uid())) {
         // load calendar UI (scripts and edit dialog template)
         if (!this.ui_loaded) {
           $.when(
@@ -53,7 +52,7 @@ function rcube_calendar(settings)
             
             me.ui_loaded = true;
             me.ui = new rcube_calendar_ui(me.settings);
-            me.create_from_mail();  // start over
+            me.create_from_mail(uid);  // start over
           });
           return;
         }
@@ -156,9 +155,6 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
     
     // register create-from-mail command to message_commands array
     if (rcmail.env.task == 'mail') {
-      // place link above 'view source'
-      $('#messagemenu a.calendarlink').parent().insertBefore($('#messagemenu a.sourcelink').parent());
-      
       rcmail.register_command('calendar-create-from-mail', function() { cal.create_from_mail() });
       rcmail.addEventListener('plugin.mail2event_dialog', function(p){ cal.mail2event_dialog(p) });
       rcmail.addEventListener('plugin.unlock_saving', function(p){ cal.ui && cal.ui.unlock_saving(); });
@@ -169,6 +165,15 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
       }
       else
         rcmail.enable_command('calendar-create-from-mail', true);
+
+      // add contextmenu item
+      if (window.rcm_contextmenu_register_command) {
+        rcm_contextmenu_register_command(
+          'calendar-create-from-mail',
+          function(cmd,el){ cal.create_from_mail() },
+          'calendar.createfrommail',
+          'moveto');
+      }
     }
   }
 
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 619a652..e77bccc 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -113,6 +113,8 @@ class tasklist extends rcube_plugin
                         'innerclass' => 'icon taskadd',
                     ))),
                 'messagemenu');
+
+                $this->api->output->add_label('tasklist.createfrommail');
             }
         }
 
diff --git a/plugins/tasklist/tasklist_base.js b/plugins/tasklist/tasklist_base.js
index e3a889c..f804c34 100644
--- a/plugins/tasklist/tasklist_base.js
+++ b/plugins/tasklist/tasklist_base.js
@@ -4,7 +4,7 @@
  * @version @package_version@
  * @author Thomas Bruederli <bruederli at kolabsys.com>
  *
- * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ * Copyright (C) 2013, Kolab Systems AG <contact at kolabsys.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -37,10 +37,9 @@ function rcube_tasklist(settings)
     /**
      * Open a new task dialog prefilled with contents from the currently selected mail message
      */
-    function create_from_mail()
+    function create_from_mail(uid)
     {
-        var uid;
-        if ((uid = rcmail.get_single_uid())) {
+        if (uid || (uid = rcmail.get_single_uid())) {
             // load calendar UI (scripts and edit dialog template)
             if (!ui_loaded) {
                 $.when(
@@ -53,7 +52,7 @@ function rcube_tasklist(settings)
 
                     ui_loaded = true;
                     me.ui = new rcube_tasklist_ui(settings);
-                    create_from_mail();  // start over
+                    create_from_mail(uid);  // start over
                 });
                 return;
             }
@@ -90,5 +89,14 @@ window.rcmail && rcmail.env.task == 'mail' && rcmail.addEventListener('init', fu
         rcmail.env.message_commands.push('tasklist-create-from-mail');
     else
         rcmail.enable_command('tasklist-create-from-mail', true);
+
+    // add contextmenu item
+    if (window.rcm_contextmenu_register_command) {
+        rcm_contextmenu_register_command(
+            'tasklist-create-from-mail',
+            function(cmd,el){ tasks.create_from_mail() },
+            'tasklist.createfrommail',
+            'moveto');
+        }
 });
 


commit fb7de4efda2324617a0784a1d68f36e430f56284
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Oct 30 15:05:24 2013 +0100

    Choose the currently selected list in new task dialog

diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index bc88a67..25ab68d 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -1188,7 +1188,7 @@ function rcube_tasklist_ui(settings)
         var recstarttime = $('#taskedit-starttime').val(rec.starttime || '');
         var complete = $('#taskedit-completeness').val((rec.complete || 0) * 100);
         completeness_slider.slider('value', complete.val());
-        var tasklist = $('#taskedit-tasklist').val(rec.list || 0).prop('disabled', rec.parent_id ? true : false);
+        var tasklist = $('#taskedit-tasklist').val(rec.list || me.selected_list).prop('disabled', rec.parent_id ? true : false);
 
         // tag-edit line
         var tagline = $(rcmail.gui_objects.edittagline).empty();


commit b705e6bc2f109fa6662c2bc6762059170e49e491
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Oct 30 15:04:23 2013 +0100

    Render newly created task, even if saved to an inactive list

diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 5f78884..bc88a67 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -753,7 +753,7 @@ function rcube_tasklist_ui(settings)
             }
         }
 
-        if (list.active) {
+        if (list.active || rec.tempid) {
             if (!filter || match_filter(rec, {}))
                 render_task(rec, oldid);
         }


commit c7474b112bb5438bcad242663ddb35233507eba0
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Oct 30 15:00:08 2013 +0100

    Select first active list on startup

diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 5035f77..5f78884 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -120,13 +120,16 @@ function rcube_tasklist_ui(settings)
                 init_tasklist_li(li, id);
             }
 
-            if (me.tasklists[id].editable && !me.selected_list) {
+            if (me.tasklists[id].editable && (!me.selected_list || (me.tasklists[id].active && !me.tasklists[me.selected_list].active))) {
                 me.selected_list = id;
-                rcmail.enable_command('addtask', true);
-                $(li).click();
             }
         }
 
+        if (me.selected_list) {
+            rcmail.enable_command('addtask', true);
+            $(rcmail.get_folder_li(me.selected_list, 'rcmlitasklist')).click();
+        }
+
         // register server callbacks
         rcmail.addEventListener('plugin.data_ready', data_ready);
         rcmail.addEventListener('plugin.update_task', update_taskitem);


commit 39a86a909b89f906dbfd2375a6d3a04b94048164
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Oct 29 17:11:45 2013 +0100

    Refresh the entire tasks list/calendar on every 10th request to sync deleted items (#2369)

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 24ffa38..ced9b20 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -925,19 +925,28 @@ class calendar extends rcube_plugin
    */
   public function refresh($attr)
   {
+     // refresh the entire calendar every 10th time to also sync deleted events
+    $refetch =  rand(0,10) == 10;
+
     foreach ($this->driver->list_calendars(true) as $cal) {
-      $events = $this->driver->load_events(
-        get_input_value('start', RCUBE_INPUT_GET),
-        get_input_value('end', RCUBE_INPUT_GET),
-        get_input_value('q', RCUBE_INPUT_GET),
-        $cal['id'],
-        1,
-        $attr['last']
-      );
+      if ($refetch) {
+        $this->rc->output->command('plugin.refresh_calendar',
+          array('source' => $cal['id'], 'refetch' => true));
+      }
+      else {
+        $events = $this->driver->load_events(
+          get_input_value('start', RCUBE_INPUT_GET),
+          get_input_value('end', RCUBE_INPUT_GET),
+          get_input_value('q', RCUBE_INPUT_GET),
+          $cal['id'],
+          1,
+          $attr['last']
+        );
 
-      foreach ($events as $event) {
-        $args = array('source' => $cal['id'], 'update' => $this->_client_event($event));
-        $this->rc->output->command('plugin.refresh_calendar', $args);
+        foreach ($events as $event) {
+          $this->rc->output->command('plugin.refresh_calendar',
+            array('source' => $cal['id'], 'update' => $this->_client_event($event)));
+        }
       }
     }
   }
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 8fd1889..5035f77 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -135,9 +135,12 @@ function rcube_tasklist_ui(settings)
         rcmail.addEventListener('plugin.insert_tasklist', insert_list);
         rcmail.addEventListener('plugin.update_tasklist', update_list);
         rcmail.addEventListener('plugin.destroy_tasklist', destroy_list);
-        rcmail.addEventListener('plugin.reload_data', function(){ list_tasks(null); });
         rcmail.addEventListener('plugin.unlock_saving', unlock_saving);
         rcmail.addEventListener('requestrefresh', before_refresh);
+        rcmail.addEventListener('plugin.reload_data', function(){
+            list_tasks(null, true);
+            setTimeout(fetch_counts, 200);
+        });
 
         // start loading tasks
         fetch_counts();
@@ -400,7 +403,7 @@ function rcube_tasklist_ui(settings)
     /**
      * List tasks matching the given selector
      */
-    function list_tasks(sel)
+    function list_tasks(sel, force)
     {
         if (rcmail.busy)
             return;
@@ -412,7 +415,7 @@ function rcube_tasklist_ui(settings)
 
         var active = active_lists(),
             basefilter = filtermask == FILTER_MASK_COMPLETE ? FILTER_MASK_COMPLETE : FILTER_MASK_ALL,
-            reload = active.join(',') != loadstate.lists || basefilter != loadstate.filter || loadstate.search != search_query;
+            reload = force || active.join(',') != loadstate.lists || basefilter != loadstate.filter || loadstate.search != search_query;
 
         if (active.length && reload) {
             ui_loading = rcmail.set_busy(true, 'loading');
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index aaeea95..619a652 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -742,6 +742,12 @@ class tasklist extends rcube_plugin
      */
     public function refresh($attr)
     {
+        // refresh the entire list every 10th time to also sync deleted items
+        if (rand(0,10) == 10) {
+            $this->rc->output->command('plugin.reload_data');
+            return;
+        }
+
         $filter = array(
             'since'  => $attr['last'],
             'search' => get_input_value('q', RCUBE_INPUT_GPC),


commit a2152768a1d61fc5469f9972a87cbd6bb591d4e9
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Oct 21 20:24:49 2013 +0200

    Periodically refresh event data from server

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 11821d7..24ffa38 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -126,6 +126,7 @@ class calendar extends rcube_plugin
       $this->register_action('mailtoevent', array($this, 'mail_message2event'));
       $this->register_action('inlineui', array($this, 'get_inline_ui'));
       $this->register_action('check-recent', array($this, 'check_recent'));
+      $this->add_hook('refresh', array($this, 'refresh'));
 
       // remove undo information...
       if ($undo = $_SESSION['calendar_event_undo']) {
@@ -919,6 +920,29 @@ class calendar extends rcube_plugin
   }
 
   /**
+   * Handler for keep-alive requests
+   * This will check for updated data in active calendars and sync them to the client
+   */
+  public function refresh($attr)
+  {
+    foreach ($this->driver->list_calendars(true) as $cal) {
+      $events = $this->driver->load_events(
+        get_input_value('start', RCUBE_INPUT_GET),
+        get_input_value('end', RCUBE_INPUT_GET),
+        get_input_value('q', RCUBE_INPUT_GET),
+        $cal['id'],
+        1,
+        $attr['last']
+      );
+
+      foreach ($events as $event) {
+        $args = array('source' => $cal['id'], 'update' => $this->_client_event($event));
+        $this->rc->output->command('plugin.refresh_calendar', $args);
+      }
+    }
+  }
+
+  /**
    * Handler for pending_alarms plugin hook triggered by the calendar module on keep-alive requests.
    * This will check for pending notifications and pass them to the client
    */
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 72243e0..d5a4308 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -2103,6 +2103,20 @@ function rcube_calendar_ui(settings)
       fc.fullCalendar('removeEvents', function(e){ return e.temp; });
     };
 
+    // modify query parameters for refresh requests
+    this.before_refresh = function(query)
+    {
+      var view = fc.fullCalendar('getView');
+
+      query.start = date2unixtime(view.visStart);
+      query.end = date2unixtime(view.visEnd);
+
+      if (this.search_query)
+        query.q = this.search_query;
+
+      return query;
+    };
+
 
     /***  event searching  ***/
 
@@ -2797,6 +2811,7 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
   rcmail.addEventListener('plugin.refresh_calendar', function(p){ cal.refresh(p); });
   rcmail.addEventListener('plugin.import_success', function(p){ cal.import_success(p); });
   rcmail.addEventListener('plugin.import_error', function(p){ cal.import_error(p); });
+  rcmail.addEventListener('requestrefresh', function(q){ return cal.before_refresh(q); });
 
   // let's go
   var cal = new rcube_calendar_ui($.extend(rcmail.env.calendar_settings, rcmail.env.libcal_settings));
diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php
index 52de901..c09d8b9 100644
--- a/plugins/calendar/drivers/calendar_driver.php
+++ b/plugins/calendar/drivers/calendar_driver.php
@@ -236,9 +236,11 @@ abstract class calendar_driver
    * @param  integer Event's new end (unix timestamp)
    * @param  string  Search query (optional)
    * @param  mixed   List of calendar IDs to load events from (either as array or comma-separated string)
+   * @param  boolean Include virtual/recurring events (optional)
+   * @param  integer Only list events modified since this time (unix timestamp)
    * @return array A list of event objects (see header of this file for struct of an event)
    */
-  abstract function load_events($start, $end, $query = null, $calendars = null);
+  abstract function load_events($start, $end, $query = null, $calendars = null, $virtual = 1, $modifiedsince = null);
 
   /**
    * Get a list of pending alarms to be displayed to the user
diff --git a/plugins/calendar/drivers/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php
index 8cd363c..a2cb903 100644
--- a/plugins/calendar/drivers/database/database_driver.php
+++ b/plugins/calendar/drivers/database/database_driver.php
@@ -724,7 +724,7 @@ class database_driver extends calendar_driver
    *
    * @see calendar_driver::load_events()
    */
-  public function load_events($start, $end, $query = null, $calendars = null)
+  public function load_events($start, $end, $query = null, $calendars = null, $virtual = 1, $modifiedsince = null)
   {
     if (empty($calendars))
       $calendars = array_keys($this->calendars);
@@ -742,6 +742,12 @@ class database_driver extends calendar_driver
       $sql_add = 'AND (' . join(' OR ', $sql_query) . ')';
     }
     
+    if (!$virtual)
+      $sql_arr .= ' AND e.recurrence_id = 0';
+    
+    if ($modifiedsince)
+      $sql_add .= ' AND e.changed >= ' . $this->rc->db->quote(date('Y-m-d H:i:s', $modifiedsince));
+    
     $events = array();
     if (!empty($calendar_ids)) {
       $result = $this->rc->db->query(sprintf(
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index b8171b7..0722d23 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -733,20 +733,25 @@ class kolab_driver extends calendar_driver
    * @param  integer Event's new end (unix timestamp)
    * @param  string  Search query (optional)
    * @param  mixed   List of calendar IDs to load events from (either as array or comma-separated string)
-   * @param  boolean Strip virtual events (optional)
+   * @param  boolean Include virtual events (optional)
+   * @param  integer Only list events modified since this time (unix timestamp)
    * @return array A list of event records
    */
-  public function load_events($start, $end, $search = null, $calendars = null, $virtual = 1)
+  public function load_events($start, $end, $search = null, $calendars = null, $virtual = 1, $modifiedsince = null)
   {
     if ($calendars && is_string($calendars))
       $calendars = explode(',', $calendars);
 
+    $query = array();
+    if ($modifiedsince)
+      $query[] = array('changed', '>=', $modifiedsince);
+
     $events = $categories = array();
     foreach (array_keys($this->calendars) as $cid) {
       if ($calendars && !in_array($cid, $calendars))
         continue;
 
-      $events = array_merge($events, $this->calendars[$cid]->list_events($start, $end, $search, $virtual));
+      $events = array_merge($events, $this->calendars[$cid]->list_events($start, $end, $search, $virtual, $query));
       $categories += $this->calendars[$cid]->categories;
     }
     


commit f35a6119c071d5c15c16cde1949979cb0f8f6fe2
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Oct 16 13:01:55 2013 +0200

    Catch errors on iCal import and provide appropriate feedback to the user (#2353)

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index f038caf..11821d7 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -975,10 +975,18 @@ class calendar extends rcube_plugin
 
     if (!$err && $_FILES['_data']['tmp_name']) {
       $calendar = get_input_value('calendar', RCUBE_INPUT_GPC);
-      $events = $this->get_ical()->import_from_file($_FILES['_data']['tmp_name']);
-
-      $count = $errors = 0;
       $rangestart = $_REQUEST['_range'] ? date_create("now -" . intval($_REQUEST['_range']) . " months") : 0;
+      $count = $errors = 0;
+
+      try {
+          $events = $this->get_ical()->import_from_file($_FILES['_data']['tmp_name'], 'UTF-8', true);
+      }
+      catch (Exception $e) {
+          $errors = 1;
+          $msg = $e->getMessage();
+          $events = array();
+      }
+
       foreach ($events as $event) {
         // TODO: correctly handle recurring events which start before $rangestart
         if ($event['end'] < $rangestart && (!$event['recurrence'] || ($event['recurrence']['until'] && $event['recurrence']['until'] < $rangestart)))
@@ -1000,8 +1008,9 @@ class calendar extends rcube_plugin
         $this->rc->output->command('display_message', $this->gettext('importnone'), 'notice');
         $this->rc->output->command('plugin.import_success', array('source' => $calendar));
       }
-      else
-        $this->rc->output->command('display_message', $this->gettext('importerror'), 'error');
+      else {
+        $this->rc->output->command('plugin.import_error', array('message' => $this->gettext('importerror') . ($msg ? ': ' . $msg : '')));
+      }
     }
     else {
       if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
@@ -1012,7 +1021,7 @@ class calendar extends rcube_plugin
         $msg = rcube_label('fileuploaderror');
       }
 
-      $this->rc->output->command('display_message', $msg, 'error');
+      $this->rc->output->command('plugin.import_error', array('message' => $msg));
       $this->rc->output->command('plugin.unlock_saving', false);
     }
 
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 986113f..72243e0 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -1981,10 +1981,17 @@ function rcube_calendar_ui(settings)
         if (form && form.elements._data.value) {
           rcmail.async_upload_form(form, 'import_events', function(e) {
             rcmail.set_busy(false, null, me.saving_lock);
+            $('.ui-dialog-buttonpane button', $dialog.parent()).button('enable');
+
+            // display error message if no sophisticated response from server arrived (e.g. iframe load error)
+            if (me.import_succeeded === null)
+              rcmail.display_message(rcmail.get_label('importerror', 'calendar'), 'error');
           });
 
           // display upload indicator
+          me.import_succeeded = null;
           me.saving_lock = rcmail.set_busy(true, 'uploading');
+          $('.ui-dialog-buttonpane button', $dialog.parent()).button('disable');
         }
       };
       
@@ -1999,6 +2006,7 @@ function rcube_calendar_ui(settings)
         closeOnEscape: false,
         title: rcmail.gettext('importevents', 'calendar'),
         close: function() {
+          $('.ui-dialog-buttonpane button', $dialog.parent()).button('enable');
           $dialog.dialog("destroy").hide();
         },
         buttons: buttons,
@@ -2010,6 +2018,7 @@ function rcube_calendar_ui(settings)
     // callback from server if import succeeded
     this.import_success = function(p)
     {
+      this.import_succeeded = true;
       $("#eventsimport:ui-dialog").dialog('close');
       rcmail.set_busy(false, null, me.saving_lock);
       rcmail.gui_objects.importform.reset();
@@ -2018,6 +2027,13 @@ function rcube_calendar_ui(settings)
         this.refresh(p);
     };
 
+    // callback from server to report errors on import
+    this.import_error = function(p)
+    {
+      this.import_succeeded = false;
+      rcmail.display_message(p.message || rcmail.get_label('importerror', 'calendar'), 'error');
+    }
+
     // show URL of the given calendar in a dialog box
     this.showurl = function(calendar)
     {
@@ -2780,6 +2796,7 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
   rcmail.addEventListener('plugin.unlock_saving', function(p){ cal.unlock_saving(); });
   rcmail.addEventListener('plugin.refresh_calendar', function(p){ cal.refresh(p); });
   rcmail.addEventListener('plugin.import_success', function(p){ cal.import_success(p); });
+  rcmail.addEventListener('plugin.import_error', function(p){ cal.import_error(p); });
 
   // let's go
   var cal = new rcube_calendar_ui($.extend(rcmail.env.calendar_settings, rcmail.env.libcal_settings));


commit fa9921726c07d273b567855351f7d70a6533c206
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Oct 28 09:33:09 2013 +0100

    Reduce font size of inactive tags (#2374)

diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index 1b51caf..7a1df99 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -190,6 +190,7 @@ body.attachmentwin #topnav .topright {
 #tagslist li.inactive {
 	color: #89b3be;
 	padding-right: 0.6em;
+	font-size: 80%;
 /*	display: none; */
 }
 


commit dd26bb533301f99d327b561179a6a64853b7e644
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Oct 24 19:26:22 2013 +0200

    Re-calculate tag counts after updating a task item

diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index c9146f2..8fd1889 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -601,6 +601,21 @@ function rcube_tasklist_ui(settings)
      */
     function update_tagcloud(counts)
     {
+        // compute counts first by iterating over all visible task items
+        if (typeof counts == 'undefined') {
+            counts = {};
+            $('li.taskitem', rcmail.gui_objects.resultlist).each(function(i,li){
+                var t, id = $(li).attr('rel'),
+                    rec = listdata[id];
+                for (var j=0; rec && rec.tags && j < rec.tags.length; j++) {
+                    t = rec.tags[j];
+                    if (typeof counts[t] == 'undefined')
+                        counts[t] = 0;
+                    counts[t]++;
+                }
+            });
+        }
+
         $(rcmail.gui_objects.tagslist).children('li').each(function(i,li){
             var elem = $(li), tag = elem.attr('rel'),
                 count = counts[tag] || 0;
@@ -741,6 +756,7 @@ function rcube_tasklist_ui(settings)
         }
 
         append_tags(rec.tags || []);
+        update_tagcloud();
         fix_tree_toggles();
     }
 


commit fb0e0417f142847b5c376969d6a807cb3e429c8c
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Oct 24 19:17:48 2013 +0200

    Small fixes for IE browsers

diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 0293d8f..c9146f2 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -585,7 +585,7 @@ function rcube_tasklist_ui(settings)
                     helper: tag_draggable_helper,
                     start: tag_draggable_start,
                     appendTo: 'body',
-                    cursor: 'pointer',
+                    cursor: 'pointer'
                 });
             });
 
@@ -1073,7 +1073,7 @@ function rcube_tasklist_ui(settings)
         // append inherited tags
         if (itags.length) {
             $.each(itags, function(i,val){
-                if (!rec.tags || rec.tags.indexOf(val) < 0)
+                if (!rec.tags || $.inArray(val, rec.tags) < 0)
                     $('<span>').addClass('tag-element inherit').html(Q(val)).appendTo(taglist);
             });
             // re-sort tags list


commit 31abc5923978ec29bf04bcef973512568ff46b8e
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Oct 24 18:52:59 2013 +0200

    Minor CSS improvements + add missing text label

diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
index a1d381b..ba331be 100644
--- a/plugins/tasklist/localization/en_US.inc
+++ b/plugins/tasklist/localization/en_US.inc
@@ -51,6 +51,7 @@ $labels['listactions'] = 'List options...';
 $labels['listname'] = 'Name';
 $labels['showalarms'] = 'Show alarms';
 $labels['import'] = 'Import';
+$labels['viewoptions'] = 'View options';
 $labels['focusview'] = 'View only this list';
 
 // date words
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index ab74a1f..1b51caf 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -258,12 +258,11 @@ body.attachmentwin #topnav .topright {
 	cursor: pointer;
 }
 
-#tasklists li span.handle:hover {
+#tasklists li:hover span.handle {
 	background-position: -20px -101px;
 }
 
-#tasklists li.focusview span.handle,
-#tasklists li.focusview span.handle:hover {
+#tasklists li.focusview span.handle {
 	background-position: -2px -101px;
 }
 
@@ -539,8 +538,8 @@ body.attachmentwin #topnav .topright {
 }
 
 .tag-draghelper li.tag {
-    list-style: none;
-    font-size: 100%;
+	list-style: none;
+	font-size: 100%;
 }
 
 .taskhead .date {


commit 05ce4c8e848be03c60434c7454a8da4ba45f2616
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Oct 24 16:29:33 2013 +0200

    Add 'focusview' mode to quickly reduce the view to tasks from the selected list only (#2380)

diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
index 57d6c3c..a1d381b 100644
--- a/plugins/tasklist/localization/en_US.inc
+++ b/plugins/tasklist/localization/en_US.inc
@@ -51,6 +51,7 @@ $labels['listactions'] = 'List options...';
 $labels['listname'] = 'Name';
 $labels['showalarms'] = 'Show alarms';
 $labels['import'] = 'Import';
+$labels['focusview'] = 'View only this list';
 
 // date words
 $labels['on'] = 'on';
diff --git a/plugins/tasklist/skins/larry/sprites.png b/plugins/tasklist/skins/larry/sprites.png
index b20b2db..5c6b9fd 100644
Binary files a/plugins/tasklist/skins/larry/sprites.png and b/plugins/tasklist/skins/larry/sprites.png differ
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index 9efc18b..ab74a1f 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -218,7 +218,7 @@ body.attachmentwin #topnav .topright {
 #tasklists li {
 	margin: 0;
 	height: 20px;
-	padding: 6px 8px 2px;
+	padding: 6px 8px 2px 6px;
 	display: block;
 	position: relative;
 	white-space: nowrap;
@@ -235,10 +235,13 @@ body.attachmentwin #topnav .topright {
 
 #tasklists li span.listname {
 	display: block;
+	position: absolute;
+	top: 7px;
+	left: 26px;
+	right: 26px;
 	cursor: default;
 	padding-bottom: 2px;
 	padding-right: 30px;
-	margin-right: 20px;
 	color: #004458;
 	overflow: hidden;
 	text-overflow: ellipsis;
@@ -247,7 +250,21 @@ body.attachmentwin #topnav .topright {
 }
 
 #tasklists li span.handle {
-	display: none;
+	display: inline-block;
+	width: 16px;
+	height: 16px;
+	margin-right: 4px;
+	background: url(sprites.png) -200px 0 no-repeat;
+	cursor: pointer;
+}
+
+#tasklists li span.handle:hover {
+	background-position: -20px -101px;
+}
+
+#tasklists li.focusview span.handle,
+#tasklists li.focusview span.handle:hover {
+	background-position: -2px -101px;
 }
 
 #tasklists li.selected span.listname {
@@ -278,6 +295,11 @@ body.attachmentwin #topnav .topright {
 	color: #aaa;
 }
 
+#tasklists li.virtual span.handle {
+	background: none;
+	cursor: default;
+}
+
 #tasklists li input {
 	position: absolute;
 	top: 5px;
@@ -610,7 +632,7 @@ ul.toolbarmenu li span.collapse {
 }
 
 ul.toolbarmenu li span.add {
-	background-position: 0 -100px;
+	background-position: 0 -302px;
 }
 
 ul.toolbarmenu li span.expand {
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 68ea117..0293d8f 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -54,6 +54,7 @@ function rcube_tasklist_ui(settings)
     var filtermask = FILTER_MASK_ALL;
     var loadstate = { filter:-1, lists:'', search:null };
     var idcount = 0;
+    var focusview;
     var saving_lock;
     var ui_loading;
     var taskcounts = {};
@@ -128,7 +129,8 @@ function rcube_tasklist_ui(settings)
 
         // register server callbacks
         rcmail.addEventListener('plugin.data_ready', data_ready);
-        rcmail.addEventListener('plugin.refresh_task', update_taskitem);
+        rcmail.addEventListener('plugin.update_task', update_taskitem);
+        rcmail.addEventListener('plugin.refresh_tasks', function(p) { update_taskitem(p, true); });
         rcmail.addEventListener('plugin.update_counts', update_counts);
         rcmail.addEventListener('plugin.insert_tasklist', insert_list);
         rcmail.addEventListener('plugin.update_tasklist', update_list);
@@ -686,11 +688,11 @@ function rcube_tasklist_ui(settings)
     /**
      * Callback from server to update a single task item
      */
-    function update_taskitem(rec)
+    function update_taskitem(rec, filter)
     {
         // handle a list of task records
         if ($.isArray(rec)) {
-            $.each(rec, function(i,r){ update_taskitem(r); });
+            $.each(rec, function(i,r){ update_taskitem(r, filter); });
             return;
         }
 
@@ -730,10 +732,13 @@ function rcube_tasklist_ui(settings)
             }
         }
 
-        if (list.active)
-            render_task(rec, oldid);
-        else
+        if (list.active) {
+            if (!filter || match_filter(rec, {}))
+                render_task(rec, oldid);
+        }
+        else {
             $('li[rel="'+id+'"]', rcmail.gui_objects.resultlist).remove();
+        }
 
         append_tags(rec.tags || []);
         fix_tree_toggles();
@@ -1525,7 +1530,11 @@ function rcube_tasklist_ui(settings)
             return cache[rec.id];
         }
 
-        var match = !filtermask || (filtermask & rec.mask) > 0
+        var match = !filtermask || (filtermask & rec.mask) > 0;
+
+        // in focusview mode, only tasks from the selected list are allowed
+        if (focusview && rec.list != focusview)
+            match = false;
 
         if (match && tagsfilter.length) {
             match = rec.tags && rec.tags.length;
@@ -1847,6 +1856,11 @@ function rcube_tasklist_ui(settings)
                 if (!this.checked) remove_tasks(id);
                 else               list_tasks(null);
                 rcmail.http_post('tasklist', { action:'subscribe', l:{ id:id, active:me.tasklists[id].active?1:0 } });
+
+                // disable focusview
+                if (!this.checked && focusview == id) {
+                    set_focusview(null);
+                }
             }
         }).data('id', id).get(0).checked = me.tasklists[id].active || false;
 
@@ -1855,6 +1869,15 @@ function rcube_tasklist_ui(settings)
             rcmail.select_folder(id, 'rcmlitasklist');
             rcmail.enable_command('list-edit', 'list-remove', 'list-import', me.tasklists[id].editable);
             me.selected_list = id;
+
+            // click on handle icon toggles focusview
+            if (e.target.className == 'handle') {
+                set_focusview(focusview == id ? null : id)
+            }
+            // disable focusview when selecting another list
+            else if (focusview && id != focusview) {
+                set_focusview(null);
+            }
         })
         .dblclick(function(e){
             list_edit_dialog($(this).data('id'));
@@ -1864,6 +1887,31 @@ function rcube_tasklist_ui(settings)
         .addClass(me.tasklists[id].editable ? null : 'readonly');
     }
 
+    /**
+     * Enable/disable focusview mode for the given list
+     */
+    function set_focusview(id)
+    {
+        if (focusview && focusview != id)
+            $(rcmail.get_folder_li(focusview, 'rcmlitasklist')).removeClass('focusview');
+
+        focusview = id;
+
+        // activate list if necessary
+        if (focusview && !me.tasklists[id].active) {
+            $('input', rcmail.get_folder_li(id, 'rcmlitasklist')).get(0).checked = true;
+            me.tasklists[id].active = true;
+            fetch_counts();
+        }
+
+        // update list
+        list_tasks(null);
+
+        if (focusview) {
+            $(rcmail.get_folder_li(focusview, 'rcmlitasklist')).addClass('focusview');
+        }
+    }
+
 
     // init dialog by default
     init_taskedit();
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 68d82c4..aaeea95 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -281,7 +281,7 @@ class tasklist extends rcube_plugin
                 foreach ($refresh as $i => $r)
                     $this->encode_task($refresh[$i]);
             }
-            $this->rc->output->command('plugin.refresh_task', $refresh);
+            $this->rc->output->command('plugin.update_task', $refresh);
         }
     }
 
@@ -751,7 +751,7 @@ class tasklist extends rcube_plugin
 
         $updates = $this->driver->list_tasks($filter, $lists);
         if (!empty($updates)) {
-            $this->rc->output->command('plugin.refresh_task', $this->tasks_data($updates, 255, $tags));
+            $this->rc->output->command('plugin.refresh_tasks', $this->tasks_data($updates, 255, $tags), true);
 
             // update counts
             $counts = $this->driver->count_tasks($lists);
diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php
index dab9b12..21faba3 100644
--- a/plugins/tasklist/tasklist_ui.php
+++ b/plugins/tasklist/tasklist_ui.php
@@ -117,7 +117,7 @@ class tasklist_ui
 
             $li .= html::tag('li', array('id' => 'rcmlitasklist' . $html_id, 'class' => $class),
                 ($prop['virtual'] ? '' : html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active']))) .
-                html::span('handle', ' ') .
+                html::span(array('class' => 'handle', 'title' => $this->plugin->gettext('focusview')), ' ') .
                 html::span(array('class' => 'listname', 'title' => $title), $prop['listname']));
         }
 


commit bc5b971642399312d3a0012945a3b62d2a236b7d
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Oct 24 14:00:36 2013 +0200

    Assign tags by drag & dropping them onto tasks (#2389); some fixes concerning saving and resorting tasks

diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index fadb62b..9efc18b 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -189,13 +189,10 @@ body.attachmentwin #topnav .topright {
 
 #tagslist li.inactive {
 	color: #89b3be;
+	padding-right: 0.6em;
 /*	display: none; */
 }
 
-#tagslist li.inactive .count {
-	display: none;
-}
-
 #tagslist li .count {
 	position: relative;
 	top: -1px;
@@ -213,6 +210,11 @@ body.attachmentwin #topnav .topright {
 	border-radius: 8px;
 }
 
+.tag-draghelper .tag .count,
+#tagslist li.inactive .count {
+	display: none;
+}
+
 #tasklists li {
 	margin: 0;
 	height: 20px;
@@ -504,6 +506,7 @@ body.attachmentwin #topnav .topright {
 	text-align: right;
 }
 
+.tag-draghelper .tag,
 .taskhead .tags .tag {
 	font-size: 85%;
 	background: #d9ecf4;
@@ -513,6 +516,11 @@ body.attachmentwin #topnav .topright {
 	margin-right: 3px;
 }
 
+.tag-draghelper li.tag {
+    list-style: none;
+    font-size: 100%;
+}
+
 .taskhead .date {
 	position: absolute;
 	top: 4px;
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index d318011..68ea117 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -64,6 +64,8 @@ function rcube_tasklist_ui(settings)
     var search_request;
     var search_query;
     var completeness_slider;
+    var task_draghelper;
+    var tag_draghelper;
     var me = this;
 
     // general datepicker settings
@@ -337,7 +339,8 @@ function rcube_tasklist_ui(settings)
                     $(input).datepicker('widget').find('button.ui-datepicker-close')
                         .html(rcmail.gettext('nodate','tasklist'))
                         .attr('onclick', '')
-                        .click(function(e){
+                        .unbind('click')
+                        .bind('click', function(e){
                             $(input).datepicker('setDate', null).datepicker('hide');
                         });
                 }, 1);
@@ -570,8 +573,19 @@ function rcube_tasklist_ui(settings)
 
         // append new tags to tag cloud
         $.each(newtags, function(i, tag){
-            $('<li>').attr('rel', tag).data('value', tag).html(Q(tag) + '<span class="count"></span>').appendTo(rcmail.gui_objects.tagslist);
-        });
+            $('<li>').attr('rel', tag).data('value', tag)
+                .html(Q(tag) + '<span class="count"></span>')
+                .appendTo(rcmail.gui_objects.tagslist)
+                .draggable({
+                    addClasses: false,
+                    revert: 'invalid',
+                    revertDuration: 300,
+                    helper: tag_draggable_helper,
+                    start: tag_draggable_start,
+                    appendTo: 'body',
+                    cursor: 'pointer',
+                });
+            });
 
         // re-sort tags list
         $(rcmail.gui_objects.tagslist).children('li').sortElements(function(a,b){
@@ -595,6 +609,59 @@ function rcube_tasklist_ui(settings)
         });
     }
 
+    /*  Helper functions for drag & drop functionality of tags  */
+    
+    function tag_draggable_helper()
+    {
+        if (!tag_draghelper)
+            tag_draghelper = $('<div class="tag-draghelper"></div>');
+        else
+            tag_draghelper.html('');
+
+        $(this).clone().addClass('tag').appendTo(tag_draghelper);
+        return tag_draghelper;
+    }
+
+    function tag_draggable_start(event, ui)
+    {
+        $('.taskhead').droppable({
+            hoverClass: 'droptarget',
+            accept: tag_droppable_accept,
+            drop: tag_draggable_dropped,
+            addClasses: false
+        });
+    }
+
+    function tag_droppable_accept(draggable)
+    {
+        if (rcmail.busy)
+            return false;
+
+        var tag = draggable.data('value'),
+            drop_id = $(this).data('id'),
+            drop_rec = listdata[drop_id];
+
+        // target already has this tag assigned
+        if (!drop_rec || (drop_rec.tags && $.inArray(tag, drop_rec.tags) >= 0)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    function tag_draggable_dropped(event, ui)
+    {
+        var drop_id = $(this).data('id'),
+            tag = ui.draggable.data('value'),
+            rec = listdata[drop_id];
+
+        if (rec && rec.id) {
+            if (!rec.tags) rec.tags = [];
+            rec.tags.push(tag);
+            save_task(rec, 'edit');
+        }
+    }
+
     /**
      *
      */
@@ -721,10 +788,10 @@ function rcube_tasklist_ui(settings)
                 revert: 'invalid',
                 addClasses: false,
                 cursorAt: { left:-10, top:12 },
-                helper: draggable_helper,
+                helper: task_draggable_helper,
                 appendTo: 'body',
-                start: draggable_start,
-                stop: draggable_stop,
+                start: task_draggable_start,
+                stop: task_draggable_stop,
                 revertDuration: 300
             });
 
@@ -769,7 +836,7 @@ function rcube_tasklist_ui(settings)
      */
     function resort_task(rec, li, animated)
     {
-        var dir = 0, index, slice, next_li, next_id, next_rec;
+        var dir = 0, index, slice, cmp, next_li, next_id, next_rec, insert_after, past_myself;
 
         // animated moving
         var insert_animated = function(li, before, after) {
@@ -795,33 +862,36 @@ function rcube_tasklist_ui(settings)
         }
 
         // find the right place to insert the task item
-        li.siblings().each(function(i, elem){
+        li.parent().children('.taskitem').each(function(i, elem){
             next_li = $(elem);
             next_id = next_li.attr('rel');
             next_rec = listdata[next_id];
 
             if (next_id == rec.id) {
-                next_li = null;
+                past_myself = true;
                 return 1; // continue
             }
 
-            if (next_rec && task_cmp(rec, next_rec) > 0) {
+            cmp = next_rec ? task_cmp(rec, next_rec) : 0;
+
+            if (cmp > 0 || (cmp == 0 && !past_myself)) {
+                insert_after = next_li;
                 return 1; // continue;
             }
-            else if (next_rec && next_li && task_cmp(rec, next_rec) < 0) {
+            else if (next_li && cmp < 0) {
                 if (animated) insert_animated(li, next_li);
                 else          li.insertBefore(next_li);
-                next_li = null;
-                return false;
+                index = $.inArray(next_id, listindex);
+                return false; // break
             }
         });
 
-        index = $.inArray(next_id, listindex);
+        if (insert_after) {
+            if (animated) insert_animated(li, null, insert_after);
+            else          li.insertAfter(insert_after);
 
-        if (next_li) {
-            if (animated) insert_animated(li, null, next_li);
-            else          li.insertAfter(next_li);
-            index++;
+            next_id = insert_after.attr('rel');
+            index = $.inArray(next_id, listindex);
         }
 
         // insert into list index
@@ -865,20 +935,20 @@ function rcube_tasklist_ui(settings)
 
     /*  Helper functions for drag & drop functionality  */
     
-    function draggable_helper()
+    function task_draggable_helper()
     {
-        if (!draghelper)
-            draghelper = $('<div class="taskitem-draghelper">&#x2714;</div>');
+        if (!task_draghelper)
+            task_draghelper = $('<div class="taskitem-draghelper">&#x2714;</div>');
 
-        return draghelper;
+        return task_draghelper;
     }
 
-    function draggable_start(event, ui)
+    function task_draggable_start(event, ui)
     {
         $('.taskhead, #rootdroppable, #'+rcmail.gui_objects.folderlist.id+' li').droppable({
             hoverClass: 'droptarget',
-            accept: droppable_accept,
-            drop: draggable_dropped,
+            accept: task_droppable_accept,
+            drop: task_draggable_dropped,
             addClasses: false
         });
 
@@ -886,13 +956,13 @@ function rcube_tasklist_ui(settings)
         $('#rootdroppable').show();
     }
 
-    function draggable_stop(event, ui)
+    function task_draggable_stop(event, ui)
     {
         $(this).parent().removeClass('dragging');
         $('#rootdroppable').hide();
     }
 
-    function droppable_accept(draggable)
+    function task_droppable_accept(draggable)
     {
         if (rcmail.busy)
             return false;
@@ -924,7 +994,7 @@ function rcube_tasklist_ui(settings)
         return true;
     }
 
-    function draggable_dropped(event, ui)
+    function task_draggable_dropped(event, ui)
     {
         var drop_id = $(this).data('id'),
             task_id = ui.draggable.data('id'),
@@ -1220,6 +1290,9 @@ function rcube_tasklist_ui(settings)
             if (!me.selected_task.list && list.id)
                 me.selected_task.list = list.id;
 
+            if (!me.selected_task.tags.length)
+                me.selected_task.tags = '';
+
             if (save_task(me.selected_task, action))
                 $dialog.dialog('close');
         };


commit b52df5e51e8159cc80a2a6d11deceacf27471da2
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Oct 24 11:24:26 2013 +0200

    List (inherited) tags only once

diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 198ba82..d318011 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -998,7 +998,8 @@ function rcube_tasklist_ui(settings)
         // append inherited tags
         if (itags.length) {
             $.each(itags, function(i,val){
-                $('<span>').addClass('tag-element inherit').html(Q(val)).appendTo(taglist);
+                if (!rec.tags || rec.tags.indexOf(val) < 0)
+                    $('<span>').addClass('tag-element inherit').html(Q(val)).appendTo(taglist);
             });
             // re-sort tags list
             $(taglist).children().sortElements(function(a,b){
@@ -1498,7 +1499,7 @@ function rcube_tasklist_ui(settings)
             }
         }
 
-        return itags;
+        return $.unqiqueStrings(itags);
     }
 
     /**
@@ -1816,6 +1817,22 @@ jQuery.fn.sortElements = (function(){
     };
 })();
 
+// equivalent to $.unique() but working on arrays of strings
+jQuery.unqiqueStrings = (function() {
+    return function(arr) {
+        var hash = {}, out = [];
+
+        for (var i = 0; i < arr.length; i++) {
+            hash[arr[i]] = 0;
+        }
+        for (var val in hash) {
+            out.push(val);
+        }
+
+        return out;
+    };
+})();
+
 
 /* tasklist plugin UI initialization */
 var rctasks;


commit 66371500664f1d4ddf4743249b676d0d41b09526
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Oct 24 10:59:01 2013 +0200

    Fix assignment to a new list in edit dialog

diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 2c1a921..198ba82 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -1208,7 +1208,7 @@ function rcube_tasklist_ui(settings)
             }
 
             // task assigned to a new list
-            if (me.selected_task.list && me.selected_task.list != rec.list) {
+            if (me.selected_task.list && listdata[id] && me.selected_task.list != listdata[id].list) {
                 me.selected_task._fromlist = rec.list;
             }
 


commit f8220576d8e0cbcc6f22b9b7dbfe08eb070f604b
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Oct 23 18:10:18 2013 +0200

    Show the number of occurences for each tag (#2365) and reduce visibility of those tags not matching any tasks (#2374) in the current selection

diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index 70ab8da..fadb62b 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -180,12 +180,39 @@ body.attachmentwin #topnav .topright {
 #tagslist li {
 	display: inline-block;
 	color: #004458;
-	margin-right: 0.5em;
+	padding-right: 0.2em;
+	margin-right: 0.3em;
 	margin-bottom: 0.4em;
 	min-width: 1.2em;
 	cursor: pointer;
 }
 
+#tagslist li.inactive {
+	color: #89b3be;
+/*	display: none; */
+}
+
+#tagslist li.inactive .count {
+	display: none;
+}
+
+#tagslist li .count {
+	position: relative;
+	top: -1px;
+	margin-left: 5px;
+	padding: 0.15em 0.5em;
+	font-size: 80%;
+	font-weight: bold;
+	color: #59838e;
+	background: #c7e3ef;
+	box-shadow: inset 0 1px 1px 0 #b0ccd7;
+	-o-box-shadow: inset 0 1px 1px 0 #b0ccd7;
+	-webkit-box-shadow: inset 0 1px 1px 0 #b0ccd7;
+	-moz-box-shadow: inset 0 1px 1px 0 #b0ccd7;
+	border-color: #b0ccd7;
+	border-radius: 8px;
+}
+
 #tasklists li {
 	margin: 0;
 	height: 20px;
diff --git a/plugins/tasklist/skins/larry/templates/mainview.html b/plugins/tasklist/skins/larry/templates/mainview.html
index b85f7fb..52463e0 100644
--- a/plugins/tasklist/skins/larry/templates/mainview.html
+++ b/plugins/tasklist/skins/larry/templates/mainview.html
@@ -157,6 +157,8 @@ $(document).ready(function(e){
 	UI.init();
 	new rcube_splitter({ id:'taskviewsplitter', p1:'#sidebar', p2:'#mainview-right',
 		orientation:'v', relative:true, start:240, min:180, size:16, offset:2 }).init();
+	new rcube_splitter({ id:'taskviewsplitterv', p1:'#tagsbox', p2:'#tasklistsbox',
+		orientation:'h', relative:true, start:242, min:120, size:16, offset:6 }).init();
 });
 
 </script>
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 742f23a..2c1a921 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -474,8 +474,9 @@ function rcube_tasklist_ui(settings)
                 listdata[listdata[id].parent_id].children.push(id);
         }
 
-        render_tasklist();
         append_tags(response.tags || []);
+        render_tasklist();
+
         rcmail.set_busy(false, 'loading', ui_loading);
     }
 
@@ -488,6 +489,7 @@ function rcube_tasklist_ui(settings)
         var id, rec,
             count = 0,
             cache = {},
+            activetags = {},
             msgbox = $('#listmessagebox').hide(),
             list = $(rcmail.gui_objects.resultlist).html('');
 
@@ -497,10 +499,19 @@ function rcube_tasklist_ui(settings)
             if (match_filter(rec, cache)) {
                 render_task(rec);
                 count++;
+
+                // keep a list of tags from all visible tasks
+                for (var t, j=0; rec.tags && j < rec.tags.length; j++) {
+                    t = rec.tags[j];
+                    if (typeof activetags[t] == 'undefined')
+                        activetags[t] = 0;
+                    activetags[t]++;
+                }
             }
         }
 
         fix_tree_toggles();
+        update_tagcloud(activetags);
 
         if (!count)
             msgbox.html(rcmail.gettext('notasksfound','tasklist')).show();
@@ -559,7 +570,7 @@ function rcube_tasklist_ui(settings)
 
         // append new tags to tag cloud
         $.each(newtags, function(i, tag){
-            $('<li>').attr('rel', tag).data('value', tag).html(Q(tag)).appendTo(rcmail.gui_objects.tagslist);
+            $('<li>').attr('rel', tag).data('value', tag).html(Q(tag) + '<span class="count"></span>').appendTo(rcmail.gui_objects.tagslist);
         });
 
         // re-sort tags list
@@ -569,6 +580,22 @@ function rcube_tasklist_ui(settings)
     }
 
     /**
+     * Display the given counts to each tag and set those inactive which don't
+     * have any matching tasks in the current view.
+     */
+    function update_tagcloud(counts)
+    {
+        $(rcmail.gui_objects.tagslist).children('li').each(function(i,li){
+            var elem = $(li), tag = elem.attr('rel'),
+                count = counts[tag] || 0;
+
+            elem.children('.count').html(count+'');
+            if (count == 0) elem.addClass('inactive');
+            else            elem.removeClass('inactive');
+        });
+    }
+
+    /**
      *
      */
     function update_counts(counts)


commit d0315100ea8bc7e42be9e62957d3b93289311098
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Oct 23 17:12:42 2013 +0200

    Add UI elements to expand/collapse all tasks (#2291)

diff --git a/plugins/tasklist/skins/larry/sprites.png b/plugins/tasklist/skins/larry/sprites.png
index 5224f6f..b20b2db 100644
Binary files a/plugins/tasklist/skins/larry/sprites.png and b/plugins/tasklist/skins/larry/sprites.png differ
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index caa5067..70ab8da 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -66,7 +66,7 @@ body.attachmentwin #topnav .topright {
 }
 
 #taskselector {
-	margin: -4px 0 0;
+	margin: -4px 40px 0 0;
 	padding: 0;
 }
 
@@ -336,6 +336,27 @@ body.attachmentwin #topnav .topright {
 	background: -ms-linear-gradient(top, #eee 0%, #dfdfdf 100%);
 	background: linear-gradient(top, #eee 0%, #dfdfdf 100%);
 	border-bottom: 1px solid #ccc;
+	position: relative;
+}
+
+#tasksview .buttonbar .buttonbar-right {
+	position: absolute;
+	top: 6px;
+	right: 8px;
+}
+
+.buttonbar-right .listmenu {
+	display: inline-block;
+	cursor: pointer;
+}
+
+.buttonbar-right .listmenu .inner {
+	display: inline-block;
+	height: 18px;
+	width: 20px;
+	padding: 0;
+	background: url(sprites.png) 0 -237px no-repeat;
+	text-indent: -5000px;
 }
 
 #thelist {
@@ -547,11 +568,24 @@ body.attachmentwin #topnav .topright {
 	border-top: 1px solid #219de6;
 }
 
-ul.toolbarmenu li span.add {
+ul.toolbarmenu li span.add,
+ul.toolbarmenu li span.expand,
+ul.toolbarmenu li span.collapse {
 	background-image: url(sprites.png);
+}
+
+ul.toolbarmenu li span.add {
 	background-position: 0 -100px;
 }
 
+ul.toolbarmenu li span.expand {
+	background-position: 0 -258px;
+}
+
+ul.toolbarmenu li span.collapse {
+	background-position: 0 -280px;
+}
+
 ul.toolbarmenu li span.delete {
 	background-position: 0 -1508px;
 }
diff --git a/plugins/tasklist/skins/larry/templates/mainview.html b/plugins/tasklist/skins/larry/templates/mainview.html
index 9196d02..b85f7fb 100644
--- a/plugins/tasklist/skins/larry/templates/mainview.html
+++ b/plugins/tasklist/skins/larry/templates/mainview.html
@@ -59,6 +59,10 @@
 				<li class="nodate"><a href="#nodate"><roundcube:label name="tasklist.nodate" ucfirst="true" /></a></li>
 				<li class="complete"><a href="#complete"><roundcube:label name="tasklist.complete" /><span class="count"></span></a></li>
 			</ul>
+
+			<div class="buttonbar-right">
+				<roundcube:button name="taskviewmenulink" id="taskviewmenulink" type="link" title="tasklist.viewoptions" class="listmenu viewoptions" onclick="UI.show_popup('taskviewmenu');return false" innerClass="inner" content="⚙" />
+			</div>
 		</div>
 		
 		<div class="scroller">
@@ -92,6 +96,13 @@
 	</ul>
 </div>
 
+<div id="taskviewmenu" class="popupmenu">
+	<ul class="toolbarmenu">
+		<li><roundcube:button command="expand-all" label="expand-all" class="icon" classAct="icon active" innerclass="icon expand" /></li>
+		<li><roundcube:button command="collapse-all" label="collapse-all" class="icon" classAct="icon active" innerclass="icon collapse" /></li>
+	</ul>
+</div>
+
 <div id="taskshow">
 	<div class="form-section" id="task-parent-title"></div>
 	<div class="form-section">
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 631494c..742f23a 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -92,6 +92,7 @@ function rcube_tasklist_ui(settings)
     this.add_childtask = add_childtask;
     this.quicksearch = quicksearch;
     this.reset_search = reset_search;
+    this.expand_collapse = expand_collapse;
     this.list_remove = list_remove;
     this.list_edit_dialog = list_edit_dialog;
     this.unlock_saving = unlock_saving;
@@ -520,6 +521,30 @@ function rcube_tasklist_ui(settings)
     }
 
     /**
+     * Expand/collapse all task items with childs
+     */
+    function expand_collapse(expand)
+    {
+        var collapsed = !expand;
+
+        $('.taskitem .childtasks')[(collapsed ? 'hide' : 'show')]();
+        $('.taskitem .childtoggle')
+            .removeClass(collapsed ? 'expanded' : 'collapsed')
+            .addClass(collapsed ? 'collapsed' : 'expanded')
+            .html(collapsed ? '▶' : '▼');
+
+        // store new toggle collapse states
+        var ids = [];
+        for (var id in listdata) {
+            if (listdata[id].children && listdata[id].children.length)
+                ids.push(id);
+        }
+        if (ids.length) {
+            rcmail.http_post('tasks/task', { action:'collapse', t:{ id:ids.join(',') }, collapsed:collapsed?1:0 });
+        }
+    }
+
+    /**
      *
      */
     function append_tags(taglist)
@@ -1781,6 +1806,8 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
 
   rcmail.register_command('search', function(){ rctasks.quicksearch(); }, true);
   rcmail.register_command('reset-search', function(){ rctasks.reset_search(); }, true);
+  rcmail.register_command('expand-all', function(){ rctasks.expand_collapse(true); }, true);
+  rcmail.register_command('collapse-all', function(){ rctasks.expand_collapse(false); }, true);
 
   rctasks.init();
 });
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index e6b5d1c..68d82c4 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -248,13 +248,15 @@ class tasklist extends rcube_plugin
             break;
 
         case 'collapse':
-            if (intval(get_input_value('collapsed', RCUBE_INPUT_GPC))) {
-                $this->collapsed_tasks[] = $rec['id'];
-            }
-            else {
-                $i = array_search($rec['id'], $this->collapsed_tasks);
-                if ($i !== false)
-                    unset($this->collapsed_tasks[$i]);
+            foreach (explode(',', $rec['id']) as $rec_id) {
+                if (intval(get_input_value('collapsed', RCUBE_INPUT_GPC))) {
+                    $this->collapsed_tasks[] = $rec_id;
+                }
+                else {
+                    $i = array_search($rec_id, $this->collapsed_tasks);
+                    if ($i !== false)
+                        unset($this->collapsed_tasks[$i]);
+                }
             }
 
             $this->rc->user->save_prefs(array('tasklist_collapsed_tasks' => join(',', array_unique($this->collapsed_tasks))));


commit 5c4530245713dab4d7ed38f273dacc31f4699365
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Oct 21 16:38:22 2013 +0200

    Refresh tasks list periodically with changes from the server (#2369)

diff --git a/plugins/tasklist/drivers/database/tasklist_database_driver.php b/plugins/tasklist/drivers/database/tasklist_database_driver.php
index 8ad776a..3c5ad38 100644
--- a/plugins/tasklist/drivers/database/tasklist_database_driver.php
+++ b/plugins/tasklist/drivers/database/tasklist_database_driver.php
@@ -286,7 +286,7 @@ class tasklist_database_driver extends tasklist_driver
 
         if ($filter['mask'] & tasklist::FILTER_MASK_COMPLETE)
             $sql_add .= ' AND complete=1';
-        else  // don't show complete tasks by default
+        else if (empty($filter['since']))  // don't show complete tasks by default
             $sql_add .= ' AND complete<1';
 
         if ($filter['mask'] & tasklist::FILTER_MASK_FLAGGED)
@@ -301,6 +301,10 @@ class tasklist_database_driver extends tasklist_driver
             $sql_add = 'AND (' . join(' OR ', $sql_query) . ')';
         }
 
+        if ($filter['since'] && is_numeric($filter['since'])) {
+            $sql_add .= ' AND changed >= ' . $this->rc->db->quote(date('Y-m-d H:i:s', $filter['since']));
+        }
+
         $tasks = array();
         if (!empty($list_ids)) {
             $result = $this->rc->db->query(sprintf(
diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index c630a41..fd3e5c3 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -371,7 +371,7 @@ class tasklist_kolab_driver extends tasklist_driver
         $query = array();
         if ($filter['mask'] & tasklist::FILTER_MASK_COMPLETE)
             $query[] = array('tags','~','x-complete');
-        else
+        else if (empty($filter['since']))
             $query[] = array('tags','!~','x-complete');
 
         // full text search (only works with cache enabled)
@@ -382,6 +382,10 @@ class tasklist_kolab_driver extends tasklist_driver
             }
         }
 
+        if ($filter['since']) {
+            $query[] = array('changed', '>=', $filter['since']);
+        }
+
         foreach ($lists as $list_id) {
             $folder = $this->folders[$list_id];
             foreach ((array)$folder->select($query) as $record) {
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 19db0fe..631494c 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -132,6 +132,7 @@ function rcube_tasklist_ui(settings)
         rcmail.addEventListener('plugin.destroy_tasklist', destroy_list);
         rcmail.addEventListener('plugin.reload_data', function(){ list_tasks(null); });
         rcmail.addEventListener('plugin.unlock_saving', unlock_saving);
+        rcmail.addEventListener('requestrefresh', before_refresh);
 
         // start loading tasks
         fetch_counts();
@@ -439,6 +440,19 @@ function rcube_tasklist_ui(settings)
     }
 
     /**
+     * Modify query parameters for refresh requests
+     */
+    function before_refresh(query)
+    {
+        query.filter = filtermask == FILTER_MASK_COMPLETE ? FILTER_MASK_COMPLETE : FILTER_MASK_ALL;
+        query.lists = active_lists().join(',');
+        if (search_query)
+            query.q = search_query;
+
+        return query;
+    }
+
+    /**
      * Callback if task data from server is ready
      */
     function data_ready(response)
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 9d5544c..e6b5d1c 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -90,6 +90,7 @@ 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->add_hook('refresh', array($this, 'refresh'));
 
             $this->collapsed_tasks = array_filter(explode(',', $this->rc->config->get('tasklist_collapsed_tasks', '')));
         }
@@ -540,8 +541,17 @@ class tasklist extends rcube_plugin
 
         }
 */
+        $data = $this->tasks_data($this->driver->list_tasks($filter, $lists), $f, $tags);
+        $this->rc->output->command('plugin.data_ready', array('filter' => $f, 'lists' => $lists, 'search' => $search, 'data' => $data, 'tags' => array_values(array_unique($tags))));
+    }
+
+    /**
+     * Prepare and sort the given task records to be sent to the client
+     */
+    private function tasks_data($records, $f, &$tags)
+    {
         $data = $tags = $this->task_tree = $this->task_titles = array();
-        foreach ($this->driver->list_tasks($filter, $lists) as $rec) {
+        foreach ($records as $rec) {
             if ($rec['parent_id']) {
                 $this->task_tree[$rec['id']] = $rec['parent_id'];
             }
@@ -558,7 +568,7 @@ class tasklist extends rcube_plugin
         array_walk($data, array($this, 'task_walk_tree'));
         usort($data, array($this, 'task_sort_cmp'));
 
-        $this->rc->output->command('plugin.data_ready', array('filter' => $f, 'lists' => $lists, 'search' => $search, 'data' => $data, 'tags' => array_values(array_unique($tags))));
+        return $data;
     }
 
     /**
@@ -724,6 +734,28 @@ class tasklist extends rcube_plugin
         exit;
     }
 
+    /**
+     * Handler for keep-alive requests
+     * This will check for updated data in active lists and sync them to the client
+     */
+    public function refresh($attr)
+    {
+        $filter = array(
+            'since'  => $attr['last'],
+            'search' => get_input_value('q', RCUBE_INPUT_GPC),
+            'mask'   => intval(get_input_value('filter', RCUBE_INPUT_GPC)) & self::FILTER_MASK_COMPLETE,
+        );
+        $lists = get_input_value('lists', RCUBE_INPUT_GPC);;
+
+        $updates = $this->driver->list_tasks($filter, $lists);
+        if (!empty($updates)) {
+            $this->rc->output->command('plugin.refresh_task', $this->tasks_data($updates, 255, $tags));
+
+            // update counts
+            $counts = $this->driver->count_tasks($lists);
+            $this->rc->output->command('plugin.update_counts', $counts);
+        }
+    }
 
     /**
      * Handler for pending_alarms plugin hook triggered by the calendar module on keep-alive requests.


commit ee52d6e52f1c85c35f85faebb73b05b0c8210741
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Oct 21 14:51:29 2013 +0200

    Show inherited tags in task show dialog (#2368)

diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index 173704d..caa5067 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -806,6 +806,12 @@ label.block {
 /*	cursor: pointer; */
 }
 
+.form-section span.tag-element.inherit {
+	color: #666;
+	background: #f2f2f2;
+	border-color: #ddd;
+}
+
 .tagedit-list li.tagedit-listelement-old a.tagedit-close,
 .tagedit-list li.tagedit-listelement-old a.tagedit-break,
 .tagedit-list li.tagedit-listelement-old a.tagedit-delete,
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index e52174b..19db0fe 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -921,10 +921,22 @@ function rcube_tasklist_ui(settings)
         $('#task-completeness .task-text').html(((rec.complete || 0) * 100) + '%');
         $('#task-list .task-text').html(Q(me.tasklists[rec.list] ? me.tasklists[rec.list].name : ''));
 
-        var taglist = $('#task-tags')[(rec.tags && rec.tags.length ? 'show' : 'hide')]().children('.task-text').empty();
+        var itags = get_inherited_tags(rec);
+        var taglist = $('#task-tags')[(rec.tags && rec.tags.length || itags.length ? 'show' : 'hide')]().children('.task-text').empty();
         if (rec.tags && rec.tags.length) {
             $.each(rec.tags, function(i,val){
-                $('<span>').addClass('tag-element').html(Q(val)).data('value', val).appendTo(taglist);
+                $('<span>').addClass('tag-element').html(Q(val)).appendTo(taglist);
+            });
+        }
+
+        // append inherited tags
+        if (itags.length) {
+            $.each(itags, function(i,val){
+                $('<span>').addClass('tag-element inherit').html(Q(val)).appendTo(taglist);
+            });
+            // re-sort tags list
+            $(taglist).children().sortElements(function(a,b){
+                return $.text([a]).toLowerCase() > $.text([b]).toLowerCase() ? 1 : -1;
             });
         }
 


commit 9fc827c2c3ca7322ee4f8d72dce6f85db4a81b2c
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Oct 21 14:26:30 2013 +0200

    Inherit tags from parent tasks for filter matching (#2373)

diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 2089c70..e52174b 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -1377,8 +1377,9 @@ function rcube_tasklist_ui(settings)
 
         if (match && tagsfilter.length) {
             match = rec.tags && rec.tags.length;
+            var alltags = get_inherited_tags(rec).concat(rec.tags || []);
             for (var i=0; match && i < tagsfilter.length; i++) {
-                if ($.inArray(tagsfilter[i], rec.tags) < 0)
+                if ($.inArray(tagsfilter[i], alltags) < 0)
                     match = false;
             }
         }
@@ -1408,6 +1409,23 @@ function rcube_tasklist_ui(settings)
     /**
      *
      */
+    function get_inherited_tags(rec)
+    {
+        var parent_id, itags = [];
+
+        if ((parent_id = rec.parent_id)) {
+            while (parent_id && listdata[parent_id]) {
+                itags = itags.concat(listdata[parent_id].tags || []);
+                parent_id = listdata[parent_id].parent_id;
+            }
+        }
+
+        return itags;
+    }
+
+    /**
+     *
+     */
     function list_edit_dialog(id)
     {
         var list = me.tasklists[id],


commit af0f782563464c80c960952879cd02b340bb4df0
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Oct 21 14:06:05 2013 +0200

    Sort tags alphabetically (#2367)

diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 6ebddc4..9d5544c 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -605,6 +605,10 @@ class tasklist extends rcube_plugin
             $rec['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);
         }
 
+        if (!is_array($rec['tags']))
+            $rec['tags'] = (array)$rec['tags'];
+        sort($rec['tags'], SORT_LOCALE_STRING);
+
         if (in_array($rec['id'], $this->collapsed_tasks))
           $rec['collapsed'] = true;
 


commit e7372b3b82894d9b3cc4ac0a9883a5ded0ea3004
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Oct 17 18:28:56 2013 +0200

    Also save tag from input box which hasn't been added to the list with <Enter>

diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index d76c9d1..2089c70 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -1102,10 +1102,16 @@ function rcube_tasklist_ui(settings)
                 }
             }
 
+            // collect tags
             $('input[type="hidden"]', rcmail.gui_objects.edittagline).each(function(i,elem){
                 if (elem.value)
                     me.selected_task.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);
+            }
 
             // serialize alarm settings
             var alarm = $('#taskedit select.edit-alarm-type').val();




More information about the commits mailing list