Branch 'dev/new-foldernav' - 5 commits - plugins/calendar plugins/libkolab plugins/tasklist

Thomas Brüderli bruederli at kolabsys.com
Wed May 21 13:57:16 CEST 2014


 plugins/calendar/drivers/kolab/kolab_calendar.php              |    2 
 plugins/calendar/drivers/kolab/kolab_driver.php                |   18 
 plugins/calendar/lib/calendar_ui.php                           |   18 
 plugins/calendar/lib/js/folderlist.js                          |    1 
 plugins/calendar/skins/larry/images/calendars.png              |binary
 plugins/calendar/skins/larry/templates/calendar.html           |    3 
 plugins/libkolab/lib/kolab_storage_folder_api.php              |   18 
 plugins/tasklist/drivers/database/tasklist_database_driver.php |   12 
 plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php       |  307 +++++++---
 plugins/tasklist/drivers/tasklist_driver.php                   |    9 
 plugins/tasklist/localization/en_US.inc                        |    3 
 plugins/tasklist/skins/larry/sprites.png                       |binary
 plugins/tasklist/skins/larry/tasklist.css                      |  137 +++-
 plugins/tasklist/skins/larry/templates/mainview.html           |   37 +
 plugins/tasklist/tasklist.js                                   |  157 +++--
 plugins/tasklist/tasklist.php                                  |   50 +
 plugins/tasklist/tasklist_ui.php                               |  127 +++-
 17 files changed, 697 insertions(+), 202 deletions(-)

New commits:
commit 8c507885c52c7b2fb8212f337960cbd31fbb1c84
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 21 13:56:44 2014 +0200

    Fix list creation/update/deletion (#3047)

diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index 52d2337..50fa371 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -139,7 +139,7 @@ class tasklist_kolab_driver extends tasklist_driver
             'virtual' => $folder->virtual,
             'children' => true,  // TODO: determine if that folder indeed has child folders
             'subscribed' => (bool)$folder->is_subscribed(),
-            'group'    => $folder->get_namespace(),
+            'group'    => $folder->default ? 'default' : $folder->get_namespace(),
             'class' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')),
         );
     }
@@ -284,8 +284,13 @@ class tasklist_kolab_driver extends tasklist_driver
             $this->rc->user->save_prefs($prefs);
 
         // force page reload to properly render folder hierarchy
-        if (!empty($prop['parent']))
+        if (!empty($prop['parent'])) {
             $prop['_reload'] = true;
+        }
+        else {
+            $folder = kolab_storage::get_folder($folder);
+            $prop += $this->folder_props($folder, $this->rc->get_storage()->get_hierarchy_delimiter(), array());
+        }
 
         return $id;
     }
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 774e9b4..75ba386 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -164,7 +164,7 @@ function rcube_tasklist_ui(settings)
                 var prop = { id:p.id, active:list.active?1:0 };
                 if (list.subscribed) prop.permanent = 1;
                 rcmail.http_post('tasklist', { action:'subscribe', l:prop });
-                list_tasks();
+                setTimeout(function(){ list_tasks(); }, 500);
             }
         });
 
@@ -737,10 +737,11 @@ function rcube_tasklist_ui(settings)
 
         var tag = draggable.data('value'),
             drop_id = $(this).data('id'),
-            drop_rec = listdata[drop_id];
+            drop_rec = listdata[drop_id],
+            list = drop_rec && me.tasklists[drop_rec.list] ? me.tasklists[drop_rec.list] : { editable:true };
 
