New hierarchical folder navigation for address book (#3046)

This commit is contained in:
Thomas Bruederli 2014-06-24 15:07:48 +02:00
parent 8503b61374
commit b120d3958f
3 changed files with 191 additions and 145 deletions

View file

@ -2,11 +2,12 @@
* Client script for the Kolab address book plugin
*
* @author Aleksander Machniak <machniak@kolabsys.com>
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* @licstart The following is the entire license notice for the
* JavaScript code in this file.
*
* Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
* Copyright (C) 2011-2014, Kolab Systems AG <contact@kolabsys.com>
*
* 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 = $('<a>').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($('<ul class="groups">').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<len; n++)
if (folders[n] == data.realname)
break;
// find the row before and re-insert after it
if (n && n < len - 1) {
name = folders[n-1];
for (n in sources)
if (sources[n].realname && sources[n].realname == name) {
row.detach().insertAfter(this.treelist.get_item(n));
break;
}
// update (remove old row)
if (old) {
this.treelist.update(old, { id: data.id, html:link, classes: classes, parent:(old != data.id ? data.parent : null) }, data.group || true);
}
else {
this.treelist.insert({ id: data.id, html:link, classes: classes, childlistclass: 'groups' }, data.parent, data.group || true);
}
if (olddata) {
// update groups
for (n in groups) {
if (groups[n].source == old) {
prop = groups[n];
prop.type = 'group';
prop.source = data.id;
id = 'G' + prop.source + prop.id;
this.env.contactfolders[data.id] = this.env.address_sources[data.id] = data;
link = $('<a>').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(/^&nbsp;&nbsp;/, '');
}
else if (level < 0) {
for (i=level; i<0; i++)
name = '&nbsp;&nbsp;' + 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;
}
};

View file

@ -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) . '</li>';
}
// 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 <ul> 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('!</ul>\n*$!', $content)) {
$content = preg_replace('!</ul>\n*$!', $child_html . '</ul>', $content);
}
else if (!empty($child_html)) {
$content .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)), $child_html);
}
}
$out .= $content . '</li>';
}
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']));

View file

@ -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)
*/