plugins/kolab_notes

Thomas Brüderli bruederli at kolabsys.com
Tue Mar 31 14:58:13 CEST 2015


 plugins/kolab_notes/kolab_notes.php                  |  251 +++++++++++++++++
 plugins/kolab_notes/kolab_notes_ui.php               |    4 
 plugins/kolab_notes/localization/en_US.inc           |   18 +
 plugins/kolab_notes/notes.js                         |  271 +++++++++++++++++--
 plugins/kolab_notes/skins/larry/notes.css            |  149 ++++++++--
 plugins/kolab_notes/skins/larry/sprites.png          |binary
 plugins/kolab_notes/skins/larry/templates/notes.html |   44 ++-
 7 files changed, 681 insertions(+), 56 deletions(-)

New commits:
commit ae6ec80e446450b395bf47b484b91d07ab28fdc7
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Mar 31 14:57:57 2015 +0200

    Implement audit trail for notes (#4904)

diff --git a/plugins/kolab_notes/kolab_notes.php b/plugins/kolab_notes/kolab_notes.php
index 83694b9..a0da120 100644
--- a/plugins/kolab_notes/kolab_notes.php
+++ b/plugins/kolab_notes/kolab_notes.php
@@ -8,7 +8,7 @@
  * @version @package_version@
  * @author Thomas Bruederli <bruederli at kolabsys.com>
  *
- * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
+ * Copyright (C) 2014-2015, Kolab Systems AG <contact at 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
@@ -35,6 +35,7 @@ class kolab_notes extends rcube_plugin
     private $folders;
     private $cache = array();
     private $message_notes = array();
+    private $bonnie_api = false;
 
     /**
      * Required startup method of a Roundcube plugin
@@ -110,6 +111,11 @@ class kolab_notes extends rcube_plugin
             $this->load_ui();
         }
 
+        // 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);
+        }
+
         // notes use fully encoded identifiers
         kolab_storage::$encode_ids = true;
     }
@@ -597,7 +603,7 @@ class kolab_notes extends rcube_plugin
         $action = rcube_utils::get_input_value('_do', rcube_utils::INPUT_POST);
         $note   = rcube_utils::get_input_value('_data', rcube_utils::INPUT_POST, true);
 
-        $success = false;
+        $success = $silent = false;
         switch ($action) {
             case 'new':
                 $temp_id = $rec['tempid'];
@@ -630,13 +636,66 @@ class kolab_notes extends rcube_plugin
                     }
                 }
                 break;
+
+            case 'changelog':
+                $data = $this->get_changelog($note);
+                if (is_array($data) && !empty($data)) {
+                    $dtformat = $this->rc->config->get('date_format') . ' ' . $this->rc->config->get('time_format');
+                    array_walk($data, function(&$change) use ($lib, $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('plugin.note_render_changelog', $data);
+                }
+                else {
+                    $this->rc->output->command('plugin.note_render_changelog', false);
+                }
+                $silent = true;
+                break;
+
+            case 'diff':
+                $silent = true;
+                $data = $this->get_diff($note, $note['rev1'], $note['rev2']);
+                if (is_array($data)) {
+                    $this->rc->output->command('plugin.note_show_diff', $data);
+                }
+                else {
+                    $this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error');
+                }
+                break;
+
+            case 'show':
+                if ($rec = $this->get_revison($note, $note['rev'])) {
+                    $this->rc->output->command('plugin.note_show_revision', $this->_client_encode($rec));
+                }
+                else {
+                    $this->rc->output->command('display_message', $this->gettext('objectnotfound'), 'error');
+                }
+                $silent = true;
+                break;
+
+            case 'restore':
+                if ($this->restore_revision($note, $note['rev'])) {
+                    $refresh = $this->get_note($note);
+                    $this->rc->output->command('display_message', $this->gettext(array('name' => 'objectrestoresuccess', 'vars' => array('rev' => $note['rev']))), 'confirmation');
+                    $this->rc->output->command('plugin.close_history_dialog');
+                }
+                else {
+                    $this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error');
+                }
+                $silent = true;
+                break;
         }
 
         // show confirmation/error message
         if ($success) {
             $this->rc->output->show_message('successfullysaved', 'confirmation');
         }
-        else {
+        else if (!$silent) {
             $this->rc->output->show_message('errorsaving', 'error');
         }
 
@@ -763,6 +822,192 @@ class kolab_notes extends rcube_plugin
     }
 
     /**
+     * Provide a list of revisions for the given object
+     *
+     * @param array  $note Hash array with note properties
+     * @return array List of changes, each as a hash array
+     */
+    public function get_changelog($note)
+    {
+        if (empty($this->bonnie_api)) {
+            return false;
+        }
+
+        list($uid, $mailbox, $msguid) = $this->_resolve_note_identity($note);
+
+        $result = $uid && $mailbox ? $this->bonnie_api->changelog('note', $uid, $mailbox, $msguid) : null;
+        if (is_array($result) && $result['uid'] == $uid) {
+            return $result['changes'];
+        }
+
+        return false;
+    }
+
+    /**
+     * Return full data of a specific revision of a note record
+     *
+     * @param mixed  $note UID string or hash array with note properties
+     * @param mixed  $rev Revision number
+     *
+     * @return array Note object as hash array
+     */
+    public function get_revison($note, $rev)
+    {
+        if (empty($this->bonnie_api)) {
+            return false;
+        }
+
+        list($uid, $mailbox, $msguid) = $this->_resolve_note_identity($note);
+
+        // call Bonnie API
+        $result = $this->bonnie_api->get('note', $uid, $rev, $mailbox, $msguid);
+        if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) {
+            $format = kolab_format::factory('note');
+            $format->load($result['xml']);
+            $rec = $format->to_array();
+
+            if ($format->is_valid()) {
+                $rec['rev'] = $result['rev'];
+                return $rec;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Get a list of property changes beteen two revisions of a note object
+     *
+     * @param array  $$note Hash array with note properties
+     * @param mixed  $rev   Revisions: "from:to"
+     *
+     * @return array List of property changes, each as a hash array
+     */
+    public function get_diff($note, $rev1, $rev2)
+    {
+        if (empty($this->bonnie_api)) {
+            return false;
+        }
+
+        list($uid, $mailbox, $msguid) = $this->_resolve_note_identity($note);
+
+        // call Bonnie API
+        $result = $this->bonnie_api->diff('note', $uid, $rev1, $rev2, $mailbox, $msguid);
+        if (is_array($result) && $result['uid'] == $uid) {
+            $result['rev1'] = $rev1;
+            $result['rev2'] = $rev2;
+
+            // convert some properties, similar to self::_client_encode()
+            $keymap = array(
+                'summary'  => 'title',
+                'lastmodified-date' => 'changed',
+            );
+
+            // map kolab object properties to keys and values the client expects
+            array_walk($result['changes'], function(&$change, $i) use ($keymap) {
+                if (array_key_exists($change['property'], $keymap)) {
+                    $change['property'] = $keymap[$change['property']];
+                }
+
+                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_);
+                    }
+                }
+
+                // compute a nice diff of note contents
+                if ($change['property'] == 'description') {
+                    $change['diff_'] = libkolab::html_diff($change['old'], $change['new']);
+                    if (!empty($change['diff_'])) {
+                        unset($change['old'], $change['new']);
+                        $change['diff_'] = preg_replace(array('!^.*<body[^>]*>!Uims','!</body>.*$!Uims'), '', $change['diff_']);
+                        $change['diff_'] = preg_replace("!</(p|li|span)>\n!", '</\\1>', $change['diff_']);
+                    }
+                }
+            });
+
+            return $result;
+        }
+
+        return false;
+    }
+
+    /**
+     * Command the backend to restore a certain revision of a note.
+     * This shall replace the current object with an older version.
+     *
+     * @param array  $note Hash array with note properties (id, list)
+     * @param mixed  $rev Revision number
+     *
+     * @return boolean True on success, False on failure
+     */
+    public function restore_revision($note, $rev)
+    {
+        if (empty($this->bonnie_api)) {
+            return false;
+        }
+
+        list($uid, $mailbox, $msguid) = $this->_resolve_note_identity($note);
+
+        $folder = $this->get_folder($note['list']);
+        $success = false;
+
+        if ($folder && ($raw_msg = $this->bonnie_api->rawdata('note', $uid, $rev, $mailbox))) {
+            $imap = $this->rc->get_storage();
+
+            // insert $raw_msg as new message
+            if ($imap->save_message($folder->name, $raw_msg, null, false)) {
+                $success = true;
+
+                // delete old revision from imap and cache
+                $imap->delete_message($msguid, $folder->name);
+                $folder->cache->set($msguid, false);
+                $this->cache = array();
+            }
+        }
+
+        return $success;
+    }
+
+    /**
+     * Helper method to resolved the given note identifier into uid and mailbox
+     *
+     * @return array (uid,mailbox,msguid) tuple
+     */
+    private function _resolve_note_identity($note)
+    {
+        $mailbox = $msguid = null;
+
+        if (!is_array($note)) {
+            $note = $this->get_note($note);
+        }
+
+        if (is_array($note)) {
+            $uid = $note['uid'] ?: $note['id'];
+            $list = $note['list'];
+        }
+        else {
+            return array(null, $mailbox, $msguid);
+        }
+
+        if ($folder = $this->get_folder($list)) {
+            $mailbox = $folder->get_mailbox_id();
+
+            // get object from storage in order to get the real object uid an msguid
+            if ($rec = $folder->get_object($uid)) {
+                $msguid = $rec['_msguid'];
+                $uid = $rec['uid'];
+            }
+        }
+
+        return array($uid, $mailbox, $msguid);
+    }
+
+
+    /**
      * Handler for client requests to list (aka folder) actions
      */
     public function list_action()
