Add UI elements to display the history of a calendar event with data from the Bonnie API (#3093, #3094) + new option to download and send single events

This commit is contained in:
Thomas Bruederli 2014-07-29 15:28:35 +02:00
parent f3b31c863d
commit a68982b028
15 changed files with 1163 additions and 110 deletions

View file

@ -194,6 +194,7 @@ class calendar extends rcube_plugin
}
$this->add_hook('messages_list', array($this, 'mail_messages_list'));
$this->add_hook('message_compose', array($this, 'mail_message_compose'));
}
else if ($args['task'] == 'addressbook') {
if ($this->rc->config->get('calendar_contact_birthdays')) {
@ -973,6 +974,95 @@ class calendar extends rcube_plugin
$success |= $this->driver->dismiss_alarm(substr($id, 4), $event['snooze']);
}
break;
case "changelog":
$data = $this->driver->get_event_changelog($event);
if (is_array($data) && !empty($data)) {
$lib = $this->lib;
array_walk($data, function($change) use ($lib) {
if ($change['date']) {
$dt = $lib->adjust_timezone($change['date']);
if ($dt instanceof DateTime)
$change['date'] = $dt->format('c');
}
});
$this->rc->output->command('plugin.render_event_changelog', $data);
}
else {
$this->rc->output->command('plugin.render_event_changelog', false);
$this->rc->output->command('display_message', $this->gettext('eventchangelognotavailable'), 'error');
}
$got_msg = true;
$reload = false;
break;
case "diff":
$data = $this->driver->get_event_diff($event, $event['rev']);
if (is_array($data)) {
// convert some properties, similar to self::_client_event()
$lib = $this->lib;
array_walk($data['changes'], function(&$change, $i) use ($event, $lib) {
// convert date cols
foreach (array('start','end','created','changed') as $col) {
if ($change['property'] == $col) {
$change['old'] = $this->lib->adjust_timezone($change['old'], strlen($change['old']) == 10)->format('c');
$change['new'] = $this->lib->adjust_timezone($change['new'], strlen($change['new']) == 10)->format('c');
}
}
// create textual representation for alarms and recurrence
if ($change['property'] == 'alarms') {
if (is_array($change['old']))
$change['old_'] = libcalendaring::alarm_text($change['old']);
if (is_array($change['new']))
$change['new_'] = libcalendaring::alarm_text(array_merge((array)$change['old'], $change['new']));
}
if ($change['property'] == 'recurrence') {
if (is_array($change['old']))
$change['old_'] = $lib->recurrence_text($change['old']);
if (is_array($change['new']))
$change['new_'] = $lib->recurrence_text(array_merge((array)$change['old'], $change['new']));
}
if ($change['property'] == 'attachments') {
if (is_array($change['old']))
$change['old']['classname'] = rcube_utils::file2class($change['old']['mimetype'], $change['old']['name']);
if (is_array($change['new']))
$change['new']['classname'] = rcube_utils::file2class($change['new']['mimetype'], $change['new']['name']);
}
// compute a nice diff of description texts
if ($change['property'] == 'description') {
$change['diff_'] = libkolab::html_diff($change['old'], $change['new']);
}
});
$this->rc->output->command('plugin.event_show_diff', $data);
}
else {
$this->rc->output->command('display_message', $this->gettext('eventdiffnotavailable'), 'error');
}
$got_msg = true;
$reload = false;
break;
case "show":
if ($event = $this->driver->get_event_revison($event, $event['rev'])) {
$this->rc->output->command('plugin.event_show_revision', $this->_client_event($event));
}
else {
$this->rc->output->command('display_message', $this->gettext('eventnotfound'), 'error');
}
$got_msg = true;
$reload = false;
break;
case "restore":
if ($success = $this->driver->restore_event_revision($event, $event['rev'])) {
}
else {
$this->rc->output->command('display_message', 'Not implemented yet', 'error');
$got_msg = true;
}
$reload = false;
break;
}
// show confirmation/error message
@ -1271,22 +1361,33 @@ class calendar extends rcube_plugin
if (!is_numeric($end))
$end = strtotime($end . ' 23:59:59');
$event_id = get_input_value('id', RCUBE_INPUT_GET);
$attachments = get_input_value('attachments', RCUBE_INPUT_GET);
$calid = $calname = get_input_value('source', RCUBE_INPUT_GET);
$calid = $filename = get_input_value('source', RCUBE_INPUT_GET);
$calendars = $this->driver->list_calendars();
$events = array();
if ($calendars[$calid]) {
$calname = $calendars[$calid]['name'] ? $calendars[$calid]['name'] : $calid;
$calname = preg_replace('/[^a-z0-9_.-]/i', '', html_entity_decode($calname)); // to 7bit ascii
if (empty($calname)) $calname = $calid;
$events = $this->driver->load_events($start, $end, null, $calid, 0);
$filename = $calendars[$calid]['name'] ? $calendars[$calid]['name'] : $calid;
$filename = asciiwords(html_entity_decode($filename)); // to 7bit ascii
if (!empty($event_id)) {
if ($event = $this->driver->get_event(array('calendar' => $calid, 'id' => $event_id))) {
$events = array($event);
$filename = asciiwords($event['title']);
if (empty($filename))
$filename = 'event';
}
}
else {
$events = $this->driver->load_events($start, $end, null, $calid, 0);
if (empty($filename))
$filename = $calid;
}
}
else
$events = array();
header("Content-Type: text/calendar");
header("Content-Disposition: inline; filename=".$calname.'.ics');
header("Content-Disposition: inline; filename=".$filename.'.ics');
$this->get_ical()->export($events, '', true, $attachments ? array($this->driver, 'get_attachment_body') : null);
@ -2672,6 +2773,34 @@ class calendar extends rcube_plugin
$this->rc->output->send();
}
/**
* Handler for the 'message_compose' plugin hook. This will check for
* a compose parameter 'calendar_event' and create an attachment with the
* referenced event in iCal format
*/
public function mail_message_compose($args)
{
// set the submitted event ID as attachment
if (!empty($args['param']['calendar_event'])) {
$this->load_driver();
list($cal, $id) = explode(':', $args['param']['calendar_event'], 2);
if ($event = $this->driver->get_event(array('id' => $id, 'calendar' => $cal))) {
$filename = asciiwords($event['title']);
if (empty($filename))
$filename = 'event';
// save ics to a temp file and register as attachment
$tmp_path = tempnam($this->rc->config->get('temp_dir'), 'rcmAttmntCal');
file_put_contents($tmp_path, $this->get_ical()->export(array($event), '', false, array($this->driver, 'get_attachment_body')));
$args['attachments'][] = array('path' => $tmp_path, 'name' => $filename . '.ics', 'mimetype' => 'text/calendar');
$args['param']['subject'] = $event['title'];
}
}
return $args;
}
/**
* Checks if specified message part is a vcalendar data

View file

@ -252,6 +252,11 @@ function rcube_calendar_ui(settings)
return date2servertime(date).replace(/[^0-9]/g, '').substr(0, (dateonly ? 8 : 14));
}
var format_datetime = function(date, mode, voice)
{
return me.format_datetime(date, mode, voice);
}
var render_link = function(url)
{
var islink = false, href = url;
@ -304,11 +309,13 @@ function rcube_calendar_ui(settings)
var load_attachment = function(event, att)
{
var qstring = '_id='+urlencode(att.id)+'&_event='+urlencode(event.recurrence_id||event.id)+'&_cal='+urlencode(event.calendar);
var query = { _id: att.id, _event: event.recurrence_id || event.id, _cal:event.calendar, _frame: 1 };
if (event.rev)
query._rev = event.rev;
// open attachment in frame if it's of a supported mimetype
if (id && att.mimetype && $.inArray(att.mimetype, settings.mimetypes)>=0) {
if (rcmail.open_window(rcmail.env.comm_path+'&_action=get-attachment&'+qstring+'&_frame=1', true, true)) {
if (rcmail.open_window(rcmail.url('get-attachment', query), true, true)) {
return;
}
}
@ -378,15 +385,23 @@ function rcube_calendar_ui(settings)
};
// event details dialog (show only)
var event_show_dialog = function(event, ev)
var event_show_dialog = function(event, ev, temp)
{
var $dialog = $("#eventshow").attr('class', 'uidialog');
var $dialog = $("#eventshow");
var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:false };
me.selected_event = event;
if (!temp)
me.selected_event = event;
if ($dialog.is(':ui-dialog'))
$dialog.dialog('close');
// convert start/end dates if not done yet by fullcalendar
if (typeof event.start == 'string')
event.start = parseISO8601(event.start);
if (typeof event.end == 'string')
event.end = parseISO8601(event.end);
// allow other plugins to do actions when event form is opened
rcmail.triggerEvent('calendar-event-init', {o: event});
@ -430,6 +445,13 @@ function rcube_calendar_ui(settings)
$('#event-sensitivity').show().children('.event-text').html(Q(sensitivitylabels[event.sensitivity]));
$dialog.addClass('sensitivity-'+event.sensitivity);
}
if (event.created || event.changed) {
var created = parseISO8601(event.created),
changed = parseISO8601(event.changed)
$('#event-created-changed .event-created').html(Q(created ? format_datetime(created) : rcmail.gettext('unknown','calendar')))
$('#event-created-changed .event-changed').html(Q(changed ? format_datetime(changed) : rcmail.gettext('unknown','calendar')))
$('#event-created-changed').show()
}
// create attachments list
if ($.isArray(event.attachments)) {
@ -451,14 +473,10 @@ function rcube_calendar_ui(settings)
return (j - k);
});
var data, dispname, tooltip, organizer = false, rsvp = false, mystatus = null, line, morelink, html = '',overflow = '';
var data, mystatus = null, rsvp, line, morelink, html = '', overflow = '';
for (var j=0; j < event.attendees.length; j++) {
data = event.attendees[j];
dispname = Q(data.name || data.email);
tooltip = '';
if (data.email) {
tooltip = data.email;
dispname = '<a href="mailto:' + data.email + '" class="mailtolink" data-cutype="' + data.cutype + '">' + dispname + '</a>';
if (data.role == 'ORGANIZER')
organizer = true;
else if (settings.identity.emails.indexOf(';'+data.email) >= 0) {
@ -467,18 +485,14 @@ function rcube_calendar_ui(settings)
rsvp = mystatus;
}
}
if (data['delegated-to'])
tooltip = rcmail.gettext('delegatedto', 'calendar') + data['delegated-to'];
else if (data['delegated-from'])
tooltip = rcmail.gettext('delegatedfrom', 'calendar') + data['delegated-from'];
line = '<span class="attendee ' + String(data.role == 'ORGANIZER' ? 'organizer' : data.status).toLowerCase() + '" title="' + Q(tooltip) + '">' + dispname + '</span> ';
line = event_attendee_html(data);
if (morelink)
overflow += line;
else
html += line;
// stop listing attendees
if (j == 7 && event.attendees.length >= 7) {
morelink = $('<a href="#more" class="morelink"></a>').html(rcmail.gettext('andnmore', 'calendar').replace('$nr', event.attendees.length - j - 1));
@ -522,7 +536,7 @@ function rcube_calendar_ui(settings)
}
var buttons = {};
if (calendar.editable && event.editable !== false) {
if (!temp && calendar.editable && event.editable !== false) {
buttons[rcmail.gettext('edit', 'calendar')] = function() {
event_edit_dialog('edit', event);
};
@ -551,6 +565,13 @@ function rcube_calendar_ui(settings)
},
close: function() {
$dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
rcmail.command('menu-close','eventoptionsmenu')
},
dragStart: function() {
rcmail.command('menu-close','eventoptionsmenu')
},
resizeStart: function() {
rcmail.command('menu-close','eventoptionsmenu')
},
buttons: buttons,
minWidth: 320,
@ -566,15 +587,38 @@ function rcube_calendar_ui(settings)
// set dialog size according to content
me.dialog_resize($dialog.get(0), $dialog.height(), 420);
/*
// add link for "more options" drop-down
$('<a>')
.attr('href', '#')
.html('More Options')
.addClass('dropdown-link')
.click(function(){ return false; })
.insertBefore($dialog.parent().find('.ui-dialog-buttonset').children().first());
*/
if (!temp) {
$('<a>')
.attr('href', '#')
.html(rcmail.gettext('eventoptions','calendar'))
.addClass('dropdown-link')
.click(function(e) {
return rcmail.command('menu-open','eventoptionsmenu', this, e)
})
.appendTo($dialog.parent().find('.ui-dialog-buttonset'));
}
rcmail.enable_command('event-history', calendar.history)
};
// render HTML code for displaying an attendee record
var event_attendee_html = function(data)
{
var dispname = Q(data.name || data.email), tooltip = '';
if (data.email) {
tooltip = data.email;
dispname = '<a href="mailto:' + data.email + '" class="mailtolink" data-cutype="' + data.cutype + '">' + dispname + '</a>';
}
if (data['delegated-to'])
tooltip = rcmail.gettext('delegatedto', 'calendar') + data['delegated-to'];
else if (data['delegated-from'])
tooltip = rcmail.gettext('delegatedfrom', 'calendar') + data['delegated-from'];
return '<span class="attendee ' + String(data.role == 'ORGANIZER' ? 'organizer' : data.status).toLowerCase() + '" title="' + Q(tooltip) + '">' + dispname + '</span> ';
};
// event handler for clicks on an attendee link
@ -586,7 +630,7 @@ function rcube_calendar_ui(settings)
event_resources_dialog(mailto);
}
else {
rcmail.redirect(rcmail.url('mail/compose', { _to:mailto }));
rcmail.command('compose', mailto, e ? e.target : null, e);
}
return false;
};
@ -876,6 +920,291 @@ function rcube_calendar_ui(settings)
window.setTimeout(load_attachments_tab, exec_deferred);
};
// show event changelog in a dialog
var event_history_dialog = function(event)
{
if (!event.id)
return false
// render dialog
$dialog = $('#eventhistory');
// close show dialog first
if ($dialog.is(':ui-dialog'))
$dialog.dialog('close');
var buttons = {};
buttons[rcmail.gettext('close', 'calendar')] = function() {
$dialog.dialog('close');
};
// hide and reset changelog table
$('#event-changelog-table').children('tbody')
.html('<tr><td colspan="6"><span class="loading">'+ rcmail.gettext('loading') +'</span></td></tr>');
// open jquery UI dialog
$dialog.dialog({
modal: false,
resizable: true,
closeOnEscape: true,
title: rcmail.gettext('eventchangelog','calendar') + ' - ' + event.title + ', ' + me.event_date_text(event),
open: function() {
$dialog.attr('aria-hidden', 'false');
setTimeout(function(){
$dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
}, 5);
},
close: function() {
$dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
},
buttons: buttons,
minWidth: 450,
width: 650,
height: 350,
minHeight: 200,
})
.data('event', event)
.show().children('.compare-button').hide();
// set dialog size according to content
// me.dialog_resize($dialog.get(0), $dialog.height(), 650);
// fetch changelog data
me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
rcmail.http_post('event', { action:'changelog', e:{ id:event.id, calendar:event.calendar } }, me.loading_lock);
// initialize event handlers for history dialog UI elements
if (!$dialog.data('initialized')) {
// compare button
$dialog.find('.compare-button input').click(function(e) {
var rev1 = $('#event-changelog-table input.diff-rev1:checked').val(),
rev2 = $('#event-changelog-table input.diff-rev2:checked').val(),
event = $('#eventhistory').data('event');
if (rev1 && rev2 && rev1 != rev2) {
// swap revisions if the user got it wrong
if (rev1 > rev2) {
var tmp = rev2;
rev2 = rev1;
rev1 = tmp;
}
me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
rcmail.http_post('event', { action:'diff', e:{ id:event.id, calendar:event.calendar, rev: rev1+':'+rev2 } }, me.loading_lock);
}
else {
alert('Invalid selection!')
}
});
// delegate handlers for list actions
$('#event-changelog-table tbody').on('click', 'td.actions a', function(e) {
var link = $(this),
action = link.hasClass('restore') ? 'restore' : 'show',
event = $('#eventhistory').data('event'),
rev = link.attr('data-rev');
// ignore clicks on first row (current revision)
if (link.closest('tr').hasClass('first')) {
return false;
}
// let the user confirm the restore action
if (action == 'restore' && !confirm(rcmail.gettext('eventrestoreconfirm','calendar').replace('$rev', rev))) {
return false;
}
me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
rcmail.http_post('event', { action:action, e:{ id:event.id, calendar:event.calendar, rev: rev } }, me.loading_lock);
return false;
});
$dialog.data('initialized', true);
}
};
// callback from server with changelog data
var render_event_changelog = function(data)
{
var $dialog = $('#eventhistory');
if (data === false || !data.length) {
$dialog.dialog('close');
return
}
var i, change, accessible, op_append, first = data.length -1, last = 0,
op_labels = { APPEND: 'actionappend', MOVE: 'actionmove', DELETE: 'actiondelete' },
actions = '<a href="#show" class="iconbutton preview" title="'+ rcmail.gettext('showrevision','calendar') +'" data-rev="{rev}" /> ' +
'<a href="#restore" class="iconbutton restore" title="'+ rcmail.gettext('restore','calendar') + '" data-rev="{rev}" />',
tbody = $('#event-changelog-table tbody').html('');
for (i=first; i >= 0; i--) {
change = data[i];
accessible = change.date && change.user;
if (change.op == 'MOVE' && change.folder) {
op_append = ' ⇢ ' + change.folder;
}
else {
op_append = '';
}
$('<tr class="' + (i == last ? 'last' : (i == first ? 'first' : '')) + (accessible ? '' : 'undisclosed') + '">')
.append('<td class="diff">' + (accessible && change.op != 'DELETE' ?
'<input type="radio" name="rev1" class="diff-rev1" value="' + change.rev + '" title="" '+ (i == last ? 'checked="checked"' : '') +' /> '+
'<input type="radio" name="rev2" class="diff-rev2" value="' + change.rev + '" title="" '+ (i == first ? 'checked="checked"' : '') +' /></td>'
: ''))
.append('<td class="revision">' + Q(change.rev) + '</td>')
.append('<td class="date">' + Q(change.date ? format_datetime(parseISO8601(change.date)) : '') + '</td>')
.append('<td class="user">' + Q(change.user || 'undisclosed') + '</td>')
.append('<td class="operation" title="' + op_append + '">' + Q(rcmail.gettext(op_labels[change.op] || '', 'calendar') + (op_append ? ' ...' : '')) + '</td>')
.append('<td class="actions">' + (accessible && change.op != 'DELETE' ? actions.replace(/\{rev\}/g, change.rev) : '') + '</td>')
.appendTo(tbody);
}
$('#eventhistory .compare-button').fadeIn(200);
// set dialog size according to content
me.dialog_resize($dialog.get(0), $dialog.height(), 600);
};
// callback from server with event diff data
var event_show_diff = function(data)
{
var event = me.selected_event,
$dialog = $("#eventdiff");
$dialog.find('div.event-section, div.event-line, h1.event-title-new').hide().data('set', false).find('.index').html('');
$dialog.find('div.event-section.clone, div.event-line.clone').remove();
// always show event title and date
$('.event-title', $dialog).html(Q(event.title)).removeClass('event-text-old').show();
$('.event-date', $dialog).html(Q(me.event_date_text(event))).show();
// show each property change
$.each(data.changes, function(i,change) {
var prop = change.property, r2, html = false,
row = $('div.event-' + prop, $dialog).first();
// special case: title
if (prop == 'title') {
$('.event-title', $dialog).addClass('event-text-old').html(Q(change.old || '--'));
$('.event-title-new', $dialog).html(Q(change.new || '--')).show();
}
// no display container for this property
if (!row.length) {
return true;
}
// clone row if already exists
if (row.data('set')) {
r2 = row.clone().addClass('clone').insertAfter(row);
row = r2;
}
// format dates
if (['start','end','changed'].indexOf(prop) >= 0) {
if (change.old) change.old_ = me.format_datetime(parseISO8601(change.old));
if (change.new) change.new_ = me.format_datetime(parseISO8601(change.new));
}
// render description text
else if (prop == 'description') {
// TODO: show real text diff
if (!change.diff_ && change.old) change.old_ = text2html(change.old);
if (!change.diff_ && change.new) change.new_ = text2html(change.new);
html = true;
}
// format attendees struct
else if (prop == 'attendees') {
if (change.old) change.old_ = event_attendee_html(change.old);
if (change.new) change.new_ = event_attendee_html($.extend({}, change.old || {}, change.new));
html = true;
}
// localize priority values
else if (prop == 'priority') {
var priolabels = [ '', rcmail.gettext('highest'), rcmail.gettext('high'), '', '', rcmail.gettext('normal'), '', '', rcmail.gettext('low'), rcmail.gettext('lowest') ];
if (change.old) change.old_ = change.old + ' ' + (priolabels[change.old] || '');
if (change.new) change.new_ = change.new + ' ' + (priolabels[change.new] || '');
}
// localize status
else if (prop == 'status') {
var status_lc = String(event.status).toLowerCase();
if (change.old) change.old_ = rcmail.gettext(String(change.old).toLowerCase(), 'calendar');
if (change.new) change.new_ = rcmail.gettext(String(change.new).toLowerCase(), 'calendar');
}
// format attachments struct
if (prop == 'attachments') {
if (change.old) event_show_attachments([change.old], row.children('.event-text-old'), event, false);
else row.children('.event-text-old').html('--');
if (change.new) event_show_attachments([$.extend({}, change.old || {}, change.new)], row.children('.event-text-new'), event, false);
else row.children('.event-text-new').html('--');
// remove click handler as we're currentyl not able to display the according attachment contents
$('.attachmentslist li a', row).unbind('click').removeAttr('href');
}
else if (change.diff_) {
row.children('.event-text-diff').html(change.diff_);
row.children('.event-text-old, .event-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('.event-text-old').html(change.old_ || change.old || '--');
row.children('.event-text-new').html(change.new_ || change.new || '--');
}
// display index number
if (typeof change.index != 'undefined') {
row.find('.index').html('(' + change.index + ')');
}
row.show().data('set', true);
// hide event-date line
if (prop == 'start' || prop == 'end')
$('.event-date', $dialog).hide();
});
var buttons = {};
buttons[rcmail.gettext('close', 'calendar')] = function() {
$dialog.dialog('close');
};
// open jquery UI dialog
$dialog.dialog({
modal: false,
resizable: true,
closeOnEscape: true,
title: rcmail.gettext('eventdiff','calendar').replace('$rev', data.rev) + ' - ' + event.title,
open: function() {
$dialog.attr('aria-hidden', 'false');
setTimeout(function(){
$dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
}, 5);
},
close: function() {
$dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
},
buttons: buttons,
minWidth: 320,
width: 450
}).show();
// set dialog size according to content
me.dialog_resize($dialog.get(0), $dialog.height(), 400);
};
// exports
this.event_show_diff = event_show_diff;
this.event_show_dialog = event_show_dialog;
this.event_history_dialog = event_history_dialog;
this.render_event_changelog = render_event_changelog;
// open a dialog to display detailed free-busy information and to find free slots
var event_freebusy_dialog = function()
{
@ -2055,7 +2384,7 @@ function rcube_calendar_ui(settings)
var dialog_check = function(e)
{
var showd = $("#eventshow");
if (showd.is(':visible') && !$(e.target).closest('.ui-dialog').length) {
if (showd.is(':visible') && !$(e.target).closest('.ui-dialog').length && !$(e.target).closest('.popupmenu').length) {
showd.dialog('close');
e.stopImmediatePropagation();
ignore_click = true;
@ -2589,6 +2918,23 @@ function rcube_calendar_ui(settings)
};
// download the selected event as iCal
this.event_download = function(event)
{
if (event && event.id) {
rcmail.goto_url('export_events', { source:event.calendar, id:event.id, attachments:1 });
}
};
// open the message compose step with a calendar_event parameter referencing the selected event.
// the server-side plugin hook will pick that up and attach the event to the message.
this.event_sendbymail = function(event, e)
{
if (event && event.id) {
rcmail.command('compose', { _calendar_event:event._id }, e ? e.target : null, e);
}
};
// show URL of the given calendar in a dialog box
this.showurl = function(calendar)
{
@ -3452,9 +3798,11 @@ function rcube_calendar_ui(settings)
$('#eventshow .changersvp').click(function(e) {
var d = $('#eventshow'),
h = $('#event-rsvp').show().height();
h -= $(this).closest('.event-line').toggle().height();
me.dialog_resize(d.get(0), d.height() + h, d.outerWidth() - 50);
h = -$(this).closest('.event-line').toggle().height();
$('#event-rsvp').slideDown(300, function() {
h += $(this).height();
me.dialog_resize(d.get(0), d.height() + h, d.outerWidth() - 50);
});
return false;
})
@ -3508,7 +3856,10 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
rcmail.register_command('calendar-remove', function(){ cal.calendar_remove(cal.calendars[cal.selected_calendar]); }, false);
rcmail.register_command('events-import', function(){ cal.import_events(cal.calendars[cal.selected_calendar]); }, true);
rcmail.register_command('calendar-showurl', function(){ cal.showurl(cal.calendars[cal.selected_calendar]); }, false);
rcmail.register_command('event-download', function(){ cal.event_download(cal.selected_event); }, true);
rcmail.register_command('event-sendbymail', function(p, obj, e){ cal.event_sendbymail(cal.selected_event, e); }, true);
rcmail.register_command('event-history', function(p, obj, e){ cal.event_history_dialog(cal.selected_event); }, false);
// search and export events
rcmail.register_command('export', function(){ cal.export_events(cal.calendars[cal.selected_calendar]); }, true);
rcmail.register_command('search', function(){ cal.quicksearch(); }, true);
@ -3528,6 +3879,9 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
rcmail.addEventListener('plugin.reload_view', function(p){ cal.reload_view(p); });
rcmail.addEventListener('plugin.resource_data', function(p){ cal.resource_data_load(p); });
rcmail.addEventListener('plugin.resource_owner', function(p){ cal.resource_owner_load(p); });
rcmail.addEventListener('plugin.render_event_changelog', function(data){ cal.render_event_changelog(data); });
rcmail.addEventListener('plugin.event_show_diff', function(data){ cal.event_show_diff(data); });
rcmail.addEventListener('plugin.event_show_revision', function(data){ cal.event_show_dialog(data, null, true); });
rcmail.addEventListener('requestrefresh', function(q){ return cal.before_refresh(q); });
// let's go

View file

@ -100,7 +100,8 @@ abstract class calendar_driver
public $attendees = false;
public $freebusy = false;
public $attachments = false;
public $undelete = false; // event undelete action
public $undelete = false;
public $history = false;
public $categoriesimmutable = false;
public $alarm_types = array('DISPLAY');
public $alarm_absolute = true;
@ -405,6 +406,77 @@ abstract class calendar_driver
return false;
}
/**
* Provide a list of revisions for the given event
*
* @param array $event Hash array with event properties:
* id: Event identifier
* calendar: Calendar identifier
*
* @return array List of changes, each as a hash array:
* rev: Revision number
* type: Type of the change (create, update, move, delete)
* date: Change date
* user: The user who executed the change
* ip: Client IP
* destination: Destination calendar for 'move' type
*/
public function get_event_changelog($event)
{
return false;
}
/**
* Get a list of property changes beteen two revisions of an event
*
* @param array $event Hash array with event properties:
* id: Event identifier
* calendar: Calendar identifier
* @param mixed $rev Revisions: "from:to"
*
* @return array List of property changes, each as a hash array:
* property: Revision number
* old: Old property value
* new: Updated property value
*/
public function get_event_diff($event, $rev)
{
return false;
}
/**
* Return full data of a specific revision of an event
*
* @param mixed UID string or hash array with event properties:
* id: Event identifier
* calendar: Calendar identifier
* @param mixed $rev Revision number
*
* @return array Event object as hash array
* @see self::get_event()
*/
public function get_event_revison($event, $rev)
{
return false;
}
/**
* Command the backend to restore a certain revision of an event.
* This shall replace the current event with an older version.
*
* @param mixed UID string or hash array with event properties:
* id: Event identifier
* calendar: Calendar identifier
* @param mixed $rev Revision number
*
* @return boolean True on success, False on failure
*/
public function restore_event_revision($event, $rev)
{
return false;
}
/**
* Callback function to produce driver-specific calendar create/edit form
*

View file

@ -30,6 +30,7 @@ class kolab_calendar extends kolab_storage_folder_api
public $readonly = true;
public $attachments = true;
public $alarms = false;
public $history = false;
public $subscriptions = true;
public $categories = array();
public $storage;
@ -589,53 +590,8 @@ class kolab_calendar extends kolab_storage_folder_api
{
$record['id'] = $record['uid'];
$record['calendar'] = $this->id;
/*
// convert from DateTime to unix timestamp
if (is_a($record['start'], 'DateTime'))
$record['start'] = $record['start']->format('U');
if (is_a($record['end'], 'DateTime'))
$record['end'] = $record['end']->format('U');
*/
// all-day events go from 12:00 - 13:00
if ($record['end'] <= $record['start'] && $record['allday']) {
$record['end'] = clone $record['start'];
$record['end']->add(new DateInterval('PT1H'));
}
if (!empty($record['_attachments'])) {
foreach ($record['_attachments'] as $key => $attachment) {
if ($attachment !== false) {
if (!$attachment['name'])
$attachment['name'] = $key;
unset($attachment['path'], $attachment['content']);
$attachments[] = $attachment;
}
}
$record['attachments'] = $attachments;
}
// Roundcube only supports one category assignment
if (is_array($record['categories']))
$record['categories'] = $record['categories'][0];
// the cancelled flag transltes into status=CANCELLED
if ($record['cancelled'])
$record['status'] = 'CANCELLED';
// The web client only supports DISPLAY type of alarms
if (!empty($record['alarms']))
$record['alarms'] = preg_replace('/:[A-Z]+$/', ':DISPLAY', $record['alarms']);
// remove empty recurrence array
if (empty($record['recurrence']))
unset($record['recurrence']);
// remove internals
unset($record['_mailbox'], $record['_msguid'], $record['_formatobj'], $record['_attachments'], $record['x-custom']);
return $record;
return kolab_driver::to_rcube_event($record);
}
/**

View file

@ -47,6 +47,7 @@ class kolab_driver extends calendar_driver
private $calendars;
private $has_writeable = false;
private $freebusy_trigger = false;
private $bonnie_api = false;
/**
* Default constructor
@ -69,6 +70,10 @@ class kolab_driver extends calendar_driver
$this->alarm_absolute = false;
}
// get configuration for the Bonnie API
if ($bonnie_config = $this->cal->rc->config->get('kolab_bonnie_api', false))
$this->bonnie_api = new kolab_bonnie_api($bonnie_config);
// calendar uses fully encoded identifiers
kolab_storage::$encode_ids = true;
}
@ -164,6 +169,7 @@ class kolab_driver extends calendar_driver
'active' => $cal->is_active(),
'title' => $cal->get_owner(),
'owner' => $cal->get_owner(),
'history' => false,
'virtual' => false,
'readonly' => true,
'group' => 'other',
@ -192,6 +198,7 @@ class kolab_driver extends calendar_driver
'color' => $cal->get_color(),
'readonly' => $cal->readonly,
'showalarms' => $cal->alarms,
'history' => !empty($this->bonnie_api),
'group' => $cal->get_namespace(),
'default' => $cal->default,
'active' => $cal->is_active(),
@ -222,6 +229,7 @@ class kolab_driver extends calendar_driver
'color' => $cal->get_color(),
'readonly' => $cal->readonly,
'showalarms' => $cal->alarms,
'history' => !empty($this->bonnie_api),
'group' => 'x-invitations',
'default' => false,
'active' => $cal->is_active(),
@ -252,6 +260,7 @@ class kolab_driver extends calendar_driver
'readonly' => true,
'default' => false,
'children' => false,
'history' => false,
);
}
}
@ -1285,6 +1294,265 @@ class kolab_driver extends calendar_driver
exit;
}
/**
* Convert from Kolab_Format to internal representation
*/
public static function to_rcube_event($record)
{
$record['id'] = $record['uid'];
// all-day events go from 12:00 - 13:00
if ($record['end'] <= $record['start'] && $record['allday']) {
$record['end'] = clone $record['start'];
$record['end']->add(new DateInterval('PT1H'));
}
if (!empty($record['_attachments'])) {
foreach ($record['_attachments'] as $key => $attachment) {
if ($attachment !== false) {
if (!$attachment['name'])
$attachment['name'] = $key;
unset($attachment['path'], $attachment['content']);
$attachments[] = $attachment;
}
}
$record['attachments'] = $attachments;
}
// Roundcube only supports one category assignment
if (is_array($record['categories']))
$record['categories'] = $record['categories'][0];
// the cancelled flag transltes into status=CANCELLED
if ($record['cancelled'])
$record['status'] = 'CANCELLED';
// The web client only supports DISPLAY type of alarms
if (!empty($record['alarms']))
$record['alarms'] = preg_replace('/:[A-Z]+$/', ':DISPLAY', $record['alarms']);
// remove empty recurrence array
if (empty($record['recurrence']))
unset($record['recurrence']);
// remove internals
unset($record['_mailbox'], $record['_msguid'], $record['_formatobj'], $record['_attachments'], $record['x-custom']);
return $record;
}
/**
* Provide a list of revisions for the given event
*
* @param array $event Hash array with event properties
*
* @return array List of changes, each as a hash array
* @see calendar_driver::get_event_changelog()
*/
public function get_event_changelog($event)
{
if (empty($this->bonnie_api)) {
return false;
}
list($uid, $folder) = $this->_resolve_event_identity($event);
$result = $this->bonnie_api->changelog('event', $uid, $folder);
if (is_array($result) && $result['uid'] == $uid) {
return $result['changes'];
}
return false;
}
/**
* Get a list of property changes beteen two revisions of an event
*
* @param array $event Hash array with event properties
* @param mixed $rev Revisions: "from:to"
*
* @return array List of property changes, each as a hash array
* @see calendar_driver::get_event_diff()
*/
public function get_event_diff($event, $rev)
{
if (empty($this->bonnie_api)) {
return false;
}
list($uid, $folder) = $this->_resolve_event_identity($event);
// call Bonnie API
$result = $this->bonnie_api->diff('event', $uid, $rev, $folder);
if (is_array($result) && $result['uid'] == $uid) {
$result['rev'] = $rev;
$keymap = array(
'dtstart' => 'start',
'dtend' => 'end',
'dstamp' => 'changed',
'summary' => 'title',
'alarm' => 'alarms',
'attendee' => 'attendees',
'attach' => 'attachments',
'rrule' => 'recurrence',
'transparency' => 'free_busy',
'classification' => 'sensitivity',
'lastmodified-date' => 'changed',
);
$prop_keymaps = array(
'attachments' => array('fmttype' => 'mimetype', 'label' => 'name'),
'attendees' => array('partstat' => 'status'),
);
$special_changes = array();
// map kolab event properties to keys the client expects
array_walk($result['changes'], function(&$change, $i) use ($keymap, $prop_keymaps, $special_changes) {
if (array_key_exists($change['property'], $keymap)) {
$change['property'] = $keymap[$change['property']];
}
// translate free_busy values
if ($change['property'] == 'free_busy') {
$change['old'] = $old['old'] ? 'free' : 'busy';
$change['new'] = $old['new'] ? 'free' : 'busy';
}
// map alarms trigger value
if ($change['property'] == 'alarms') {
if (is_array($change['old']) && is_array($change['old']['trigger']))
$change['old']['trigger'] = $change['old']['trigger']['value'];
if (is_array($change['new']) && is_array($change['new']['trigger']))
$change['new']['trigger'] = $change['new']['trigger']['value'];
}
// make all property keys uppercase
if ($change['property'] == 'recurrence') {
$special_changes['recurrence'] = $i;
foreach (array('old','new') as $m) {
if (is_array($change[$m])) {
$props = array();
foreach ($change[$m] as $k => $v)
$props[strtoupper($k)] = $v;
$change[$m] = $props;
}
}
}
// map property keys names
if (is_array($prop_keymaps[$change['property']])) {
foreach ($prop_keymaps[$change['property']] as $k => $dest) {
if (is_array($change['old']) && array_key_exists($k, $change['old'])) {
$change['old'][$dest] = $change['old'][$k];
unset($change['old'][$k]);
}
if (is_array($change['new']) && array_key_exists($k, $change['new'])) {
$change['new'][$dest] = $change['new'][$k];
unset($change['new'][$k]);
}
}
}
if ($change['property'] == 'exdate') {
$special_changes['exdate'] = $i;
}
else if ($change['property'] == 'rdate') {
$special_changes['rdate'] = $i;
}
});
// merge some recurrence changes
foreach (array('exdate','rdate') as $prop) {
if (array_key_exists($prop, $special_changes)) {
$exdate = $result['changes'][$special_changes[$prop]];
if (array_key_exists('recurrence', $special_changes)) {
$recurrence = &$result['changes'][$special_changes['recurrence']];
}
else {
$i = count($result['changes']);
$result['changes'][$i] = array('property' => 'recurrence', 'old' => array(), 'new' => array());
$recurrence = &$result['changes'][$i]['recurrence'];
}
$key = strtoupper($prop);
$recurrence['old'][$key] = $exdate['old'];
$recurrence['new'][$key] = $exdate['new'];
unset($result['changes'][$special_changes[$prop]]);
}
}
return $result;
}
return false;
}
/**
* Return full data of a specific revision of an event
*
* @param array Hash array with event properties
* @param mixed $rev Revision number
*
* @return array Event object as hash array
* @see calendar_driver::get_event_revison()
*/
public function get_event_revison($event, $rev)
{
if (empty($this->bonnie_api)) {
return false;
}
$calid = $event['calendar'];
list($uid, $folder) = $this->_resolve_event_identity($event);
// call Bonnie API
$result = $this->bonnie_api->get('event', $uid, $rev, $folder);
if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) {
$format = kolab_format::factory('event');
$format->load($result['xml']);
$event = $format->to_array();
if ($format->is_valid()) {
if ($result['folder'] && ($cal = $this->get_calendar(kolab_storage::id_encode($result['folder'])))) {
$event['calendar'] = $cal->id;
}
else {
$event['calendar'] = $calid;
}
$event['rev'] = $result['rev'];
return self::to_rcube_event($event);
}
}
return false;
}
/**
* Helper method to resolved the given event identifier into uid and folder
*
* @return array (uid,folder) tuple
*/
private function _resolve_event_identity($event)
{
$folder = null;
if (is_array($event)) {
$uid = $event['id'] ?: $event['uid'];
if ($cal = $this->get_calendar($event['calendar']) && !($cal instanceof kolab_invitation_calendar)) {
$folder = $cal->name;
}
}
else {
$uid = $event;
}
// FIXME: hard-code UID for static Bonnie API demo
$demo_uids = $this->rc->config->get('kolab_static_bonnie_uids', array('0015c5fe-9baf-0561-11e3-d584fa2894b7'));
if (!in_array($uid, $demo_uids))
$uid = reset($demo_uids);
return array($uid, $folder);
}
/**
* Callback function to produce driver-specific calendar create/edit form
*

View file

@ -391,9 +391,7 @@ class kolab_user_calendar extends kolab_calendar
$record['id'] = $record['uid'];
$record['calendar'] = $this->id;
// TODO: implement this
return $record;
return kolab_driver::to_rcube_event($record);
}
}

View file

@ -97,6 +97,7 @@ class calendar_ui
$this->cal->register_handler('plugin.angenda_options', array($this, 'angenda_options'));
$this->cal->register_handler('plugin.events_import_form', array($this, 'events_import_form'));
$this->cal->register_handler('plugin.events_export_form', array($this, 'events_export_form'));
$this->cal->register_handler('plugin.event_changelog_table', array($this, 'event_changelog_table'));
$this->cal->register_handler('plugin.searchform', array($this->rc->output, 'search_form')); // use generic method from rcube_template
}
@ -840,6 +841,22 @@ class calendar_ui
return $table->show($attrib);
}
/**
* Table oultine for event changelog display
*/
function event_changelog_table($attrib = array())
{
$table = new html_table(array('cols' => 5, 'border' => 0, 'cellspacing' => 0));
$table->add_header('diff', '');
$table->add_header('revision', $this->cal->gettext('revision'));
$table->add_header('date', $this->cal->gettext('date'));
$table->add_header('user', $this->cal->gettext('user'));
$table->add_header('operation', $this->cal->gettext('operation'));
$table->add_header('actions', '&nbsp;');
return $table->show($attrib);
}
/**
*
*/

View file

@ -81,7 +81,12 @@ $labels['private'] = 'private';
$labels['confidential'] = 'confidential';
$labels['alarms'] = 'Reminder';
$labels['comment'] = 'Comment';
$labels['created'] = 'Created';
$labels['changed'] = 'Last Modified';
$labels['unknown'] = 'Unknown';
$labels['eventoptions'] = 'Options';
$labels['generated'] = 'generated at';
$labels['eventhistory'] = 'History';
$labels['printdescriptions'] = 'Print descriptions';
$labels['parentcalendar'] = 'Insert inside';
$labels['searchearlierdates'] = '« Search for earlier events';
@ -253,6 +258,24 @@ $labels['birthdayscalendarsources'] = 'From these address books';
$labels['birthdayeventtitle'] = '$name\'s Birthday';
$labels['birthdayage'] = 'Age $age';
// history dialog
$labels['eventchangelog'] = 'Change History';
$labels['eventdiff'] = 'Changes from revisions $rev';
$labels['revision'] = 'Revision';
$labels['user'] = 'User';
$labels['operation'] = 'Action';
$labels['actionappend'] = 'Saved';
$labels['actionmove'] = 'Moved';
$labels['actiondelete'] = 'Deleted';
$labels['compare'] = 'Compare';
$labels['showrevision'] = 'Show this version';
$labels['restore'] = 'Restore this version';
$labels['eventnotfound'] = 'Failed to load event data';
$labels['eventchangelognotavailable'] = 'Change history is not available for this event';
$labels['eventdiffnotavailable'] = 'No comparison possible for the selected revisions';
$labels['eventrestoreconfirm'] = 'Do you really want to restore revision $rev of this event? This will replace the current event with the old version.';
// (hidden) titles and labels for accessibility annotations
$labels['arialabelminical'] = 'Calendar date selection';
$labels['arialabelcalendarview'] = 'Calendar view';

View file

@ -526,6 +526,10 @@ a.miniColors-trigger {
/* jQuery UI overrides */
.calendarmain .ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset {
float: left;
}
#eventshow h1 {
font-size: 20px;
margin: 0.1em 0 0.4em 0;

View file

@ -115,6 +115,13 @@
<roundcube:object name="plugin.event_rsvp_buttons" id="event-rsvp" style="display:none" />
</div>
<div id="eventoptionsmenu" class="popupmenu">
<ul>
<li><roundcube:button command="event-download" label="download" classAct="active" /></li>
<li><roundcube:button command="event-sendbymail" label="send" classAct="active" /></li>
</ul>
</div>
<roundcube:include file="/templates/eventedit.html" />
<div id="eventresourcesdialog" class="uidialog">

View file

@ -663,53 +663,160 @@ a.miniColors-trigger {
/* jQuery UI overrides */
#eventshow h1 {
.calendarmain .eventdialog h1 {
font-size: 18px;
margin: -0.3em 0 0.4em 0;
}
#eventshow label,
#eventshow h5.label {
.calendarmain .eventdialog label,
.calendarmain .eventdialog h5.label {
font-weight: normal;
font-size: 1em;
color: #999;
margin: 0 0 0.2em 0;
}
#eventshow {
.calendarmain .eventdialog label span.index,
.calendarmain .eventdialog h5.label .index {
vertical-align: inherit;
margin-left: 0.6em;
}
.calendarmain .eventdialog {
margin: 0 -0.2em;
}
#eventshow.status-cancelled {
.calendarmain .eventdialog.status-cancelled {
background: url(images/badge_cancelled.png) top right no-repeat;
}
#eventshow.sensitivity-private {
.calendarmain .eventdialog.sensitivity-private {
background: url(images/badge_private.png) top right no-repeat;
}
#eventshow.sensitivity-confidential {
.calendarmain .eventdialog.sensitivity-confidential {
background: url(images/badge_confidential.png) top right no-repeat;
}
.sensitivity-private #event-title {
.calendarmain .sensitivity-private #event-title {
margin-right: 50px;
}
.sensitivity-confidential #event-title {
.calendarmain .sensitivity-confidential #event-title {
margin-right: 60px;
}
#eventshow div.event-line {
.calendarmain .eventdialog div.event-line {
margin-top: 0.1em;
margin-bottom: 0.3em;
}
#eventshow div.event-line a.iconbutton {
.calendarmain .eventdialog div.event-line a.iconbutton {
margin-left: 0.5em;
line-height: 17px;
}
.calendarmain .eventdialog div.event-line span.event-text + label {
margin-left: 2em;
}
.calendarmain .eventdialog #event-created-changed {
margin-top: 0.6em;
}
.eventdialog .event-text-old,
.eventdialog .event-text-new,
.eventdialog .event-text-diff {
padding: 2px;
}
.eventdialog .event-text-diff del,
.eventdialog .event-text-diff ins {
text-decoration: none;
color: inherit;
}
.eventdialog .event-text-old,
.eventdialog .event-text-diff del {
background-color: #fdd;
/* text-decoration: line-through; */
}
.eventdialog .event-text-new,
.eventdialog .event-text-diff ins {
background-color: #dfd;
}
#eventdiff .attachmentslist li a,
#eventdiff .attachmentslist li a:hover {
cursor: default;
text-decoration: none;
}
#eventhistory .loading {
color: #666;
margin: 1em 0;
padding: 1px 0 2px 24px;
background: url(images/loading_blue.gif) top left no-repeat;
}
#eventhistory .compare-button {
margin: 4px 0;
}
#event-changelog-table tbody td {
padding: 4px 7px;
vertical-align: middle;
}
#event-changelog-table tbody tr.undisclosed td.date,
#event-changelog-table tbody tr.undisclosed td.user {
font-style: italic;
}
#event-changelog-table .diff {
width: 4em;
padding: 2px;
}
#event-changelog-table .revision {
width: 5em;
}
#event-changelog-table .date {
width: 11em;
}
#event-changelog-table .user {
width: auto;
}
#event-changelog-table .operation {
width: 15%;
}
#event-changelog-table .actions {
width: 50px;
text-align: right;
padding: 4px;
}
#event-changelog-table td a.iconbutton.restore,
#event-changelog-table td a.iconbutton.preview {
background-image: url(images/calendars.png);
background-position: 1px -147px;
}
#event-changelog-table td a.iconbutton.restore {
background-image: url(images/calendars.png);
background-position: 1px -167px;
}
#event-changelog-table tr.first td a.iconbutton {
opacity: 0.3;
cursor: default;
}
#event-partstat .changersvp {
cursor: pointer;
color: #333;
@ -745,7 +852,7 @@ a.miniColors-trigger {
}
div.form-section,
#eventshow div.event-section,
.calendarmain .eventdialog div.event-section,
#eventtabs div.event-section {
margin-top: 0.2em;
margin-bottom: 0.6em;
@ -757,7 +864,7 @@ div.form-section,
border-bottom: 2px solid #fafafa;
}
#eventshow label,
.calendarmain .eventdialog label,
#eventedit label,
.form-section label {
display: inline-block;
@ -830,7 +937,7 @@ td.topalign {
#event-rsvp,
#edit-attendees-notify {
margin: 0.3em 0;
margin: 0.6em 0 0.3em 0;
padding: 0.5em;
}
@ -1186,13 +1293,13 @@ td.topalign {
}
a.dropdown-link {
font-size: 12px;
font-size: 11px;
text-decoration: none;
}
a.dropdown-link:after {
content: ' ▼';
font-size: 11px;
font-size: 10px;
color: #666;
}
@ -1201,7 +1308,10 @@ a.dropdown-link:after {
}
.ui-dialog-buttonset a.dropdown-link {
margin-right: 1em;
position: relative;
top: 2px;
margin: 0 1em;
color: #333;
}
#calendarsidebar .ui-datepicker-calendar {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 5 KiB

View file

@ -77,7 +77,7 @@
</ul>
</div>
<div id="eventshow" class="uidialog" aria-hidden="true">
<div id="eventshow" class="uidialog eventdialog" aria-hidden="true">
<h1 id="event-title">Event Title</h1>
<div class="event-section" id="event-location">Location</div>
<div class="event-section" id="event-date">From-To</div>
@ -136,10 +136,109 @@
<label><roundcube:label name="attachments" /></label>
<div class="event-text"></div>
</div>
<div class="event-line" id="event-created-changed">
<label><roundcube:label name="calendar.created" /></label>
<span class="event-text event-created"></span>
<label><roundcube:label name="calendar.changed" /></label>
<span class="event-text event-changed"></span>
</div>
<roundcube:object name="plugin.event_rsvp_buttons" id="event-rsvp" class="event-dialog-message" style="display:none" />
</div>
<div id="eventoptionsmenu" class="popupmenu" aria-hidden="true">
<h3 id="aria-label-eventoptions" class="voice"><roundcube:label name="calendar.eventoptions" /></h3>
<ul id="eventoptionsmenu-menu" class="toolbarmenu" role="menu" aria-labelledby="aria-label-eventoptions">
<li role="menuitem"><roundcube:button command="event-download" label="download" classAct="active" /></li>
<li role="menuitem"><roundcube:button command="event-sendbymail" label="send" classAct="active" /></li>
<roundcube:if condition="env:calendar_driver == 'kolab' && config:kolab_bonnie_api" />
<li role="menuitem"><roundcube:button command="event-history" type="link" label="calendar.eventhistory" classAct="active" /></li>
<roundcube:endif />
</ul>
</div>
<div id="eventdiff" class="uidialog eventdialog" aria-hidden="true">
<h1 class="event-title">Event Title</h1>
<h1 class="event-title-new event-text-new"></h1>
<div class="event-section event-date"></div>
<div class="event-section event-location">
<h5 class="label"><roundcube:label name="calendar.location" /></h5>
<div class="event-text-old"></div>
<div class="event-text-new"></div>
</div>
<div class="event-section event-description">
<h5 class="label"><roundcube:label name="calendar.description" /></h5>
<div class="event-text-diff" style="white-space:pre-wrap"></div>
<div class="event-text-old"></div>
<div class="event-text-new"></div>
</div>
<div class="event-section event-url">
<h5 class="label"><roundcube:label name="calendar.url" /></h5>
<div class="event-text-old"></div>
<div class="event-text-new"></div>
</div>
<div class="event-section event-recurrence">
<h5 class="label"><roundcube:label name="calendar.repeat" /></h5>
<div class="event-text-old"></div>
<div class="event-text-new"></div>
</div>
<div class="event-section event-alarms">
<h5 class="label"><roundcube:label name="calendar.alarms" /><span class="index"></span></h5>
<div class="event-text-old"></div>
<div class="event-text-new"></div>
</div>
<div class="event-line event-start">
<label><roundcube:label name="calendar.start" /></label>
<span class="event-text-old"></span> &#8674;
<span class="event-text-new"></span>
</div>
<div class="event-line event-end">
<label><roundcube:label name="calendar.end" /></label>
<span class="event-text-old"></span> &#8674;
<span class="event-text-new"></span>
</div>
<div class="event-line event-attendees">
<label><roundcube:label name="calendar.tabattendees" /><span class="index"></span></label>
<span class="event-text-old"></span> &#8674;
<span class="event-text-new"></span>
</div>
<div class="event-line event-calendar">
<label><roundcube:label name="calendar.calendar" /></label>
<span class="event-text-old"></span> &#8674;
<span class="event-text-new"></span>
</div>
<div class="event-line event-categories">
<label><roundcube:label name="calendar.category" /></label>
<span class="event-text-old"></span> &#8674;
<span class="event-text-new"></span>
</div>
<div class="event-line event-status">
<label><roundcube:label name="calendar.status" /></label>
<span class="event-text-old"></span> &#8674;
<span class="event-text-new"></span>
</div>
<div class="event-line event-free_busy">
<label><roundcube:label name="calendar.freebusy" /></label>
<span class="event-text-old"></span> &#8674;
<span class="event-text-new"></span>
</div>
<div class="event-line event-priority">
<label><roundcube:label name="calendar.priority" /></label>
<span class="event-text-old"></span> &#8674;
<span class="event-text-new"></span>
</div>
<div class="event-line event-sensitivity">
<label><roundcube:label name="calendar.sensitivity" /></label>
<span class="event-text-old"></span> &#8674;
<span class="event-text-new"></span>
</div>
<div class="event-section event-attachments">
<label><roundcube:label name="attachments" /><span class="index"></span></label>
<div class="event-text-old"></div>
<div class="event-text-new"></div>
</div>
</div>
<roundcube:include file="/templates/eventedit.html" />
<div id="eventresourcesdialog" class="uidialog" aria-hidden="true">
@ -223,6 +322,11 @@
</div>
</div>
<div id="eventhistory" class="uidialog" aria-hidden="true">
<roundcube:object name="plugin.event_changelog_table" id="event-changelog-table" class="records-table" />
<div class="compare-button"><input type="button" class="button" value="↳ <roundcube:label name='calendar.compare' />" /></div>
</div>
<div id="calendarform" class="uidialog" aria-hidden="true">
<roundcube:label name="loading" />
</div>

View file

@ -746,7 +746,17 @@ class libcalendaring extends rcube_plugin
$until = $this->gettext('forever');
}
return rtrim($freq . $details . ', ' . $until);
$except = '';
if (is_array($rrule['EXDATE']) && !empty($rrule['EXDATE'])) {
$format = self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']));
$exdates = array_map(
function($dt) use ($format) { return format_date($dt, $format); },
array_slice($rrule['EXDATE'], 0, 10)
);
$except = '; ' . $this->gettext('except') . ' ' . join(', ');
}
return rtrim($freq . $details . ', ' . $until . $except);
}
/**

View file

@ -66,6 +66,7 @@ $labels['fourth'] = 'fourth';
$labels['last'] = 'last';
$labels['dayofmonth'] = 'Day of month';
$labels['addrdate'] = 'Add repeat date';
$labels['except'] = 'except';
// itip related labels
$labels['itipinvitation'] = 'Invitation to';