Implement audit trail for notes (#4904)

This commit is contained in:
Thomas Bruederli 2015-03-31 14:57:57 +02:00
parent dfa8e1e4de
commit ae6ec80e44
7 changed files with 681 additions and 56 deletions

View file

@ -8,7 +8,7 @@
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
* Copyright (C) 2014-2015, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@ -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');
}
@ -762,6 +821,192 @@ class kolab_notes extends rcube_plugin
return $status;
}
/**
* 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
*/

View file

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

View file

@ -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';

View file

@ -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@kolabsys.com>
* Copyright (C) 2014-2015, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@ -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;
@ -970,6 +985,22 @@ function rcube_kolab_notes_ui(settings)
$(window).resize();
}
/**
*
*/
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
*/
@ -987,6 +1018,193 @@ function rcube_kolab_notes_ui(settings)
return '<pre>' + Q(str).replace(link_pattern, link_replace) + '</pre>';
}
/**
*
*/
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');
});
};
/**
*
*/
@ -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 (!?)
}
}

View file

@ -1,7 +1,7 @@
/**
* Kolab Notes plugin styles for skin "Larry"
*
* Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
* Copyright (C) 2014-2015, Kolab Systems AG <contact@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;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -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> &#8674;
<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">