diff --git a/plugins/kolab_notes/kolab_notes_ui.php b/plugins/kolab_notes/kolab_notes_ui.php
index bbb15b2..080a586 100644
--- a/plugins/kolab_notes/kolab_notes_ui.php
+++ b/plugins/kolab_notes/kolab_notes_ui.php
@@ -51,6 +51,7 @@ class kolab_notes_ui
         $this->plugin->register_handler('plugin.notetitle', array($this, 'notetitle'));
         $this->plugin->register_handler('plugin.detailview', array($this, 'detailview'));
         $this->plugin->register_handler('plugin.attachments_list', array($this, 'attachments_list'));
+        $this->plugin->register_handler('plugin.object_changelog_table', array('libkolab', 'object_changelog_table'));
 
         $this->rc->output->include_script('list.js');
         $this->rc->output->include_script('treelist.js');
@@ -61,6 +62,7 @@ class kolab_notes_ui
         // include kolab folderlist widget if available
         if (in_array('libkolab', $this->plugin->api->loaded_plugins())) {
             $this->plugin->api->include_script('libkolab/js/folderlist.js');
+            $this->plugin->api->include_script('libkolab/js/audittrail.js');
         }
 
         // load config options and user prefs relevant for the UI
@@ -101,7 +103,7 @@ class kolab_notes_ui
 
         $this->rc->output->set_env('kolab_notes_settings', $settings);
 
