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