From b120d3958fc934d3377fb15c15af8f666b2b00c5 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Tue, 24 Jun 2014 15:07:48 +0200 Subject: [PATCH] New hierarchical folder navigation for address book (#3046) --- .../kolab_addressbook/kolab_addressbook.js | 122 ++--------- .../kolab_addressbook/kolab_addressbook.php | 196 ++++++++++++++---- .../lib/rcube_kolab_contacts.php | 18 ++ 3 files changed, 191 insertions(+), 145 deletions(-) diff --git a/plugins/kolab_addressbook/kolab_addressbook.js b/plugins/kolab_addressbook/kolab_addressbook.js index 79628e87..82b6d9dd 100644 --- a/plugins/kolab_addressbook/kolab_addressbook.js +++ b/plugins/kolab_addressbook/kolab_addressbook.js @@ -2,11 +2,12 @@ * Client script for the Kolab address book plugin * * @author Aleksander Machniak + * @author Thomas Bruederli * * @licstart The following is the entire license notice for the * JavaScript code in this file. * - * Copyright (C) 2011, Kolab Systems AG + * Copyright (C) 2011-2014, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -220,124 +221,39 @@ rcube_webmail.prototype.book_delete_done = function(id, recur) }; // action executed after book create/update -rcube_webmail.prototype.book_update = function(data, old, recur) +rcube_webmail.prototype.book_update = function(data, old) { - var n, i, id, len, link, row, prop, olddata, oldid, name, sources, level, - folders = [], classes = ['addressbook'], - groups = this.env.contactgroups; + var link, classes = [(data.group || ''), 'addressbook']; - this.env.contactfolders[data.id] = this.env.address_sources[data.id] = data; this.show_contentframe(false); - // update (remove old row) - if (old && old != data.id) { - olddata = this.env.address_sources[old]; - delete this.env.address_sources[old]; - delete this.env.contactfolders[old]; - this.treelist.remove(old); - } - - sources = this.env.address_sources; - // set row attributes if (data.readonly) classes.push('readonly'); - if (data.class_name) - classes.push(data.class_name); - // updated currently selected book - if (this.env.source != '' && this.env.source == old) { - classes.push('selected'); - this.env.source = data.id; - } + if (data.group) + classes.push(data.group); link = $('').html(data.name) .attr({ - href: '#', rel: data.id, + href: this.url('', { _source: data.id }), + rel: data.id, onclick: "return rcmail.command('list', '" + data.id + "', this)" }); - // add row at the end of the list - // treelist widget is not very smart, we need - // to do sorting and add groups list by ourselves - this.treelist.insert({id: data.id, html:link, classes: classes, childlistclass: 'groups'}, '', false); - row = $(this.treelist.get_item(data.id)); - row.append($('
    ').hide()); - - // we need to sort rows because treelist can't sort by property - $.each(sources, function(i, v) { - if (v.kolab && v.realname) - folders.push(v.realname); - }); - folders.sort(); - - for (n=0, len=folders.length; n').text(prop.name) - .attr({ - href: '#', rel: prop.source + ':' + prop.id, - onclick: "return rcmail.command('listgroup', {source: '"+prop.source+"', id: '"+prop.id+"'}, this)" - }); - - this.treelist.insert({id:id, html:link, classes:['contactgroup']}, prop.source, true); - - this.env.contactfolders[id] = this.env.contactgroups[id] = prop; - delete this.env.contactgroups[n]; - delete this.env.contactfolders[n]; - } - } - - if (recur) - return; - - // update subfolders - old += '_'; - level = olddata.realname.split(this.env.delimiter).length - data.realname.split(this.env.delimiter).length; - olddata.realname += this.env.delimiter; - - for (n in sources) { - if (sources[n].realname && sources[n].realname.indexOf(olddata.realname) == 0) { - prop = sources[n]; - oldid = sources[n].id; - // new ID - prop.id = data.id + '_' + n.substr(old.length); - prop.realname = data.realname + prop.realname.substr(olddata.realname.length - 1); - name = prop.name; - - // update display name - if (level > 0) { - for (i=level; i>0; i--) - name = name.replace(/^  /, ''); - } - else if (level < 0) { - for (i=level; i<0; i++) - name = '  ' + name; - } - - prop.name = name; - this.book_update(prop, oldid, true) - } - } + // updated currently selected book + if (this.env.source != '' && this.env.source == old) { + this.treelist.select(data.id); + this.env.source = data.id; } }; diff --git a/plugins/kolab_addressbook/kolab_addressbook.php b/plugins/kolab_addressbook/kolab_addressbook.php index f28d4966..530580bb 100644 --- a/plugins/kolab_addressbook/kolab_addressbook.php +++ b/plugins/kolab_addressbook/kolab_addressbook.php @@ -32,6 +32,7 @@ class kolab_addressbook extends rcube_plugin public $task = '?(?!login|logout).*'; private $sources; + private $folders; private $rc; private $ui; @@ -60,6 +61,7 @@ class kolab_addressbook extends rcube_plugin if ($this->rc->task == 'addressbook') { $this->add_texts('localization'); $this->add_hook('contact_form', array($this, 'contact_form')); + $this->add_hook('template_object_directorylist', array($this, 'directorylist_html')); // Plugin actions $this->register_action('plugin.book', array($this, 'book_actions')); @@ -103,24 +105,9 @@ class kolab_addressbook extends rcube_plugin } $sources = array(); - $names = array(); - foreach ($this->_list_sources() as $abook_id => $abook) { - $name = kolab_storage::folder_displayname($abook->get_name(), $names); - // register this address source - $sources[$abook_id] = array( - 'id' => $abook_id, - 'name' => $name, - 'readonly' => $abook->readonly, - 'editable' => $abook->editable, - 'groups' => $abook->groups, - 'undelete' => $abook->undelete && $undelete, - 'realname' => rcube_charset::convert($abook->get_realname(), 'UTF7-IMAP'), // IMAP folder name - 'class_name' => $abook->get_namespace(), - 'carddavurl' => $abook->get_carddav_url(), - 'kolab' => true, - ); + $sources[$abook_id] = $this->abook_prop($abook_id, $abook); } // Add personal address sources to the list @@ -141,6 +128,139 @@ class kolab_addressbook extends rcube_plugin return $p; } + /** + * Helper method to build a hash array of address book properties + */ + protected function abook_prop($id, $abook) + { + return array( + 'id' => $id, + 'name' => $abook->get_name(), + 'listname' => $abook->get_foldername(), + 'readonly' => $abook->readonly, + 'editable' => $abook->editable, + 'groups' => $abook->groups, + 'undelete' => $abook->undelete && $this->rc->config->get('undo_timeout'), + 'realname' => rcube_charset::convert($abook->get_realname(), 'UTF7-IMAP'), // IMAP folder name + 'group' => $abook->get_namespace(), + 'carddavurl' => $abook->get_carddav_url(), + 'kolab' => true, + ); + } + + /** + * + */ + public function directorylist_html($args) + { + $out = ''; + $jsdata = array(); + $sources = (array)$this->rc->get_address_sources(); + + // list all non-kolab sources first + foreach (array_filter($sources, function($source){ return empty($source['kolab']); }) as $j => $source) { + $id = strval(strlen($source['id']) ? $source['id'] : $j); + $out .= $this->addressbook_list_item($id, $source, $jsdata) . ''; + } + + // render a hierarchical list of kolab contact folders + kolab_storage::folder_hierarchy($this->folders, $tree); + $out .= $this->folder_tree_html($tree, $sources, $jsdata); + + $this->rc->output->set_env('contactgroups', $jsdata); + + $args['content'] = html::tag('ul', $args, $out, html::$common_attrib); + return $args; + } + + /** + * Return html for a structured list
      for the folder tree + */ + public function folder_tree_html($node, $data, &$jsdata) + { + $out = ''; + foreach ($node->children as $folder) { + $id = $folder->id; + $source = $data[$id]; + $is_collapsed = strpos($this->rc->config->get('collapsed_abooks',''), '&'.rawurlencode($id).'&') !== false; + + if ($folder->virtual) { + $source = array( + 'id' => $folder->id, + 'name' => $folder->get_name(), + 'listname' => $folder->get_foldername(), + 'group' => $folder->get_namespace(), + 'readonly' => true, + 'editable' => false, + 'kolab' => true, + 'virtual' => true, + ); + } + + $content = $this->addressbook_list_item($id, $source, $jsdata); + + if (!empty($folder->children)) { + $child_html = $this->folder_tree_html($folder, $data, $jsdata); + + if (!empty($child_html) && preg_match('!
    \n*$!', $content)) { + $content = preg_replace('!
\n*$!', $child_html . '', $content); + } + else if (!empty($child_html)) { + $content .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)), $child_html); + } + } + + $out .= $content . ''; + } + + return $out; + } + + /** + * + */ + protected function addressbook_list_item($id, $source, &$jsdata, $checkbox = false) + { + $folder = $this->folders[$id]; + $current = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC); + + // set class name(s) + $classes = array($source['group'] ?: '000', 'addressbook'); + if ($current === $id) + $classes[] = 'selected'; + if ($source['readonly']) + $classes[] = 'readonly'; + if ($source['virtual']) + $classes[] = 'virtual'; + if ($source['class_name']) + $classes[] = $source['class_name']; + + $name = !empty($source['listname']) ? $source['listname'] : (!empty($source['name']) ? $source['name'] : $id); + $out .= html::tag('li', array( + 'id' => 'rcmli' . rcube_utils::html_identifier($id, true), + 'class' => join(' ', $classes), + 'noclose' => true, + ), + ($source['virtual'] ? + html::a(array('tabindex' => '0'), $name) : + html::a(array( + 'href' => $this->rc->url(array('_source' => $id)), + 'rel' => $source['id'], + 'onclick' => "return " . rcmail_output::JS_OBJECT_NAME.".command('list','" . rcube::JQ($id) . "',this)", + ), $name) + ) + ); + + $groupdata = array('out' => '', 'jsdata' => $jsdata, 'source' => $id); + if ($source['groups'] && function_exists('rcmail_contact_groups')) { + $groupdata = rcmail_contact_groups($groupdata); + } + + $jsdata = $groupdata['jsdata']; + $out .= $groupdata['out']; + + return $out; + } /** * Sets autocomplete_addressbooks option according to @@ -203,6 +323,16 @@ class kolab_addressbook extends rcube_plugin if ($this->sources[$p['id']]) { $p['instance'] = $this->sources[$p['id']]; } + else { + $folder = kolab_storage::get_folder(kolab_storage::id_decode($p['id'])); + if ($folder->type) { // try with unencoded (old-style) identifier + $folder = kolab_storage::get_folder(kolab_storage::id_decode($p['id'], false)); + } + if ($folder->type) { + $this->sources[$p['id']] = new rcube_kolab_contacts($folder->name); + $p['instance'] = $this->sources[$p['id']]; + } + } } return $p; @@ -215,7 +345,9 @@ class kolab_addressbook extends rcube_plugin if (isset($this->sources)) return $this->sources; + kolab_storage::$encode_ids = true; $this->sources = array(); + $this->folders = array(); $abook_prio = $this->addressbook_prio(); @@ -247,9 +379,10 @@ class kolab_addressbook extends rcube_plugin $names = array(); foreach ($folders as $folder) { // create instance of rcube_contacts - $abook_id = kolab_storage::folder_id($folder->name, false); + $abook_id = $folder->id; $abook = new rcube_kolab_contacts($folder->name); $this->sources[$abook_id] = $abook; + $this->folders[$abook_id] = $folder; } } @@ -436,40 +569,19 @@ class kolab_addressbook extends rcube_plugin if ($result) { $storage = $this->rc->get_storage(); $delimiter = $storage->get_hierarchy_delimiter(); - $kolab_folder = new rcube_kolab_contacts($folder); - - // create display name for the folder (see self::address_sources()) - if (strpos($folder, $delimiter)) { - $names = array(); - foreach ($this->_list_sources() as $abook) { - $realname = $abook->get_realname(); - // The list can be not updated yet, handle old folder name - if ($type == 'update' && $realname == $prop['oldname']) { - $abook = $kolab_folder; - $realname = $folder; - } - - $name = kolab_storage::folder_displayname($abook->get_name(), $names); - - if ($realname == $folder) { - break; - } - } - } - else { - $name = $kolab_folder->get_name(); - } + $kolab_folder = kolab_storage::get_folder($folder); $this->rc->output->show_message('kolab_addressbook.book'.$type.'d', 'confirmation'); $this->rc->output->command('set_env', 'delimiter', $delimiter); $this->rc->output->command('book_update', array( 'id' => kolab_storage::folder_id($folder), - 'name' => $name, + 'name' => $kolab_folder->get_foldername(), 'readonly' => false, 'editable' => true, 'groups' => true, 'realname' => rcube_charset::convert($folder, 'UTF7-IMAP'), // IMAP folder name - 'class_name' => $kolab_folder->get_namespace(), + 'group' => $kolab_folder->get_namespace(), + 'parent' => kolab_storage::folder_id($kolab_folder->get_parent()), 'kolab' => true, ), kolab_storage::folder_id($prop['oldname'])); diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php index ffce9b5e..2e93d462 100644 --- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php +++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php @@ -154,6 +154,14 @@ class rcube_kolab_contacts extends rcube_addressbook return $folder; } + /** + * Wrapper for kolab_storage_folder::get_foldername() + */ + public function get_foldername() + { + return $this->storagefolder->get_foldername(); + } + /** * Getter for the IMAP folder name @@ -180,6 +188,16 @@ class rcube_kolab_contacts extends rcube_addressbook return $this->namespace; } + /** + * Getter for parent folder path + * + * @return string Full path to parent folder + */ + public function get_parent() + { + return $this->storagefolder->get_parent(); + } + /** * Compose an URL for CardDAV access to this address book (if configured) */