-        // target already has this tag assigned
-        if (!drop_rec || (drop_rec.tags && $.inArray(tag, drop_rec.tags) >= 0)) {
+        // target is not writeable or already has this tag assigned
+        if (!drop_rec || drop_rec.readonly || !list.editable || (drop_rec.tags && $.inArray(tag, drop_rec.tags) >= 0)) {
             return false;
         }
 
@@ -1048,7 +1049,7 @@ function rcube_tasklist_ui(settings)
 
     function task_draggable_start(event, ui)
     {
-        $('.taskhead, #rootdroppable, #'+rcmail.gui_objects.folderlist.id+' li').droppable({
+        $('.taskhead, #rootdroppable, #'+rcmail.gui_objects.tasklistslist.id+' li').droppable({
             hoverClass: 'droptarget',
             accept: task_droppable_accept,
             drop: task_draggable_dropped,
@@ -1187,7 +1188,7 @@ function rcube_tasklist_ui(settings)
      */
     function task_show_dialog(id)
     {
-        var $dialog = $('#taskshow'), rec;
+        var $dialog = $('#taskshow'), rec, list;
 
         if ($dialog.is(':ui-dialog'))
           $dialog.dialog('close');
@@ -1196,6 +1197,7 @@ function rcube_tasklist_ui(settings)
             return;
 
         me.selected_task = rec;
+        list = rec.list && me.tasklists[rec.list] ? me.tasklists[rec.list] : {};
 
         // fill dialog data
         $('#task-parent-title').html(Q(rec.parent_title || '')+' »').css('display', rec.parent_title ? 'block' : 'none');
@@ -1247,22 +1249,24 @@ function rcube_tasklist_ui(settings)
 
         // define dialog buttons
         var buttons = [];
-        buttons.push({
-            text: rcmail.gettext('edit','tasklist'),
-            click: function() {
-                task_edit_dialog(me.selected_task.id, 'edit');
-            },
-            disabled: rcmail.busy
-        });
+        if (list.editable && !rec.readonly) {
+              buttons.push({
+                text: rcmail.gettext('edit','tasklist'),
+                click: function() {
+                    task_edit_dialog(me.selected_task.id, 'edit');
+                },
+                disabled: rcmail.busy
+            });
 
-        buttons.push({
-            text: rcmail.gettext('delete','tasklist'),
-            click: function() {
-                if (delete_task(me.selected_task.id))
-                    $dialog.dialog('close');
-            },
-            disabled: rcmail.busy
-        });
+            buttons.push({
+                text: rcmail.gettext('delete','tasklist'),
+                click: function() {
+                    if (delete_task(me.selected_task.id))
+                        $dialog.dialog('close');
+                },
+                disabled: rcmail.busy
+            });
+        }
 
         // open jquery UI dialog
         $dialog.dialog({
@@ -1828,15 +1832,12 @@ function rcube_tasklist_ui(settings)
             delete_ids.push(prop.id);
         }
 
-        // delete all calendars in the list
+        // delete all subfolders in the list
         for (var i=0; i < delete_ids.length; i++) {
             id = delete_ids[i];
             list = me.tasklists[id];
-            li = rcmail.get_folder_li(id, 'rcmlitasklist');
+            tasklists_widget.remove(id);
 
-            if (li) {
-                $(li).remove();
-            }
             if (list) {
                 list.active = false;
                 // delete me.tasklists[prop.id];
@@ -1857,13 +1858,15 @@ function rcube_tasklist_ui(settings)
             return;
         }
 
-        var li = $('<li>').attr('id', 'rcmlitasklist'+prop.id)
-            .append('<input type="checkbox" name="_list[]" value="'+prop.id+'" checked="checked" />')
-            .append('<span class="handle"> </span>')
-            .append('<span class="listname">'+Q(prop.name)+'</span>');
-        $(rcmail.gui_objects.folderlist).append(li);
+        tasklists_widget.insert({
+            id: prop.id,
+            classes: [ prop.group || '' ],
+            virtual: prop.virtual,
+            html: prop.html
+        }, prop.parent || null, prop.group);
+
+        delete prop.html;
         me.tasklists[prop.id] = prop;
-        init_tasklist_li(li.get(0), prop.id);
 
         // append to list selector in task edit dialog, too (#2985)
         $('<option>').attr('value', prop.id).html(Q(prop.name)).appendTo('#taskedit-tasklist');
@@ -1875,7 +1878,7 @@ function rcube_tasklist_ui(settings)
     function update_list(prop)
     {
         var id = prop.oldid || prop.id,
-            li = rcmail.get_folder_li(id, 'rcmlitasklist');
+            li = tasklists_widget.get_item(id);
 
         if (prop._reload) {
             rcmail.redirect(rcmail.url(''));
@@ -1885,10 +1888,9 @@ function rcube_tasklist_ui(settings)
         if (me.tasklists[id] && li) {
             delete me.tasklists[id];
             me.tasklists[prop.id] = prop;
-            $(li).data('id', prop.id)
-                .attr('id', 'rcmlitasklist'+prop.id)
-                .find('input').data('id', prop.id);
-            $('.listname', li).html(Q(prop.name));
+            $(li).find('input').first().val(prop.id);
+            $(li).find('.listname').first().html(Q(prop.name));
+            tasklists_widget.update(id, { id:prop.id, html:li.children().first() });
         }
     }
 
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 1a70699..3004435 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -607,6 +607,11 @@ class tasklist extends rcube_plugin
             $list += array('showalarms' => true, 'active' => true, 'editable' => true);
             if ($insert_id = $this->driver->create_list($list)) {
                 $list['id'] = $insert_id;
+                if (!$list['_reload']) {
+                    $this->load_ui();
+                    $list['html'] = $this->ui->tasklist_list_item($insert_id, $list, $jsenv);
+                    $list += (array)$jsenv[$insert_id];
+                }
                 $this->rc->output->command('plugin.insert_tasklist', $list);
                 $success = true;
             }
diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php
index 6f92d26..05acafd 100644
--- a/plugins/tasklist/tasklist_ui.php
+++ b/plugins/tasklist/tasklist_ui.php
@@ -188,7 +188,7 @@ class tasklist_ui
 
         if (!$activeonly || $prop['active']) {
             return html::div(join(' ', $classes),
-                html::span(array('class' => 'listname', 'title' => $title), $prop['listname']) .
+                html::span(array('class' => 'listname', 'title' => $title), $prop['listname'] ?: $prop['name']) .
                   ($prop['virtual'] ? '' :
                     html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active'])) .
                     html::span(array('class' => 'quickview', 'title' => $this->plugin->gettext('focusview')), ' ') .


commit f4f5a30e0ae0d6e0c4e4b6d04c8d64174d0bf53d
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 21 13:04:18 2014 +0200

    Add new folder navigation to tasks module (#3047)

diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 70e0c37..edcca42 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -119,8 +119,8 @@ class calendar_ui
     $this->rc->output->include_script('treelist.js');
 
     // include kolab folderlist widget if available
-    if (is_readable($this->cal->home . '/lib/js/folderlist.js')) {
-      $this->cal->include_script('lib/js/folderlist.js');
+    if (is_readable($this->cal->api->dir . 'libkolab/js/folderlist.js')) {
+        $this->cal->api->include_script('libkolab/js/folderlist.js');
     }
 
     jqueryui::miniColors();
diff --git a/plugins/calendar/lib/js/folderlist.js b/plugins/calendar/lib/js/folderlist.js
deleted file mode 120000
index c49706b..0000000
--- a/plugins/calendar/lib/js/folderlist.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../libkolab/js/folderlist.js
\ No newline at end of file
diff --git a/plugins/libkolab/lib/kolab_storage_folder_api.php b/plugins/libkolab/lib/kolab_storage_folder_api.php
index 5af8c34..4a90467 100644
--- a/plugins/libkolab/lib/kolab_storage_folder_api.php
+++ b/plugins/libkolab/lib/kolab_storage_folder_api.php
@@ -149,6 +149,24 @@ abstract class kolab_storage_folder_api
         return rcube_charset::convert(end($parts), 'UTF7-IMAP');
     }
 
+    /**
+     * Getter for parent folder path
+     *
+     * @return string Full path to parent folder
+     */
+    public function get_parent()
+    {
+        $path = explode('/', $this->name);
+        array_pop($path);
+
+        // don't list top-level namespace folder
+        if (count($path) == 1 && in_array($this->get_namespace(), array('other', 'shared'))) {
+            $path = array();
+        }
+
+        return join('/', $path);
+    }
+
 
     /**
      * Get the color value stored in metadata
diff --git a/plugins/tasklist/drivers/database/tasklist_database_driver.php b/plugins/tasklist/drivers/database/tasklist_database_driver.php
index d9bf414..cab4fa7 100644
--- a/plugins/tasklist/drivers/database/tasklist_database_driver.php
+++ b/plugins/tasklist/drivers/database/tasklist_database_driver.php
@@ -200,6 +200,18 @@ class tasklist_database_driver extends tasklist_driver
     }
 
     /**
+     * Search for shared or otherwise not listed tasklists the user has access
+     *
+     * @param string Search string
+     * @param string Section/source to search
+     * @return array List of tasklists
+     */
+    public function search_lists($query, $source)
+    {
+        return array();
+    }
+
+    /**
      * Get number of tasks matching the given filter
      *
      * @param array List of lists to count tasks of
diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index ad36777..52d2337 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -45,12 +45,15 @@ class tasklist_kolab_driver extends tasklist_driver
         $this->rc = $plugin->rc;
         $this->plugin = $plugin;
 
-        $this->_read_lists();
-
         if (kolab_storage::$version == '2.0') {
             $this->alarm_absolute = false;
         }
 
+        // tasklist use fully encoded identifiers
+        kolab_storage::$encode_ids = true;
+
+        $this->_read_lists();
+
         $this->plugin->register_action('folder-acl', array($this, 'folder_acl'));
     }
 
@@ -83,87 +86,171 @@ class tasklist_kolab_driver extends tasklist_driver
 
         $delim = $this->rc->get_storage()->get_hierarchy_delimiter();
         $prefs = $this->rc->config->get('kolab_tasklists', array());
-        $listnames = array();
+
+        foreach ($folders as $folder) {
+            $tasklist = $this->folder_props($folder, $delim, $prefs);
+
+            $this->lists[$tasklist['id']] = $tasklist;
+            $this->folders[$tasklist['id']] = $folder;
+            $this->folders[$folder->name] = $folder;
+        }
+    }
+
+    /**
+     * Derive list properties from the given kolab_storage_folder object
+     */
+    protected function folder_props($folder, $delim, $prefs)
+    {
+        if ($folder->get_namespace() == 'personal') {
+            $norename = false;
+            $readonly = false;
+            $alarms = true;
+        }
+        else {
+            $alarms = false;
+            $readonly = true;
+            if (($rights = $folder->get_myrights()) && !PEAR::isError($rights)) {
+                if (strpos($rights, 'i') !== false)
+                  $readonly = false;
+            }
+            $info = $folder->get_folder_info();
+            $norename = $readonly || $info['norename'] || $info['protected'];
+        }
+
+        $list_id = $folder->id; #kolab_storage::folder_id($folder->name);
+        $old_id = kolab_storage::folder_id($folder->name, false);
+
+        if (!isset($prefs[$list_id]['showalarms']) && isset($prefs[$old_id]['showalarms'])) {
+            $prefs[$list_id]['showalarms'] = $prefs[$old_id]['showalarms'];
+        }
+
+        return array(
+            'id' => $list_id,
+            'name' => $folder->get_name(),
+            'listname' => $folder->get_foldername(),
+            'editname' => $folder->get_foldername(),
+            'color' => $folder->get_color('0000CC'),
+            'showalarms' => isset($prefs[$list_id]['showalarms']) ? $prefs[$list_id]['showalarms'] : $alarms,
+            'editable' => !$readonly,
+            'norename' => $norename,
+            'active' => $folder->is_active(),
+            'parentfolder' => $folder->get_parent(),
+            'default' => $folder->default,
+            'virtual' => $folder->virtual,
+            'children' => true,  // TODO: determine if that folder indeed has child folders
+            'subscribed' => (bool)$folder->is_subscribed(),
+            'group'    => $folder->get_namespace(),
+            'class' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')),
+        );
+    }
+
+    /**
+     * Get a list of available task lists from this source
+     */
+    public function get_lists(&$tree = null)
+    {
+        // attempt to create a default list for this user
+        if (empty($this->lists)) {
+            $prop = array('name' => 'Tasks', 'color' => '0000CC', 'default' => true);
+            if ($this->create_list($prop))
+                $this->_read_lists(true);
+        }
+
+        $folders = array();
+        foreach ($this->lists as $id => $list) {
+            if (!empty($this->folders[$id])) {
+                $folders[] = $this->folders[$id];
+            }
+        }
 
         // include virtual folders for a full folder tree
-        if (!$this->rc->output->ajax_call && in_array($this->rc->action, array('index','')))
-            $folders = kolab_storage::folder_hierarchy($folders);
+        if (!is_null($tree)) {
+            $folders = kolab_storage::folder_hierarchy($folders, $tree);
+        }
 
+        $delim = $this->rc->get_storage()->get_hierarchy_delimiter();
+        $prefs = $this->rc->config->get('kolab_tasklists', array());
+
+        $lists = array();
         foreach ($folders as $folder) {
-            $utf7name = $folder->name;
+            $list_id = $folder->id; #kolab_storage::folder_id($folder->name);
+            $imap_path = explode($delim, $folder->name);
 
-            $path_imap = explode($delim, $utf7name);
-            $editname = rcube_charset::convert(array_pop($path_imap), 'UTF7-IMAP');  // pop off raw name part
-            $path_imap = join($delim, $path_imap);
+            // find parent
+            do {
+              array_pop($imap_path);
+              $parent_id = kolab_storage::folder_id(join($delim, $imap_path));
+            }
+            while (count($imap_path) > 1 && !$this->folders[$parent_id]);
+
+            // restore "real" parent ID
+            if ($parent_id && !$this->folders[$parent_id]) {
+                $parent_id = kolab_storage::folder_id($folder->get_parent());
+            }
 
             $fullname = $folder->get_name();
-            $listname = kolab_storage::folder_displayname($fullname, $listnames);
+            $listname = $folder->get_foldername();
 
             // special handling for virtual folders
-            if ($folder->virtual) {
-                $list_id = kolab_storage::folder_id($utf7name);
-                $this->lists[$list_id] = array(
-                    'id' => $list_id,
-                    'name' => $fullname,
+            if ($folder instanceof kolab_storage_folder_user) {
+                $lists[$list_id] = array(
+                    'id'       => $list_id,
+                    'name'     => $folder->get_name(),
                     'listname' => $listname,
-                    'virtual' => true,
+                    'title'    => $folder->get_owner(),
+                    'virtual'  => true,
                     'editable' => false,
+                    'group'    => 'other virtual',
+                    'class'    => 'user',
+                    'parent'   => $parent_id,
                 );
-                continue;
             }
-
-            if ($folder->get_namespace() == 'personal') {
-                $norename = false;
-                $readonly = false;
-                $alarms = true;
+            else if ($folder->virtual) {
+                $lists[$list_id] = array(
+                    'id'       => $list_id,
+                    'name'     => kolab_storage::object_name($fullname),
+                    'listname' => $listname,
+                    'virtual'  => true,
+                    'editable' => false,
+                    'group'    => $folder->get_namespace(),
+                    'class'    => 'folder',
+                    'parent'   => $parent_id,
+                );
             }
             else {
-                $alarms = false;
-                $readonly = true;
-                if (($rights = $folder->get_myrights()) && !PEAR::isError($rights)) {
-                    if (strpos($rights, 'i') !== false)
-                      $readonly = false;
+                if (!$this->lists[$list_id]) {
+                    $this->lists[$list_id] = $this->folder_props($folder, $delim, $prefs);
+                    $this->folders[$list_id] = $folder;
                 }
-                $info = $folder->get_folder_info();
-                $norename = $readonly || $info['norename'] || $info['protected'];
+                $this->lists[$list_id]['parent'] = $parent_id;
+                $lists[$list_id] = $this->lists[$list_id];
             }
-
-            $list_id = kolab_storage::folder_id($utf7name);
-            $tasklist = array(
-                'id' => $list_id,
-                'name' => $fullname,
-                'listname' => $listname,
-                'editname' => $editname,
-                'color' => $folder->get_color('0000CC'),
-                'showalarms' => isset($prefs[$list_id]['showalarms']) ? $prefs[$list_id]['showalarms'] : $alarms,
-                'editable' => !$readionly,
-                'norename' => $norename,
-                'active' => $folder->is_active(),
-                'parentfolder' => $path_imap,
-                'default' => $folder->default,
-                'children' => true,  // TODO: determine if that folder indeed has child folders
-                'class_name' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')),
-            );
-            $this->lists[$tasklist['id']] = $tasklist;
-            $this->folders[$tasklist['id']] = $folder;
-            $this->folders[$folder->name] = $folder;
         }
+
+        return $lists;
     }
 
     /**
-     * Get a list of available task lists from this source
+     * Get the kolab_calendar instance for the given calendar ID
+     *
+     * @param string List identifier (encoded imap folder name)
+     * @return object kolab_storage_folder Object nor null if list doesn't exist
      */
-    public function get_lists()
+    protected function get_folder($id)
     {
-        // attempt to create a default list for this user
-        if (empty($this->lists)) {
-            if ($this->create_list(array('name' => 'Tasks', 'color' => '0000CC', 'default' => true)))
-                $this->_read_lists(true);
+        // create list and folder instance if necesary
+        if (!$this->lists[$id]) {
+            $folder = kolab_storage::get_folder(kolab_storage::id_decode($id));
+            if ($folder->type) {
+                $this->folders[$id] = $folder;
+                $this->lists[$id] = $this->folder_props($folder, $this->rc->get_storage()->get_hierarchy_delimiter(), $this->rc->config->get('kolab_tasklists', array()));
+            }
         }
 
-        return $this->lists;
+        return $this->folders[$id];
     }
 
+
     /**
      * Create a new list assigned to the current user
      *
@@ -215,7 +302,7 @@ class tasklist_kolab_driver extends tasklist_driver
      */
     public function edit_list(&$prop)
     {
-        if ($prop['id'] && ($folder = $this->folders[$prop['id']])) {
+        if ($prop['id'] && ($folder = $this->get_folder($prop['id']))) {
             $prop['oldname'] = $folder->name;
             $prop['type'] = 'task';
             $newfolder = kolab_storage::folder_update($prop);
@@ -254,12 +341,18 @@ class tasklist_kolab_driver extends tasklist_driver
      * @param array Hash array with list properties
      *          id: List Identifier
      *      active: True if list is active, false if not
+     *   permanent: True if list is to be subscribed permanently
      * @return boolean True on success, Fales on failure
      */
     public function subscribe_list($prop)
     {
-        if ($prop['id'] && ($folder = $this->folders[$prop['id']])) {
-            return $folder->activate($prop['active']);
+        if ($prop['id'] && ($folder = $this->get_folder($prop['id']))) {
+            $ret = false;
+            if (isset($prop['permanent']))
+                $ret |= $folder->subscribe(intval($prop['permanent']));
+            if (isset($prop['active']))
+                $ret |= $folder->activate(intval($prop['active']));
+            return $ret;
         }
         return false;
     }
@@ -273,7 +366,7 @@ class tasklist_kolab_driver extends tasklist_driver
      */
     public function remove_list($prop)
     {
-        if ($prop['id'] && ($folder = $this->folders[$prop['id']])) {
+        if ($prop['id'] && ($folder = $this->get_folder($prop['id']))) {
           if (kolab_storage::folder_delete($folder->name))
               return true;
           else
@@ -284,6 +377,63 @@ class tasklist_kolab_driver extends tasklist_driver
     }
 
     /**
+     * Search for shared or otherwise not listed tasklists the user has access
+     *
+     * @param string Search string
+     * @param string Section/source to search
+     * @return array List of tasklists
+     */
+    public function search_lists($query, $source)
+    {
+        if (!kolab_storage::setup()) {
+            return array();
+        }
+
+        $this->search_more_results = false;
+        $this->lists = $this->folders = array();
+
+        $delim = $this->rc->get_storage()->get_hierarchy_delimiter();
+
+        // find unsubscribed IMAP folders that have "event" type
+        if ($source == 'folders') {
+            foreach ((array)kolab_storage::search_folders('task', $query, array('other')) as $folder) {
+                $this->folders[$folder->id] = $folder;
+                $this->lists[$folder->id] = $this->folder_props($folder, $delim, array());
+            }
+        }
+        // search other user's namespace via LDAP
+        else if ($source == 'users') {
+            $limit = $this->rc->config->get('autocomplete_max', 15) * 2;  // we have slightly more space, so display twice the number
+            foreach (kolab_storage::search_users($query, 0, array(), $limit * 10) as $user) {
+                $folders = array();
+                // search for tasks folders shared by this user
+                foreach (kolab_storage::list_user_folders($user, 'task', false) as $foldername) {
+                    $folders[] = new kolab_storage_folder($foldername, 'task');
+                }
+
+                if (count($folders)) {
+                    $userfolder = new kolab_storage_folder_user($user['kolabtargetfolder'], '', $user);
+                    $this->folders[$userfolder->id] = $userfolder;
+                    $this->lists[$userfolder->id] = $this->folder_props($userfolder, $delim, array());
+
+                    foreach ($folders as $folder) {
+                        $this->folders[$folder->id] = $folder;
+                        $this->lists[$folder->id] = $this->folder_props($folder, $delim, array());
+                        $count++;
+                    }
+                }
+
+                if ($count >= $limit) {
+                    $this->search_more_results = true;
+                    break;
+                }
+            }
+        }
+
+        return $this->get_lists();
+    }
+
+    /**
      * Get number of tasks matching the given filter
      *
      * @param array List of lists to count tasks of
@@ -303,7 +453,9 @@ class tasklist_kolab_driver extends tasklist_driver
 
         $counts = array('all' => 0, 'flagged' => 0, 'today' => 0, 'tomorrow' => 0, 'overdue' => 0, 'nodate' => 0);
         foreach ($lists as $list_id) {
-            $folder = $this->folders[$list_id];
+            if (!$folder = $this->get_folder($list_id)) {
+                continue;
+            }
             foreach ($folder->select(array(array('tags','!~','x-complete'))) as $record) {
                 $rec = $this->_to_rcube_task($record);
 
@@ -367,7 +519,9 @@ class tasklist_kolab_driver extends tasklist_driver
         }
 
         foreach ($lists as $list_id) {
-            $folder = $this->folders[$list_id];
+            if (!$folder = $this->get_folder($list_id)) {
+                continue;
+            }
             foreach ($folder->select($query) as $record) {
                 $task = $this->_to_rcube_task($record);
                 $task['list'] = $list_id;
@@ -391,11 +545,11 @@ class tasklist_kolab_driver extends tasklist_driver
     {
         $id = is_array($prop) ? ($prop['uid'] ?: $prop['id']) : $prop;
         $list_id = is_array($prop) ? $prop['list'] : null;
-        $folders = $list_id ? array($list_id => $this->folders[$list_id]) : $this->folders;
+        $folders = $list_id ? array($list_id => $this->get_folder($list_id)) : $this->folders;
 
         // find task in the available folders
         foreach ($folders as $list_id => $folder) {
-            if (is_numeric($list_id))
+            if (is_numeric($list_id) || !$folder)
                 continue;
             if (!$this->tasks[$id] && ($object = $folder->get_object($id))) {
                 $this->tasks[$id] = $this->_to_rcube_task($object);
@@ -424,7 +578,7 @@ class tasklist_kolab_driver extends tasklist_driver
         $childs = array();
         $list_id = $prop['list'];
         $task_ids = array($prop['id']);
-        $folder = $this->folders[$list_id];
+        $folder = $this->get_folder($list_id);
 
         // query for childs (recursively)
         while ($folder && !empty($task_ids)) {
@@ -484,7 +638,7 @@ class tasklist_kolab_driver extends tasklist_driver
             if (!$list['showalarms'] || ($lists && !in_array($lid, $lists)))
                 continue;
 
-            $folder = $this->folders[$lid];
+            $folder = $this->get_folder($lid);
             foreach ($folder->select($query) as $record) {
                 if (!($record['valarms'] || $record['alarms']) || $record['status'] == 'COMPLETED' || $record['complete'] == 100)  // don't trust query :-)
                     continue;
@@ -756,11 +910,11 @@ class tasklist_kolab_driver extends tasklist_driver
     public function edit_task($task)
     {
         $list_id = $task['list'];
-        if (!$list_id || !($folder = $this->folders[$list_id]))
+        if (!$list_id || !($folder = $this->get_folder($list_id)))
             return false;
 
         // moved from another folder
-        if ($task['_fromlist'] && ($fromfolder = $this->folders[$task['_fromlist']])) {
+        if ($task['_fromlist'] && ($fromfolder = $this->get_folder($task['_fromlist']))) {
             if (!$fromfolder->move($task['id'], $folder->name))
                 return false;
 
@@ -809,11 +963,11 @@ class tasklist_kolab_driver extends tasklist_driver
     public function move_task($task)
     {
         $list_id = $task['list'];
-        if (!$list_id || !($folder = $this->folders[$list_id]))
+        if (!$list_id || !($folder = $this->get_folder($list_id)))
             return false;
 
         // execute move command
-        if ($task['_fromlist'] && ($fromfolder = $this->folders[$task['_fromlist']])) {
+        if ($task['_fromlist'] && ($fromfolder = $this->get_folder($task['_fromlist']))) {
             return $fromfolder->move($task['id'], $folder->name);
         }
 
@@ -831,7 +985,7 @@ class tasklist_kolab_driver extends tasklist_driver
     public function delete_task($task, $force = true)
     {
         $list_id = $task['list'];
-        if (!$list_id || !($folder = $this->folders[$list_id]))
+        if (!$list_id || !($folder = $this->get_folder($list_id)))
             return false;
 
         return $folder->delete($task['id']);
@@ -892,7 +1046,7 @@ class tasklist_kolab_driver extends tasklist_driver
      */
     public function get_attachment_body($id, $task)
     {
-        if ($storage = $this->folders[$task['list']]) {
+        if ($storage = $this->get_folder($task['list'])) {
             return $storage->get_attachment($task['id'], $id);
         }
 
@@ -905,7 +1059,7 @@ class tasklist_kolab_driver extends tasklist_driver
     public function tasklist_edit_form($action, $list, $fieldprop)
     {
         if ($list['id'] && ($list = $this->lists[$list['id']])) {
-            $folder_name = $this->folders[$list['id']]->name; // UTF7
+            $folder_name = $this->get_folder($list['id'])->name; // UTF7
         }
         else {
             $folder_name = '';
diff --git a/plugins/tasklist/drivers/tasklist_driver.php b/plugins/tasklist/drivers/tasklist_driver.php
index 6c31fa7..908c808 100644
--- a/plugins/tasklist/drivers/tasklist_driver.php
+++ b/plugins/tasklist/drivers/tasklist_driver.php
@@ -126,6 +126,15 @@ abstract class tasklist_driver
     abstract function remove_list($prop);
 
     /**
+     * Search for shared or otherwise not listed tasklists the user has access
+     *
+     * @param string Search string
+     * @param string Section/source to search
+     * @return array List of tasklists
+     */
+    abstract function search_lists($query, $source);
+
+    /**
      * Get number of tasks matching the given filter
      *
      * @param array List of lists to count tasks of
diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
index 18456fa..0195d64 100644
--- a/plugins/tasklist/localization/en_US.inc
+++ b/plugins/tasklist/localization/en_US.inc
@@ -5,6 +5,9 @@ $labels['navtitle'] = 'Tasks';
 $labels['lists'] = 'Tasklists';
 $labels['list'] = 'Tasklist';
 $labels['tags'] = 'Tags';
+$labels['tasklistsubscribe'] = 'List permanently';
+$labels['listsearchresults'] = 'Available Tasklists';
+$labels['findlists'] = 'Find tasklists...';
 
 $labels['newtask'] = 'New Task';
 $labels['createnewtask'] = 'Create new Task (e.g. Saturday, Mow the lawn)';
diff --git a/plugins/tasklist/skins/larry/sprites.png b/plugins/tasklist/skins/larry/sprites.png
index 5c6b9fd..1446573 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 c940dff..f33f525 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -74,6 +74,32 @@ body.attachmentwin #topnav .topright {
 	bottom: 0px;
 }
 
+#tasklistsbox .boxtitle a.iconbutton.search {
+	position: absolute;
+	top: 8px;
+	right: 8px;
+	width: 16px;
+	cursor: pointer;
+	background-position: -2px -317px;
+}
+
+#tasklistsbox .listsearchbox {
+	display: none;
+}
+
+#tasklistsbox .listsearchbox.expanded {
+	display: block;
+}
+
+#tasklistsbox .scroller {
+	top: 34px;
+}
+
+#tasklistsbox .listsearchbox.expanded + .scroller {
+	top: 68px;
+}
+
+
 #taskselector {
 	margin: -4px 40px 0 0;
 	padding: 0;
@@ -225,32 +251,48 @@ body.attachmentwin #topnav .topright {
 	display: none;
 }
 
-#tasklists li {
+#tasklistsbox .treelist li {
+	margin: 0;
+	display: block;
+	position: relative;
+}
+
+#tasklistsbox .treelist li div.tasklist {
 	margin: 0;
 	height: 20px;
 	padding: 6px 8px 2px 6px;
-	display: block;
 	position: relative;
 	white-space: nowrap;
 }
 
-#tasklists li.virtual {
-	height: 12px;
+#tasklistsbox .treelist li.virtual > div.tasklist {
+	height: 14px;
+}
+
+#tasklistsbox .treelist ul li > div.tasklist {
+	margin-left: 16px;
+}
+
+#tasklistsbox .treelist ul ul li > div.tasklist {
+	margin-left: 32px;
+}
+
+#tasklistsbox .treelist ul ul ul li > div.tasklist {
+	margin-left: 48px;
 }
 
-#tasklists li label {
+#tasklistsbox .treelist li label {
 	display: block;
 }
 
-#tasklists li span.listname {
+#tasklistsbox .treelist li span.listname {
 	display: block;
 	position: absolute;
 	top: 7px;
-	left: 26px;
-	right: 26px;
+	left: 38px;
+	right: 40px;
 	cursor: default;
-	padding-bottom: 2px;
-	padding-right: 30px;
+	padding: 0px 30px 2px 2px;
 	color: #004458;
 	overflow: hidden;
 	text-overflow: ellipsis;
@@ -258,8 +300,11 @@ body.attachmentwin #topnav .topright {
 	background: url(sprites.png) right 20px no-repeat;
 }
 
-#tasklists li span.handle {
+#tasklistsbox .treelist li span.quickview {
 	display: inline-block;
+	position: absolute;
+	top: 6px;
+	right: 20px;
 	width: 16px;
 	height: 16px;
 	margin-right: 4px;
@@ -267,52 +312,76 @@ body.attachmentwin #topnav .topright {
 	cursor: pointer;
 }
 
-#tasklists li:hover span.handle {
-	background-position: -20px -101px;
+#tasklistsbox .treelist li a.subscribed {
+	display: inline-block;
+	position: absolute;
+	top: 6px;
+	right: 5px;
+	height: 16px;
+	width: 16px;
+	padding: 0;
+	background: url(sprites.png) -100px 0 no-repeat;
+	overflow: hidden;
+	text-indent: -5000px;
+	cursor: pointer;
 }
 
-#tasklists li.focusview span.handle {
-	background-position: -2px -101px;
+#tasklistsbox .treelist div:hover > a.subscribed {
+	background-position: -2px -215px;
 }
 
-#tasklists li.selected span.listname {
-	font-weight: bold;
+#tasklistsbox .treelist div.subscribed a.subscribed {
+	background-position: -20px -215px;
 }
 
-#tasklists li.readonly span.listname {
-	background-position: right -142px;
+#tasklistsbox .treelist li div:hover > span.quickview {
+	background-position: -20px -101px;
 }
 
-#tasklists li.other span.listname {
-	background-position: right -160px;
+#tasklistsbox .treelist li div.focusview > span.quickview {
+	background-position: -2px -101px;
 }
 
-#tasklists li.other.readonly span.listname {
-	background-position: right -178px;
+#tasklistsbox .searchresults .treelist li span.quickview {
+	display: none;
+}
+
+#tasklistsbox .treelist li.selected > div > span.listname {
+	font-weight: bold;
 }
 
-#tasklists li.shared span.listname {
-	background-position: right -196px;
+#tasklistsbox .treelist .readonly > span.listname {
+	background-position: right -142px;
 }
 
-#tasklists li.shared.readonly span.listname {
-	background-position: right -214px;
+#tasklistsbox .treelist .user > span.listname {
+	background-position: right -160px;
 }
 
-#tasklists li.virtual span.listname {
+#tasklistsbox .treelist .virtual > span.listname {
 	color: #aaa;
-	top: 2px;
+	top: 4px;
+	left: 20px;
+	right: 5px;
 }
 
-#tasklists li.virtual span.handle {
-	background: none;
-	cursor: default;
+#tasklistsbox .treelist.flat li span.calname {
+	left: 24px;
+	right: 22px;
 }
 
-#tasklists li input {
+#tasklistsbox .treelist li input {
 	position: absolute;
 	top: 5px;
-	right: 5px;
+	left: 18px;
+}
+
+#tasklistsbox .treelist li .treetoggle {
+	top: 8px;
+}
+
+#tasklistsbox .treelist li.virtual > .treetoggle {
+	top: 6px;
 }
 
 #mainview-right {
diff --git a/plugins/tasklist/skins/larry/templates/mainview.html b/plugins/tasklist/skins/larry/templates/mainview.html
index fe3f88b..0686646 100644
--- a/plugins/tasklist/skins/larry/templates/mainview.html
+++ b/plugins/tasklist/skins/larry/templates/mainview.html
@@ -24,9 +24,18 @@
 		</div>
 
 		<div id="tasklistsbox" class="uibox listbox">
-			<h2 class="boxtitle"><roundcube:label name="tasklist.lists" /></h2>
+			<h2 class="boxtitle"><roundcube:label name="tasklist.lists" />
+				<a class="iconbutton search" title="<roundcube:label name='tasklist.findlists' />"></a>
+			</h2>
+			<div class="listsearchbox">
+				<div class="searchbox">
+					<input type="text" name="q" id="tasklistsearch" placeholder="<roundcube:label name='tasklist.findlists' />" />
+					<a class="iconbutton searchicon"></a>
+					<roundcube:button command="reset-listsearch" id="tasklistsearch-reset" class="iconbutton reset" title="resetsearch" content="x" />
+				</div>
+			</div>
 			<div class="scroller withfooter">
-			<roundcube:object name="plugin.tasklists" id="tasklists" class="listing" />
+			<roundcube:object name="plugin.tasklists" id="tasklists" class="treelist listing" />
 			</div>
 			<div class="boxfooter">
 				<roundcube:button command="list-create" type="link" title="tasklist.createlist" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" content="+" /><roundcube:button name="tasklistoptionslink" id="tasklistoptionsmenulink" type="link" title="tasklist.listactions" class="listbutton groupactions" onclick="UI.show_popup('tasklistoptionsmenu', undefined, { above:true });return false" innerClass="inner" content="⚙" />
@@ -171,6 +180,30 @@ $(document).ready(function(e){
 		orientation:'v', relative:true, start:240, min:180, size:12 }).init();
 	new rcube_splitter({ id:'taskviewsplitterv', p1:'#tagsbox', p2:'#tasklistsbox',
 		orientation:'h', relative:true, start:242, min:120, size:12, offset:4 }).init();
+
+	// animation to unfold list search box
+	$('#tasklistsbox .boxtitle a.search').click(function(e){
+		var box = $('#tasklistsbox .listsearchbox'),
+			dir = box.is(':visible') ? -1 : 1;
+
+		box.slideToggle({
+			duration: 160,
+			progress: function(animation, progress) {
+				if (dir < 0) progress = 1 - progress;
+				$('#tasklistsbox .scroller').css('top', (34 + 34 * progress) + 'px');
+			},
+			complete: function() {
+				box.toggleClass('expanded');
+				if (box.is(':visible')) {
+					box.find('input[type=text]').focus();
+				}
+				else {
+					$('#tasklistsearch-reset').click();
+				}
+				// TODO: save state in localStorage
+			}
+		});
+	});
 });
 
 </script>
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index fcf4306..774e9b4 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -79,6 +79,7 @@ function rcube_tasklist_ui(settings)
     var scroll_speed = 20;
     var scroll_sensitivity = 40;
     var scroll_timer;
+    var tasklists_widget;
     var me = this;
 
     // general datepicker settings
@@ -127,18 +128,80 @@ function rcube_tasklist_ui(settings)
     {
         // initialize task list selectors
         for (var id in me.tasklists) {
-            if ((li = rcmail.get_folder_li(id, 'rcmlitasklist'))) {
-                init_tasklist_li(li, id);
-            }
-
             if (me.tasklists[id].editable && (!me.selected_list || (me.tasklists[id].active && !me.tasklists[me.selected_list].active))) {
                 me.selected_list = id;
+                break;
             }
         }
 
+        // initialize treelist widget that controls the tasklists list
+        var widget_class = window.kolab_folderlist || rcube_treelist_widget;
+        tasklists_widget = new widget_class(rcmail.gui_objects.tasklistslist, {
+            id_prefix: 'rcmlitasklist',
+            selectable: true,
+            save_state: true,
+            searchbox: '#tasklistsearch',
+            search_action: 'tasks/tasklist',
+            search_sources: [ 'folders', 'users' ],
+            search_title: rcmail.gettext('listsearchresults','tasklist')
+        });
+        tasklists_widget.addEventListener('select', function(node) {
+            var id = $(this).data('id');
+            rcmail.enable_command('list-edit', 'list-remove', 'list-import', me.tasklists[node.id].editable);
+            me.selected_list = node.id;
+        });
+        tasklists_widget.addEventListener('subscribe', function(p) {
+            var list;
+            if ((list = me.tasklists[p.id])) {
+                list.subscribed = p.subscribed || false;
+                rcmail.http_post('tasklist', { action:'subscribe', l:{ id:p.id, active:list.active?1:0, permanent:list.subscribed?1:0 } });
+            }
+        });
+        tasklists_widget.addEventListener('insert-item', function(p) {
+            var list = p.data;
+            if (list && list.id && !list.virtual) {
+                me.tasklists[list.id] = list;
+                var prop = { id:p.id, active:list.active?1:0 };
+                if (list.subscribed) prop.permanent = 1;
+                rcmail.http_post('tasklist', { action:'subscribe', l:prop });
+                list_tasks();
+            }
+        });
+
+        // init (delegate) event handler on tasklist checkboxes
+        tasklists_widget.container.on('click', 'input[type=checkbox]', function(e){
+            var list, id = this.value;
+            if ((list = me.tasklists[id])) {
+                list.active = this.checked;
+                fetch_counts();
+                if (!this.checked) remove_tasks(id);
+                else               list_tasks(null);
+                rcmail.http_post('tasklist', { action:'subscribe', l:{ id:id, active:list.active?1:0 } });
+
+                // disable focusview
+                if (!this.checked && focusview == id) {
+                    set_focusview(null);
+                }
+            }
+            e.stopPropagation();
+        });
+
+        // handler for clicks on quickview buttons
+        tasklists_widget.container.on('click', '.quickview', function(e){
+            var id = $(this).closest('li').attr('id').replace(/^rcmlitasklist/, '');
+            set_focusview(focusview == id ? null : id)
+            e.stopPropagation();
+        });
+
+        // register dbl-click handler to open calendar edit dialog
+        tasklists_widget.container.on('dblclick', ':not(.virtual) > .tasklist', function(e){
+            var id = $(this).closest('li').attr('id').replace(/^rcmlitasklist/, '');
+            list_edit_dialog(id);
+        });
+
         if (me.selected_list) {
             rcmail.enable_command('addtask', true);
-            $(rcmail.get_folder_li(me.selected_list, 'rcmlitasklist')).click();
+            tasklists_widget.select(me.selected_list);
         }
 
         // register server callbacks
@@ -1972,7 +2035,7 @@ function rcube_tasklist_ui(settings)
             me.selected_list = id;
 
             // click on handle icon toggles focusview
-            if (e.target.className == 'handle') {
+            if (e.target.className == 'quickview') {
                 set_focusview(focusview == id ? null : id)
             }
             // disable focusview when selecting another list
@@ -1994,13 +2057,15 @@ function rcube_tasklist_ui(settings)
     function set_focusview(id)
     {
         if (focusview && focusview != id)
-            $(rcmail.get_folder_li(focusview, 'rcmlitasklist')).removeClass('focusview');
+            $(tasklists_widget.get_item(focusview)).find('.tasklist').first().removeClass('focusview');
 
         focusview = id;
 
+        var li = $(tasklists_widget.get_item(id)).find('.tasklist').first();
+
         // activate list if necessary
         if (focusview && !me.tasklists[id].active) {
-            $('input', rcmail.get_folder_li(id, 'rcmlitasklist')).get(0).checked = true;
+            li.find('input[type=checkbox]').get(0).checked = true;
             me.tasklists[id].active = true;
             fetch_counts();
         }
@@ -2009,7 +2074,7 @@ function rcube_tasklist_ui(settings)
         list_tasks(null);
 
         if (focusview) {
-            $(rcmail.get_folder_li(focusview, 'rcmlitasklist')).addClass('focusview');
+            li.addClass('focusview');
         }
     }
 
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 65376d7..1a70699 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -52,6 +52,7 @@ class tasklist extends rcube_plugin
     public $driver;
     public $timezone;
     public $ui;
+    public $home;  // declare public to be used in other classes
 
     private $collapsed_tasks = array();
 
@@ -134,8 +135,7 @@ class tasklist extends rcube_plugin
         }
 
         if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) {
-            require_once($this->home . '/tasklist_ui.php');
-            $this->ui = new tasklist_ui($this);
+            $this->load_ui();
             $this->ui->init();
         }
 
@@ -144,6 +144,16 @@ class tasklist extends rcube_plugin
         $this->add_hook('dismiss_alarms', array($this, 'dismiss_alarms'));
     }
 
+    /**
+     *
+     */
+    private function load_ui()
+    {
+        if (!$this->ui) {
+            require_once($this->home . '/tasklist_ui.php');
+            $this->ui = new tasklist_ui($this);
+        }
+    }
 
     /**
      * Helper method to load the backend driver according to local config
@@ -619,6 +629,30 @@ class tasklist extends rcube_plugin
             if (($success = $this->driver->remove_list($list)))
                 $this->rc->output->command('plugin.destroy_tasklist', $list);
             break;
+
+        case 'search':
+            $this->load_ui();
+            $results = array();
+            foreach ((array)$this->driver->search_lists(get_input_value('q', RCUBE_INPUT_GPC), get_input_value('source', RCUBE_INPUT_GPC)) as $id => $prop) {
+                $editname = $prop['editname'];
+                unset($prop['editname']);  // force full name to be displayed
+                $prop['active'] = false;
+
+                // let the UI generate HTML and CSS representation for this calendar
+                $html = $this->ui->tasklist_list_item($id, $prop, $jsenv);
+                $prop += (array)$jsenv[$id];
+                $prop['editname'] = $editname;
+                $prop['html'] = $html;
+
+                $results[] = $prop;
+            }
+            // report more results available
+            if ($this->driver->search_more_results) {
+                $this->rc->output->show_message('autocompletemore', 'info');
+            }
+
+            $this->rc->output->command('multi_thread_http_response', $results, get_input_value('_reqid', RCUBE_INPUT_GPC));
+            return;
         }
 
         if ($success)
@@ -875,6 +909,13 @@ class tasklist extends rcube_plugin
     {
         $this->ui->init();
         $this->ui->init_templates();
+
+        // set autocompletion env
+        $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');
+
         $this->rc->output->set_pagetitle($this->gettext('navtitle'));
         $this->rc->output->send('tasklist.mainview');
     }
diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php
index 6988a61..6f92d26 100644
--- a/plugins/tasklist/tasklist_ui.php
+++ b/plugins/tasklist/tasklist_ui.php
@@ -81,6 +81,12 @@ class tasklist_ui
 
         $this->plugin->include_script('jquery.tagedit.js');
         $this->plugin->include_script('tasklist.js');
+        $this->rc->output->include_script('treelist.js');
+
+        // include kolab folderlist widget if available
+        if (is_readable($this->plugin->api->dir . 'libkolab/js/folderlist.js')) {
+            $this->plugin->api->include_script('libkolab/js/folderlist.js');
+        }
 
         $this->plugin->include_stylesheet($this->plugin->local_skin_path() . '/tagedit.css');
     }
@@ -88,45 +94,110 @@ class tasklist_ui
     /**
      *
      */
-    function tasklists($attrib = array())
+    public function tasklists($attrib = array())
+    {
+        $tree = true;
+        $jsenv = array();
+        $lists = $this->plugin->driver->get_lists($tree);
+
+        // walk folder tree
+        if (is_object($tree)) {
+            $html = $this->list_tree_html($tree, $lists, $jsenv, $attrib);
+        }
+        else {
+            // fall-back to flat folder listing
+            $attrib['class'] .= ' flat';
+
+            $html = '';
+            foreach ((array)$lists as $id => $prop) {
+                if ($attrib['activeonly'] && !$prop['active'])
+                  continue;
+
+                $html .= html::tag('li', array(
+                        'id' => 'rcmlitasklist' . rcube_utils::html_identifier($id),
+                        'class' => $prop['group'],
+                    ),
+                    $this->tasklist_list_item($id, $prop, $jsenv, $attrib['activeonly'])
+                );
+            }
+        }
+
+        $this->rc->output->set_env('tasklists', $jsenv);
+        $this->rc->output->add_gui_object('tasklistslist', $attrib['id']);
+
+        return html::tag('ul', $attrib, $html, html::$common_attrib);
+    }
+
+    /**
+     * Return html for a structured list <ul> for the folder tree
+     */
+    public function list_tree_html($node, $data, &$jsenv, $attrib)
     {
-        $lists = $this->plugin->driver->get_lists();
+        $out = '';
+        foreach ($node->children as $folder) {
+            $id = $folder->id;
+            $prop = $data[$id];
+            $is_collapsed = false; // TODO: determine this somehow?
+
+            $content = $this->tasklist_list_item($id, $prop, $jsenv, $attrib['activeonly']);
+
+            if (!empty($folder->children)) {
+                $content .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
+                    $this->list_tree_html($folder, $data, $jsenv, $attrib));
+            }
+
+            if (strlen($content)) {
+                $out .= html::tag('li', array(
+                      'id' => 'rcmlitasklist' . rcube_utils::html_identifier($id),
+                      'class' => $prop['group'] . ($prop['virtual'] ? ' virtual' : ''),
+                    ),
+                    $content);
+            }
+        }
 
-        $li = '';
-        foreach ((array)$lists as $id => $prop) {
-            if ($attrib['activeonly'] && !$prop['active'])
-              continue;
+        return $out;
+    }
 
+    /**
+     * Helper method to build a tasklist item (HTML content and js data)
+     */
+    public function tasklist_list_item($id, $prop, &$jsenv, $activeonly = false)
+    {
+        // enrich list properties with settings from the driver
+        if (!$prop['virtual']) {
             unset($prop['user_id']);
             $prop['alarms'] = $this->plugin->driver->alarms;
             $prop['undelete'] = $this->plugin->driver->undelete;
             $prop['sortable'] = $this->plugin->driver->sortable;
             $prop['attachments'] = $this->plugin->driver->attachments;
-
-            if (!$prop['virtual'])
-                $jsenv[$id] = $prop;
-
-            $html_id = html_identifier($id);
-            $class = 'tasks-'  . asciiwords($id, true);
-            $title = $prop['name'] != $prop['listname'] ? html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '';
-
-            if ($prop['virtual'])
-                $class .= ' virtual';
-            else if (!$prop['editable'])
-                $class .= ' readonly';
-            if ($prop['class_name'])
-                $class .= ' '.$prop['class_name'];
-
-            $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(array('class' => 'handle', 'title' => $this->plugin->gettext('focusview')), ' ') .
-                html::span(array('class' => 'listname', 'title' => $title), $prop['listname']));
+            $jsenv[$id] = $prop;
         }
 
-        $this->rc->output->set_env('tasklists', $jsenv);
-        $this->rc->output->add_gui_object('folderlist', $attrib['id']);
+        $classes = array('tasklist');
+        $title = $prop['title'] ?: ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
+          html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '');
+
+        if ($prop['virtual'])
+            $classes[] = 'virtual';
+        else if (!$prop['editable'])
+            $classes[] = 'readonly';
+        if ($prop['subscribed'])
+            $classes[] = 'subscribed';
+        if ($prop['class'])
+            $classes[] = $prop['class'];
+
+        if (!$activeonly || $prop['active']) {
+            return html::div(join(' ', $classes),
+                html::span(array('class' => 'listname', 'title' => $title), $prop['listname']) .
+                  ($prop['virtual'] ? '' :
+                    html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active'])) .
+                    html::span(array('class' => 'quickview', 'title' => $this->plugin->gettext('focusview')), ' ') .
+                    (isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->plugin->gettext('tasklistsubscribe')), ' ') : '')
+                )
+            );
+        }
 
-        return html::tag('ul', $attrib, $li, html::$common_attrib);
+        return '';
     }
 
 


commit 184730b3477ea1c7410b82913a0282baeb6a5bb2
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 21 12:44:26 2014 +0200

    Remove invisible text because that's considered for searching

diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index f973e88..70e0c37 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -307,7 +307,7 @@ class calendar_ui
         html::span(array('class' => 'calname', 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname']) .
         ($prop['virtual'] ? '' :
           html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') .
-          (isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->cal->gettext('calendarsubscribe')), $this->cal->gettext('subscribed')) : '') .
+          (isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->cal->gettext('calendarsubscribe')), ' ') : '') .
           html::span(array('class' => 'handle', 'style' => "background-color: #" . ($prop['color'] ?: 'f00')), ' ')
         )
       );


commit 709bd160167ad33b8c7a86d12213bcd06d7d38bc
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 21 10:50:36 2014 +0200

    Minor bugfixes and visual enhancements for new folder navigation

diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index c845c15..3e5d28a 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -52,7 +52,7 @@ class kolab_calendar extends kolab_storage_folder_api
     $imap = $calendar->rc->get_storage();
     $imap_folder = kolab_storage::id_decode($id);
     $info = $imap->folder_info($imap_folder, true);
-    if (empty($info) || $info['noselect'] || kolab_storage::folder_type($imap_folder) != 'event') {
+    if (empty($info) || $info['noselect'] || strpos(kolab_storage::folder_type($imap_folder), 'event') !== 0) {
       return new kolab_user_calendar($imap_folder, $calendar);
     }
     else {
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 217fe42..a95e1c2 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -117,8 +117,9 @@ class kolab_driver extends calendar_driver
       }
     }
 
+    $delim = $this->rc->get_storage()->get_hierarchy_delimiter();
     $folders = $this->filter_calendars(false, $active, $personal);
-    $calendars = $names = array();
+    $calendars = array();
 
     // include virtual folders for a full folder tree
     if (!is_null($tree))
@@ -127,14 +128,19 @@ class kolab_driver extends calendar_driver
     foreach ($folders as $id => $cal) {
       $fullname = $cal->get_name();
       $listname = $cal->get_foldername();
-      $imap_path = explode('/', $cal->name);
+      $imap_path = explode($delim, $cal->name);
 
       // find parent
       do {
         array_pop($imap_path);
-        $parent_id = kolab_storage::folder_id(join('/', $imap_path));
+        $parent_id = kolab_storage::folder_id(join($delim, $imap_path));
+      }
+      while (count($imap_path) > 1 && !$this->calendars[$parent_id]);
+
+      // restore "real" parent ID
+      if ($parent_id && !$this->calendars[$parent_id]) {
+          $parent_id = kolab_storage::folder_id($cal->get_parent());
       }
-      while (count($imap_path) > 0 && !$this->calendars[$parent_id]);
 
       // turn a kolab_storage_folder object into a kolab_calendar
       if ($cal instanceof kolab_storage_folder) {
@@ -360,9 +366,9 @@ class kolab_driver extends calendar_driver
     if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) {
       $ret = false;
       if (isset($prop['permanent']))
-        $ret |= $cal->storage->subscribe($prop['permanent']);
+        $ret |= $cal->storage->subscribe(intval($prop['permanent']));
       if (isset($prop['active']))
-        $ret |= $cal->storage->activate($prop['active']);
+        $ret |= $cal->storage->activate(intval($prop['active']));
       return $ret;
     }
     else {
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 3329bf4..f973e88 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -273,7 +273,7 @@ class calendar_ui
   /**
    * Helper method to build a calendar list item (HTML content and js data)
    */
-  public function calendar_list_item($id, $prop, &$jsenv, $activeonly)
+  public function calendar_list_item($id, $prop, &$jsenv, $activeonly = false)
   {
     // enrich calendar properties with settings from the driver
     if (!$prop['virtual']) {
diff --git a/plugins/calendar/skins/larry/images/calendars.png b/plugins/calendar/skins/larry/images/calendars.png
index 1f97abc..117d329 100644
Binary files a/plugins/calendar/skins/larry/images/calendars.png and b/plugins/calendar/skins/larry/images/calendars.png differ
diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html
index ce07112..2e45fcf 100644
--- a/plugins/calendar/skins/larry/templates/calendar.html
+++ b/plugins/calendar/skins/larry/templates/calendar.html
@@ -256,6 +256,9 @@ $(document).ready(function(e){
 				if (box.is(':visible')) {
 					box.find('input[type=text]').focus();
 				}
+				else {
+					$('#calendarlistsearch-reset').click();
+				}
 				// TODO: save state in localStorage
 			}
 		});


commit 36248fb46897c56c0a17bc010fc453c37d00d72c
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue May 20 20:26:47 2014 +0200

    Code cleanup and small fixes

diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 62b19f7..3329bf4 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -230,7 +230,7 @@ class calendar_ui
         continue;
 
       $html .= html::tag('li', array('id' => 'rcmlical' . $id, 'class' => $prop['group']),
-        $content = $this->calendar_list_item($id, $prop, $jsenv)
+        $content = $this->calendar_list_item($id, $prop, $jsenv, $attrib['activeonly'])
       );
     }
 
@@ -249,8 +249,9 @@ class calendar_ui
     foreach ($node->children as $folder) {
       $id = $folder->id;
       $prop = $data[$id];
+      $is_collapsed = false; // TODO: determine this somehow?
 
-      $content = $this->calendar_list_item($id, $prop, $jsenv);
+      $content = $this->calendar_list_item($id, $prop, $jsenv, $attrib['activeonly']);
 
       if (!empty($folder->children)) {
         $content .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
@@ -272,7 +273,7 @@ class calendar_ui
   /**
    * Helper method to build a calendar list item (HTML content and js data)
    */
-  public function calendar_list_item($id, $prop, &$jsenv)
+  public function calendar_list_item($id, $prop, &$jsenv, $activeonly)
   {
     // enrich calendar properties with settings from the driver
     if (!$prop['virtual']) {
@@ -290,19 +291,18 @@ class calendar_ui
     $classes = array('calendar', 'cal-'  . asciiwords($id, true));
     $title = $prop['title'] ?: ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
       html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '');
-    $is_collapsed = false; // TODO: determine this somehow?
 
     if ($prop['virtual'])
       $classes[] = 'virtual';
     else if ($prop['readonly'])
       $classes[] = 'readonly';
     if ($prop['subscribed'])
-      $classes[] = ' subscribed';
+      $classes[] = 'subscribed';
     if ($prop['class'])
       $classes[] = $prop['class'];
 
     $content = '';
-    if (!$attrib['activeonly'] || $prop['active']) {
+    if (!$activeonly || $prop['active']) {
       $content = html::div(join(' ', $classes),
         html::span(array('class' => 'calname', 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname']) .
         ($prop['virtual'] ? '' :




More information about the commits mailing list