From dd986e6fe1d2602dd1d2f38b6fe2b94768f25207 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Thu, 16 Apr 2015 14:50:16 +0200 Subject: [PATCH] Display object history for contacts (#4972) Yet incomplete: show and restore old revisions not yet implemented --- .../kolab_addressbook/kolab_addressbook.js | 184 ++++++++++++++- .../kolab_addressbook/kolab_addressbook.php | 221 +++++++++++++++++- .../lib/kolab_addressbook_ui.php | 43 +++- .../kolab_addressbook/localization/en_US.inc | 14 ++ .../skins/classic/templates/audittrail.html | 0 .../skins/larry/folder_icons.png | Bin 1611 -> 1782 bytes .../skins/larry/kolab_addressbook.css | 105 +++++++++ .../skins/larry/templates/audittrail.html | 144 ++++++++++++ plugins/libkolab/js/audittrail.js | 2 +- 9 files changed, 706 insertions(+), 7 deletions(-) create mode 100644 plugins/kolab_addressbook/skins/classic/templates/audittrail.html create mode 100644 plugins/kolab_addressbook/skins/larry/templates/audittrail.html diff --git a/plugins/kolab_addressbook/kolab_addressbook.js b/plugins/kolab_addressbook/kolab_addressbook.js index b9402501..fb280b1d 100644 --- a/plugins/kolab_addressbook/kolab_addressbook.js +++ b/plugins/kolab_addressbook/kolab_addressbook.js @@ -126,6 +126,17 @@ if (window.rcmail) { rcmail.display_message(rcmail.gettext('noaddressbooksfound','kolab_addressbook'), 'info'); }); } + + // append button to show contact audit trail + if (rcmail.env.action == 'show' && rcmail.env.kolab_audit_trail && rcmail.env.cid) { + $('' + rcmail.get_label('kolab_addressbook.showhistory') + '') + .click(function(e) { + var rc = rcmail.is_framed() && parent.rcmail.contact_history_dialog ? parent.rcmail : rcmail; + rc.contact_history_dialog(); + return false; + }) + .appendTo($('
').addClass('formbuttons-secondary-kolab').appendTo('.formbuttons')); + } }); rcmail.addEventListener('listupdate', function() { @@ -139,7 +150,6 @@ if (window.rcmail) { source = rcmail.env.source ? rcmail.env.address_sources[rcmail.env.source] : null; if (selected && source.kolab) { - console.log('select', source.rights) rcmail.enable_command('delete', 'move', selected && source.rights.indexOf('t') >= 0); } }); @@ -150,7 +160,7 @@ if (window.rcmail) { rcube_webmail.prototype.set_book_actions = function() { var source = !this.env.group ? this.env.source : null, - sources = this.env.address_sources; + sources = this.env.address_sources || {}; var props = source && sources[source] && sources[source].kolab ? sources[source] : { removable: false, rights: '' } this.enable_command('book-create', true); @@ -344,6 +354,176 @@ rcube_webmail.prototype.book_realname = function() return source != '' && sources[source] && sources[source].realname ? sources[source].realname : ''; }; +// open dialog to show the current contact's changelog +rcube_webmail.prototype.contact_history_dialog = function() +{ + var $dialog, rec = { cid: this.get_single_cid(), source: rcmail.env.source }, + source = this.env.address_sources ? this.env.address_sources[rcmail.env.source] || {} : {}; + + if (!rec.cid || !window.libkolab_audittrail || !source.audittrail) { + return false; + } + + // render dialog + $dialog = libkolab_audittrail.object_history_dialog({ + module: 'kolab_addressbooks', + container: '#contacthistory', + title: rcmail.gettext('objectchangelog','kolab_addressbook'), + + // callback function for list actions + listfunc: function(action, rev) { + var rec = $dialog.data('rec'); + console.log(action, rev, rec) + //rcmail.loading_lock = rcmail.set_busy(true, 'loading', this.loading_lock); + //rcmail.http_post('action', { _do: action, _data: { uid: rec.uid, list:rec.list, rev: rev } }, saving_lock); + }, + + // callback function for comparing two object revisions + comparefunc: function(rev1, rev2) { + var rec = $dialog.data('rec'); + rcmail.kab_loading_lock = rcmail.set_busy(true, 'loading', rcmail.kab_loading_lock); + rcmail.http_post('plugin.contact-diff', { cid: rec.cid, source: rec.source, rev1: rev1, rev2: rev2 }, rcmail.kab_loading_lock); + } + }); + + $dialog.data('rec', rec); + + // fetch changelog data + this.kab_loading_lock = rcmail.set_busy(true, 'loading', this.kab_loading_lock); + this.http_post('plugin.contact-changelog', rec, this.kab_loading_lock); +}; + +// callback for displaying a contact's change history +rcube_webmail.prototype.contact_render_changelog = function(data) +{ + var $dialog = $('#contacthistory'), + rec = $dialog.data('rec'); + + if (data === false || !data.length || !event) { + // display 'unavailable' message + $('
' + rcmail.gettext('objectchangelognotavailable','kolab_addressbook') + '
') + .insertBefore($dialog.find('.changelog-table').hide()); + return; + } + + source = this.env.address_sources[rec.source] || {} + // source.editable = !source.readonly + + data.module = 'kolab_addressbook'; + libkolab_audittrail.render_changelog(data, rec, source); + + // set dialog size according to content + // dialog_resize($dialog.get(0), $dialog.height(), 600); +}; + +// callback for rendering a diff view of two contact revisions +rcube_webmail.prototype.contact_show_diff = function(data) +{ + var $dialog = $('#contactdiff'), + rec = {}, namediff = { 'old': '', 'new': '', 'set': false }; + + if (this.contact_list && this.contact_list.data[data.cid]) { + rec = this.contact_list.data[data.cid]; + } + + $dialog.find('div.form-section, h2.contact-names-new').hide().data('set', false); + $dialog.find('div.form-section.clone').remove(); + + var name_props = ['prefix','firstname','middlename','surname','suffix']; + + // Quote HTML entities + var Q = function(str){ + return String(str).replace(//g, '>').replace(/"/g, '"'); + }; + + // show each property change + $.each(data.changes, function(i, change) { + var prop = change.property, r2, html = !!change.ishtml, + row = $('div.contact-' + prop, $dialog).first(); + + // special case: names + if ($.inArray(prop, name_props) >= 0) { + namediff['old'] += change['old'] + ' '; + namediff['new'] += change['new'] + ' '; + namediff['set'] = true; + return true; + } + + // no display container for this property + if (!row.length) { + return true; + } + + // clone row if already exists + if (row.data('set')) { + r2 = row.clone().addClass('clone').insertAfter(row); + row = r2; + } + + // render photo as image with data: url + if (prop == 'photo') { + row.children('.diff-img-old').attr('src', change['old'] ? 'data:' + (change['old'].mimetype || 'image/gif') + ';base64,' + change['old'].base64 : 'data:image/gif;base64,R0lGODlhAQABAPAAAOjq6gAAACH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAAAsAAAAAAEAAQAAAgJEAQA7'); + row.children('.diff-img-new').attr('src', change['new'] ? 'data:' + (change['new'].mimetype || 'image/gif') + ';base64,' + change['new'].base64 : 'data:image/gif;base64,R0lGODlhAQABAPAAAOjq6gAAACH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAAAsAAAAAAEAAQAAAgJEAQA7'); + } + else if (change.diff_) { + row.children('.diff-text-diff').html(change.diff_); + row.children('.diff-text-old, .diff-text-new').hide(); + } + else { + if (!html) { + // escape HTML characters + change.old_ = Q(change.old_ || change['old'] || '--') + change.new_ = Q(change.new_ || change['new'] || '--') + } + row.children('.diff-text-old').html(change.old_ || change['old'] || '--').show(); + row.children('.diff-text-new').html(change.new_ || change['new'] || '--').show(); + } + + // display index number + if (typeof change.index != 'undefined') { + row.find('.index').html('(' + change.index + ')'); + } + + row.show().data('set', true); + }); + + // always show name + if (namediff.set) { + $('.contact-names', $dialog).html($.trim(namediff['old'] || '--')).addClass('diff-text-old').show(); + $('.contact-names-new', $dialog).html($.trim(namediff['new'] || '--')).show(); + } + else { + $('.contact-names', $dialog).text(rec.name).removeClass('diff-text-old').show(); + } + + // open jquery UI dialog + $dialog.dialog({ + modal: false, + resizable: true, + closeOnEscape: true, + title: rcmail.gettext('objectdiff','kolab_addressbook').replace('$rev1', data.rev1).replace('$rev2', data.rev2), + open: function() { + $dialog.attr('aria-hidden', 'false'); + }, + close: function() { + $dialog.dialog('destroy').attr('aria-hidden', 'true').hide(); + }, + buttons: [ + { + text: rcmail.gettext('close'), + click: function() { $dialog.dialog('close'); }, + autofocus: true + } + ], + minWidth: 400, + width: 480 + }).show(); + + // set dialog size according to content + // dialog_resize($dialog.get(0), $dialog.height(), rcmail.gui_containers.notedetailview.width() - 40); +}; + + function kolab_addressbook_contextmenu() { if (!window.rcm_callbackmenu_init) { diff --git a/plugins/kolab_addressbook/kolab_addressbook.php b/plugins/kolab_addressbook/kolab_addressbook.php index 6a2aaa6e..35908bbf 100644 --- a/plugins/kolab_addressbook/kolab_addressbook.php +++ b/plugins/kolab_addressbook/kolab_addressbook.php @@ -11,7 +11,7 @@ * @author Thomas Bruederli * @author Aleksander Machniak * - * Copyright (C) 2011, Kolab Systems AG + * Copyright (C) 2011-2015, 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 @@ -36,6 +36,8 @@ class kolab_addressbook extends rcube_plugin private $rc; private $ui; + public $bonnie_api = false; + const GLOBAL_FIRST = 0; const PERSONAL_FIRST = 1; const GLOBAL_ONLY = 2; @@ -69,6 +71,15 @@ class kolab_addressbook extends rcube_plugin $this->register_action('plugin.book-search', array($this, 'book_search')); $this->register_action('plugin.book-subscribe', array($this, 'book_subscribe')); + $this->register_action('plugin.contact-changelog', array($this, 'contact_changelog')); + $this->register_action('plugin.contact-diff', array($this, 'contact_diff')); + $this->register_action('plugin.contact-show', array($this, 'contact_show')); + + // get configuration for the Bonnie API + if ($bonnie_config = $this->rc->config->get('kolab_bonnie_api', false)) { + $this->bonnie_api = new kolab_bonnie_api($bonnie_config); + } + // Load UI elements if ($this->api->output->type == 'html') { $this->load_config(); @@ -168,8 +179,9 @@ class kolab_addressbook extends rcube_plugin 'group' => $abook->get_namespace(), 'subscribed' => $abook->is_subscribed(), 'carddavurl' => $abook->get_carddav_url(), - 'removable' => true, - 'kolab' => true, + 'removable' => true, + 'kolab' => true, + 'audittrail' => !empty($this->bonnie_api), ); } } @@ -497,10 +509,213 @@ class kolab_addressbook extends rcube_plugin */ } + if ($this->bonnie_api && $this->rc->action == 'show') { + $this->rc->output->set_env('kolab_audit_trail', true); + } + return $p; } + /** + * Handler for contact audit trail changelog requests + */ + public function contact_changelog() + { + if (empty($this->bonnie_api)) { + return false; + } + $contact = rcube_utils::get_input_value('cid', rcube_utils::INPUT_POST, true); + $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_POST); + + list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($contact, $source); + + $result = $uid && $mailbox ? $this->bonnie_api->changelog('contact', $uid, $mailbox, $msguid) : null; + if (is_array($result) && $result['uid'] == $uid) { + if (is_array($result['changes'])) { + $dtformat = $this->rc->config->get('date_format') . ' ' . $this->rc->config->get('time_format'); + array_walk($result['changes'], function(&$change) use ($dtformat) { + if ($change['date']) { + $dt = rcube_utils::anytodatetime($change['date']); + if ($dt instanceof DateTime) { + $change['date'] = $this->rc->format_date($dt, $dtformat); + } + } + }); + } + $this->rc->output->command('contact_render_changelog', $result['changes']); + } + else { + $this->rc->output->command('contact_render_changelog', false); + } + + $this->rc->output->send(); + } + + /** + * Handler for audit trail diff view requests + */ + public function contact_diff() + { + if (empty($this->bonnie_api)) { + return false; + } + + $contact = rcube_utils::get_input_value('cid', rcube_utils::INPUT_POST, true); + $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_POST); + $rev1 = rcube_utils::get_input_value('rev1', rcube_utils::INPUT_POST); + $rev2 = rcube_utils::get_input_value('rev2', rcube_utils::INPUT_POST); + + list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($contact, $source); + + $result = $this->bonnie_api->diff('contact', $uid, $rev1, $rev2, $mailbox, $msguid); + if (is_array($result) && $result['uid'] == $uid) { + $result['rev1'] = $rev1; + $result['rev2'] = $rev2; + $result['cid'] = $contact; + + // convert some properties, similar to rcube_kolab_contacts::_to_rcube_contact() + $keymap = array( + 'lastmodified-date' => 'changed', + 'additional' => 'middlename', + 'fn' => 'name', + 'tel' => 'phone', + 'url' => 'website', + 'bday' => 'birthday', + 'note' => 'notes', + 'role' => 'profession', + 'title' => 'jobtitle', + ); + + $propmap = array('email' => 'address', 'website' => 'url', 'phone' => 'number'); + $date_format = $this->rc->config->get('date_format', 'Y-m-d'); + + // map kolab object properties to keys and values the client expects + array_walk($result['changes'], function(&$change, $i) use ($keymap, $propmap, $date_format) { + if (array_key_exists($change['property'], $keymap)) { + $change['property'] = $keymap[$change['property']]; + } + + // format date-time values + if ($change['property'] == 'created' || $change['property'] == 'changed') { + if ($old_ = rcube_utils::anytodatetime($change['old'])) { + $change['old_'] = $this->rc->format_date($old_); + } + if ($new_ = rcube_utils::anytodatetime($change['new'])) { + $change['new_'] = $this->rc->format_date($new_); + } + } + // format dates + else if ($change['property'] == 'birthday' || $change['property'] == 'anniversary') { + if ($old_ = rcube_utils::anytodatetime($change['old'])) { + $change['old_'] = $this->rc->format_date($old_, $date_format); + } + if ($new_ = rcube_utils::anytodatetime($change['new'])) { + $change['new_'] = $this->rc->format_date($new_, $date_format); + } + } + // convert email, website, phone values + else if (array_key_exists($change['property'], $propmap)) { + $propname = $propmap[$change['property']]; + foreach (array('old','new') as $k) { + $k_ = $k . '_'; + if (!empty($change[$k])) { + $change[$k_] = html::quote($change[$k][$propname] ?: '--'); + if ($change[$k]['type']) { + $change[$k_] .= ' ' . html::span('subtype', rcmail_get_type_label($change[$k]['type'])); + } + $change['ishtml'] = true; + } + } + } + // serialize address structs + if ($change['property'] == 'address') { + foreach (array('old','new') as $k) { + $k_ = $k . '_'; + $change[$k]['zipcode'] = $change[$k]['code']; + $template = $this->rc->config->get('address_template', '{'.join('} {', array_keys($change[$k])).'}'); + $composite = array(); + foreach ($change[$k] as $p => $val) { + if (strlen($val)) + $composite['{'.$p.'}'] = $val; + } + $change[$k_] = preg_replace('/\{\w+\}/', '', strtr($template, $composite)); + if ($change[$k]['type']) { + $change[$k_] .= html::div('subtype', rcmail_get_type_label($change[$k]['type'])); + } + $change['ishtml'] = true; + } + + $change['diff_'] = libkolab::html_diff($change['old_'], $change['new_'], true); + } + // localize gender values + else if ($change['property'] == 'gender') { + if ($change['old']) $change['old_'] = $this->rc->gettext($change['old']); + if ($change['new']) $change['new_'] = $this->rc->gettext($change['new']); + } + // translate 'key' entries in individual properties + else if ($change['property'] == 'key') { + $p = $change['old'] ?: $change['new']; + $t = $p['type']; + $change['property'] = $t . 'publickey'; + $change['old'] = $change['old'] ? $change['old']['key'] : ''; + $change['new'] = $change['new'] ? $change['new']['key'] : ''; + } + // compute a nice diff of notes + else if ($change['property'] == 'notes') { + $change['diff_'] = libkolab::html_diff($change['old'], $change['new'], false); + } + }); + + $this->rc->output->command('contact_show_diff', $result); + } + else { + $this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error'); + } + + $this->rc->output->send(); + } + + /** + * Handler for audit trail revision view requests + */ + public function contact_show() + { + + $this->rc->output->send(); + } + + + /** + * Helper method to resolved the given contact identifier into uid and mailbox + * + * @return array (uid,mailbox,msguid) tuple + */ + private function _resolve_contact_identity($id, $abook) + { + $mailbox = $msguid = null; + + $source = $this->get_address_book(array('id' => $abook)); + if ($source['instance']) { + $uid = $source['instance']->id2uid($id); + $list = kolab_storage::id_decode($abook); + } + else { + return array(null, $mailbox, $msguid); + } + + // get resolve message UID and mailbox identifier + if ($folder = kolab_storage::get_folder($list)) { + $mailbox = $folder->get_mailbox_id(); + $msguid = $folder->cache->uid2msguid($uid); + } + + return array($uid, $mailbox, $msguid); + } + + /** + * + */ private function _sort_form_fields($contents, $source) { $block = array(); diff --git a/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php b/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php index 65476488..e20cf14d 100644 --- a/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php +++ b/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php @@ -43,7 +43,7 @@ class kolab_addressbook_ui */ private function init_ui() { - if (!empty($this->rc->action) && !preg_match('/^plugin\.book/', $this->rc->action)) { + if (!empty($this->rc->action) && !preg_match('/^plugin\.book/', $this->rc->action) && $this->rc->action != 'show') { return; } @@ -105,6 +105,33 @@ class kolab_addressbook_ui 'kolab_addressbook.noaddressbooksfound', 'kolab_addressbook.foldersubscribe', 'resetsearch'); + + + if ($this->plugin->bonnie_api) { + $this->plugin->api->include_script('libkolab/js/audittrail.js'); + + $this->rc->output->add_label( + 'kolab_addressbook.showhistory', + 'kolab_addressbook.compare', + 'kolab_addressbook.objectchangelog', + 'kolab_addressbook.objectdiff', + 'kolab_addressbook.showrevision', + 'kolab_addressbook.actionappend', + 'kolab_addressbook.actionmove', + 'kolab_addressbook.actiondelete', + 'kolab_addressbook.objectdiffnotavailable', + 'kolab_addressbook.objectchangelognotavailable', + 'close' + ); + + $this->plugin->add_hook('render_page', array($this, 'render_audittrail_page')); + $this->plugin->register_handler('plugin.object_changelog_table', array('libkolab', 'object_changelog_table')); + } + } + // include stylesheet for audit trail + else if ($this->rc->action == 'show' && $this->plugin->bonnie_api) { + $this->plugin->include_stylesheet($this->plugin->local_skin_path().'/kolab_addressbook.css'); + $this->rc->output->add_label('kolab_addressbook.showhistory'); } // book create/edit form else { @@ -247,6 +274,20 @@ class kolab_addressbook_ui return $out; } + /** + * + */ + public function render_audittrail_page($p) + { + // append audit trail UI elements to contact page + if ($p['template'] === 'addressbook' && !$p['kolab-audittrail']) { + $this->rc->output->add_footer($this->rc->output->parse('kolab_addressbook.audittrail', false, false)); + $p['kolab-audittrail'] = true; + } + + return $p; + } + private function get_form_part($form) { diff --git a/plugins/kolab_addressbook/localization/en_US.inc b/plugins/kolab_addressbook/localization/en_US.inc index b636d4d5..a65f46c9 100644 --- a/plugins/kolab_addressbook/localization/en_US.inc +++ b/plugins/kolab_addressbook/localization/en_US.inc @@ -51,6 +51,20 @@ $labels['foldersubscribe'] = 'List permanently'; $labels['nraddressbooksfound'] = '$nr address books found'; $labels['noaddressbooksfound'] = 'No address books found'; +// history dialog +$labels['showhistory'] = 'Show History'; +$labels['compare'] = 'Compare'; +$labels['objectchangelog'] = 'Change History'; +$labels['objectdiff'] = 'Changes from $rev1 to $rev2'; +$labels['actionappend'] = 'Saved'; +$labels['actionmove'] = 'Moved'; +$labels['actiondelete'] = 'Deleted'; +$labels['showrevision'] = 'Show this version'; +$labels['restore'] = 'Restore this version'; +$labels['objectnotfound'] = 'Failed to load contact data'; +$labels['objectchangelognotavailable'] = 'Change history is not available for this contact'; +$labels['objectdiffnotavailable'] = 'No comparison possible for the selected revisions'; + $messages['bookdeleteconfirm'] = 'Do you really want to delete the selected address book and all contacts in it?'; $messages['bookdeleting'] = 'Deleting address book...'; $messages['booksaving'] = 'Saving address book...'; diff --git a/plugins/kolab_addressbook/skins/classic/templates/audittrail.html b/plugins/kolab_addressbook/skins/classic/templates/audittrail.html new file mode 100644 index 00000000..e69de29b diff --git a/plugins/kolab_addressbook/skins/larry/folder_icons.png b/plugins/kolab_addressbook/skins/larry/folder_icons.png index b9c59b5a0acb6bc758bcdc4715e7caf542a63b1f..19b8cfabac863973ccade78b67818db15e04f9fc 100644 GIT binary patch delta 1779 zcmV7k}6Y1^@s6io4-}000KPNklF7{}{oGP4g$KkkbO z%VJ_evNSPU#AF#RYCxyFj5lS#H5A%XDJ=!WTHx@~@+z-VD9C%UC{T46=hzlzbi%Tz zi3Z&gBZy%z<{%>$ow%Hxr@4@Zw*u+CAC~8r{Bv*5^CZvzJb(9`a^VCCf*=TjAP9mW z2!aU3XJJRT`iC9c#;$LPTKMy**lmHw61t*$;7`@@+xNI}AIIfc0 z4<3?&Ry&E!GLb{^NjpU`keFMfQ=h3L<5SOLD5bI|zfUH^XI)WwoG1q33(kT@8GAdfv=UWuHBsdiCy8nxrK*ACUg#tF-PcG~=OtopwP%WIAfv%X%$L6- zg;%~O`F}m%kgP_VQB(uwPRF8w0>^cd+0cEc!L+tX)zMKy5!*5Hoe(PyOd#t?ZtK4GY_UDzQ?;R9bC-*!0R#O|0=xu=rL z>kJ+JKc>KTj5XvMU>xE?72bL{K^+jOH;{yaGJld<+d@t@c9M)n8_8_46MbDPNidxu z5QjX-_o{)|w9_BT^GZpYwVhl22XBc}TO6j~Y!BbWow0 z1rr-JFu>H}_9#X+Vj?Qt_=~*cEKRL%r+?8oMH@50&-&gfkHp{OKRs{?a*WNqptF~r zu(VQD)j4LOmoLH@P~Y&MjX=yHE-$?-x=w$8OJ4P!%_`2Cd*8@{C_f< zYH6l9?LGgdu=Z=MHma%bpp}km)N$iBo%(CKXss2`t+EwI_`)l4^Ul3eUG+Jd+R*8e z&LtbwHFh$cr`fF+=&aLulTy0)Yo7-b%f@6L-X~?F6MxKRwfEA@wjP?^a*?Jrbx}Rj z$!0r!GCna5h1dJVLl;ay+d&I)Pk$zE-GAuJV8t_;k&M=EX24EsFI{^LG0A%1TRj8f z%}8NPvQCtRm46!Bx+&@U+SQ3PW+0`a&bi*=+J)J>wVvOOo;j@4XyZ-g^!D(t&atWI zv)LWJG`+ct8XMaFSx<548qc42-1o6^X`C^C?x*|1l)*vEpH5EA8km8u{(pgq^>!dw zvPocp{rq6~1r&E*6HGHqL?QxEFj#^h{|kbGf@C2fA^sq^7p-t;XsCa5bo78+E~iSR z(j~ZuF^CCTJTfwpgMtQwftt-`T2fL1f_v8(#2`nI;^E=pQmt0&G?`4arlyA0*Vj|4 z)e3@p*BHbg2Xeh`K&@66<$vYn(c0QtD7vB`204)Hbpx!;Bc-LKbZBUZ4h{~kD2PE0 zvaPbi)98Dn?|D{adB}g3Sy80xn4KW*Vi`!vojivRIk^s zD2PE01h{04u9kd^)kVYj*e1? z!?Emp0LCB&IYPhD_%jVmPEOLXu`!q69>yRh5exoZ&Yzb|f*^?4?Aol~=EslwxtZ!0%mftWi$u5?@^gzv@bO$>o;g2rw=q8$Tawqq z^hgK;0lWb}4+bI@^K85YKdT|PXWZmwV3{ca3@pwH^oYsVZ4FeoJsKF3!{>z@FPq?t z^DLQlE`L7+5_&7jJ?s*>kOuU8ks-d_e*D000IONklr@RbOltCZEOIs?XrGQuqTwYpU<<%An0tJdifvUqe=e97T6P86y zG?*wV-JV$>t<04e-WP)5^+Q!L@F~`RJxAgH9}Abq8Uik8$OX2n3>geiQT++ zpOxC3EGgf__9v%*B#MEwqH>-3WD}d5d6EbK(uaRcXW+B0yhbjHf#lMYEWM_Q0YE4| z^XvPnY6~+owtr>^z4-nV^?OQQOvPs$snUh*lqH5mrt88Km5nUV(q`BzDBcaK0cAm{ z6lbq#Y-0(AViuE;AQ&n3T z#)E61cVM@Q7uC0(StcCEc+gis!Dp(oIhIKw#tK-ReSf=CD^D>^Q`a)#IL3o&;OWA` zHutk<%>C^7GU52b^XJ=wY5)M<;R;zrnDs*fh#wLw~mfKW_I@q57J`VOCQ+!Mz<%nzsm`-Mx}@$6$U-1#dB z=lB3vA-rvJP#M5ClOG)cd2}ANBsI_eZ@y>itpg zkAENtf*=TjAP9mW2!bF8f*MxTu%d<)HLR#%MGY%zSP=w45dPBF*C%j=NjAv#gR#P= zU&e%Ow78UO&MT>8oqQITAl-9B3A^NJ|LlmCNjGW$xY$Na*n0bAhm0mb?Neyx(ZqTU z3~_b5_EC(j$3%RN@z<2fQ;^l%1qp@a>wh!ho%Nj+K9cY?|LH+cu)}=jr9A_1)M^J+ z{b_EZ0wU5hup=fue5In=SF`pO3VkDf$15iB!Or+3?XJY+p}onf4%smVx>N0t-Qswe zVeJI|S_kP3ZJw1-7nRvpDvnDWg+<<6YOksDSR9Qlo3T7q8=GZ-#C$UdCDo8+ZGVTt zuKxcr`SNRR4$w4rL!Ij~xUSuVnZIYtSKINz3P*9wTi7SpZ{4ZVHJpa5mL89E9yvhQ z+QW4Q3hZZL-tE2)08792`DFsf{5b&tARV9jb3VUo0P;HfA*bUUWViK#p6gh<6CO@Z zP5LhEd$#b(1TH&D;si_@K7}#N!Q6lw;Io^n zsHgxATTPgQxdAs|wOZ$JvuQLMCYQ^73S*dqxdAsYI5;?lvojivpx5ht3S*dqxdAsY zJ3Cw6(9i(&_4QC&TkBI8!yL>F#?SH4+qZAK*E$85gSo+anGnXu$3Vg5axMEFz%h(r zPO#r-!nuZ~r>9|JV!|W5$1#ja#KQY77w(l*LJ)5={{!O{@17m~^#A|>002ovPDHLk FV1ki<3~>Me diff --git a/plugins/kolab_addressbook/skins/larry/kolab_addressbook.css b/plugins/kolab_addressbook/skins/larry/kolab_addressbook.css index 5484807e..2d2d8795 100644 --- a/plugins/kolab_addressbook/skins/larry/kolab_addressbook.css +++ b/plugins/kolab_addressbook/skins/larry/kolab_addressbook.css @@ -139,3 +139,108 @@ #directorylist a.contextRow { background-color: #C7E3EF; } + +#contactdiff, +#contacthistory { + display: none; +} + +.formbuttons .formbuttons-secondary-kolab { + display: block; + margin: 2px 2px 0 0; + text-align: center; +} + +.formbuttons .btn-contact-history { + display: inline-block; + padding: 1px; + color: #333; + text-decoration: none; +} + +.formbuttons .btn-contact-history:hover { + text-decoration: underline; +} + +.formbuttons .btn-contact-history:before { + content: ""; + display: inline-block; + position: relative; + top: 5px; + width: 16px; + height: 16px; + margin-right: 3px; + background: url('folder_icons.png') 0px -200px no-repeat; +} + +#contactdiff .contact-names, +#contactdiff .contact-names-new { + margin-top: 0; +} + +#contactdiff .contact-names.diff-text-old { + margin-bottom: 0; +} + +#contactdiff .diff-text-diff del, +#contactdiff .diff-text-diff ins { + text-decoration: none; + color: inherit; +} + +#contactdiff .diff-img-old, +#contactdiff .diff-text-old, +#contactdiff .diff-text-diff del { + background-color: #fdd; + text-decoration: line-through; +} + +#contactdiff .diff-text-new, +#contactdiff .diff-img-new, +#contactdiff .diff-text-diff ins, +#contactdiff .diff-text-diff .diffmod img { + background-color: #dfd; +} + +#contactdiff .diff-img-old, +#contactdiff .diff-img-new { + min-width: 48px; + max-width: 112px; +} + +#contactdiff label { + color: #666; + font-weight: bold; +} + +#contactdiff label span.index { + vertical-align: inherit; + margin-left: 0.6em; + font-weight: normal; +} + +#contactdiff .contact-name { + font-size: 120%; + font-weight: bold; +} + +#contactdiff .subtype { + color: #666; +} + +#contactdiff span.subtype { + margin-left: 0.5em; +} + +#contactdiff div.subtype { + margin-top: 0.2em; +} + +#contactdiff .subtype:before { + content: "("; +} + +#contactdiff .subtype:after { + content: ")"; +} + diff --git a/plugins/kolab_addressbook/skins/larry/templates/audittrail.html b/plugins/kolab_addressbook/skins/larry/templates/audittrail.html new file mode 100644 index 00000000..9411b03a --- /dev/null +++ b/plugins/kolab_addressbook/skins/larry/templates/audittrail.html @@ -0,0 +1,144 @@ + + + \ No newline at end of file diff --git a/plugins/libkolab/js/audittrail.js b/plugins/libkolab/js/audittrail.js index 42cdbf0c..87340b3f 100644 --- a/plugins/libkolab/js/audittrail.js +++ b/plugins/libkolab/js/audittrail.js @@ -70,7 +70,7 @@ libkolab_audittrail.object_history_dialog = function(p) minWidth: 450, width: 650, height: 350, - minHeight: 200, + minHeight: 200 }) .show().children('.compare-button').hide();