-        $this->rc->output->add_label('save','cancel','delete');
+        $this->rc->output->add_label('save','cancel','delete','close');
     }
 
     public function folders($attrib)
diff --git a/plugins/kolab_notes/localization/en_US.inc b/plugins/kolab_notes/localization/en_US.inc
index e0ede7a..1c1eed0 100644
--- a/plugins/kolab_notes/localization/en_US.inc
+++ b/plugins/kolab_notes/localization/en_US.inc
@@ -56,6 +56,24 @@ $labels['invalidlistproperties'] = 'Invalid notebook properties! Please set a va
 $labels['entertitle'] = 'Please enter a title for this note!';
 $labels['aclnorights'] = 'You do not have administrator rights for this notebook.';
 
+// 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 note data';
+$labels['objectchangelognotavailable'] = 'Change history is not available for this note';
+$labels['objectdiffnotavailable'] = 'No comparison possible for the selected revisions';
+$labels['revisionrestoreconfirm'] = 'Do you really want to restore revision $rev of this note? This will replace the current note with the old version.';
+$labels['objectrestoresuccess'] = 'Revision $rev successfully restored';
+$labels['objectrestoreerror'] = 'Failed to restore the old revision';
+
+// (hidden) titles and labels for accessibility annotations
 $labels['arialabelnoteslist'] = 'List of notes';
 $labels['arialabelnotesearchform'] = 'Notes search form';
 $labels['arialabelnotesquicksearchbox'] = 'Notes search input';
diff --git a/plugins/kolab_notes/notes.js b/plugins/kolab_notes/notes.js
index 5c8a02f..eedd6c4 100644
--- a/plugins/kolab_notes/notes.js
+++ b/plugins/kolab_notes/notes.js
@@ -6,7 +6,7 @@
  * @licstart  The following is the entire license notice for the
  * JavaScript code in this file.
  *
- * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
+ * Copyright (C) 2014-2015, Kolab Systems AG <contact at 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
@@ -68,6 +68,7 @@ function rcube_kolab_notes_ui(settings)
         rcmail.register_command('reset-search', reset_search, true);
         rcmail.register_command('sendnote', send_note, false);
         rcmail.register_command('print', print_note, false);
+        rcmail.register_command('history', show_history_dialog, false);
 
         // register server callbacks
         rcmail.addEventListener('plugin.data_ready', data_ready);
@@ -84,6 +85,11 @@ function rcube_kolab_notes_ui(settings)
             }
         });
 
