From f4f5a30e0ae0d6e0c4e4b6d04c8d64174d0bf53d Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Wed, 21 May 2014 13:04:18 +0200 Subject: [PATCH] Add new folder navigation to tasks module (#3047) --- plugins/calendar/lib/calendar_ui.php | 4 +- plugins/calendar/lib/js/folderlist.js | 1 - .../libkolab/lib/kolab_storage_folder_api.php | 18 + .../database/tasklist_database_driver.php | 12 + .../drivers/kolab/tasklist_kolab_driver.php | 314 +++++++++++++----- plugins/tasklist/drivers/tasklist_driver.php | 9 + plugins/tasklist/localization/en_US.inc | 3 + plugins/tasklist/skins/larry/sprites.png | Bin 4577 -> 5007 bytes plugins/tasklist/skins/larry/tasklist.css | 139 ++++++-- .../skins/larry/templates/mainview.html | 37 ++- plugins/tasklist/tasklist.js | 83 ++++- plugins/tasklist/tasklist.php | 45 ++- plugins/tasklist/tasklist_ui.php | 127 +++++-- 13 files changed, 633 insertions(+), 159 deletions(-) delete mode 120000 plugins/calendar/lib/js/folderlist.js diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php index 70e0c37a..edcca42e 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 c49706b3..00000000 --- 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 5af8c34a..4a904671 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 d9bf4145..cab4fa7e 100644 --- a/plugins/tasklist/drivers/database/tasklist_database_driver.php +++ b/plugins/tasklist/drivers/database/tasklist_database_driver.php @@ -199,6 +199,18 @@ class tasklist_database_driver extends tasklist_driver return false; } + /** + * 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 * diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php index ad36777b..52d23375 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(); - - // 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); foreach ($folders as $folder) { - $utf7name = $folder->name; + $tasklist = $this->folder_props($folder, $delim, $prefs); - $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); - - $fullname = $folder->get_name(); - $listname = kolab_storage::folder_displayname($fullname, $listnames); - - // 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, - 'listname' => $listname, - 'virtual' => true, - 'editable' => false, - ); - continue; - } - - 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 = 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; } } + /** + * 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() + public function get_lists(&$tree = null) { // attempt to create a default list for this user if (empty($this->lists)) { - if ($this->create_list(array('name' => 'Tasks', 'color' => '0000CC', 'default' => true))) + $prop = array('name' => 'Tasks', 'color' => '0000CC', 'default' => true); + if ($this->create_list($prop)) $this->_read_lists(true); } - return $this->lists; + $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 (!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) { + $list_id = $folder->id; #kolab_storage::folder_id($folder->name); + $imap_path = explode($delim, $folder->name); + + // 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 = $folder->get_foldername(); + + // special handling for virtual folders + if ($folder instanceof kolab_storage_folder_user) { + $lists[$list_id] = array( + 'id' => $list_id, + 'name' => $folder->get_name(), + 'listname' => $listname, + 'title' => $folder->get_owner(), + 'virtual' => true, + 'editable' => false, + 'group' => 'other virtual', + 'class' => 'user', + 'parent' => $parent_id, + ); + } + 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 { + if (!$this->lists[$list_id]) { + $this->lists[$list_id] = $this->folder_props($folder, $delim, $prefs); + $this->folders[$list_id] = $folder; + } + $this->lists[$list_id]['parent'] = $parent_id; + $lists[$list_id] = $this->lists[$list_id]; + } + } + + return $lists; } + /** + * 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 + */ + protected function get_folder($id) + { + // 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->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 @@ -283,6 +376,63 @@ class tasklist_kolab_driver extends tasklist_driver return false; } + /** + * 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 * @@ -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 6c31fa7a..908c8081 100644 --- a/plugins/tasklist/drivers/tasklist_driver.php +++ b/plugins/tasklist/drivers/tasklist_driver.php @@ -125,6 +125,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 * diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc index 18456fa6..0195d649 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 5c6b9fdec3166a19d8b5fc2ebbbea9a06efb3b3d..144657392c5c5d51bdff1d720a9ed073e31a2108 100644 GIT binary patch literal 5007 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 Z{tt`ukJwX#pLzfQ002ovPDHLkV1kB*j9LHy literal 4577 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 div.tasklist { + height: 14px; } -#tasklists li label { +#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; +} + +#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 { +#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; +} + +#tasklistsbox .treelist div:hover > a.subscribed { + background-position: -2px -215px; +} + +#tasklistsbox .treelist div.subscribed a.subscribed { + background-position: -20px -215px; +} + +#tasklistsbox .treelist li div:hover > span.quickview { background-position: -20px -101px; } -#tasklists li.focusview span.handle { +#tasklistsbox .treelist li div.focusview > span.quickview { background-position: -2px -101px; } -#tasklists li.selected span.listname { +#tasklistsbox .searchresults .treelist li span.quickview { + display: none; +} + +#tasklistsbox .treelist li.selected > div > span.listname { font-weight: bold; } -#tasklists li.readonly span.listname { +#tasklistsbox .treelist .readonly > span.listname { background-position: right -142px; } -#tasklists li.other span.listname { +#tasklistsbox .treelist .user > span.listname { background-position: right -160px; } -#tasklists li.other.readonly span.listname { - background-position: right -178px; -} - -#tasklists li.shared span.listname { - background-position: right -196px; -} - -#tasklists li.shared.readonly span.listname { - background-position: right -214px; -} - -#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 fe3f88bf..06866468 100644 --- a/plugins/tasklist/skins/larry/templates/mainview.html +++ b/plugins/tasklist/skins/larry/templates/mainview.html @@ -24,9 +24,18 @@
-

+

+ +

+
+ +
- +
@@ -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 + } + }); + }); }); diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js index fcf43062..774e9b4d 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 65376d7c..1a706995 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 6988a618..6f92d261 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()) { - $lists = $this->plugin->driver->get_lists(); + $tree = true; + $jsenv = array(); + $lists = $this->plugin->driver->get_lists($tree); - $li = ''; - foreach ((array)$lists as $id => $prop) { - if ($attrib['activeonly'] && !$prop['active']) - continue; + // 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
    for the folder tree + */ + public function list_tree_html($node, $data, &$jsenv, $attrib) + { + $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); + } + } + + 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) : ''); - return html::tag('ul', $attrib, $li, html::$common_attrib); + 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 ''; }