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:
parent
f3b31c863d
commit
a68982b028
15 changed files with 1163 additions and 110 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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', ' ');
|
||||
|
||||
return $table->show($attrib);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 |
|
@ -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> ⇢
|
||||
<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> ⇢
|
||||
<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> ⇢
|
||||
<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> ⇢
|
||||
<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> ⇢
|
||||
<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> ⇢
|
||||
<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> ⇢
|
||||
<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> ⇢
|
||||
<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> ⇢
|
||||
<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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Add table
Reference in a new issue