+        rcmail.addEventListener('plugin.close_history_dialog', close_history_dialog);
+        rcmail.addEventListener('plugin.note_render_changelog', render_changelog);
+        rcmail.addEventListener('plugin.note_show_revision', render_revision);
+        rcmail.addEventListener('plugin.note_show_diff', show_diff);
+
         // initialize folder selectors
         if (settings.selected_list && !me.notebooks[settings.selected_list]) {
             settings.selected_list = null;
@@ -209,7 +215,7 @@ function rcube_kolab_notes_ui(settings)
 
                 rcmail.enable_command('delete', me.notebooks[me.selected_list] && has_permission(me.notebooks[me.selected_list], 'td') && list.selection.length > 0);
                 rcmail.enable_command('sendnote', list.selection.length > 0);
-                rcmail.enable_command('print', list.selection.length == 1);
+                rcmail.enable_command('print', 'history', list.selection.length == 1);
             })
             .addEventListener('dragstart', function(e) {
                 folder_drop_target = null;
@@ -828,7 +834,7 @@ function rcube_kolab_notes_ui(settings)
     /**
      *
      */
-    function render_note(data, retry)
+    function render_note(data, container, temp, retry)
     {
         rcmail.set_busy(false, 'loading', ui_loading);
 
@@ -837,20 +843,25 @@ function rcube_kolab_notes_ui(settings)
             return;
         }
 
+        if (!container) {
+            container = rcmail.gui_containers['notedetailview'];
+        }
+
         var list = me.notebooks[data.list] || me.notebooks[me.selected_list] || { rights: 'lrs', editable: false };
             content = $('#notecontent').val(data.description),
             readonly = data.readonly || !(list.editable || !data.uid && has_permission(list,'i')),
-            attachmentslist = $(rcmail.gui_objects.notesattachmentslist).html('');
-        $('.notetitle', rcmail.gui_objects.noteviewtitle).val(data.title).prop('disabled', readonly).show();
-        $('.dates .notecreated', rcmail.gui_objects.noteviewtitle).html(Q(data.created || ''));
-        $('.dates .notechanged', rcmail.gui_objects.noteviewtitle).html(Q(data.changed || ''));
-        $(rcmail.gui_objects.notebooks).filter('select').val(list.id);
+            attachmentslist = gui_object('notesattachmentslist', container).html(''),
+            titlecontainer = container || rcmail.gui_objects.noteviewtitle;
+
+        $('.notetitle', titlecontainer).val(data.title).prop('disabled', readonly).show();
+        $('.dates .notecreated', titlecontainer).html(Q(data.created || ''));
+        $('.dates .notechanged', titlecontainer).html(Q(data.changed || ''));
         if (data.created || data.changed) {
-            $('.dates', rcmail.gui_objects.noteviewtitle).show();
+            $('.dates', titlecontainer).show();
         }
 
         // tag-edit line
-        var tagline = $('.tagline', rcmail.gui_objects.noteviewtitle).empty()[readonly?'addClass':'removeClass']('disabled').show();
+        var tagline = $('.tagline', titlecontainer).empty()[readonly?'addClass':'removeClass']('disabled').show();
         $.each(typeof data.tags == 'object' && data.tags.length ? data.tags : [''], function(i,val) {
             $('<input>')
                 .attr('name', 'tags[]')
@@ -867,7 +878,7 @@ function rcube_kolab_notes_ui(settings)
               .click(function(e) { $(this).parent().find('.tagedit-list').trigger('click'); });
         }
 
-        $('.tagline input.tag', rcmail.gui_objects.noteviewtitle).tagedit({
+        $('.tagline input.tag', titlecontainer).tagedit({
             animSpeed: 100,
             allowEdit: false,
             allowAdd: !readonly,
@@ -904,7 +915,7 @@ function rcube_kolab_notes_ui(settings)
         }
 
         if (!readonly) {
-            $('.tagedit-list', rcmail.gui_objects.noteviewtitle)
+            $('.tagedit-list', titlecontainer)
                 .on('click', function(){ $('.tagline .placeholder').hide(); });
         }
 
@@ -912,9 +923,13 @@ function rcube_kolab_notes_ui(settings)
             data.list = list.id;
 
         data.readonly = readonly;
-        me.selected_note = data;
-        me.selected_note.id = rcmail.html_identifier_encode(data.uid);
-        rcmail.enable_command('save', !readonly);
+
+        if (!temp) {
+            $(rcmail.gui_objects.notebooks).filter('select').val(list.id);
+            me.selected_note = data;
+            me.selected_note.id = rcmail.html_identifier_encode(data.uid);
+            rcmail.enable_command('save', !readonly);
+        }
 
         var html = data.html || data.description;
 
@@ -930,15 +945,15 @@ function rcube_kolab_notes_ui(settings)
         if (!readonly && !editor && $('#notecontent').length && retry < 5) {
           // ... give it some more time
           setTimeout(function() {
-              $(rcmail.gui_objects.noteseditform).show();
-              render_note(data, retry+1);
+              gui_object('noteseditform', container).show();
+              render_note(data, container, temp, retry+1);
           }, 200);
           return;
         }
 
         if (!readonly && editor) {
-            $(rcmail.gui_objects.notesdetailview).hide();
-            $(rcmail.gui_objects.noteseditform).show();
+            gui_object('notesdetailview', container).hide();
+            gui_object('noteseditform', container).show();
             editor.setContent(html);
             node = editor.getContentAreaContainer().childNodes[0];
             if (node) node.tabIndex = content.get(0).tabIndex;
@@ -948,15 +963,15 @@ function rcube_kolab_notes_ui(settings)
                     editor.getBody().focus();
             }
             else
-                $('.notetitle', rcmail.gui_objects.noteviewtitle).focus().select();
+                $('.notetitle', titlecontainer).focus().select();
 
             // read possibly re-formatted content back from editor for later comparison
             me.selected_note.description = editor.getContent({ format:'html' }).replace(/^\s*(<p><\/p>\n*)?/, '');
             is_html = true;
         }
         else {
-            $(rcmail.gui_objects.noteseditform).hide();
-            $(rcmail.gui_objects.notesdetailview).html(html).show();
+            gui_object('noteseditform', container).hide();
+            gui_object('notesdetailview', container).html(html).show();
         }
 
         render_no_focus = false;
@@ -971,6 +986,22 @@ function rcube_kolab_notes_ui(settings)
     }
 
     /**
+     *
+     */
+    function gui_object(name, container)
+    {
+        var elem = rcmail.gui_objects[name], selector = elem;
+        if (elem && elem.className && container) {
+            selector = '.' + String(elem.className).split(' ').join('.');
+        }
+        else if (elem && elem.id) {
+            selector = '#' + elem.id;
+        }
+
+        return $(selector, container);
+    }
+
+    /**
      * Convert the given plain text to HTML contents to be displayed in editor
      */
     function text2html(str)
@@ -990,6 +1021,193 @@ function rcube_kolab_notes_ui(settings)
     /**
      *
      */
+    function show_history_dialog()
+    {
+        var dialog, rec = me.selected_note;
+        if (!rec || !rec.uid || !window.libkolab_audittrail) {
+            return false;
+        }
+
+        // render dialog
+        $dialog = libkolab_audittrail.object_history_dialog({
+            module: 'kolab_notes',
+            container: '#notehistory',
+            title: rcmail.gettext('objectchangelog','kolab_notes') + ' - ' + rec.title,
+
+            // callback function for list actions
+            listfunc: function(action, rev) {
+                var rec = $dialog.data('rec');
+                saving_lock = rcmail.set_busy(true, 'loading', saving_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');
+                saving_lock = rcmail.set_busy(true, 'loading', saving_lock);
+                rcmail.http_post('action', { _do: 'diff', _data: { uid: rec.uid, list: rec.list, rev1: rev1, rev2: rev2 } }, saving_lock);
+            }
+        });
+
+        $dialog.data('rec', rec);
+
+        // fetch changelog data
+        saving_lock = rcmail.set_busy(true, 'loading', saving_lock);
+        rcmail.http_post('action', { _do: 'changelog', _data: { uid: rec.uid, list: rec.list } }, saving_lock);
+    }
+
+    /**
+     *
+     */
+    function render_changelog(data)
+    {
+        var $dialog = $('#notehistory'),
+            rec = $dialog.data('rec');
+
+        if (data === false || !data.length || !event) {
+          // display 'unavailable' message
+          $('<div class="notfound-message note-dialog-message warning">' + rcmail.gettext('objectchangelognotavailable','kolab_notes') + '</div>')
+              .insertBefore($dialog.find('.changelog-table').hide());
+          return;
+        }
+
+        data.module = 'kolab_notes';
+        libkolab_audittrail.render_changelog(data, rec, me.notebooks[rec.list]);
+
+        // set dialog size according to content
+        dialog_resize($dialog.get(0), $dialog.height(), 600);
+    }
+
+    /**
+     *
+     */
+    function render_revision(data)
+    {
+        data.readonly = true;
+
+        // clone view and render data into a dialog
+        var model = rcmail.gui_containers['notedetailview'],
+            container = model.clone();
+
+        container
+            .removeAttr('id style class role')
+            .find('.mce-container').remove();
+
+        // reset custom styles
+        container.children('div, form').removeAttr('id style');
+
+        // open jquery UI dialog
+        container.dialog({
+            modal: false,
+            resizable: true,
+            closeOnEscape: true,
+            title: data.title + ' @ ' + data.rev,
+            close: function() {
+                container.dialog('destroy').remove();
+            },
+            buttons: [
+                {
+                    text: rcmail.gettext('close'),
+                    click: function() { container.dialog('close'); },
+                    autofocus: true
+                }
+            ],
+            width: model.width(),
+            height: model.height(),
+            minWidth: 450,
+            minHeight: 400,
+        })
+        .show();
+
+        render_note(data, container, true);
+    }
+
+    /**
+     *
+     */
+    function show_diff(data)
+    {
+        var rec = me.selected_note,
+            $dialog = $('#notediff');
+
+        $dialog.find('div.form-section, h2.note-title-new').hide().data('set', false);
+
+        // always show title
+        $('.note-title', $dialog).text(rec.title).removeClass('diff-text-old').show();
+
+        // show each property change
+        $.each(data.changes, function(i, change) {
+            var prop = change.property, r2, html = false,
+                row = $('div.note-' + prop, $dialog).first();
+
+            // special case: title
+            if (prop == 'title') {
+                $('.note-title', $dialog).addClass('diff-text-old').text(change['old'] || '--');
+                $('.note-title-new', $dialog).text(change['new'] || '--').show();
+            }
+
+            // no display container for this property
+            if (!row.length) {
+                return true;
+            }
+
+            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();
+            }
+
+            row.show().data('set', true);
+        });
+
+        // open jquery UI dialog
+        $dialog.dialog({
+            modal: false,
+            resizable: true,
+            closeOnEscape: true,
+            title: rcmail.gettext('objectdiff','kolab_notes').replace('$rev1', data.rev1).replace('$rev2', data.rev2) + ' - ' + rec.title,
+            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);
+    }
+
+    // close the event history dialog
+    function close_history_dialog()
+    {
+        $('#notehistory, #notediff').each(function(i, elem) {
+        var $dialog = $(elem);
+        if ($dialog.is(':ui-dialog'))
+            $dialog.dialog('close');
+        });
+    };
+
+    /**
+     *
+     */
     function remove_link(elem, uri)
     {
         // remove the link item matching the given uri
@@ -1176,6 +1394,7 @@ function rcube_kolab_notes_ui(settings)
      */
     function reset_view()
     {
+        close_history_dialog();
         me.selected_note = null;
         $('.notetitle', rcmail.gui_objects.noteviewtitle).val('').hide();
         $('.tagline, .dates', rcmail.gui_objects.noteviewtitle).hide();
@@ -1491,6 +1710,14 @@ function rcube_kolab_notes_ui(settings)
         }
     }
 
+    // resize and reposition (center) the dialog window
+    function dialog_resize(id, height, width)
+    {
+        var win = $(window), w = win.width(), h = win.height();
+            $(id).dialog('option', { height: Math.min(h-20, height+130), width: Math.min(w-20, width+50) })
+                .dialog('option', 'position', ['center', 'center']);  // only works in a separate call (!?)
+    }
+
 }
 
 
diff --git a/plugins/kolab_notes/skins/larry/notes.css b/plugins/kolab_notes/skins/larry/notes.css
index 0c18e8b..2c18762 100644
--- a/plugins/kolab_notes/skins/larry/notes.css
+++ b/plugins/kolab_notes/skins/larry/notes.css
@@ -1,7 +1,7 @@
 /**
  * Kolab Notes plugin styles for skin "Larry"
  *
- * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
+ * Copyright (C) 2014-2015, Kolab Systems AG <contact at kolabsys.com>
  * Screendesign by FLINT / Büro für Gestaltung, bueroflint.com
  *
  * The contents are subject to the Creative Commons Attribution-ShareAlike
@@ -147,8 +147,45 @@
 	background: #f9f9f9;
 }
 
-.notesview #noteform,
-.notesview #notedetails {
+.notesview #notedetailsbox .footerright {
+	float: right;
+}
+
+.notesview #notedetailsbox .formbuttons:after {
+	content: "";
+	display: inline;
+	clear: both;
+}
+
+.notesview .btn-note-history {
+	display: inline-block;
+	padding: 2px;
+	text-decoration: none;
+	visibility: hidden;
+}
+
+.notesview .btn-note-history.active {
+	visibility: visible;
+	color: #333;
+}
+
+.notesview .btn-note-history:before {
+	content: "";
+	display: inline-block;
+	position: relative;
+	top: 4px;
+	width: 16px;
+	height: 16px;
+	margin-right: 4px;
+	background: url('sprites.png') -1px -302px no-repeat;
+}
+
+.notesview .btn-note-history.active:hover {
+	text-decoration: underline;
+}
+
+.notesview .noteform,
+.notesview .notedetails {
 	display: none;
 	position: absolute;
 	top: 82px;
@@ -157,7 +194,7 @@
 	width: 100%;
 }
 
-.notesview #notedetails {
+.notesview .notedetails {
 	padding: 8px;
 	-webkit-box-sizing: border-box;
 	   -moz-box-sizing: border-box;
@@ -167,12 +204,12 @@
 	background: #fff;
 }
 
-.notesdialog #noteform,
-.notesdialog #notedetails {
+.notesdialog .noteform,
+.notesdialog .notedetails {
 	bottom: 30px;
 }
 
-.notesview #notedetails pre {
+.notesview .notedetails pre {
 	font-family: "Lucida Grande", Verdana,  Arial, Helvetica, sans-serif;
 	font-size: 12px;
 	margin: 0;
@@ -211,13 +248,35 @@
 	border: 0;
 }
 
-.notesview #notedetailstitle {
+.notesview .ui-dialog-content .noteform,
+.notesview .ui-dialog-content .notedetails,
+.notesview .ui-dialog-content .notereferences {
+	position: relative;
+	width: auto;
+	height: auto;
+	top: auto;
+	bottom: auto;
+	overflow: visible;
+	border: 0;
+}
+
+.notesview .notetitle {
 	height: auto;
 	min-height: 20px;
 }
 
-.notesview #notedetailstitle .disabled .tagedit-list,
-.notesview #notedetailstitle input.inline-edit:disabled {
+.notesview .ui-dialog-content .notetitle {
+	padding: 0 0 6px 0;
+	margin-top: -6px;
+	border-bottom: 0;
+}
+
+.notesview .ui-dialog-content .tagline {
+	display: none !important;
+}
+
+.notesview .notetitle .disabled .tagedit-list,
+.notesview .notetitle input.inline-edit:disabled {
 	outline: none;
 	padding-left: 0;
 	border: 0;
@@ -228,8 +287,8 @@
 	        box-shadow: none;
 }
 
-.notesview #notedetailstitle input.notetitle,
-.notesview #notedetailstitle input.notetitle:focus {
+.notesview .notetitle input.notetitle,
+.notesview .notetitle input.notetitle:focus {
 	width: 100%;
 	font-size: 14px;
 	font-weight: bold;
@@ -240,8 +299,13 @@
 	        box-sizing: border-box;
 }
 
-.notesview #notedetailstitle .dates,
-.notesview #notedetailstitle .tagline,
+.notesview .ui-dialog-content .formbuttons,
+.notesview .ui-dialog-content .notetitle input {
+	display: none !important;
+}
+
+.notesview .notetitle .dates,
+.notesview .notetitle .tagline,
 .notesdialog .notebookselect label {
 	color: #999;
 	font-weight: normal;
@@ -253,24 +317,28 @@
 	margin-top: 4px;
 }
 
-.notesview #notedetailstitle .tagline {
+.notesview .notetitle .tagline {
 	position: relative;
 	cursor: text;
 	margin: 4px 0 0 0;
 }
 
-.notesview #notedetailstitle .tagline.disabled {
+.notesview .notetitle .tagline.disabled {
 	margin-top: 0;
 }
 
-.notesview #notedetailstitle .tagline .placeholder {
+.notesview .notetitle .tagline .placeholder {
 	position: absolute;
 	top: 6px;
 	left: 6px;
 	z-index: 2;
 }
 
-.notesview #notedetailstitle .tagedit-list {
+.notesview .notetitle .tagline.disabled .placeholder {
+	left: 0;
+}
+
+.notesview .notetitle .tagedit-list {
 	position: relative;
 	z-index: 1;
 	min-height: 32px;
@@ -282,15 +350,15 @@
 }
 
 /* Firefox 3.6 */
-_:not(), _:-moz-handler-blocked, .notesview #notedetailstitle .tagedit-list {
+_:not(), _:-moz-handler-blocked, .notesview .notetitle .tagedit-list {
 	min-height: 26px;
 }
 
-.notesview #notedetailstitle .disabled .tagedit-list {
+.notesview .notetitle .disabled .tagedit-list {
 	min-height: 26px;
 }
 
-.notesview #notedetailstitle #tagedit-input {
+.notesview .notetitle #tagedit-input {
 	background: none;
 }
 
@@ -298,15 +366,15 @@ _:not(), _:-moz-handler-blocked, .notesview #notedetailstitle .tagedit-list {
 	z-index: 1000;
 }
 
-.notesview #notedetailstitle .notecreated,
-.notesview #notedetailstitle .notechanged {
+.notesview .notetitle .notecreated,
+.notesview .notetitle .notechanged {
 	display: inline-block;
 	padding-left: 0.4em;
 	padding-right: 2em;
 	color: #777;
 }
 
-.notesview #notereferences {
+.notesview .notereferences {
 	position: absolute;
 	left: 0;
 	right: 0;
@@ -316,10 +384,41 @@ _:not(), _:-moz-handler-blocked, .notesview #notedetailstitle .tagedit-list {
 	padding-left: 10px;
 }
 
-.notesdialog #notereferences {
+.notesdialog .notereferences {
 	bottom: 0;
 }
 
+.notesview #notediff .note-title,
+.notesview #notediff .note-title-new {
+	margin-top: 0;
+}
+
+.notesview .note-title.diff-text-old {
+	margin-bottom: 0;
+}
+
+.notesview .diff-text-diff del,
+.notesview .diff-text-diff ins {
+	text-decoration: none;
+	color: inherit;
+}
+
+.notesview .diff-text-old,
+.notesview .diff-text-diff del {
+	background-color: #fdd;
+	text-decoration: line-through;
+}
+
+.notesview .diff-text-new,
+.notesview .diff-text-diff ins,
+.notesview .diff-text-diff .diffmod img {
+	background-color: #dfd;
+}
+
+.notesview .diff-text-diff img {
+	border: 1px solid #999;
+}
+
 .notesview #notebooksbox .scroller {
 	top: 34px;
 }
