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 5c6b9fde..14465739 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 c940dff9..f33f525c 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; } -#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 ''; }