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 @@