diff --git a/plugins/kolab_notes/skins/larry/sprites.png b/plugins/kolab_notes/skins/larry/sprites.png
index acb32d7..1dc4e81 100644
Binary files a/plugins/kolab_notes/skins/larry/sprites.png and b/plugins/kolab_notes/skins/larry/sprites.png differ
diff --git a/plugins/kolab_notes/skins/larry/templates/notes.html b/plugins/kolab_notes/skins/larry/templates/notes.html
index 6cfcb71..88cb4e4 100644
--- a/plugins/kolab_notes/skins/larry/templates/notes.html
+++ b/plugins/kolab_notes/skins/larry/templates/notes.html
@@ -86,22 +86,56 @@
 
         <div id="notedetailsbox" class="uibox contentbox" role="main" aria-labelledby="aria-label-noteform">
             <h3 id="aria-label-noteform" class="voice"><roundcube:label name="kolab_notes.arialabelnoteform" /></h3>
-            <roundcube:object name="plugin.notetitle" id="notedetailstitle" class="boxtitle" />
-            <roundcube:object name="plugin.editform" id="noteform" />
-            <roundcube:object name="plugin.detailview" id="notedetails" class="scroller" />
-            <div id="notereferences">
+            <roundcube:object name="plugin.notetitle" id="notedetailstitle" class="notetitle boxtitle" />
+            <roundcube:object name="plugin.editform" id="noteform" class="noteform" />
+            <roundcube:object name="plugin.detailview" id="notedetails" class="notedetails scroller" />
+            <div id="notereferences" class="notereferences">
                 <h3 id="aria-label-messagereferences" class="voice"><roundcube:label name="kolab_notes.arialabelmessagereferences" /></h3>
                 <roundcube:object name="plugin.attachments_list" id="attachment-list" class="attachmentslist" role="region" aria-labelledby="aria-label-messagereferences" />
             </div>
-            <div class="footerleft formbuttons">
+            <div class="formbuttons">
                 <roundcube:button command="save" type="input" class="button mainaction" label="save" id="btn-save-note" />
+                <div class="footerright">
+                <roundcube:if condition="config:kolab_bonnie_api" />
+                    <roundcube:button command="history" type="link" label="kolab_notes.showhistory" class="btn-note-history" classAct="btn-note-history active" />
+                <roundcube:endif />
+                </div>
             </div>
+            <roundcube:container name="notedetailview" id="notedetailsbox" />
         </div>
     </div>
 
     </div>
 </div>
 
+<roundcube:if condition="config:kolab_bonnie_api" />
+<div id="notehistory" class="uidialog" aria-hidden="true">
+    <roundcube:object name="plugin.object_changelog_table" class="records-table changelog-table" domain="calendar" />
+    <div class="compare-button"><input type="button" class="button" value="↳ <roundcube:label name='kolab_notes.compare' />" /></div>
+</div>
+
+<div id="notediff" class="uidialog contentbox" aria-hidden="true">
+	<h2 class="note-title">Note Title</h2>
+	<h2 class="note-title-new diff-text-new"></h2>
+
+	<div class="form-section note-tags">
+		<span class="diff-text-old"></span> ⇢
+		<span class="diff-text-new"></span>
+	</div>
+
+	<div class="form-section note-description">
+		<div class="diff-text-diff" style="white-space:pre-wrap"></div>
+		<div class="diff-text-old"></div>
+		<div class="diff-text-new"></div>
+	</div>
+
+	<div class="form-section notereferences">
+		<div class="diff-text-old"></div>
+		<div class="diff-text-new"></div>
+	</div>
+</div>
+<roundcube:endif />
+
 <roundcube:object name="message" id="messagestack" />
 
 <div id="notessortmenu" class="popupmenu" aria-hidden="true">





More information about the commits mailing list