16 commits - plugins/calendar plugins/libcalendaring plugins/tasklist

Thomas Brüderli bruederli at kolabsys.com
Thu Mar 20 18:16:43 CET 2014


 dev/null                                                  |binary
 plugins/calendar/calendar.php                             |  182 ++++
 plugins/calendar/calendar_base.js                         |    2 
 plugins/calendar/calendar_ui.js                           |  520 ++++++++++++--
 plugins/calendar/config.inc.php.dist                      |    7 
 plugins/calendar/drivers/calendar_driver.php              |   19 
 plugins/calendar/drivers/kolab/kolab_calendar.php         |   14 
 plugins/calendar/drivers/kolab/kolab_driver.php           |    2 
 plugins/calendar/drivers/ldap/resources_driver_ldap.php   |  150 ++++
 plugins/calendar/drivers/resources_driver.php             |  114 +++
 plugins/calendar/lib/calendar_ui.php                      |  104 ++
 plugins/calendar/localization/en_US.inc                   |   14 
 plugins/calendar/skins/classic/calendar.css               |  223 +++++-
 plugins/calendar/skins/classic/images/attendee-status.gif |binary
 plugins/calendar/skins/classic/templates/calendar.html    |   43 -
 plugins/calendar/skins/classic/templates/eventedit.html   |   25 
 plugins/calendar/skins/larry/calendar.css                 |  174 +++-
 plugins/calendar/skins/larry/images/attendee-status.png   |binary
 plugins/calendar/skins/larry/templates/calendar.html      |   39 +
 plugins/calendar/skins/larry/templates/eventedit.html     |   20 
 plugins/libcalendaring/lib/libcalendaring_itip.php        |   19 
 plugins/libcalendaring/libcalendaring.js                  |   76 +-
 plugins/libcalendaring/localization/en_US.inc             |   10 
 plugins/libcalendaring/skins/larry/libcal.css             |   24 
 plugins/tasklist/tasklist.js                              |   10 
 25 files changed, 1592 insertions(+), 199 deletions(-)

New commits:
commit 9762bcae4016199aead1d0e821853a148338b2b8
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Mar 20 18:15:10 2014 +0100

    Load RSVP texts from libcalendaring plugin

diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 3e6b223..ae39687 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -905,12 +905,12 @@ class calendar_ui
         'name' => $attrib['iname'],
         'class' => 'button',
         'rel' => $method,
-        'value' => $this->cal->gettext('itip' . $method),
+        'value' => $this->rc->gettext('itip' . $method, 'libcalendaring'),
       ));
     }
     
     return html::div($attrib,
-      html::div('label', $this->cal->gettext('acceptinvitation')) .
+      html::div('label', $this->rc->gettext('acceptinvitation', 'libcalendaring')) .
       html::div('rsvp-buttons', $buttons));
   }
 


commit 6fbd652bac03cd4b190f017da51794134786b0bc
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Mar 20 18:06:35 2014 +0100

    Add option for the user to enter a comment text for iTip REPLY messages

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index aa5e141..770d50c 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -2403,6 +2403,7 @@ class calendar extends rcube_plugin
 
     // send iTip reply
     if ($this->ical->method == 'REQUEST' && $organizer && !in_array(strtolower($organizer['email']), $emails) && !$error_msg) {
+      $event['comment'] = get_input_value('_comment', RCUBE_INPUT_POST);
       $itip = $this->load_itip();
       $itip->set_sender_email($reply_sender);
       if ($itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index 04847ed..cc8b397 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -149,7 +149,7 @@ $labels['itipobjectnotfound'] = 'The event referred by this message was not foun
 $labels['itipmailbodyaccepted'] = "\$sender has accepted the invitation to the following event:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
 $labels['itipmailbodytentative'] = "\$sender has tentatively accepted the invitation to the following event:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
 $labels['itipmailbodydeclined'] = "\$sender has declined the invitation to the following event:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
-$labels['itipmailbodycancel'] = "\$sender has rejected your participation in the following event:\n\n*\$title*\n\nWhen: \$date\n\n\$comment";
+$labels['itipmailbodycancel'] = "\$sender has rejected your participation in the following event:\n\n*\$title*\n\nWhen: \$date";
 
 $labels['itipdeclineevent'] = 'Do you want to decline your invitation to this event?';
 $labels['declinedeleteconfirm'] = 'Do you also want to delete this declined event from your calendar?';
diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index c5a4afa..a9e612b 100644
--- a/plugins/libcalendaring/lib/libcalendaring_itip.php
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php
@@ -115,10 +115,14 @@ class libcalendaring_itip
                 'attendees' => join(', ', $attendees_list),
                 'sender' => $this->sender['name'],
                 'organizer' => $this->sender['name'],
-                'comment' => $event['comment'],
             )
         ));
 
+        // append sender's comment
+        if (!empty($event['comment'])) {
+            $mailbody .= "\n\n" . $this->gettext('itipsendercomment') . $event['comment'];
+        }
+
         // append links for direct invitation replies
         if ($method == 'REQUEST' && ($token = $this->store_invitation($event, $recipient['email']))) {
             $mailbody .= "\n\n" . $this->gettext(array(
@@ -398,7 +402,7 @@ class libcalendaring_itip
                 $rsvp_buttons .= html::tag('input', array(
                     'type' => 'button',
                     'class' => "button $method",
-                    'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . JQ($mime_id) . "', '$task', '$method')",
+                    'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . JQ($mime_id) . "', '$task', '$method', '$dom_id')",
                     'value' => $this->gettext('itip' . $method),
                 ));
             }
@@ -421,6 +425,12 @@ class libcalendaring_itip
                 }
             }
 
+            // add input field for reply comment
+            $rsvp_buttons .= html::a(array('href' => '#toggle', 'class' => 'reply-comment-toggle'), $this->gettext('itipeditresponse'));
+            $rsvp_buttons .= html::div('itip-reply-comment',
+                html::tag('textarea', array('id' => 'reply-comment-'.$dom_id, 'cols' => 40, 'rows' => 6, 'style' => 'display:none', 'placeholder' => $this->gettext('itipcomment')), '')
+            );
+
             $buttons[] = html::div(array('id' => 'rsvp-'.$dom_id, 'class' => 'rsvp-buttons', 'style' => 'display:none'), $rsvp_buttons);
         }
         // for CANCEL messages, we can:
diff --git a/plugins/libcalendaring/libcalendaring.js b/plugins/libcalendaring/libcalendaring.js
index 931a464..3808de1 100644
--- a/plugins/libcalendaring/libcalendaring.js
+++ b/plugins/libcalendaring/libcalendaring.js
@@ -440,7 +440,7 @@ function rcube_libcalendaring(settings)
 /**
  *
  */
-rcube_libcalendaring.add_from_itip_mail = function(mime_id, task, status)
+rcube_libcalendaring.add_from_itip_mail = function(mime_id, task, status, dom_id)
 {
     // ask user to delete the declined event from the local calendar (#1670)
     var del = false;
@@ -448,13 +448,19 @@ rcube_libcalendaring.add_from_itip_mail = function(mime_id, task, status)
         del = confirm(rcmail.gettext('itip.declinedeleteconfirm'));
     }
 
+    var comment = '';
+    if (dom_id) {
+      comment = $('#reply-comment-'+dom_id).val();
+    }
+
     rcmail.http_post(task + '/mailimportitip', {
         _uid: rcmail.env.uid,
         _mbox: rcmail.env.mailbox,
         _part: mime_id,
         _folder: $('#itip-saveto').val(),
         _status: status,
-        _del: del?1:0
+        _del: del?1:0,
+        _comment: comment
       }, rcmail.set_busy(true, 'itip.savingdata'));
 
     return false;
@@ -544,9 +550,9 @@ rcube_libcalendaring.update_itip_object_status = function(p)
     $('#rsvp-'+p.id+' input.button').prop('disabled', false)
       .filter('.'+String(p.status||'unknown').toLowerCase()).prop('disabled', p.latest);
   }
-
+ 
   // show rsvp/import buttons (with calendar selector)
-  $('#'+p.action+'-'+p.id).show().append(p.select);
+  $('#'+p.action+'-'+p.id).show().find('input.button').last().after(p.select);
 };
 
 
@@ -571,4 +577,8 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
 
   rcmail.addEventListener('plugin.update_itip_object_status', rcube_libcalendaring.update_itip_object_status);
   rcmail.addEventListener('plugin.fetch_itip_object_status', rcube_libcalendaring.fetch_itip_object_status);
+
+  $('.rsvp-buttons').on('click', 'a.reply-comment-toggle', function(e){
+    $(this).hide().parent().find('textarea').show().focus();
+  });
 });
diff --git a/plugins/libcalendaring/localization/en_US.inc b/plugins/libcalendaring/localization/en_US.inc
index 7392e8f..d381768 100644
--- a/plugins/libcalendaring/localization/en_US.inc
+++ b/plugins/libcalendaring/localization/en_US.inc
@@ -36,6 +36,9 @@ $labels['itipreply'] = 'Reply to';
 $labels['itipaccepted'] = 'Accept';
 $labels['itiptentative'] = 'Maybe';
 $labels['itipdeclined'] = 'Decline';
+$labels['itipcomment'] = 'Your response';
+$labels['itipeditresponse'] = 'Enter a response text';
+$labeöls['itipsendercomment'] = 'Sender\'s comment: ';
 
 $labels['itipobjectnotfound'] = 'The object referred by this message was not found in your account.';
 $labels['itipsubjectaccepted'] = '"$title" has been accepted by $name';
diff --git a/plugins/libcalendaring/skins/larry/libcal.css b/plugins/libcalendaring/skins/larry/libcal.css
index 47323a6..e0bd514 100644
--- a/plugins/libcalendaring/skins/larry/libcal.css
+++ b/plugins/libcalendaring/skins/larry/libcal.css
@@ -53,6 +53,22 @@ span.edit-alarm-set {
 	height: 16px;
 }
 
+.itip-reply-comment {
+	padding-left: 2px;
+}
+
+a.reply-comment-toggle {
+	display: inline-block;
+	margin-left: 1em;
+	color: #333;
+}
+
+.itip-reply-comment textarea {
+	display: block;
+	width: 90%;
+	margin-top: 0.5em;
+}
+
 .itip-dialog-confirm-text {
 	margin-bottom: 1em;
 }


commit 65cd18c1b7ef63514a05887750d2420a01b2907c
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Mar 19 21:13:06 2014 +0100

    Send CANCEL iTip message to declined attendees (with optional comment)

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index cd8567d..aa5e141 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -144,6 +144,7 @@ class calendar extends rcube_plugin
       $this->register_action('check-recent', array($this, 'check_recent'));
       $this->register_action('itip-status', array($this, 'event_itip_status'));
       $this->register_action('itip-remove', array($this, 'event_itip_remove'));
+      $this->register_action('itip-decline-reply', array($this, 'mail_itip_decline_reply'));
       $this->register_action('resources-list', array($this, 'resources_list'));
       $this->register_action('resources-owner', array($this, 'resources_owner'));
       $this->register_action('resources-calendar', array($this, 'resources_calendar'));
@@ -2222,18 +2223,10 @@ class calendar extends rcube_plugin
   }
 
 
-  /**
-   * Handler for POST request to import an event attached to a mail message
-   */
-  public function mail_import_event()
+  private function mail_get_itip_event($mbox, $uid, $mime_id)
   {
-    $uid = get_input_value('_uid', RCUBE_INPUT_POST);
-    $mbox = get_input_value('_mbox', RCUBE_INPUT_POST);
-    $mime_id = get_input_value('_part', RCUBE_INPUT_POST);
-    $status = get_input_value('_status', RCUBE_INPUT_POST);
-    $delete = intval(get_input_value('_del', RCUBE_INPUT_POST));
     $charset = RCMAIL_CHARSET;
-    
+
     // establish imap connection
     $imap = $this->rc->get_storage();
     $imap->set_mailbox($mbox);
@@ -2253,6 +2246,26 @@ class calendar extends rcube_plugin
 
     // successfully parsed events?
     if (!empty($events) && ($event = $events[$index])) {
+      return $event;
+    }
+  }
+
+  /**
+   * Handler for POST request to import an event attached to a mail message
+   */
+  public function mail_import_event()
+  {
+    $uid = get_input_value('_uid', RCUBE_INPUT_POST);
+    $mbox = get_input_value('_mbox', RCUBE_INPUT_POST);
+    $mime_id = get_input_value('_part', RCUBE_INPUT_POST);
+    $status = get_input_value('_status', RCUBE_INPUT_POST);
+    $delete = intval(get_input_value('_del', RCUBE_INPUT_POST));
+
+    $error_msg = $this->gettext('errorimportingevent');
+    $success = false;
+
+    // successfully parsed events?
+    if ($event = $this->mail_get_itip_event($mbox, $uid, $mime_id)) {
       // find writeable calendar to store event
       $cal_id = !empty($_REQUEST['_folder']) ? get_input_value('_folder', RCUBE_INPUT_POST) : null;
       $calendars = $this->driver->list_calendars(false, true);
@@ -2403,6 +2416,37 @@ class calendar extends rcube_plugin
 
 
   /**
+   * Handler for calendar/itip-remove requests
+   */
+  function mail_itip_decline_reply()
+  {
+    $uid = get_input_value('_uid', RCUBE_INPUT_POST);
+    $mbox = get_input_value('_mbox', RCUBE_INPUT_POST);
+    $mime_id = get_input_value('_part', RCUBE_INPUT_POST);
+
+    if (($event = $this->mail_get_itip_event($mbox, $uid, $mime_id)) && $this->ical->method == 'REPLY') {
+      $event['comment'] = get_input_value('_comment', RCUBE_INPUT_POST);
+
+      foreach ($event['attendees'] as $_attendee) {
+        if ($_attendee['role'] != 'ORGANIZER') {
+          $attendee = $_attendee;
+          break;
+        }
+      }
+
+      $itip = $this->load_itip();
+      if ($itip->send_itip_message($event, 'CANCEL', $attendee, 'itipsubjectcancel', 'itipmailbodycancel'))
+        $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $attendee['name'] ? $attendee['name'] : $attendee['email']))), 'confirmation');
+      else
+        $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
+    }
+    else {
+      $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
+    }
+  }
+
+
+  /**
    * Read email message and return contents for a new event based on that message
    */
   public function mail_message2event()
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index de8e7dd..04847ed 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -149,6 +149,8 @@ $labels['itipobjectnotfound'] = 'The event referred by this message was not foun
 $labels['itipmailbodyaccepted'] = "\$sender has accepted the invitation to the following event:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
 $labels['itipmailbodytentative'] = "\$sender has tentatively accepted the invitation to the following event:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
 $labels['itipmailbodydeclined'] = "\$sender has declined the invitation to the following event:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
+$labels['itipmailbodycancel'] = "\$sender has rejected your participation in the following event:\n\n*\$title*\n\nWhen: \$date\n\n\$comment";
+
 $labels['itipdeclineevent'] = 'Do you want to decline your invitation to this event?';
 $labels['declinedeleteconfirm'] = 'Do you also want to delete this declined event from your calendar?';
 
diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index 2fed51f..c5a4afa 100644
--- a/plugins/libcalendaring/lib/libcalendaring_itip.php
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php
@@ -115,6 +115,7 @@ class libcalendaring_itip
                 'attendees' => join(', ', $attendees_list),
                 'sender' => $this->sender['name'],
                 'organizer' => $this->sender['name'],
+                'comment' => $event['comment'],
             )
         ));
 
@@ -465,9 +466,9 @@ class libcalendaring_itip
         $this->rc->output->add_script("rcube_libcalendaring.fetch_itip_object_status(" . json_serialize($metadata) . ")", 'docready');
 
         // get localized texts from the right domain
-        $this->rc->output->command('add_label', 'itip.savingdata', $this->gettext('savingdata'));
-        $this->rc->output->command('add_label', 'itip.declinedeleteconfirm', $this->gettext('declinedeleteconfirm'));
-        $this->rc->output->command('add_label', 'itip.declinedeleteconfirm', $this->gettext('declinedeleteconfirm'));
+        foreach (array('savingdata','deleteobjectconfirm','declinedeleteconfirm','declineattendee','declineattendeeconfirm','cancel') as $label) {
+          $this->rc->output->command('add_label', "itip.$label", $this->gettext($label));
+        }
 
         // show event details with buttons
         return $this->itip_object_details_table($event, $title) .
diff --git a/plugins/libcalendaring/libcalendaring.js b/plugins/libcalendaring/libcalendaring.js
index 4acb942..931a464 100644
--- a/plugins/libcalendaring/libcalendaring.js
+++ b/plugins/libcalendaring/libcalendaring.js
@@ -449,12 +449,12 @@ rcube_libcalendaring.add_from_itip_mail = function(mime_id, task, status)
     }
 
     rcmail.http_post(task + '/mailimportitip', {
-        '_uid': rcmail.env.uid,
-        '_mbox': rcmail.env.mailbox,
-        '_part': mime_id,
-        '_folder': $('#itip-saveto').val(),
-        '_status': status,
-        '_del': del?1:0
+        _uid: rcmail.env.uid,
+        _mbox: rcmail.env.mailbox,
+        _part: mime_id,
+        _folder: $('#itip-saveto').val(),
+        _status: status,
+        _del: del?1:0
       }, rcmail.set_busy(true, 'itip.savingdata'));
 
     return false;
@@ -465,21 +465,53 @@ rcube_libcalendaring.add_from_itip_mail = function(mime_id, task, status)
  */
 rcube_libcalendaring.remove_from_itip = function(uid, task, title)
 {
-  if (confirm(rcmail.gettext('itip.deleteobjectconfirm').replace('$title', title))) {
-    rcmail.http_post(task + '/itip-remove',
-      { uid: uid },
-      rcmail.set_busy(true, 'itip.savingdata'));
-  }
+    if (confirm(rcmail.gettext('itip.deleteobjectconfirm').replace('$title', title))) {
+        rcmail.http_post(task + '/itip-remove',
+            { uid: uid },
+            rcmail.set_busy(true, 'itip.savingdata')
+        );
+    }
 };
 
 /**
  *
  */
-rcube_libcalendaring.decline_attendee_reply = function(mime_id)
+rcube_libcalendaring.decline_attendee_reply = function(mime_id, task)
 {
-  // TODO: show dialog for entering a comment and send to server
+    // show dialog for entering a comment and send to server
+    var html = '<div class="itip-dialog-confirm-text">' + rcmail.gettext('itip.declineattendeeconfirm') + '</div>' +
+        '<textarea id="itip-decline-comment" class="itip-comment" cols="40" rows="8"></textarea>';
+
+    var dialog, buttons = [];
+    buttons.push({
+        text: rcmail.gettext('declineattendee', 'itip'),
+        click: function() {
+            rcmail.http_post(task + '/itip-decline-reply', {
+                _uid: rcmail.env.uid,
+                _mbox: rcmail.env.mailbox,
+                _part: mime_id,
+                _comment: $('#itip-decline-comment', window.parent.document).val()
+            }, rcmail.set_busy(true, 'itip.savingdata'));
+          dialog.dialog("close");
+        }
+    });
 
-  return false;
+    buttons.push({
+        text: rcmail.gettext('cancel', 'itip'),
+        click: function() {
+          dialog.dialog('close');
+        }
+    });
+
+    dialog = rcmail.show_popup_dialog(html, rcmail.gettext('declineattendee', 'itip'), buttons, {
+        width: 460,
+        open: function() {
+            $(this).parent().find('.ui-button').first().addClass('mainaction');
+            $('#itip-decline-comment').focus();
+        }
+    });
+
+    return false;
 };
 
 /**
diff --git a/plugins/libcalendaring/localization/en_US.inc b/plugins/libcalendaring/localization/en_US.inc
index ea0ad10..7392e8f 100644
--- a/plugins/libcalendaring/localization/en_US.inc
+++ b/plugins/libcalendaring/localization/en_US.inc
@@ -41,12 +41,15 @@ $labels['itipobjectnotfound'] = 'The object referred by this message was not fou
 $labels['itipsubjectaccepted'] = '"$title" has been accepted by $name';
 $labels['itipsubjecttentative'] = '"$title" has been tentatively accepted by $name';
 $labels['itipsubjectdeclined'] = '"$title" has been declined by $name';
+$labels['itipsubjectcancel'] = 'Your participation in "$title" has been cancelled';
 
 $labels['itipnewattendee'] = 'This is a reply from a new participant';
 $labels['updateattendeestatus'] = 'Update the participant\'s status';
 $labels['acceptinvitation'] = 'Do you accept this invitation?';
-$labels['acceptattendee'] = 'Accept attendee';
-$labels['declineattendee'] = 'Decline attendee';
+$labels['acceptattendee'] = 'Accept participant';
+$labels['declineattendee'] = 'Decline participant';
+$labels['declineattendeeconfirm'] = 'Enter a message to the declined participant (optional):';
+
 $labels['youhaveaccepted'] = 'You have accepted this invitation';
 $labels['youhavetentative'] = 'You have tentatively accepted this invitation';
 $labels['youhavedeclined'] = 'You have declined this invitation';
diff --git a/plugins/libcalendaring/skins/larry/libcal.css b/plugins/libcalendaring/skins/larry/libcal.css
index 1e3a4c8..47323a6 100644
--- a/plugins/libcalendaring/skins/larry/libcal.css
+++ b/plugins/libcalendaring/skins/larry/libcal.css
@@ -52,3 +52,11 @@ span.edit-alarm-set {
 	width: 20px;
 	height: 16px;
 }
+
+.itip-dialog-confirm-text {
+	margin-bottom: 1em;
+}
+
+.popup textarea.itip-comment {
+	width: 98%;
+}
\ No newline at end of file


commit 54cc4aac74c1506fb3e143157e7652da2ba04fe4
Merge: 52b219a d3b35a6
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Mar 19 18:49:07 2014 +0100

    Merge branch 'dev/calendar-resources'
    
    Add resource display and booking features to master

diff --cc plugins/calendar/calendar.php
index a4bf1ab,c549840..cd8567d
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@@ -141,8 -142,10 +142,12 @@@ class calendar extends rcube_plugi
        $this->register_action('mailtoevent', array($this, 'mail_message2event'));
        $this->register_action('inlineui', array($this, 'get_inline_ui'));
        $this->register_action('check-recent', array($this, 'check_recent'));
 +      $this->register_action('itip-status', array($this, 'event_itip_status'));
 +      $this->register_action('itip-remove', array($this, 'event_itip_remove'));
+       $this->register_action('resources-list', array($this, 'resources_list'));
+       $this->register_action('resources-owner', array($this, 'resources_owner'));
+       $this->register_action('resources-calendar', array($this, 'resources_calendar'));
+       $this->register_action('resources-autocomplete', array($this, 'resources_autocomplete'));
        $this->add_hook('refresh', array($this, 'refresh'));
  
        // remove undo information...
@@@ -1908,66 -1930,105 +1908,163 @@@
    }
  
  
+   /****  Resource management functions  ****/
+ 
+   /**
+    * Getter for the configured implementation of the resource directory interface
+    */
+   private function resources_directory()
+   {
+     if (is_object($this->resources_dir)) {
+       return $this->resources_dir;
+     }
+ 
+     if ($driver_name = $this->rc->config->get('calendar_resources_driver')) {
+       $driver_class = 'resources_driver_' . $driver_name;
+ 
+       require_once($this->home . '/drivers/resources_driver.php');
+       require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php');
+ 
+       $this->resources_dir = new $driver_class($this);
+     }
+ 
+     return $this->resources_dir;
+   }
+ 
+   /**
+    * Handler for resoruce autocompletion requests
+    */
+   public function resources_autocomplete()
+   {
+     $search = rcube_utils::get_input_value('_search', rcube_utils::INPUT_GPC, true);
+     $sid    = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
+     $maxnum = (int)$this->rc->config->get('autocomplete_max', 15);
+     $results = array();
+ 
+     if ($directory = $this->resources_directory()) {
+       foreach ($directory->load_resources($search, $maxnum) as $rec) {
+         $results[]  = array(
+             'name'  => $rec['name'],
+             'email' => $rec['email'],
+             'type'  => $rec['_type'],
+         );
+       }
+     }
+ 
+     $this->rc->output->command('ksearch_query_results', $results, $search, $sid);
+     $this->rc->output->send();
+   }
+ 
+   /**
+    * Handler for load-requests for resource data
+    */
+   function resources_list()
+   {
+     $data = array();
+ 
+     if ($directory = $this->resources_directory()) {
+       foreach ($directory->load_resources() as $rec) {
+         $data[] = $rec;
+       }
+     }
+ 
+     $this->rc->output->command('plugin.resource_data', $data);
+     $this->rc->output->send();
+   }
+ 
+   /**
+    * Handler for requests loading resource owner information
+    */
+   function resources_owner()
+   {
+     if ($directory = $this->resources_directory()) {
+       $id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
+       $data = $directory->get_resource_owner($id);
+     }
+ 
+     $this->rc->output->command('plugin.resource_owner', $data);
+     $this->rc->output->send();
+   }
+ 
+   /**
+    * Deliver event data for a resource's calendar
+    */
+   function resources_calendar()
+   {
+     $events = array();
+ 
+     if ($directory = $this->resources_directory()) {
+       $events = $directory->get_resource_calendar(
+         rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC),
+         rcube_utils::get_input_value('start', RCUBE_INPUT_GET),
+         rcube_utils::get_input_value('end', RCUBE_INPUT_GET));
+     }
+ 
+     echo $this->encode($events);
+     exit;
+   }
+ 
+ 
    /****  Event invitation plugin hooks ****/
 -  
 +
 +  /**
 +   * Handler for calendar/itip-status requests
 +   */
 +  function event_itip_status()
 +  {
 +    $data = get_input_value('data', RCUBE_INPUT_POST, true);
 +
 +    // find local copy of the referenced event
 +    $this->load_driver();
 +    $existing = $this->driver->get_event($data, true, false, true);
 +
 +    $itip = $this->load_itip();
 +    $response = $itip->get_itip_status($data, $existing);
 +
 +    // get a list of writeable calendars to save new events to
 +    if (!$existing && $response['action'] == 'rsvp' || $response['action'] == 'import') {
 +      $calendars = $this->driver->list_calendars(false, true);
 +      $calendar_select = new html_select(array('name' => 'calendar', 'id' => 'itip-saveto', 'is_escaped' => true));
 +      $numcals = 0;
 +      foreach ($calendars as $calendar) {
 +        if (!$calendar['readonly']) {
 +          $calendar_select->add($calendar['name'], $calendar['id']);
 +          $numcals++;
 +        }
 +      }
 +      if ($numcals <= 1)
 +        $calendar_select = null;
 +    }
 +
 +    if ($calendar_select) {
 +      $default_calendar = $this->get_default_calendar(true);
 +      $response['select'] = html::span('folder-select', $this->gettext('saveincalendar') . ' ' .
 +        $calendar_select->show($this->rc->config->get('calendar_default_calendar', $default_calendar['id'])));
 +    }
 +
 +    $this->rc->output->command('plugin.update_itip_object_status', $response);
 +  }
 +
 +  /**
 +   * Handler for calendar/itip-remove requests
 +   */
 +  function event_itip_remove()
 +  {
 +    $success = false;
 +
 +    // search for event if only UID is given
 +    if ($event = $this->driver->get_event(array('uid' => get_input_value('uid', RCUBE_INPUT_POST)), true)) {
 +      $success = $this->driver->remove_event($event, true);
 +    }
 +    
 +    if ($success) {
 +      $this->rc->output->show_message('calendar.successremoval', 'confirmation');
 +    }
 +    else {
 +      $this->rc->output->show_message('calendar.errorsaving', 'error');
 +    }
 +  }
 +
    /**
     * Handler for URLs that allow an invitee to respond on his invitation mail
     */
diff --cc plugins/calendar/localization/en_US.inc
index 990838e,055092a..de8e7dd
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@@ -153,8 -177,7 +155,17 @@@ $labels['declinedeleteconfirm'] = 'Do y
  $labels['notanattendee'] = 'You\'re not listed as an attendee of this event';
  $labels['eventcancelled'] = 'The event has been cancelled';
  $labels['saveincalendar'] = 'save in';
 +$labels['updatemycopy'] = 'Update in my calendar';
 +
++// resources
++$labels['resource'] = 'Resource';
++$labels['addresource'] = 'Book resource';
++$labels['findresources'] = 'Find resources';
++$labels['resourcedetails'] = 'Details';
++$labels['resourceavailability'] = 'Availability';
++$labels['resourceowner'] = 'Owner';
++$labels['resourceadded'] = 'The resource was added to your event';
+ 
  // event dialog tabs
  $labels['tabsummary'] = 'Summary';
  $labels['tabrecurrence'] = 'Recurrence';
diff --cc plugins/calendar/skins/classic/templates/eventedit.html
index 03e47e6,e05a55c..3bc4a48
--- a/plugins/calendar/skins/classic/templates/eventedit.html
+++ b/plugins/calendar/skins/classic/templates/eventedit.html
@@@ -84,13 -85,10 +85,13 @@@
        <div class="recurrence-form" id="recurrence-form-until">
          <roundcube:object name="plugin.recurrence_form" part="until" class="event-section" />
        </div>
 +      <div class="recurrence-form" id="recurrence-form-rdate">
 +        <roundcube:object name="plugin.recurrence_form" part="rdate" class="event-section" />
 +      </div>
      </div>
      <!-- attendees list -->
-     <div id="event-tab-3">
-       <roundcube:object name="plugin.attendees_list" id="edit-attendees-table" cellspacing="0" cellpadding="0" border="0" />
+     <div id="event-panel-attendees">
+       <roundcube:object name="plugin.attendees_list" id="edit-attendees-table" class="edit-attendees-table" cellspacing="0" cellpadding="0" border="0" />
        <roundcube:object name="plugin.attendees_form" id="edit-attendees-form" />
        <roundcube:include file="/templates/freebusylegend.html" />
      </div>
diff --cc plugins/calendar/skins/larry/calendar.css
index c9e7a8c,9d219fe..d2ade1a
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@@ -1461,18 -1555,12 +1583,18 @@@ div.calendar-invitebox .rsvp-status.loa
  	background: url(images/loading_blue.gif) top left no-repeat;
  }
  
 +div.calendar-invitebox .rsvp-status.hint {
 +	color: #666;
 +	text-shadow: none;
 +	font-style: italic;
 +}
 +
  div.calendar-invitebox .rsvp-status.declined,
  div.calendar-invitebox .rsvp-status.tentative,
 -div.calendar-invitebox .rsvp-status.delegated,
 -div.calendar-invitebox .rsvp-status.accepted {
 +div.calendar-invitebox .rsvp-status.accepted,
 +div.calendar-invitebox .rsvp-status.delegated  {
  	padding: 0 0 1px 22px;
- 	background: url(images/attendee-status.gif) 2px -20px no-repeat;
+ 	background: url(images/attendee-status.png) 2px -20px no-repeat;
  }
  
  div.calendar-invitebox .rsvp-status.declined {
diff --cc plugins/calendar/skins/larry/templates/eventedit.html
index 4af08f3,9f2a374..95be8ea
--- a/plugins/calendar/skins/larry/templates/eventedit.html
+++ b/plugins/calendar/skins/larry/templates/eventedit.html
@@@ -81,13 -81,10 +81,13 @@@
  			<div class="recurrence-form" id="recurrence-form-until">
  				<roundcube:object name="plugin.recurrence_form" part="until" class="event-section" />
  			</div>
 +			<div class="recurrence-form" id="recurrence-form-rdate">
 +				<roundcube:object name="plugin.recurrence_form" part="rdate" class="event-section" />
 +			</div>
  		</div>
  		<!-- attendees list -->
- 		<div id="event-tab-3">
- 			<roundcube:object name="plugin.attendees_list" id="edit-attendees-table" class="records-table" />
+ 		<div id="event-panel-attendees">
+ 			<roundcube:object name="plugin.attendees_list" id="edit-attendees-table" class="records-table edit-attendees-table" coltitle="attendee" />
  			<roundcube:object name="plugin.attendees_form" id="edit-attendees-form" />
  			<roundcube:include file="/templates/freebusylegend.html" />
  		</div>


commit d3b35a6d47f4c6fb40bd05d17f0a2d3b732574cc
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Mar 19 18:42:21 2014 +0100

    Display delegated-from/to information in attendees list (hover title)

diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index f74880e..b94b24c 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -350,19 +350,26 @@ function rcube_calendar_ui(settings)
           return (j - k);
         });
 
-        var data, dispname, organizer = false, rsvp = false, line,  morelink, html = '',overflow = '';
+        var data, dispname, tooltip, organizer = false, rsvp = false, 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) {
-            dispname = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink" data-cutype="' + data.cutype + '">' + dispname + '</a>';
+            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 ((data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE' || data.rsvp) && settings.identity.emails.indexOf(';'+data.email) >= 0)
               rsvp = data.status.toLowerCase();
           }
           
-          line = '<span class="attendee ' + String(data.role == 'ORGANIZER' ? 'organizer' : data.status).toLowerCase() + '">' + dispname + '</span> ';
+          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> ';
           if (morelink)
             overflow += line;
           else
@@ -1524,11 +1531,17 @@ function rcube_calendar_ui(settings)
       // delete icon
       var icon = rcmail.env.deleteicon ? '<img src="' + rcmail.env.deleteicon + '" alt="" />' : rcmail.gettext('delete');
       var dellink = '<a href="#delete" class="iconlink delete deletelink" title="' + Q(rcmail.gettext('delete')) + '">' + icon + '</a>';
-      
+      var tooltip = data.status || '';
+
+      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'];
+
       var html = '<td class="role">' + select + '</td>' +
         '<td class="name">' + dispname + '</td>' +
         '<td class="availability"><img src="./program/resources/blank.gif" class="availabilityicon ' + avail + '" data-email="' + data.email + '" /></td>' +
-        '<td class="confirmstate"><span class="' + String(data.status).toLowerCase() + '" title="' + Q(data.status || '') + '">' + Q(data.status || '') + '</span></td>' +
+        '<td class="confirmstate"><span class="' + String(data.status).toLowerCase() + '" title="' + Q(tooltip) + '">' + Q(data.status || '') + '</span></td>' +
         '<td class="options">' + (organizer || readonly ? '' : dellink) + '</td>';
 
       var table = rcmail.env.calendar_resources && data.cutype == 'RESOURCE' ? resources_list : attendees_list;
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index 8d68d72..055092a 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -122,6 +122,8 @@ $labels['availbusy'] = 'Busy';
 $labels['availunknown'] = 'Unknown';
 $labels['availtentative'] = 'Tentative';
 $labels['availoutofoffice'] = 'Out of Office';
+$labels['delegatedto'] = 'Delegated to: ';
+$labels['delegatedfrom'] = 'Delegated from: ';
 $labels['scheduletime'] = 'Find availability';
 $labels['sendinvitations'] = 'Send invitations';
 $labels['sendnotifications'] = 'Notify participants about modifications';


commit a4770d4d5d3cd214493d2f528e159315e55ebffc
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Mar 17 21:24:21 2014 +0100

    Fix treelist toggle icons in classic skin

diff --git a/plugins/calendar/skins/classic/calendar.css b/plugins/calendar/skins/classic/calendar.css
index f8361d8..49776b3 100644
--- a/plugins/calendar/skins/classic/calendar.css
+++ b/plugins/calendar/skins/classic/calendar.css
@@ -1143,8 +1143,8 @@ span.spacer {
 	top: -2px;
 }
 
-#resources-list li ul li a {
-	padding-left: 35px;
+#resources-list li ul div.treetoggle {
+	left: 23px !important;
 }
 
 #resource-selection {


commit 761c79dbadc33cbe37e8ccca52acc2d8e0ab8f65
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Mar 17 21:11:36 2014 +0100

    Fix resolving and displaying tree structure of resource records loaded from the server

diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 9d03987..f74880e 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -1333,7 +1333,7 @@ function rcube_calendar_ui(settings)
       var event = me.selected_event,
         eventstart = clone_date(event.start, event.allDay ? 1 : 0).getTime(),  // calculate with integers
         eventend = clone_date(event.end, event.allDay ? 2 : 0).getTime(),
-        duration = eventend - eventstart - (event.allDay ? HOUR_MS : 0),  // make sure we don't cross day borders on DST change
+        duration = eventend - eventstart - (event.allDay ? HOUR_MS : 0),  /* make sure we don't cross day borders on DST change */
         sinterval = freebusy_data.interval * 60000,
         intvlslots = 1,
         numslots = Math.ceil(duration / sinterval),
@@ -1769,25 +1769,46 @@ function rcube_calendar_ui(settings)
     // callback from server for resource listing
     var resource_data_load = function(data)
     {
-      data.sort(function(a,b) {
-        var j = a._type == 'collection' ? 1 : 0,
-            k = b._type == 'collection' ? 1 : 0;
-        return k != j ? (j - k) : (a.name < b.name ? 1 : 0);
-      });
+      var resources_tree = {};
 
-      // assign parent-relations
+      // store data by ID
       $.each(data, function(i, rec) {
         resources_data[rec.ID] = rec;
-        resources_index.push(rec.ID);
 
+        // assign parent-relations
         if (rec.members) {
           $.each(rec.members, function(j, m){
-            resources_data[m].parent_id = rec.ID;
+            resources_tree[m] = rec.ID;
           });
         }
       });
 
-      resources_index.reverse();
+      // walk the parent-child tree to determine the depth of each node
+      $.each(data, function(i, rec) {
+        rec._depth = 0;
+        if (resources_tree[rec.ID])
+          rec.parent_id = resources_tree[rec.ID];
+
+        var parent_id = resources_tree[rec.ID];
+        while (parent_id) {
+          rec._depth++;
+          parent_id = resources_tree[parent_id];
+        }
+      });
+
+      // sort by depth, collection and name
+      data.sort(function(a,b) {
+        var j = a._type == 'collection' ? 1 : 0,
+            k = b._type == 'collection' ? 1 : 0,
+            d = a._depth - b._depth;
+        if (!d) d = (k - j);
+        if (!d) d = b.name < a.name ? 1 : -1;
+        return d;
+      });
+
+      $.each(data, function(i, rec) {
+        resources_index.push(rec.ID);
+      });
 
       // apply search filter...
       if ($('#resourcesearchbox').val() != '')
@@ -2130,7 +2151,7 @@ function rcube_calendar_ui(settings)
           date: date.getDate(),
           month: date.getMonth(),
           year: date.getFullYear(),
-          ignoreTimezone: true,  // will treat the given date strings as in local (browser's) timezone
+          ignoreTimezone: true,  /* will treat the given date strings as in local (browser's) timezone */
           eventSources: sources,
           monthNames : settings['months'],
           monthNamesShort : settings['months_short'],
diff --git a/plugins/calendar/drivers/ldap/resources_driver_ldap.php b/plugins/calendar/drivers/ldap/resources_driver_ldap.php
index 2f10a1e..c377393 100644
--- a/plugins/calendar/drivers/ldap/resources_driver_ldap.php
+++ b/plugins/calendar/drivers/ldap/resources_driver_ldap.php
@@ -127,6 +127,11 @@ class resources_driver_ldap extends resources_driver
         $rec['attributes'] = $attributes;
       }
 
+      // force $rec['members'] to be an array
+      if (!empty($rec['members']) && !is_array($rec['members'])) {
+        $rec['members'] = array($rec['members']);
+      }
+
       // remove unused cruft
       unset($rec['_raw_attrib']);
 


commit 0946cc37a420c3698228e0157aaad7e2fa343048
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Mar 17 17:29:12 2014 +0100

    Display resource's availability in a small calendar widget. Data is derived from the resource free/busy data

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 365ce9e..c549840 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -144,6 +144,7 @@ class calendar extends rcube_plugin
       $this->register_action('check-recent', array($this, 'check_recent'));
       $this->register_action('resources-list', array($this, 'resources_list'));
       $this->register_action('resources-owner', array($this, 'resources_owner'));
+      $this->register_action('resources-calendar', array($this, 'resources_calendar'));
       $this->register_action('resources-autocomplete', array($this, 'resources_autocomplete'));
       $this->add_hook('refresh', array($this, 'refresh'));
 
@@ -2007,6 +2008,24 @@ class calendar extends rcube_plugin
     $this->rc->output->send();
   }
 
+  /**
+   * Deliver event data for a resource's calendar
+   */
+  function resources_calendar()
+  {
+    $events = array();
+
+    if ($directory = $this->resources_directory()) {
+      $events = $directory->get_resource_calendar(
+        rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC),
+        rcube_utils::get_input_value('start', RCUBE_INPUT_GET),
+        rcube_utils::get_input_value('end', RCUBE_INPUT_GET));
+    }
+
+    echo $this->encode($events);
+    exit;
+  }
+
 
   /****  Event invitation plugin hooks ****/
   
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index f0440d9..9d03987 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -52,6 +52,7 @@ function rcube_calendar_ui(settings)
     var resources_data = {};
     var resources_index = [];
     var resource_owners = {};
+    var resources_events_source = { url:null, editable:false };
     var freebusy_ui = { workinhoursonly:false, needsupdate:false };
     var freebusy_data = {};
     var current_view = null;
@@ -1624,6 +1625,10 @@ function rcube_calendar_ui(settings)
         close: function() {
           $dialog.dialog('destroy').hide();
         },
+        resize: function(e) {
+          var container = $(rcmail.gui_objects.resourceinfocalendar)
+          container.fullCalendar('option', 'height', container.height() + 4);
+        },
         buttons: buttons,
         width: 900,
         height: 500
@@ -1654,6 +1659,7 @@ function rcube_calendar_ui(settings)
             rcmail.enable_command('add-resource', false);
             $(rcmail.gui_objects.resourceinfo).hide();
             $(rcmail.gui_objects.resourceownerinfo).hide();
+            $(rcmail.gui_objects.resourceinfocalendar).fullCalendar('removeEventSource', resources_events_source);
           }
         });
 
@@ -1663,6 +1669,43 @@ function rcube_calendar_ui(settings)
 
         // register button
         rcmail.register_button('add-resource', 'rcmbtncalresadd', 'uibutton');
+
+        // initialize resource calendar display
+        var resource_cal = $(rcmail.gui_objects.resourceinfocalendar);
+        resource_cal.fullCalendar({
+          header: { left: '', center: '', right: '' },
+          height: resource_cal.height() + 4,
+          defaultView: 'agendaWeek',
+          ignoreTimezone: true,
+          eventSources: [],
+          monthNames: settings['months'],
+          monthNamesShort: settings['months_short'],
+          dayNames: settings['days'],
+          dayNamesShort : settings['days_short'],
+          firstDay: settings['first_day'],
+          firstHour: settings['first_hour'],
+          slotMinutes: 60,
+          allDaySlot: false,
+          timeFormat: { '': settings['time_format'] },
+          axisFormat: settings['time_format'],
+          columnFormat: { day: 'dddd ' + settings['date_short'] },
+          titleFormat: { day: 'dddd ' + settings['date_long'] },
+          currentTimeIndicator: settings.time_indicator,
+          eventRender: function(event, element, view) {
+            element.addClass('status-' + event.status);
+            element.find('.fc-event-head').hide();
+            element.find('.fc-event-title').text(rcmail.get_label(event.status, 'calendar'));
+          }
+        });
+
+        $('#resource-calendar-prev').click(function(){
+          resource_cal.fullCalendar('prev');
+          return false;
+        });
+        $('#resource-calendar-next').click(function(){
+          resource_cal.fullCalendar('next');
+          return false;
+        });
       }
       else if (search) {
         resource_search();
@@ -1670,6 +1713,10 @@ function rcube_calendar_ui(settings)
       else {
         resource_render_list(resources_index);
       }
+
+      if (me.selected_event && me.selected_event.start) {
+        $(rcmail.gui_objects.resourceinfocalendar).fullCalendar('gotoDate', me.selected_event.start);
+      }
     };
 
     // render the resource details UI box
@@ -1699,6 +1746,7 @@ function rcube_calendar_ui(settings)
         }
 
         $(rcmail.gui_objects.resourceownerinfo).hide();
+        $(rcmail.gui_objects.resourceinfocalendar).fullCalendar('removeEventSource', resources_events_source);
 
         if (resource.owner) {
           // display cached data
@@ -1711,6 +1759,10 @@ function rcube_calendar_ui(settings)
             rcmail.http_request('resources-owner', { _id: resource.owner }, me.loading_lock);
           }
         }
+
+        // load resource calendar
+        resources_events_source.url = "./?_task=calendar&_action=resources-calendar&_id="+escape(resource.ID);
+        $(rcmail.gui_objects.resourceinfocalendar).fullCalendar('addEventSource', resources_events_source);
       }
     };
 
diff --git a/plugins/calendar/drivers/ldap/resources_driver_ldap.php b/plugins/calendar/drivers/ldap/resources_driver_ldap.php
index 23439b6..2f10a1e 100644
--- a/plugins/calendar/drivers/ldap/resources_driver_ldap.php
+++ b/plugins/calendar/drivers/ldap/resources_driver_ldap.php
@@ -27,7 +27,6 @@
 class resources_driver_ldap extends resources_driver
 {
     private $rc;
-    private $cal;
     private $ldap;
 
     /**
@@ -81,7 +80,7 @@ class resources_driver_ldap extends resources_driver
       $rec = null;
 
       if ($ldap = $this->connect()) {
-        $rec = $ldap->get_record(rcube_ldap::dn_encode($dn));
+        $rec = $ldap->get_record(rcube_ldap::dn_encode($dn), true);
 
         if (!empty($rec)) {
           $rec = $this->decode_resource($rec);
diff --git a/plugins/calendar/drivers/resources_driver.php b/plugins/calendar/drivers/resources_driver.php
index b1fed9c..c51e922 100644
--- a/plugins/calendar/drivers/resources_driver.php
+++ b/plugins/calendar/drivers/resources_driver.php
@@ -27,6 +27,15 @@
  */
 abstract class resources_driver
 {
+  protected$cal;
+
+  /**
+   * Default constructor
+   */
+  function __construct($cal)
+  {
+      $this->cal = $cal;
+  }
 
   /**
    * Fetch resource objects to be displayed for booking
@@ -52,7 +61,54 @@ abstract class resources_driver
    */
   public function get_resource_owner($id)
   {
-    return null;
+      return null;
+  }
+
+  /**
+   * Get event data to display a resource's calendar
+   *
+   * The default implementation extracts the resource's email address
+   * and fetches free-busy data using the calendar backend driver.
+   *
+   * @param  integer Event's new start (unix timestamp)
+   * @param  integer Event's new end (unix timestamp)
+   * @return array A list of event objects (see calendar_driver specification)
+   */
+  public function get_resource_calendar($id, $start, $end)
+  {
+      $events = array();
+      $rec = $this->get_resource($id);
+      if ($rec && !empty($rec['email']) && $this->cal->driver) {
+          $fbtypemap = array(
+              calendar::FREEBUSY_BUSY => 'busy',
+              calendar::FREEBUSY_TENTATIVE => 'tentative',
+              calendar::FREEBUSY_OOF => 'outofoffice',
+          );
+
+          // if the backend has free-busy information
+          $fblist = $this->cal->driver->get_freebusy_list($rec['email'], $start, $end);
+          if (is_array($fblist)) {
+              foreach ($fblist as $slot) {
+                  list($from, $to, $type) = $slot;
+                  if ($type == calendar::FREEBUSY_FREE || $type == calendar::FREEBUSY_UNKNOWN) {
+                      continue;
+                  }
+                  if ($from < $end && $to > $start) {
+                      $event = array(
+                          'id'     => sha1($id . $from . $to),
+                          'title'  => $rec['name'],
+                          'start'  => new DateTime('@' . $from),
+                          'end'    => new DateTime('@' . $to),
+                          'status' => $fbtypemap[$type],
+                          'calendar' => '_resource',
+                      );
+                      $events[] = $event;
+                  }
+              }
+          }
+      }
+
+      return $events;
   }
 
 }
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 127456f..5f04575 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -88,6 +88,7 @@ class calendar_ui
     $this->cal->register_handler('plugin.resources_list', array($this, 'resources_list'));
     $this->cal->register_handler('plugin.resources_searchform', array($this, 'resources_search_form'));
     $this->cal->register_handler('plugin.resource_info', array($this, 'resource_info'));
+    $this->cal->register_handler('plugin.resource_calendar', array($this, 'resource_calendar'));
     $this->cal->register_handler('plugin.attendees_freebusy_table', array($this, 'attendees_freebusy_table'));
     $this->cal->register_handler('plugin.edit_attendees_notify', array($this, 'edit_attendees_notify'));
     $this->cal->register_handler('plugin.edit_recurring_warning', array($this, 'recurring_event_warning'));
@@ -816,6 +817,18 @@ class calendar_ui
   }
 
   /**
+   *
+   */
+  public function resource_calendar($attrib = array())
+  {
+    $attrib += array('id' => 'calendar-resources-calendar');
+
+    $this->rc->output->add_gui_object('resourceinfocalendar', $attrib['id']);
+
+    return html::div($attrib, '');
+  }
+
+  /**
    * GUI object 'searchform' for the resource finder dialog
    *
    * @param array Named parameters
diff --git a/plugins/calendar/skins/classic/calendar.css b/plugins/calendar/skins/classic/calendar.css
index 07917c3..f8361d8 100644
--- a/plugins/calendar/skins/classic/calendar.css
+++ b/plugins/calendar/skins/classic/calendar.css
@@ -1086,7 +1086,7 @@ span.spacer {
 	top: 0;
 	left: 0;
 	right: 0;
-	height: 56%;
+	height: 48%;
 	border: 1px solid #999;
 	background-color: #F9F9F9;
 	overflow: auto;
@@ -1095,7 +1095,8 @@ span.spacer {
 #resource-availability {
 	top: auto;
 	bottom: 0;
-	height: 41%;
+	height: 49%;
+	overflow: hidden;
 }
 
 #resource-info .boxtitle,
@@ -1103,6 +1104,40 @@ span.spacer {
 	margin-top: 0;
 }
 
+#resource-freebusy-calendar {
+	position: absolute;
+	top: 20px;
+	left: -1px;
+	right: -1px;
+	bottom: -1px;
+}
+
+#resource-freebusy-calendar .fc-content {
+	top: 0;
+}
+
+#resource-freebusy-calendar .fc-content .fc-event-bg {
+	background: 0;
+}
+
+#resource-freebusy-calendar .fc-event.status-busy,
+#resource-freebusy-calendar .status-busy .fc-event-skin {
+	border-color: #e26569;
+	background-color: #e26569;
+}
+
+#resource-freebusy-calendar .fc-event.status-tentative,
+#resource-freebusy-calendar .status-tentative .fc-event-skin {
+	border-color: #8383fc;
+	background: #8383fc;
+}
+
+#resource-freebusy-calendar .fc-event.status-outofoffice,
+#resource-freebusy-calendar .status-outofoffice .fc-event-skin {
+	border-color: #fbaa68;
+	background: #fbaa68;
+}
+
 #resources-list div.treetoggle {
 	left: 3px !important;
 	top: -2px;
diff --git a/plugins/calendar/skins/classic/templates/calendar.html b/plugins/calendar/skins/classic/templates/calendar.html
index b6012e7..fa93afc 100644
--- a/plugins/calendar/skins/classic/templates/calendar.html
+++ b/plugins/calendar/skins/classic/templates/calendar.html
@@ -125,7 +125,7 @@
 
     <div id="resource-availability">
       <h2 class="boxtitle"><roundcube:label name="calendar.resourceavailability" /></h2>
-      <div id="resource-freebusy-calendar"></div>
+      <roundcube:object name="plugin.resource_calendar" id="resource-freebusy-calendar" />
     </div>
   </div>
 </div>
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 1806467..9d219fe 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -1081,7 +1081,7 @@ a.dropdown-link:after {
 	top: 0;
 	left: 0;
 	right: 0;
-	height: 56%;
+	height: 48%;
 }
 
 #resource-info table {
@@ -1100,7 +1100,41 @@ a.dropdown-link:after {
 	bottom: 0;
 	left: 0;
 	right: 0;
-	height: 40%;
+	height: 49%;
+}
+
+#resource-freebusy-calendar {
+	position: absolute;
+	top: 33px;
+	left: -1px;
+	right: -1px;
+	bottom: -1px;
+}
+
+#resource-freebusy-calendar .fc-content {
+	top: 0;
+}
+
+#resource-freebusy-calendar .fc-content .fc-event-bg {
+	background: 0;
+}
+
+#resource-freebusy-calendar .fc-event.status-busy,
+#resource-freebusy-calendar .status-busy .fc-event-skin {
+	border-color: #e26569;
+	background-color: #e26569;
+}
+
+#resource-freebusy-calendar .fc-event.status-tentative,
+#resource-freebusy-calendar .status-tentative .fc-event-skin {
+	border-color: #8383fc;
+	background: #8383fc;
+}
+
+#resource-freebusy-calendar .fc-event.status-outofoffice,
+#resource-freebusy-calendar .status-outofoffice .fc-event-skin {
+	border-color: #fbaa68;
+	background: #fbaa68;
 }
 
 #resourcequicksearch {
diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html
index c189491..036a4fc 100644
--- a/plugins/calendar/skins/larry/templates/calendar.html
+++ b/plugins/calendar/skins/larry/templates/calendar.html
@@ -139,7 +139,11 @@
 
 		<div id="resource-availability" class="uibox contentbox">
 			<h2 class="boxtitle"><roundcube:label name="calendar.resourceavailability" /></h2>
-			<div id="resource-freebusy-calendar"></div>
+			<roundcube:object name="plugin.resource_calendar" id="resource-freebusy-calendar" />
+			<div class="boxpagenav">
+				<roundcube:button name="resource-cal-prev" id="resource-calendar-prev" type="link" class="icon prevpage" title="calendar.prevslot" content="&lt;" />
+				<roundcube:button name="resource-cal-next" id="resource-calendar-next" type="link" class="icon nextpage" title="calendar.nextslot" content="&gt;" />
+			</div>
 		</div>
 	</div>
 </div>


commit 17013f732fd4d99edc77c15cd07f8a09d7221e5d
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Mar 11 11:54:49 2014 +0100

    Fix state of resources listing when opening the dialog

diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 86cd6b0..f0440d9 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -1664,10 +1664,11 @@ function rcube_calendar_ui(settings)
         // register button
         rcmail.register_button('add-resource', 'rcmbtncalresadd', 'uibutton');
       }
+      else if (search) {
+        resource_search();
+      }
       else {
-        resources_treelist.select('__none__');
-        if (search)
-          resource_search();
+        resource_render_list(resources_index);
       }
     };
 


commit 149dcbaa1ac71c2a8c50399baed7aec3ca12e6ec
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Mar 11 11:36:58 2014 +0100

    Get classic calendar skin up to speed with the new features

diff --git a/plugins/calendar/skins/classic/calendar.css b/plugins/calendar/skins/classic/calendar.css
index 9138b13..07917c3 100644
--- a/plugins/calendar/skins/classic/calendar.css
+++ b/plugins/calendar/skins/classic/calendar.css
@@ -16,7 +16,7 @@ body.calendarmain {
 #main {
 	position: absolute;
 	clear: both;
-	top: 90px;
+	top: 72px;
 	left: 0;
 	right: 0;
 	bottom: 10px;
@@ -24,13 +24,15 @@ body.calendarmain {
 
 #calendarsidebar {
 	position: absolute;
-	top: 37px;
+	top: 0px;
 	left: 10px;
 	bottom: 0;
 	width: 230px;
 }
 
 #datepicker {
+	position: relative;
+	top: 42px;
 	width: 100%;
 }
 
@@ -62,7 +64,7 @@ body.calendarmain {
 	position: absolute;
 	left: 244px;
 	width: 8px;
-	top: 37px;
+	top: 4px;
 	bottom: 0;
 	background: url(images/toggle.gif) 0 48% no-repeat transparent;
 	cursor: pointer;
@@ -78,7 +80,7 @@ div.sidebarclosed {
 
 #calendar {
 	position: absolute;
-	top: 0;
+	top: 4px;
 	left: 256px;
 	right: 10px;
 	bottom: 0;
@@ -94,7 +96,7 @@ pre {
 
 #calendars {
 	position: absolute;
-	top: 220px;
+	top: 228px;
 	left: 0;
 	bottom: 0;
 	right: 0;
@@ -207,8 +209,8 @@ pre {
 
 #calendartoolbar {
 	position: absolute;
-	top: 45px;
-	left: 256px;
+	top: 0px;
+	left: 0px;
 	height: 35px;
 }
 
@@ -269,7 +271,8 @@ pre {
 	background-position: -128px -32px;
 }
 
-#quicksearchbar {
+.calendarmain #quicksearchbar {
+	top: 82px;
 	right: 4px;
 }
 
@@ -410,7 +413,7 @@ a.miniColors-trigger {
 }
 
 .event-attendees span.delegated {
-	background-position: right -160px;
+	background-position: right -180px;
 }
 
 .event-attendees span.organizer {
@@ -599,7 +602,7 @@ td.topalign {
 	border: 1px solid #C2D071;
 }
 
-#edit-attendees-table {
+.edit-attendees-table {
 	width: 100%;
 	display: table;
 	table-layout: fixed;
@@ -607,49 +610,51 @@ td.topalign {
 	border: 1px solid #ccc;
 }
 
-#edit-attendees-table td {
+.edit-attendees-table td {
 	padding: 3px;
 	border-bottom: 1px solid #ccc;
 }
 
-#edit-attendees-table td.role {
+.edit-attendees-table td.role {
 	width: 8em;
 }
 
-#edit-attendees-table td.availability,
-#edit-attendees-table td.confirmstate {
+.edit-attendees-table td.availability,
+.edit-attendees-table td.confirmstate {
 	width: 4em;
 }
 
-#edit-attendees-table td.options {
+.edit-attendees-table td.options {
 	width: 3em;
 	text-align: right;
 	padding-right: 4px;
 }
 
-#edit-attendees-table td.name {
+.edit-attendees-table td.name {
 	width: auto;
 	white-space: nowrap;
 	overflow: hidden;
 	text-overflow: ellipsis;
 }
 
-#edit-attendees-table thead td {
+.edit-attendees-table thead td {
 	background: url(images/listheader.gif) top left repeat-x #CCC;
 }
 
-#edit-attendees-form {
+#edit-attendees-form,
+#edit-resources-form {
 	position: relative;
 	margin-top: 1em;
 }
 
-#edit-attendees-form #edit-attendee-schedule {
+#edit-attendees-form #edit-attendee-schedule,
+#edit-resources-form #edit-resource-find {
 	position: absolute;
 	top: 0;
 	right: 0;
 }
 
-#edit-attendees-table select.edit-attendee-role {
+.edit-attendees-table select.edit-attendee-role {
 	border: 0;
 	padding: 2px;
 	background: white;
@@ -722,35 +727,35 @@ td.topalign {
 	vertical-align: middle;
 }
 
-#edit-attendees-table tbody td.confirmstate {
+.edit-attendees-table tbody td.confirmstate {
 	overflow: hidden;
 	white-space: nowrap;
 	text-indent: -2000%;
 }
 
-#edit-attendees-table td.confirmstate span {
+.edit-attendees-table td.confirmstate span {
 	display: block;
 	width: 20px;
 	background: url(images/attendee-status.gif) 5px 0 no-repeat;
 }
 
-#edit-attendees-table td.confirmstate span.needs-action {
+.edit-attendees-table td.confirmstate span.needs-action {
 }
 
-#edit-attendees-table td.confirmstate span.accepted {
+.edit-attendees-table td.confirmstate span.accepted {
 	background-position: 5px -20px;
 }
 
-#edit-attendees-table td.confirmstate span.declined {
+.edit-attendees-table td.confirmstate span.declined {
 	background-position: 5px -40px;
 }
 
-#edit-attendees-table td.confirmstate span.tentative {
+.edit-attendees-table td.confirmstate span.tentative {
 	background-position: 5px -60px;
 }
 
-#edit-attendees-table td.confirmstate span.delegated {
-	background-position: 5px -160px;
+.edit-attendees-table td.confirmstate span.delegated {
+	background-position: 5px -180px;
 }
 
 #attendees-freebusy-table {
@@ -814,10 +819,14 @@ td.topalign {
 	background-position: 2px -117px;
 }
 
-.attendees-list .chair {
+.attendees-list .non-participant {
 	background-position: 2px -137px;
 }
 
+.attendees-list .chair {
+	background-position: 2px -157px;
+}
+
 .attendees-list .loading {
 	background: url(images/loading_blue.gif) 1px 50% no-repeat;
 }
@@ -1063,12 +1072,124 @@ span.spacer {
   padding-right: 10px;
 }
 
+#resource-dialog-right {
+	position: absolute;
+	top: 10px;
+	left: 300px;
+	right: 8px;
+	bottom: 10px;
+}
+
+#resource-info,
+#resource-availability {
+	position: absolute;
+	top: 0;
+	left: 0;
+	right: 0;
+	height: 56%;
+	border: 1px solid #999;
+	background-color: #F9F9F9;
+	overflow: auto;
+}
+
+#resource-availability {
+	top: auto;
+	bottom: 0;
+	height: 41%;
+}
+
+#resource-info .boxtitle,
+#resource-availability .boxtitle {
+	margin-top: 0;
+}
+
+#resources-list div.treetoggle {
+	left: 3px !important;
+	top: -2px;
+}
+
+#resources-list li ul li a {
+	padding-left: 35px;
+}
+
+#resource-selection {
+  position: absolute;
+	top: 10px;
+	bottom: 10px;
+	left: 8px;
+	width: 280px;
+	border: 1px solid #999999;
+	background-color: #F9F9F9;
+	overflow: hidden;
+}
+
+#resource-selection .boxlistcontent {
+	top: 25px;
+	border-top: 1px solid #eee;
+}
+
+#resourcequicksearch {
+	position: absolute;
+	top: 3px;
+	left: 7px;
+	right: 4px;
+	height: 17px;
+	background: #fff;
+	border: 1px solid #888;
+	border-radius: 10px;
+	-webkit-box-shadow: inset 1px 1px 1px 0px rgba(0, 0, 0, 0.3);
+	-moz-box-shadow: inset 1px 1px 1px 0px rgba(0, 0, 0, 0.3);
+	box-shadow: inset 1px 1px 1px 0px rgba(0, 0, 0, 0.3);
+}
+
+#resourcesearchbox {
+	position: absolute;
+	top: 1px;
+	left: 24px;
+	width: 140px;
+	height: 15px;
+	font-size: 11px;
+	padding: 0px;
+	border: none;
+	outline: none;
+	background: #fff;
+}
+
+#resourcesearchreset {
+	position: absolute;
+	top: 2px;
+	right: 2px;
+	text-decoration: none;
+}
+
+#resource-details,
+#resource-details-owner {
+	margin: 8px;
+}
+
+#resource-details td.title,
+#resource-details-owner td.title {
+	color: #666;
+	padding-right: 10px;
+	min-width: 10em;
+}
+
+#resource-details-owner thead td {
+	color: #333;
+	font-size: 13px;
+	font-weight: bold;
+}
 
 /* fullcalendar style overrides */
 
+#calendar .fc-header-right {
+	padding-right: 200px;
+	padding-top: 4px;
+}
+
 .rcube-fc-content {
 	position: absolute !important;
-	top: 37px;
+	top: 38px;
 	left: 0;
 	right: 0;
 	bottom: 0;
@@ -1147,7 +1268,7 @@ div.fc-event-location {
 
 .fc-view-list div.fc-list-header,
 .fc-view-table td.fc-list-header,
-#edit-attendees-table thead td {
+.edit-attendees-table thead td {
 	padding: 3px;
 	background: #dddddd;
 	background-image: -moz-linear-gradient(center top, #f4f4f4, #d2d2d2);
@@ -1251,6 +1372,7 @@ div.calendar-invitebox .rsvp-status.loading {
 
 div.calendar-invitebox .rsvp-status.declined,
 div.calendar-invitebox .rsvp-status.tentative,
+div.calendar-invitebox .rsvp-status.delegated,
 div.calendar-invitebox .rsvp-status.accepted {
 	padding: 0 0 1px 22px;
 	background: url(images/attendee-status.gif) 2px -20px no-repeat;
@@ -1264,6 +1386,10 @@ div.calendar-invitebox .rsvp-status.tentative {
 	background-position: 2px -60px;
 }
 
+div.calendar-invitebox .rsvp-status.delegated {
+	background-position: 2px -180px;
+}
+
 /* iTIP attend reply page */
 
 .calendaritipattend .centerbox {
diff --git a/plugins/calendar/skins/classic/images/attendee-status.gif b/plugins/calendar/skins/classic/images/attendee-status.gif
index 4c561e4..fd3b926 100644
Binary files a/plugins/calendar/skins/classic/images/attendee-status.gif and b/plugins/calendar/skins/classic/images/attendee-status.gif differ
diff --git a/plugins/calendar/skins/classic/templates/calendar.html b/plugins/calendar/skins/classic/templates/calendar.html
index da77999..b6012e7 100644
--- a/plugins/calendar/skins/classic/templates/calendar.html
+++ b/plugins/calendar/skins/classic/templates/calendar.html
@@ -13,6 +13,14 @@
 
 <div id="main">
   <div id="calendarsidebar">
+    <div id="calendartoolbar">
+      <roundcube:button command="addevent" type="link" class="buttonPas addevent" classAct="button addevent" classSel="button addeventSel" title="calendar.new_event" content=" " />
+      <roundcube:button command="print" type="link" class="buttonPas print" classAct="button print" classSel="button printSel" title="calendar.print" content=" " />
+      <roundcube:button command="events-import" type="link" class="buttonPas import" classAct="button import" classSel="button importSel" title="calendar.importevents" content=" " />
+      <roundcube:button command="export" type="link" class="buttonPas export" classAct="button export" classSel="button exportSel" title="calendar.export" content=" " />
+      <roundcube:container name="toolbar" id="calendartoolbar" />
+    </div>
+
     <div id="datepicker"></div>
     <div id="calendars" style="visibility:hidden">
       <div class="boxtitle"><roundcube:label name="calendar.calendars" /></div>
@@ -96,6 +104,32 @@
 
 <roundcube:include file="/templates/eventedit.html" />
 
+<div id="eventresourcesdialog" class="uidialog">
+  <div id="resource-dialog-left">
+    <div id="resource-selection" class="">
+      <div id="resourcequicksearch">
+        <roundcube:object name="plugin.resources_searchform" id="resourcesearchbox" />
+        <roundcube:button command="reset-resource-search" id="resourcesearchreset" image="/images/icons/reset.gif" title="resetsearch" width="13" height="13" />
+      </div>
+      <div class="boxlistcontent">
+        <roundcube:object name="plugin.resources_list" id="resources-list" class="treelist" />
+      </div>
+    </div>
+  </div>
+
+  <div id="resource-dialog-right">
+    <div id="resource-info">
+      <h2 class="boxtitle"><roundcube:label name="calendar.resourcedetails" /></h2>
+      <roundcube:object name="plugin.resource_info" id="resource-details" />
+    </div>
+
+    <div id="resource-availability">
+      <h2 class="boxtitle"><roundcube:label name="calendar.resourceavailability" /></h2>
+      <div id="resource-freebusy-calendar"></div>
+    </div>
+  </div>
+</div>
+
 <div id="eventfreebusy" class="uidialog">
   <roundcube:object name="plugin.attendees_freebusy_table" id="attendees-freebusy-table" cellspacing="0" cellpadding="0" border="0" />
   
@@ -134,6 +168,7 @@
     <span class="attendee organizer"><roundcube:label name="calendar.roleorganizer" /></span>
     <span class="attendee req-participant"><roundcube:label name="calendar.rolerequired" /></span>
     <span class="attendee opt-participant"><roundcube:label name="calendar.roleoptional" /></span>
+    <span class="attendee non-participant"><roundcube:label name="calendar.rolenonparticipant" /></span>
     <span class="attendee chair"><roundcube:label name="calendar.rolechair" /></span>
   </div>
 </div>
@@ -159,14 +194,6 @@
   </div>
 </div>
 
-<div id="calendartoolbar">
-  <roundcube:button command="addevent" type="link" class="buttonPas addevent" classAct="button addevent" classSel="button addeventSel" title="calendar.new_event" content=" " />
-  <roundcube:button command="print" type="link" class="buttonPas print" classAct="button print" classSel="button printSel" title="calendar.print" content=" " />
-  <roundcube:button command="events-import" type="link" class="buttonPas import" classAct="button import" classSel="button importSel" title="calendar.importevents" content=" " />
-  <roundcube:button command="export" type="link" class="buttonPas export" classAct="button export" classSel="button exportSel" title="calendar.export" content=" " />
-  <roundcube:container name="toolbar" id="calendartoolbar" />
-</div>
-
 <div id="quicksearchbar">
 <roundcube:button name="searchmenulink" id="searchmenulink" image="/images/icons/glass.png" />
 <roundcube:object name="plugin.searchform" id="quicksearchbox" />
diff --git a/plugins/calendar/skins/classic/templates/eventedit.html b/plugins/calendar/skins/classic/templates/eventedit.html
index 1bc1a12..e05a55c 100644
--- a/plugins/calendar/skins/classic/templates/eventedit.html
+++ b/plugins/calendar/skins/classic/templates/eventedit.html
@@ -4,6 +4,7 @@
       <li><a href="#event-panel-1"><roundcube:label name="calendar.tabsummary" /></a></li>
       <li id="edit-tab-recurrence"><a href="#event-panel-recurrence"><roundcube:label name="calendar.tabrecurrence" /></a></li>
       <li id="edit-tab-attendees"><a href="#event-panel-attendees"><roundcube:label name="calendar.tabattendees" /></a></li>
+      <li id="edit-tab-resources"><a href="#event-panel-resources"><roundcube:label name="calendar.tabresources" /></a></li>
       <li id="edit-tab-attachments"><a href="#event-panel-attachments"><roundcube:label name="calendar.tabattachments" /></a></li>
     </ul>
     <!-- basic info -->
@@ -87,10 +88,16 @@
     </div>
     <!-- attendees list -->
     <div id="event-panel-attendees">
-      <roundcube:object name="plugin.attendees_list" id="edit-attendees-table" cellspacing="0" cellpadding="0" border="0" />
+      <roundcube:object name="plugin.attendees_list" id="edit-attendees-table" class="edit-attendees-table" cellspacing="0" cellpadding="0" border="0" />
       <roundcube:object name="plugin.attendees_form" id="edit-attendees-form" />
       <roundcube:include file="/templates/freebusylegend.html" />
     </div>
+    <!-- resources list -->
+    <div id="event-panel-resources">
+      <roundcube:object name="plugin.attendees_list" id="edit-resources-table" class="edit-attendees-table" cellspacing="0" cellpadding="0" border="0" coltitle="resource" />
+      <roundcube:object name="plugin.resources_form" id="edit-resources-form" />
+      <roundcube:include file="/templates/freebusylegend.html" />
+    </div>
     <!-- attachments list (with upload form) -->
     <div id="event-panel-attachments">
       <div id="edit-attachments" class="attachments-list">


commit 7fe882108a4d2b4be0c7d7b62bc09f777aaf8e80
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Mar 11 10:14:49 2014 +0100

    Display resource information on click to resource-type attendee; Fix css class update

diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 986ea8c..86cd6b0 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -286,7 +286,7 @@ function rcube_calendar_ui(settings)
     // event details dialog (show only)
     var event_show_dialog = function(event)
     {
-      var $dialog = $("#eventshow").removeClass().addClass('uidialog');
+      var $dialog = $("#eventshow").attr('class', 'uidialog');
       var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:false };
       me.selected_event = event;
 
@@ -314,9 +314,9 @@ function rcube_calendar_ui(settings)
         $('#event-alarm').show().children('.event-text').html(Q(event.alarms_text));
       
       if (calendar.name)
-        $('#event-calendar').show().children('.event-text').html(Q(calendar.name)).removeClass().addClass('event-text').addClass('cal-'+calendar.id);
+        $('#event-calendar').show().children('.event-text').html(Q(calendar.name)).attr('class', 'event-text').addClass('cal-'+calendar.id);
       if (event.categories)
-        $('#event-category').show().children('.event-text').html(Q(event.categories)).removeClass().addClass('event-text cat-'+String(event.categories).toLowerCase().replace(rcmail.identifier_expr, ''));
+        $('#event-category').show().children('.event-text').html(Q(event.categories)).attr('class', 'event-text cat-'+String(event.categories).toLowerCase().replace(rcmail.identifier_expr, ''));
       if (event.free_busy)
         $('#event-free-busy').show().children('.event-text').html(Q(rcmail.gettext(event.free_busy, 'calendar')));
       if (event.priority > 0) {
@@ -354,7 +354,7 @@ function rcube_calendar_ui(settings)
           data = event.attendees[j];
           dispname = Q(data.name || data.email);
           if (data.email) {
-            dispname = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink">' + dispname + '</a>';
+            dispname = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink" data-cutype="' + data.cutype + '">' + dispname + '</a>';
             if (data.role == 'ORGANIZER')
               organizer = true;
             else if ((data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE' || data.rsvp) && settings.identity.emails.indexOf(';'+data.email) >= 0)
@@ -377,7 +377,7 @@ function rcube_calendar_ui(settings)
           $('#event-attendees').show()
             .children('.event-text')
             .html(html)
-            .find('a.mailtolink').click(function(e) { rcmail.redirect(rcmail.url('mail/compose', { _to:this.href.substr(7) })); return false; });
+            .find('a.mailtolink').click(event_attendee_click);
 
           // display all attendees in a popup when clicking the "more" link
           if (morelink) {
@@ -388,7 +388,7 @@ function rcube_calendar_ui(settings)
                 rcmail.gettext('tabattendees','calendar'),
                 null,
                 { width:450, modal:false });
-              $('#all-event-attendees a.mailtolink').click(function(e) { rcmail.redirect(rcmail.url('mail/compose', { _to:this.href.substr(7) })); return false; });
+              $('#all-event-attendees a.mailtolink').click(event_attendee_click);
               return false;
             })
           }
@@ -444,6 +444,20 @@ function rcube_calendar_ui(settings)
 */
     };
 
+    // event handler for clicks on an attendee link
+    var event_attendee_click = function(e)
+    {
+      var cutype = $(this).attr('data-cutype'),
+        mailto = this.href.substr(7);
+      if (rcmail.env.calendar_resources && cutype == 'RESOURCE') {
+        event_resources_dialog(mailto);
+      }
+      else {
+        rcmail.redirect(rcmail.url('mail/compose', { _to:mailto }));
+      }
+      return false;
+    };
+
     // bring up the event dialog (jquery-ui popup)
     var event_edit_dialog = function(action, event)
     {
@@ -885,7 +899,7 @@ function rcube_calendar_ui(settings)
               var j = $.inArray(attendee.role, roles);
               j = (j+1) % roles.length;
               attendee.role = roles[j];
-              $(e.target).parent().removeClass().addClass('attendee '+String(attendee.role).toLowerCase());
+              $(e.target).parent().attr('class', 'attendee '+String(attendee.role).toLowerCase());
               
               // update total display if required-status changed
               if (req != (roles[j] != 'OPT-PARTICIPANT' && roles[j] != 'NON-PARTICIPANT')) {
@@ -1481,7 +1495,7 @@ function rcube_calendar_ui(settings)
 
       var dispname = Q(data.name || data.email);
       if (data.email)
-        dispname = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink">' + dispname + '</a>';
+        dispname = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink" data-cutype="' + data.cutype + '">' + dispname + '</a>';
       
       // role selection
       var organizer = data.role == 'ORGANIZER';
@@ -1523,7 +1537,7 @@ function rcube_calendar_ui(settings)
         .appendTo(table);
 
       tr.find('a.deletelink').click({ id:(data.email || data.name) }, function(e) { remove_attendee(this, e.data.id); return false; });
-      tr.find('a.mailtolink').click(function(e) { rcmail.redirect(rcmail.url('mail/compose', { _to:this.href.substr(7) })); return false; });
+      tr.find('a.mailtolink').click(event_attendee_click);
 
       // select organizer identity
       if (data.identity_id)
@@ -1555,11 +1569,11 @@ function rcube_calendar_ui(settings)
     {
       var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { freebusy:false };
       if (!calendar.freebusy) {
-        $(icon).removeClass().addClass('availabilityicon unknown');
+        $(icon).attr('class', 'availabilityicon unknown');
         return;
       }
       
-      icon = $(icon).removeClass().addClass('availabilityicon loading');
+      icon = $(icon).attr('class', 'availabilityicon loading');
       
       $.ajax({
         type: 'GET',
@@ -1583,10 +1597,9 @@ function rcube_calendar_ui(settings)
     };
 
     // open a dialog to display detailed free-busy information and to find free slots
-    var event_resources_dialog = function()
+    var event_resources_dialog = function(search)
     {
-      var $dialog = $('#eventresourcesdialog'),
-        event = me.selected_event;
+      var $dialog = $('#eventresourcesdialog');
 
       if ($dialog.is(':ui-dialog'))
         $dialog.dialog('close');
@@ -1621,6 +1634,9 @@ function rcube_calendar_ui(settings)
 
       me.dialog_resize($dialog.get(0), 540, Math.min(1000, $(window).width() - 50));
 
+      // set search query
+      $('#resourcesearchbox').val(search || '');
+
       // initialize the treelist widget
       if (!resources_treelist) {
         resources_treelist = new rcube_treelist_widget(rcmail.gui_objects.resourceslist, {
@@ -1632,7 +1648,7 @@ function rcube_calendar_ui(settings)
         resources_treelist.addEventListener('select', function(node) {
           if (resources_data[node.id]) {
             resource_showinfo(resources_data[node.id]);
-            rcmail.enable_command('add-resource', me.selected_event ? true : false);
+            rcmail.enable_command('add-resource', me.selected_event && $("#eventedit").is(':visible') ? true : false);
           }
           else {
             rcmail.enable_command('add-resource', false);
@@ -1650,6 +1666,8 @@ function rcube_calendar_ui(settings)
       }
       else {
         resources_treelist.select('__none__');
+        if (search)
+          resource_search();
       }
     };
 
@@ -1717,7 +1735,13 @@ function rcube_calendar_ui(settings)
       });
 
       resources_index.reverse();
-      resource_render_list(resources_index);
+
+      // apply search filter...
+      if ($('#resourcesearchbox').val() != '')
+        resource_search();
+      else  // ...or render full list
+        resource_render_list(resources_index);
+
       rcmail.set_busy(false, null, me.loading_lock);
     };
 
@@ -1752,7 +1776,7 @@ function rcube_calendar_ui(settings)
             continue;
 
           table.append($('<tr>').addClass(k)
-            .append('<td class="title">' + Q(ucfirst(rcmail.get_label('owner'+k, 'calendar'))) + '</td>')
+            .append('<td class="title">' + Q(ucfirst(rcmail.get_label(k, 'calendar'))) + '</td>')
             .append('<td class="value">' + text2html(data[k]) + '</td>')
           );
         }
@@ -1771,7 +1795,7 @@ function rcube_calendar_ui(settings)
         // search by iterating over all resource records
         for (var dn in resources_data) {
           rec = resources_data[dn];
-          if (String(rec.name).toLowerCase().indexOf(q) >= 0) {
+          if (String(rec.name).toLowerCase().indexOf(q) >= 0 || String(rec.email).toLowerCase() == q) {
             dataset.push(rec.ID);
           }
         }
@@ -2634,6 +2658,11 @@ function rcube_calendar_ui(settings)
 
     /***  startup code  ***/
 
+    // destroy wrongly configured treelist widget for the calendars list
+    if (rcmail.gui_objects.folderlist && rcmail.treelist) {
+      rcmail.treelist = null;
+    }
+
     // create list of event sources AKA calendars
     this.calendars = {};
     var id, li, cal, active, color, brightness, event_sources = [];
@@ -2705,6 +2734,9 @@ function rcube_calendar_ui(settings)
     if (settings.default_calendar && this.calendars[settings.default_calendar] && !this.calendars[settings.default_calendar].readonly)
       this.selected_calendar = settings.default_calendar;
     
+    if (this.selected_calendar)
+      rcmail.select_folder(this.selected_calendar, 'rcmlical');
+    
     var viewdate = new Date();
     if (rcmail.env.date)
       viewdate.setTime(fromunixtime(rcmail.env.date));
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 14bc422..127456f 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -797,6 +797,9 @@ class calendar_ui
     $this->rc->output->add_gui_object('resourceinfo', $attrib['id']);
     $this->rc->output->add_gui_object('resourceownerinfo', $attrib['id'] . '-owner');
 
+    // copy address book labels for owner details to client
+    $this->rc->output->add_label('name','firstname','surname','department','jobtitle','email','phone','address');
+
     $table_attrib = array('id','class','style','width','summary','cellpadding','cellspacing','border');
 
     return html::tag('table', $attrib,


commit 0b07e01b151ce3c1dbafa07d073fce9f36127ce0
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Mar 11 10:13:14 2014 +0100

    Add icon for role NON-PARTICIPANT

diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 7efe868..1806467 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -441,7 +441,7 @@ a.miniColors-trigger {
 .event-attendees span.attendee {
 	padding-right: 18px;
 	margin-right: 0.5em;
-	background: url(images/attendee-status.gif) right 0 no-repeat;
+	background: url(images/attendee-status.png) right 0 no-repeat;
 }
 
 .event-attendees span.attendee a.mailtolink {
@@ -467,7 +467,7 @@ a.miniColors-trigger {
 }
 
 .event-attendees span.delegated {
-	background-position: right -160px;
+	background-position: right -180px;
 }
 
 .event-attendees span.organizer {
@@ -789,7 +789,7 @@ td.topalign {
 .edit-attendees-table td.confirmstate span {
 	display: block;
 	width: 20px;
-	background: url(images/attendee-status.gif) 5px 0 no-repeat;
+	background: url(images/attendee-status.png) 5px 0 no-repeat;
 }
 
 .edit-attendees-table td.confirmstate span.needs-action {
@@ -808,7 +808,7 @@ td.topalign {
 }
 
 .edit-attendees-table td.confirmstate span.delegated {
-	background-position: 5px -160px;
+	background-position: 5px -180px;
 }
 
 #attendees-freebusy-table {
@@ -840,7 +840,7 @@ td.topalign {
 
 .attendees-list .attendee {
 	padding: 4px 4px 4px 1px;
-	background: url(images/attendee-status.gif) 2px -97px no-repeat;
+	background: url(images/attendee-status.png) 2px -97px no-repeat;
 	white-space: nowrap;
 }
 
@@ -868,10 +868,14 @@ td.topalign {
 	background-position: 2px -117px;
 }
 
-.attendees-list .chair {
+.attendees-list .non-participant {
 	background-position: 2px -137px;
 }
 
+.attendees-list .chair {
+	background-position: 2px -157px;
+}
+
 .attendees-list .loading {
 	background: url(images/loading_blue.gif) 1px 50% no-repeat;
 }
@@ -1519,9 +1523,10 @@ div.calendar-invitebox .rsvp-status.loading {
 
 div.calendar-invitebox .rsvp-status.declined,
 div.calendar-invitebox .rsvp-status.tentative,
+div.calendar-invitebox .rsvp-status.delegated,
 div.calendar-invitebox .rsvp-status.accepted {
 	padding: 0 0 1px 22px;
-	background: url(images/attendee-status.gif) 2px -20px no-repeat;
+	background: url(images/attendee-status.png) 2px -20px no-repeat;
 }
 
 div.calendar-invitebox .rsvp-status.declined {
@@ -1532,6 +1537,10 @@ div.calendar-invitebox .rsvp-status.tentative {
 	background-position: 2px -60px;
 }
 
+div.calendar-invitebox .rsvp-status.delegated {
+	background-position: 2px -180px;
+}
+
 /* iTIP attend reply page */
 
 .calendaritipattend .centerbox {
diff --git a/plugins/calendar/skins/larry/images/attendee-status.gif b/plugins/calendar/skins/larry/images/attendee-status.gif
deleted file mode 100644
index 60c5d95..0000000
Binary files a/plugins/calendar/skins/larry/images/attendee-status.gif and /dev/null differ
diff --git a/plugins/calendar/skins/larry/images/attendee-status.png b/plugins/calendar/skins/larry/images/attendee-status.png
new file mode 100644
index 0000000..59b4493
Binary files /dev/null and b/plugins/calendar/skins/larry/images/attendee-status.png differ
diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html
index 96be36e..c189491 100644
--- a/plugins/calendar/skins/larry/templates/calendar.html
+++ b/plugins/calendar/skins/larry/templates/calendar.html
@@ -182,6 +182,7 @@
 		<span class="attendee organizer"><roundcube:label name="calendar.roleorganizer" /></span>
 		<span class="attendee req-participant"><roundcube:label name="calendar.rolerequired" /></span>
 		<span class="attendee opt-participant"><roundcube:label name="calendar.roleoptional" /></span>
+		<span class="attendee non-participant"><roundcube:label name="calendar.rolenonparticipant" /></span>
 		<span class="attendee chair"><roundcube:label name="calendar.rolechair" /></span>
 	</div>
 </div>


commit 51fe7c26fb93ed201551ca44ffd590ad93424f8d
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Mar 10 14:45:24 2014 +0100

    Refactored resources directory to be an individual part and not dependant on a specific calendar backend driver

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 152cc53..365ce9e 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -38,6 +38,7 @@ class calendar extends rcube_plugin
   public $rc;
   public $lib;
   public $driver;
+  public $resources_dir;
   public $home;  // declare public to be used in other classes
   public $urlbase;
   public $timezone;
@@ -211,16 +212,10 @@ class calendar extends rcube_plugin
     require_once($this->home . '/drivers/calendar_driver.php');
     require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php');
 
-    switch ($driver_name) {
-      case "kolab":
-        $this->require_plugin('libkolab');
-      default:
-        $this->driver = new $driver_class($this);
-        break;
-     }
+    $this->driver = new $driver_class($this);
 
-     if ($this->driver->undelete)
-        $this->driver->undelete = $this->rc->config->get('undo_timeout', 0) > 0;
+    if ($this->driver->undelete)
+      $this->driver->undelete = $this->rc->config->get('undo_timeout', 0) > 0;
   }
 
   /**
@@ -297,7 +292,7 @@ class calendar extends rcube_plugin
 
     $this->rc->output->set_env('timezone', $this->timezone->getName());
     $this->rc->output->set_env('calendar_driver', $this->rc->config->get('calendar_driver'), false);
-    $this->rc->output->set_env('resources', (bool)$this->driver->resources);
+    $this->rc->output->set_env('calendar_resources', (bool)$this->rc->config->get('calendar_resources_driver'));
     $this->rc->output->set_env('mscolors', $this->driver->get_color_values());
     $this->rc->output->set_env('identities-selector', $this->ui->identity_select(array('id' => 'edit-identities-list')));
 
@@ -1936,6 +1931,30 @@ class calendar extends rcube_plugin
 
   /****  Resource management functions  ****/
 
+  /**
+   * Getter for the configured implementation of the resource directory interface
+   */
+  private function resources_directory()
+  {
+    if (is_object($this->resources_dir)) {
+      return $this->resources_dir;
+    }
+
+    if ($driver_name = $this->rc->config->get('calendar_resources_driver')) {
+      $driver_class = 'resources_driver_' . $driver_name;
+
+      require_once($this->home . '/drivers/resources_driver.php');
+      require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php');
+
+      $this->resources_dir = new $driver_class($this);
+    }
+
+    return $this->resources_dir;
+  }
+
+  /**
+   * Handler for resoruce autocompletion requests
+   */
   public function resources_autocomplete()
   {
     $search = rcube_utils::get_input_value('_search', rcube_utils::INPUT_GPC, true);
@@ -1943,12 +1962,14 @@ class calendar extends rcube_plugin
     $maxnum = (int)$this->rc->config->get('autocomplete_max', 15);
     $results = array();
 
-    foreach ($this->driver->load_resources($search, $maxnum) as $rec) {
-      $results[]  = array(
-          'name'  => $rec['name'],
-          'email' => $rec['email'],
-          'type'  => $rec['_type'],
-      );
+    if ($directory = $this->resources_directory()) {
+      foreach ($directory->load_resources($search, $maxnum) as $rec) {
+        $results[]  = array(
+            'name'  => $rec['name'],
+            'email' => $rec['email'],
+            'type'  => $rec['_type'],
+        );
+      }
     }
 
     $this->rc->output->command('ksearch_query_results', $results, $search, $sid);
@@ -1961,9 +1982,11 @@ class calendar extends rcube_plugin
   function resources_list()
   {
     $data = array();
-    foreach ($this->driver->load_resources() as $rec) {
-      $rec['dn'] = rcube_ldap::dn_decode($rec['ID']);
-      $data[] = $rec;
+
+    if ($directory = $this->resources_directory()) {
+      foreach ($directory->load_resources() as $rec) {
+        $data[] = $rec;
+      }
     }
 
     $this->rc->output->command('plugin.resource_data', $data);
@@ -1975,8 +1998,10 @@ class calendar extends rcube_plugin
    */
   function resources_owner()
   {
-    $id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
-    $data = $this->driver->get_resource_owner($id);
+    if ($directory = $this->resources_directory()) {
+      $id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
+      $data = $directory->get_resource_owner($id);
+    }
 
     $this->rc->output->command('plugin.resource_owner', $data);
     $this->rc->output->send();
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index f39c4c0..986ea8c 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -762,7 +762,7 @@ function rcube_calendar_ui(settings)
 
       // show/hide tabs according to calendar's feature support
       $('#edit-tab-attendees')[(calendar.attendees?'show':'hide')]();
-      $('#edit-tab-resources')[(calendar.resources?'show':'hide')]();
+      $('#edit-tab-resources')[(rcmail.env.calendar_resources?'show':'hide')]();
       $('#edit-tab-attachments')[(calendar.attachments?'show':'hide')]();
 
       // activate the first tab
@@ -1516,7 +1516,7 @@ function rcube_calendar_ui(settings)
         '<td class="confirmstate"><span class="' + String(data.status).toLowerCase() + '" title="' + Q(data.status || '') + '">' + Q(data.status || '') + '</span></td>' +
         '<td class="options">' + (organizer || readonly ? '' : dellink) + '</td>';
 
-      var table = calendar.resources && data.cutype == 'RESOURCE' ? resources_list : attendees_list;
+      var table = rcmail.env.calendar_resources && data.cutype == 'RESOURCE' ? resources_list : attendees_list;
       var tr = $('<tr>')
         .addClass(String(data.role).toLowerCase())
         .html(html)
@@ -1706,12 +1706,12 @@ function rcube_calendar_ui(settings)
 
       // assign parent-relations
       $.each(data, function(i, rec) {
-        resources_data[rec.dn] = rec;
-        resources_index.push(rec.dn);
+        resources_data[rec.ID] = rec;
+        resources_index.push(rec.ID);
 
         if (rec.members) {
           $.each(rec.members, function(j, m){
-            resources_data[m].parent_id = rec.dn;
+            resources_data[m].parent_id = rec.ID;
           });
         }
       });
@@ -1730,10 +1730,10 @@ function rcube_calendar_ui(settings)
       $.each(index, function(i, dn) {
         if (rec = resources_data[dn]) {
           link = $('<a>').attr('href', '#')
-            .attr('rel', rec.dn)
+            .attr('rel', rec.ID)
             .html(Q(rec.name));
 
-          resources_treelist.insert({ id:rec.dn, html:link, classes:[rec._type], collapsed:true }, rec.parent_id, false);
+          resources_treelist.insert({ id:rec.ID, html:link, classes:[rec._type], collapsed:true }, rec.parent_id, false);
         }
       });
     };
@@ -1772,7 +1772,7 @@ function rcube_calendar_ui(settings)
         for (var dn in resources_data) {
           rec = resources_data[dn];
           if (String(rec.name).toLowerCase().indexOf(q) >= 0) {
-            dataset.push(rec.dn);
+            dataset.push(rec.ID);
           }
         }
 
diff --git a/plugins/calendar/config.inc.php.dist b/plugins/calendar/config.inc.php.dist
index 9a472a7..f09d30f 100644
--- a/plugins/calendar/config.inc.php.dist
+++ b/plugins/calendar/config.inc.php.dist
@@ -135,4 +135,11 @@ $rcmail_config['calendar_itip_smtp_pass'] = '123456';
 // %i - Calendar UUID
 // $rcmail_config['calendar_caldav_url'] = 'http://%h/iRony/calendars/%u/%i';
 
+// Driver to provide a resource directory ('ldap' is the only implementation yet).
+// Leave empty or commented to disable resources support.
+// $rcmail_config['calendar_resources_driver'] = 'ldap';
+
+// LDAP directory configuration to find avilable resources for events
+// $rcmail_config['calendar_resources_directory'] = array(/* ldap_public-like address book configuration */)
+
 ?>
diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php
index 3d8d083..20f9f16 100644
--- a/plugins/calendar/drivers/calendar_driver.php
+++ b/plugins/calendar/drivers/calendar_driver.php
@@ -86,7 +86,6 @@ abstract class calendar_driver
   // features supported by backend
   public $alarms = false;
   public $attendees = false;
-  public $resources = false;
   public $freebusy = false;
   public $attachments = false;
   public $undelete = false; // event undelete action
@@ -530,54 +529,4 @@ abstract class calendar_driver
     return $events;
   }
 
-  /**
-   * Store alarm dismissal for birtual birthay events
-   *
-   * @param  string  Event identifier
-   * @param  integer Suspend the alarm for this number of seconds
-   */
-  public function dismiss_birthday_alarm($event_id, $snooze = 0)
-  {
-    $rcmail = rcmail::get_instance();
-    $cache  = $rcmail->get_cache('calendar.birthdayalarms', 'db', 86400 * 30);
-    $cache->remove($event_id);
-
-    // compute new notification time or disable if not snoozed
-    $notifyat = $snooze > 0 ? time() + $snooze : null;
-    $cache->set($event_id, array('snooze' => $snooze, 'notifyat' => $notifyat));
-
-    return true;
-  }
-
-
-  /**
-   * Fetch resource objects to be displayed for booking
-   *
-   * @param  string  Search query (optional)
-   * @return array  List of resource records available for booking
-   */
-  public function load_resources($query = null)
-  {
-    return array();
-  }
-
-  /**
-   * Return properties of a single resource
-   *
-   * @param mixed  UID string
-   * @return array Resource object as hash array
-   */
-  public function get_resource($uid)
-  {
-    return null;
-  }
-
-  /**
-   *
-   */
-  public function get_resource_owner($id)
-  {
-    return null;
-  }
-
 }
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 1bf4e17..0bcab59 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -47,6 +47,8 @@ class kolab_driver extends calendar_driver
    */
   public function __construct($cal)
   {
+    $cal->require_plugin('libkolab');
+
     $this->cal = $cal;
     $this->rc = $cal->rc;
     $this->_read_calendars();
@@ -60,10 +62,6 @@ class kolab_driver extends calendar_driver
         $this->alarm_types = array('DISPLAY');
         $this->alarm_absolute = false;
     }
-
-    if ($this->rc->config->get('calendar_resources_directory')) {
-      $this->resources = true;
-    }
   }
 
 
@@ -1287,105 +1285,4 @@ class kolab_driver extends calendar_driver
         'FFDEAD');
   }
 
-
-  private function resurces_ldap()
-  {
-    if (!isset($this->resources_dir)) {
-      $this->resources_dir = new rcube_ldap($this->rc->config->get('calendar_resources_directory'), true);
-    }
-
-    return $this->resources_dir->ready ? $this->resources_dir : null;
-  }
-
-
-  /**
-   * Fetch resource objects to be displayed for booking
-   *
-   * @param  string  Search query (optional)
-   * @return array  List of resource records available for booking
-   */
-  public function load_resources($query = null, $num = 5000)
-  {
-    if (!($ldap = $this->resurces_ldap())) {
-      return array();
-    }
-
-    // TODO: apply paging
-    $ldap->set_pagesize($num);
-
-    if (isset($query)) {
-      $results = $ldap->search('*', $query, 0, true, true);
-    }
-    else {
-      $results = $ldap->list_records();
-    }
-
-    if ($results instanceof ArrayAccess) {
-      foreach ($results as $i => $rec) {
-        $results[$i] = $this->decode_resource($rec);
-      }
-    }
-
-    return $results;
-  }
-
-  /**
-   * Return properties of a single resource
-   *
-   * @param mixed  UID string
-   * @return array Resource object as hash array
-   */
-  public function get_resource($uid)
-  {
-    $rec = null;
-
-    if ($ldap = $this->resurces_ldap()) {
-      $rec = $ldap->get_record($uid);
-
-      if (!empty($rec)) {
-        $rec = $this->decode_resource($rec);
-      }
-    }
-
-    return $rec;
-  }
-
-  /**
-   *
-   */
-  public function get_resource_owner($dn)
-  {
-    $owner = null;
-
-    if ($ldap = $this->resurces_ldap()) {
-      $owner = $ldap->get_record(rcube_ldap::dn_encode($dn), true);
-      $owner['ID'] = rcube_ldap::dn_decode($owner['ID']);
-      unset($owner['_raw_attrib'], $owner['_type']);
-    }
-
-    return $owner;
-  }
-
-  /**
-   * Extract JSON-serialized attributes
-   */
-  private function decode_resource($rec)
-  {
-    if (is_array($rec['attributes']) && $rec['attributes'][0]) {
-      $attributes = array();
-
-      foreach ($rec['attributes'] as $sattr) {
-        $attr = @json_decode($sattr, true);
-        $attributes += $attr;
-      }
-
-      $rec['attributes'] = $attributes;
-    }
-
-    // remove unused cruft
-    unset($rec['_raw_attrib']);
-
-    return $rec;
-  }
-
 }
diff --git a/plugins/calendar/drivers/ldap/resources_driver_ldap.php b/plugins/calendar/drivers/ldap/resources_driver_ldap.php
new file mode 100644
index 0000000..23439b6
--- /dev/null
+++ b/plugins/calendar/drivers/ldap/resources_driver_ldap.php
@@ -0,0 +1,146 @@
+<?php
+
+/**
+ * LDAP-based resource directory class using rcube_ldap functionality
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * LDAP-based resource directory implementation
+ */
+class resources_driver_ldap extends resources_driver
+{
+    private $rc;
+    private $cal;
+    private $ldap;
+
+    /**
+     * Default constructor
+     */
+    function __construct($cal)
+    {
+        $this->cal = $cal;
+        $this->rc = $cal->rc;
+    }
+
+    /**
+     * Fetch resource objects to be displayed for booking
+     *
+     * @param  string  Search query (optional)
+     * @return array  List of resource records available for booking
+     */
+    public function load_resources($query = null, $num = 5000)
+    {
+      if (!($ldap = $this->connect())) {
+        return array();
+      }
+
+      // TODO: apply paging
+      $ldap->set_pagesize($num);
+
+      if (isset($query)) {
+        $results = $ldap->search('*', $query, 0, true, true);
+      }
+      else {
+        $results = $ldap->list_records();
+      }
+
+      if ($results instanceof ArrayAccess) {
+        foreach ($results as $i => $rec) {
+          $results[$i] = $this->decode_resource($rec);
+        }
+      }
+
+      return $results;
+    }
+
+    /**
+     * Return properties of a single resource
+     *
+     * @param string  Unique resource identifier
+     * @return array Resource object as hash array
+     */
+    public function get_resource($dn)
+    {
+      $rec = null;
+
+      if ($ldap = $this->connect()) {
+        $rec = $ldap->get_record(rcube_ldap::dn_encode($dn));
+
+        if (!empty($rec)) {
+          $rec = $this->decode_resource($rec);
+        }
+      }
+
+      return $rec;
+    }
+
+    /**
+     * Return properties of a resource owner
+     *
+     * @param string  Owner identifier
+     * @return array  Resource object as hash array
+     */
+    public function get_resource_owner($dn)
+    {
+      $owner = null;
+
+      if ($ldap = $this->connect()) {
+        $owner = $ldap->get_record(rcube_ldap::dn_encode($dn), true);
+        $owner['ID'] = rcube_ldap::dn_decode($owner['ID']);
+        unset($owner['_raw_attrib'], $owner['_type']);
+      }
+
+      return $owner;
+    }
+
+    /**
+     * Extract JSON-serialized attributes
+     */
+    private function decode_resource($rec)
+    {
+      $rec['ID'] = rcube_ldap::dn_decode($rec['ID']);
+
+      if (is_array($rec['attributes']) && $rec['attributes'][0]) {
+        $attributes = array();
+
+        foreach ($rec['attributes'] as $sattr) {
+          $attr = @json_decode($sattr, true);
+          $attributes += $attr;
+        }
+
+        $rec['attributes'] = $attributes;
+      }
+
+      // remove unused cruft
+      unset($rec['_raw_attrib']);
+
+      return $rec;
+    }
+
+    private function connect()
+    {
+      if (!isset($this->ldap)) {
+        $this->ldap = new rcube_ldap($this->rc->config->get('calendar_resources_directory'), true);
+      }
+
+      return $this->ldap->ready ? $this->ldap : null;
+    }
+
+}
\ No newline at end of file
diff --git a/plugins/calendar/drivers/resources_driver.php b/plugins/calendar/drivers/resources_driver.php
new file mode 100644
index 0000000..b1fed9c
--- /dev/null
+++ b/plugins/calendar/drivers/resources_driver.php
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * Resources directory interface definition
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+/**
+ * Interface definition for a resources directory driver classe
+ */
+abstract class resources_driver
+{
+
+  /**
+   * Fetch resource objects to be displayed for booking
+   *
+   * @param  string  Search query (optional)
+   * @return array  List of resource records available for booking
+   */
+  abstract public function load_resources($query = null);
+
+  /**
+   * Return properties of a single resource
+   *
+   * @param string  Unique resource identifier
+   * @return array  Resource object as hash array
+   */
+  abstract public function get_resource($id);
+
+  /**
+   * Return properties of a resource owner
+   *
+   * @param string  Owner identifier
+   * @return array  Resource object as hash array
+   */
+  public function get_resource_owner($id)
+  {
+    return null;
+  }
+
+}
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 710898f..14bc422 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -196,7 +196,6 @@ class calendar_ui
       unset($prop['user_id']);
       $prop['alarms'] = $this->cal->driver->alarms;
       $prop['attendees'] = $this->cal->driver->attendees;
-      $prop['resources'] = $this->cal->driver->resources;
       $prop['freebusy'] = $this->cal->driver->freebusy;
       $prop['attachments'] = $this->cal->driver->attachments;
       $prop['undelete'] = $this->cal->driver->undelete;
diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html
index 574cded..96be36e 100644
--- a/plugins/calendar/skins/larry/templates/calendar.html
+++ b/plugins/calendar/skins/larry/templates/calendar.html
@@ -126,14 +126,6 @@
 			<div class="scroller">
 				<roundcube:object name="plugin.resources_list" id="resources-list" class="listing treelist" />
 			</div>
-		<!--
-			<div class="boxpagenav">
-				<roundcube:button command="firstpage" type="link" class="icon firstpage disabled" classAct="icon firstpage" title="firstpage" content="|&lt;" />
-				<roundcube:button command="previouspage" type="link" class="icon prevpage disabled" classAct="icon prevpage" title="previouspage" content="&lt;" />
-				<roundcube:button command="nextpage" type="link" class="icon nextpage disabled" classAct="icon nextpage" title="nextpage" content="&gt;" />
-				<roundcube:button command="lastpage" type="link" class="icon lastpage disabled" classAct="icon lastpage" title="lastpage" content="&gt;|" />
-			</div>
-		-->
 		</div>
 	</div>
 


commit 0b2e726857cf7cb9af7464ae415adeb029895a02
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Sun Mar 9 16:20:58 2014 +0100

    Cache resource owner info lookups

diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index b1c1a08..f39c4c0 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -51,6 +51,7 @@ function rcube_calendar_ui(settings)
     var resources_treelist;
     var resources_data = {};
     var resources_index = [];
+    var resource_owners = {};
     var freebusy_ui = { workinhoursonly:false, needsupdate:false };
     var freebusy_data = {};
     var current_view = null;
@@ -1681,9 +1682,15 @@ function rcube_calendar_ui(settings)
         $(rcmail.gui_objects.resourceownerinfo).hide();
 
         if (resource.owner) {
-          // fetch owner data from server
-          me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
-          rcmail.http_request('resources-owner', { _id: resource.owner }, me.loading_lock);
+          // display cached data
+          if (resource_owners[resource.owner]) {
+            resource_owner_load(resource_owners[resource.owner]);
+          }
+          else {
+            // fetch owner data from server
+            me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
+            rcmail.http_request('resources-owner', { _id: resource.owner }, me.loading_lock);
+          }
         }
       }
     };
@@ -1735,12 +1742,13 @@ function rcube_calendar_ui(settings)
     var resource_owner_load = function(data)
     {
       if (data) {
-        // TODO: cache this!
+        // cache this!
+        resource_owners[data.ID] = data;
 
         var table = $(rcmail.gui_objects.resourceownerinfo).find('tbody').html('');
 
         for (var k in data) {
-          if (k == 'event')
+          if (k == 'event' || k == 'ID')
             continue;
 
           table.append($('<tr>').addClass(k)
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 0b23f64..1bf4e17 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -1359,7 +1359,8 @@ class kolab_driver extends calendar_driver
 
     if ($ldap = $this->resurces_ldap()) {
       $owner = $ldap->get_record(rcube_ldap::dn_encode($dn), true);
-      unset($owner['_raw_attrib'], $owner['_type'], $owner['ID']);
+      $owner['ID'] = rcube_ldap::dn_decode($owner['ID']);
+      unset($owner['_raw_attrib'], $owner['_type']);
     }
 
     return $owner;


commit 6fe1cfcdd2dbd89fcc18774dfdd5aabfd663de39
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Sun Mar 9 16:11:38 2014 +0100

    Set 'mainaction' class to dialog buttons

diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 61e02a0..b1c1a08 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -779,6 +779,9 @@ function rcube_calendar_ui(settings)
         resizable: (!bw.ie6 && !bw.ie7),  // disable for performance reasons
         closeOnEscape: false,
         title: rcmail.gettext((action == 'edit' ? 'edit_event' : 'new_event'), 'calendar'),
+        open: function() {
+          $dialog.parent().find('.ui-dialog-buttonset .ui-button').first().addClass('mainaction');
+        },
         close: function() {
           editform.hide().appendTo(document.body);
           $dialog.dialog("destroy").remove();
@@ -1608,10 +1611,15 @@ function rcube_calendar_ui(settings)
           $dialog.dialog('destroy').hide();
         },
         buttons: buttons,
-        width: Math.min(1000, $(window).width() - 50),
+        width: 900,
         height: 500
       }).show();
 
+      // define add-button as main action
+      $('.ui-dialog-buttonset .ui-button', $dialog.parent()).first().addClass('mainaction').attr('id', 'rcmbtncalresadd');
+
+      me.dialog_resize($dialog.get(0), 540, Math.min(1000, $(window).width() - 50));
+
       // initialize the treelist widget
       if (!resources_treelist) {
         resources_treelist = new rcube_treelist_widget(rcmail.gui_objects.resourceslist, {
@@ -1635,14 +1643,13 @@ function rcube_calendar_ui(settings)
         // fetch (all) resource data from server
         me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
         rcmail.http_request('resources-list', {}, me.loading_lock);
+
+        // register button
+        rcmail.register_button('add-resource', 'rcmbtncalresadd', 'uibutton');
       }
       else {
         resources_treelist.select('__none__');
       }
-
-      // register button
-      $('.ui-dialog-buttonset .ui-button', $dialog.parent()).first().addClass('mainaction').attr('id', 'rcmbtncalresadd');
-      rcmail.register_button('add-resource', 'rcmbtncalresadd', 'input');
     };
 
     // render the resource details UI box
@@ -1728,6 +1735,8 @@ function rcube_calendar_ui(settings)
     var resource_owner_load = function(data)
     {
       if (data) {
+        // TODO: cache this!
+
         var table = $(rcmail.gui_objects.resourceownerinfo).find('tbody').html('');
 
         for (var k in data) {
@@ -1899,13 +1908,8 @@ function rcube_calendar_ui(settings)
           return false;
         });
         
-        var buttons = [{
-          text: rcmail.gettext('cancel', 'calendar'),
-          click: function() {
-            $(this).dialog("close");
-          }
-        }];
-        
+        var buttons = [];
+
         if (!event.recurrence) {
           buttons.push({
             text: rcmail.gettext((action == 'remove' ? 'remove' : 'save'), 'calendar'),
@@ -1917,13 +1921,23 @@ function rcube_calendar_ui(settings)
             }
           });
         }
-      
+
+        buttons.push({
+          text: rcmail.gettext('cancel', 'calendar'),
+          click: function() {
+            $(this).dialog("close");
+          }
+        });
+
         $dialog.dialog({
           modal: true,
           width: 460,
           dialogClass: 'warning',
           title: rcmail.gettext((action == 'remove' ? 'removeeventconfirm' : 'changeeventconfirm'), 'calendar'),
           buttons: buttons,
+          open: function() {
+            $dialog.parent().find('.ui-button').first().focus();
+          },
           close: function(){
             $dialog.dialog("destroy").hide();
             if (!rcmail.busy)
@@ -2156,6 +2170,9 @@ function rcube_calendar_ui(settings)
         resizable: true,
         closeOnEscape: false,
         title: rcmail.gettext((calendar.id ? 'editcalendar' : 'createcalendar'), 'calendar'),
+        open: function() {
+          $dialog.parent().find('.ui-dialog-buttonset .ui-button').first().addClass('mainaction');
+        },
         close: function() {
           $dialog.html('').dialog("destroy").hide();
         },
@@ -2249,6 +2266,9 @@ function rcube_calendar_ui(settings)
         resizable: false,
         closeOnEscape: false,
         title: rcmail.gettext('importevents', 'calendar'),
+        open: function() {
+          $dialog.parent().find('.ui-dialog-buttonset .ui-button').first().addClass('mainaction');
+        },
         close: function() {
           $('.ui-dialog-buttonpane button', $dialog.parent()).button('enable');
           $dialog.dialog("destroy").hide();
@@ -2326,6 +2346,9 @@ function rcube_calendar_ui(settings)
         resizable: false,
         closeOnEscape: false,
         title: rcmail.gettext('exporttitle', 'calendar'),
+        open: function() {
+          $dialog.parent().find('.ui-dialog-buttonset .ui-button').first().addClass('mainaction');
+        },
         close: function() {
           $('.ui-dialog-buttonpane button', $dialog.parent()).button('enable');
           $dialog.dialog("destroy").hide();
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 8e5b24b..745d676 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -1341,6 +1341,9 @@ function rcube_tasklist_ui(settings)
           resizable: (!bw.ie6 && !bw.ie7),  // disable for performance reasons
           closeOnEscape: false,
           title: rcmail.gettext((action == 'edit' ? 'edittask' : 'newtask'), 'tasklist'),
+          open: function() {
+            $dialog.parent().find('.ui-dialog-buttonset .ui-button').first().addClass('mainaction');
+          },
           close: function() {
               editform.hide().appendTo(document.body);
               $dialog.dialog('destroy').remove();
@@ -1664,7 +1667,12 @@ function rcube_tasklist_ui(settings)
           resizable: true,
           closeOnEscape: false,
           title: rcmail.gettext((list.id ? 'editlist' : 'createlist'), 'tasklist'),
-          close: function() { $dialog.dialog('destroy').hide(); },
+          open: function() {
+            $dialog.parent().find('.ui-dialog-buttonset .ui-button').first().addClass('mainaction');
+          },
+          close: function() {
+            $dialog.dialog('destroy').hide();
+          },
           buttons: buttons,
           minWidth: 400,
           width: 420


commit f0dd07fa289db0506836363fc5fb0896378bd908
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri Mar 7 16:15:25 2014 +0100

    Add resource searching/booking capabilities to the calendar module

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 4e8995e..152cc53 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -141,6 +141,9 @@ class calendar extends rcube_plugin
       $this->register_action('mailtoevent', array($this, 'mail_message2event'));
       $this->register_action('inlineui', array($this, 'get_inline_ui'));
       $this->register_action('check-recent', array($this, 'check_recent'));
+      $this->register_action('resources-list', array($this, 'resources_list'));
+      $this->register_action('resources-owner', array($this, 'resources_owner'));
+      $this->register_action('resources-autocomplete', array($this, 'resources_autocomplete'));
       $this->add_hook('refresh', array($this, 'refresh'));
 
       // remove undo information...
@@ -287,13 +290,14 @@ class calendar extends rcube_plugin
     $this->ui->addJS();
 
     $this->ui->init_templates();
-    $this->rc->output->add_label('lowest','low','normal','high','highest','delete','cancel','uploading','noemailwarning');
+    $this->rc->output->add_label('lowest','low','normal','high','highest','delete','cancel','uploading','noemailwarning','close');
 
     // initialize attendees autocompletion
     rcube_autocomplete_init();
 
     $this->rc->output->set_env('timezone', $this->timezone->getName());
     $this->rc->output->set_env('calendar_driver', $this->rc->config->get('calendar_driver'), false);
+    $this->rc->output->set_env('resources', (bool)$this->driver->resources);
     $this->rc->output->set_env('mscolors', $this->driver->get_color_values());
     $this->rc->output->set_env('identities-selector', $this->ui->identity_select(array('id' => 'edit-identities-list')));
 
@@ -1930,6 +1934,55 @@ class calendar extends rcube_plugin
   }
 
 
+  /****  Resource management functions  ****/
+
+  public function resources_autocomplete()
+  {
+    $search = rcube_utils::get_input_value('_search', rcube_utils::INPUT_GPC, true);
+    $sid    = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
+    $maxnum = (int)$this->rc->config->get('autocomplete_max', 15);
+    $results = array();
+
+    foreach ($this->driver->load_resources($search, $maxnum) as $rec) {
+      $results[]  = array(
+          'name'  => $rec['name'],
+          'email' => $rec['email'],
+          'type'  => $rec['_type'],
+      );
+    }
+
+    $this->rc->output->command('ksearch_query_results', $results, $search, $sid);
+    $this->rc->output->send();
+  }
+
+  /**
+   * Handler for load-requests for resource data
+   */
+  function resources_list()
+  {
+    $data = array();
+    foreach ($this->driver->load_resources() as $rec) {
+      $rec['dn'] = rcube_ldap::dn_decode($rec['ID']);
+      $data[] = $rec;
+    }
+
+    $this->rc->output->command('plugin.resource_data', $data);
+    $this->rc->output->send();
+  }
+
+  /**
+   * Handler for requests loading resource owner information
+   */
+  function resources_owner()
+  {
+    $id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
+    $data = $this->driver->get_resource_owner($id);
+
+    $this->rc->output->command('plugin.resource_owner', $data);
+    $this->rc->output->send();
+  }
+
+
   /****  Event invitation plugin hooks ****/
   
   /**
diff --git a/plugins/calendar/calendar_base.js b/plugins/calendar/calendar_base.js
index 33fe9e4..e703959 100644
--- a/plugins/calendar/calendar_base.js
+++ b/plugins/calendar/calendar_base.js
@@ -48,7 +48,7 @@ function rcube_calendar(settings)
           ).then(function() {
             // disable attendees feature (autocompletion and stuff is not initialized)
             for (var c in rcmail.env.calendars)
-              rcmail.env.calendars[c].attendees = false;
+              rcmail.env.calendars[c].attendees = rcmail.env.calendars[c].resources = false;
             
             me.ui_loaded = true;
             me.ui = new rcube_calendar_ui(me.settings);
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index ff6342b..61e02a0 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -47,6 +47,10 @@ function rcube_calendar_ui(settings)
     var event_defaults = { free_busy:'busy', alarms:'' };
     var event_attendees = [];
     var attendees_list;
+    var resources_list;
+    var resources_treelist;
+    var resources_data = {};
+    var resources_index = [];
     var freebusy_ui = { workinhoursonly:false, needsupdate:false };
     var freebusy_data = {};
     var current_view = null;
@@ -103,6 +107,11 @@ function rcube_calendar_ui(settings)
       return result;
     };
 
+    // Change the first charcter to uppercase
+    var ucfirst = function(str)
+    {
+        return str.charAt(0).toUpperCase() + str.substr(1);
+    };
 
     // clone the given date object and optionally adjust time
     var clone_date = function(date, adjust)
@@ -332,6 +341,13 @@ function rcube_calendar_ui(settings)
 
       // list event attendees
       if (calendar.attendees && event.attendees) {
+        // sort resources to the end
+        event.attendees.sort(function(a,b) {
+          var j = a.cutype == 'RESOURCE' ? 1 : 0,
+              k = b.cutype == 'RESOURCE' ? 1 : 0;
+          return (j - k);
+        });
+
         var data, dispname, organizer = false, rsvp = false, line,  morelink, html = '',overflow = '';
         for (var j=0; j < event.attendees.length; j++) {
           data = event.attendees[j];
@@ -340,7 +356,7 @@ function rcube_calendar_ui(settings)
             dispname = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink">' + dispname + '</a>';
             if (data.role == 'ORGANIZER')
               organizer = true;
-            else if ((data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE') && settings.identity.emails.indexOf(';'+data.email) >= 0)
+            else if ((data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE' || data.rsvp) && settings.identity.emails.indexOf(';'+data.email) >= 0)
               rsvp = data.status.toLowerCase();
           }
           
@@ -567,6 +583,7 @@ function rcube_calendar_ui(settings)
         allow_invitations = organizer || (calendar.owner && calendar.owner == 'anonymous') || settings.invite_shared;
       event_attendees = [];
       attendees_list = $('#edit-attendees-table > tbody').html('');
+      resources_list = $('#edit-resources-table > tbody').html('');
       $('#edit-attendees-notify')[(notify.checked && allow_invitations ? 'show' : 'hide')]();
       $('#edit-localchanges-warning')[(has_attendees(event) && !(allow_invitations || (calendar.owner && is_organizer(event, calendar.owner))) ? 'show' : 'hide')]();
 
@@ -744,6 +761,7 @@ function rcube_calendar_ui(settings)
 
       // show/hide tabs according to calendar's feature support
       $('#edit-tab-attendees')[(calendar.attendees?'show':'hide')]();
+      $('#edit-tab-resources')[(calendar.resources?'show':'hide')]();
       $('#edit-tab-attachments')[(calendar.attachments?'show':'hide')]();
 
       // activate the first tab
@@ -1405,9 +1423,10 @@ function rcube_calendar_ui(settings)
         $('#edit-startdate').data('duration', Math.round((me.selected_event.end.getTime() - me.selected_event.start.getTime()) / 1000));
       }
     };
-    
+
+
     // add the given list of participants
-    var add_attendees = function(names)
+    var add_attendees = function(names, params)
     {
       names = explode_quoted_string(names.replace(/,\s*$/, ''), ',');
 
@@ -1430,9 +1449,8 @@ function rcube_calendar_ui(settings)
           email = RegExp.$1;
           name = item.replace(email, '').replace(/^["\s<>]+/, '').replace(/["\s<>]+$/, '');
         }
-        
         if (email) {
-          add_attendee({ email:email, name:name, role:'REQ-PARTICIPANT', status:'NEEDS-ACTION' });
+          add_attendee($.extend({ email:email, name:name }, params));
           success = true;
         }
         else {
@@ -1442,16 +1460,21 @@ function rcube_calendar_ui(settings)
       
       return success;
     };
-    
+
     // add the given attendee to the list
     var add_attendee = function(data, readonly)
     {
+      if (!me.selected_event)
+        return false;
+
       // check for dupes...
       var exists = false;
       $.each(event_attendees, function(i, v){ exists |= (v.email == data.email); });
       if (exists)
         return false;
       
+      var calendar = me.selected_event && me.calendars[me.selected_event.calendar] ? me.calendars[me.selected_event.calendar] : me.calendars[me.selected_calendar];
+
       var dispname = Q(data.name || data.email);
       if (data.email)
         dispname = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink">' + dispname + '</a>';
@@ -1464,8 +1487,10 @@ function rcube_calendar_ui(settings)
       opts['REQ-PARTICIPANT'] = rcmail.gettext('calendar.rolerequired');
       opts['OPT-PARTICIPANT'] = rcmail.gettext('calendar.roleoptional');
       opts['NON-PARTICIPANT'] = rcmail.gettext('calendar.rolenonparticipant');
-      opts['CHAIR'] =  rcmail.gettext('calendar.rolechair');
-      
+
+      if (data.cutype != 'RESOURCE')
+        opts['CHAIR'] =  rcmail.gettext('calendar.rolechair');
+
       if (organizer && !readonly)
           dispname = rcmail.env['identities-selector'];
       
@@ -1483,15 +1508,16 @@ function rcube_calendar_ui(settings)
       
       var html = '<td class="role">' + select + '</td>' +
         '<td class="name">' + dispname + '</td>' +
-        '<td class="availability"><img src="./program/resources/blank.gif" class="availabilityicon ' + avail + '" /></td>' +
+        '<td class="availability"><img src="./program/resources/blank.gif" class="availabilityicon ' + avail + '" data-email="' + data.email + '" /></td>' +
         '<td class="confirmstate"><span class="' + String(data.status).toLowerCase() + '" title="' + Q(data.status || '') + '">' + Q(data.status || '') + '</span></td>' +
         '<td class="options">' + (organizer || readonly ? '' : dellink) + '</td>';
-      
+
+      var table = calendar.resources && data.cutype == 'RESOURCE' ? resources_list : attendees_list;
       var tr = $('<tr>')
         .addClass(String(data.role).toLowerCase())
         .html(html)
-        .appendTo(attendees_list);
-      
+        .appendTo(table);
+
       tr.find('a.deletelink').click({ id:(data.email || data.name) }, function(e) { remove_attendee(this, e.data.id); return false; });
       tr.find('a.mailtolink').click(function(e) { rcmail.redirect(rcmail.url('mail/compose', { _to:this.href.substr(7) })); return false; });
 
@@ -1505,16 +1531,17 @@ function rcube_calendar_ui(settings)
       }
       
       event_attendees.push(data);
+      return true;
     };
     
     // iterate over all attendees and update their free-busy status display
     var update_freebusy_status = function(event)
     {
-      var icons = attendees_list.find('img.availabilityicon');
-      for (var i=0; i < event_attendees.length; i++) {
-        if (icons.get(i) && event_attendees[i].email)
-          check_freebusy_status(icons.get(i), event_attendees[i].email, event);
-      }
+      attendees_list.find('img.availabilityicon').each(function(i,v) {
+        var email, icon = $(this);
+        if (email = icon.attr('data-email'))
+          check_freebusy_status(icon, email, event);
+      });
       
       freebusy_ui.needsupdate = false;
     };
@@ -1550,7 +1577,217 @@ function rcube_calendar_ui(settings)
       $(elem).closest('tr').remove();
       event_attendees = $.grep(event_attendees, function(data){ return (data.name != id && data.email != id) });
     };
-    
+
+    // open a dialog to display detailed free-busy information and to find free slots
+    var event_resources_dialog = function()
+    {
+      var $dialog = $('#eventresourcesdialog'),
+        event = me.selected_event;
+
+      if ($dialog.is(':ui-dialog'))
+        $dialog.dialog('close');
+  
+      // dialog buttons
+      var buttons = {};
+  
+      buttons[rcmail.gettext('addresource', 'calendar')] = function() {
+        rcmail.command('add-resource');
+      };
+  
+      buttons[rcmail.gettext('close')] = function() {
+        $dialog.dialog("close");
+      };
+
+      // open jquery UI dialog
+      $dialog.dialog({
+        modal: true,
+        resizable: true,
+        closeOnEscape: true,
+        title: rcmail.gettext('findresources', 'calendar'),
+        close: function() {
+          $dialog.dialog('destroy').hide();
+        },
+        buttons: buttons,
+        width: Math.min(1000, $(window).width() - 50),
+        height: 500
+      }).show();
+
+      // initialize the treelist widget
+      if (!resources_treelist) {
+        resources_treelist = new rcube_treelist_widget(rcmail.gui_objects.resourceslist, {
+          id_prefix: 'rcres',
+          id_encode: rcmail.html_identifier_encode,
+          id_decode: rcmail.html_identifier_decode,
+          selectable: true
+        });
+        resources_treelist.addEventListener('select', function(node) {
+          if (resources_data[node.id]) {
+            resource_showinfo(resources_data[node.id]);
+            rcmail.enable_command('add-resource', me.selected_event ? true : false);
+          }
+          else {
+            rcmail.enable_command('add-resource', false);
+            $(rcmail.gui_objects.resourceinfo).hide();
+            $(rcmail.gui_objects.resourceownerinfo).hide();
+          }
+        });
+
+        // fetch (all) resource data from server
+        me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
+        rcmail.http_request('resources-list', {}, me.loading_lock);
+      }
+      else {
+        resources_treelist.select('__none__');
+      }
+
+      // register button
+      $('.ui-dialog-buttonset .ui-button', $dialog.parent()).first().addClass('mainaction').attr('id', 'rcmbtncalresadd');
+      rcmail.register_button('add-resource', 'rcmbtncalresadd', 'input');
+    };
+
+    // render the resource details UI box
+    var resource_showinfo = function(resource)
+    {
+      // inline function to render a resource attribute
+      function render_attrib(value) {
+        if (typeof value == 'boolean') {
+          return value ? rcmail.get_label('yes') : rcmail.get_label('no');
+        }
+
+        return value;
+      }
+
+      if (rcmail.gui_objects.resourceinfo) {
+        var tr, table = $(rcmail.gui_objects.resourceinfo).show().find('tbody').html(''),
+          attribs = $.extend({ name:resource.name }, resource.attributes||{})
+          attribs.description = resource.description;
+
+        for (var k in attribs) {
+          if (typeof attribs[k] == 'undefined')
+            continue;
+          table.append($('<tr>').addClass(k)
+            .append('<td class="title">' + Q(ucfirst(rcmail.get_label(k, 'calendar'))) + '</td>')
+            .append('<td class="value">' + text2html(render_attrib(attribs[k])) + '</td>')
+          );
+        }
+
+        $(rcmail.gui_objects.resourceownerinfo).hide();
+
+        if (resource.owner) {
+          // fetch owner data from server
+          me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
+          rcmail.http_request('resources-owner', { _id: resource.owner }, me.loading_lock);
+        }
+      }
+    };
+
+    // callback from server for resource listing
+    var resource_data_load = function(data)
+    {
+      data.sort(function(a,b) {
+        var j = a._type == 'collection' ? 1 : 0,
+            k = b._type == 'collection' ? 1 : 0;
+        return k != j ? (j - k) : (a.name < b.name ? 1 : 0);
+      });
+
+      // assign parent-relations
+      $.each(data, function(i, rec) {
+        resources_data[rec.dn] = rec;
+        resources_index.push(rec.dn);
+
+        if (rec.members) {
+          $.each(rec.members, function(j, m){
+            resources_data[m].parent_id = rec.dn;
+          });
+        }
+      });
+
+      resources_index.reverse();
+      resource_render_list(resources_index);
+      rcmail.set_busy(false, null, me.loading_lock);
+    };
+
+    // renders the given list of resource records into the treelist
+    var resource_render_list = function(index) {
+      var rec, link;
+
+      resources_treelist.reset();
+
+      $.each(index, function(i, dn) {
+        if (rec = resources_data[dn]) {
+          link = $('<a>').attr('href', '#')
+            .attr('rel', rec.dn)
+            .html(Q(rec.name));
+
+          resources_treelist.insert({ id:rec.dn, html:link, classes:[rec._type], collapsed:true }, rec.parent_id, false);
+        }
+      });
+    };
+
+    // callback from server for owner information display
+    var resource_owner_load = function(data)
+    {
+      if (data) {
+        var table = $(rcmail.gui_objects.resourceownerinfo).find('tbody').html('');
+
+        for (var k in data) {
+          if (k == 'event')
+            continue;
+
+          table.append($('<tr>').addClass(k)
+            .append('<td class="title">' + Q(ucfirst(rcmail.get_label('owner'+k, 'calendar'))) + '</td>')
+            .append('<td class="value">' + text2html(data[k]) + '</td>')
+          );
+        }
+
+        table.parent().show();
+      }
+    }
+
+    // quick-filter the loaded resource data
+    var resource_search = function()
+    {
+      var dataset, rec, q = $('#resourcesearchbox').val().toLowerCase();
+      if (q.length && resources_data) {
+        dataset = [];
+
+        // search by iterating over all resource records
+        for (var dn in resources_data) {
+          rec = resources_data[dn];
+          if (String(rec.name).toLowerCase().indexOf(q) >= 0) {
+            dataset.push(rec.dn);
+          }
+        }
+
+        resource_render_list(dataset);
+
+        // select single match
+        if (dataset.length == 1) {
+          resources_treelist.select(dataset[0]);
+        }
+      }
+      else {
+        $('#resourcesearchbox').val('');
+      }
+    };
+
+    // 
+    var reset_resource_search = function()
+    {
+      $('#resourcesearchbox').val('').focus();
+      resource_render_list(resources_index);
+    };
+
+    // 
+    var add_resource2event = function()
+    {
+      var resource = resources_data[resources_treelist.get_selection()];
+      if (resource) {
+        if (add_attendee($.extend({ role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'RESOURCE' }, resource)))
+          rcmail.display_message(rcmail.get_label('resourceadded', 'calendar'), 'confirmation');
+      }
+    }
+
     // when the user accepts or declines an event invitation
     var event_rsvp = function(response)
     {
@@ -2073,7 +2310,7 @@ function rcube_calendar_ui(settings)
           if (range == 'custom')
             start = date2unixtime(parse_datetime('00:00', $('#event-export-startdate').val()));
           else if (range > 0)
-            start = 'today -' + range + '^months';
+            start = 'today -' + range + ' months';
 
           rcmail.goto_url('export_events', { source:source, start:start, attachments:attachmt?1:0 });
         }
@@ -2211,6 +2448,12 @@ function rcube_calendar_ui(settings)
         window.history.replaceState({}, document.title, rcmail.url('', query).replace('&_action=', ''));
     };
 
+    this.resource_search = resource_search;
+    this.reset_resource_search = reset_resource_search;
+    this.add_resource2event = add_resource2event;
+    this.resource_data_load = resource_data_load;
+    this.resource_owner_load = resource_owner_load;
+
 
     /***  event searching  ***/
 
@@ -2759,8 +3002,9 @@ function rcube_calendar_ui(settings)
       // init event dialog
       $('#eventtabs').tabs({
         show: function(event, ui) {
-          if (ui.panel.id == 'event-tab-3') {
-            $('#edit-attendee-name').select();
+          if (ui.panel.id == 'event-panel-attendees' || ui.panel.id == 'event-panel-resources') {
+            var tab = ui.panel.id == 'event-panel-resources' ? 'resource' : 'attendee';
+            $('#edit-'+tab+'-name').select();
             // update free-busy status if needed
             if (freebusy_ui.needsupdate && me.selected_event)
               update_freebusy_status(me.selected_event);
@@ -2827,15 +3071,38 @@ function rcube_calendar_ui(settings)
         };
       }
       rcmail.init_address_input_events($('#edit-attendee-name'), ac_props);
-      rcmail.addEventListener('autocomplete_insert', function(e){ $('#edit-attendee-add').click(); });
+      rcmail.addEventListener('autocomplete_insert', function(e){
+        if (e.field.name == 'participant') {
+          $('#edit-attendee-add').click();
+        }
+        else if (e.field.name == 'resource' && e.data && e.data.email) {
+          add_attendee($.extend(e.data, { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'RESOURCE' }));
+          e.field.value = '';
+        }
+      });
 
       $('#edit-attendee-add').click(function(){
         var input = $('#edit-attendee-name');
         rcmail.ksearch_blur();
-        if (add_attendees(input.val())) {
+        if (add_attendees(input.val(), { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'INDIVIDUAL' })) {
+          input.val('');
+        }
+      });
+
+      rcmail.init_address_input_events($('#edit-resource-name'), { action:'calendar/resources-autocomplete' });
+
+      $('#edit-resource-add').click(function(){
+        var input = $('#edit-resource-name');
+        rcmail.ksearch_blur();
+        if (add_attendees(input.val(), { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'RESOURCE' })) {
           input.val('');
         }
       });
+      
+      $('#edit-resource-find').click(function(){
+        event_resources_dialog();
+        return false;
+      });
 
       // keep these two checkboxes in sync
       $('#edit-attendees-donotify, #edit-attendees-invite').click(function(){
@@ -2919,6 +3186,11 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
   rcmail.register_command('search', function(){ cal.quicksearch(); }, true);
   rcmail.register_command('reset-search', function(){ cal.reset_quicksearch(); }, true);
 
+  // resource invitation dialog
+  rcmail.register_command('search-resource', function(){ cal.resource_search(); }, true);
+  rcmail.register_command('reset-resource-search', function(){ cal.reset_resource_search(); }, true);
+  rcmail.register_command('add-resource', function(){ cal.add_resource2event(); }, false);
+
   // register callback commands
   rcmail.addEventListener('plugin.destroy_source', function(p){ cal.calendar_destroy_source(p.id); });
   rcmail.addEventListener('plugin.unlock_saving', function(p){ cal.unlock_saving(); });
@@ -2926,6 +3198,8 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
   rcmail.addEventListener('plugin.import_success', function(p){ cal.import_success(p); });
   rcmail.addEventListener('plugin.import_error', function(p){ cal.import_error(p); });
   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('requestrefresh', function(q){ return cal.before_refresh(q); });
 
   // let's go
diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php
index fb29740..3d8d083 100644
--- a/plugins/calendar/drivers/calendar_driver.php
+++ b/plugins/calendar/drivers/calendar_driver.php
@@ -86,6 +86,7 @@ abstract class calendar_driver
   // features supported by backend
   public $alarms = false;
   public $attendees = false;
+  public $resources = false;
   public $freebusy = false;
   public $attachments = false;
   public $undelete = false; // event undelete action
@@ -548,4 +549,35 @@ abstract class calendar_driver
     return true;
   }
 
+
+  /**
+   * Fetch resource objects to be displayed for booking
+   *
+   * @param  string  Search query (optional)
+   * @return array  List of resource records available for booking
+   */
+  public function load_resources($query = null)
+  {
+    return array();
+  }
+
+  /**
+   * Return properties of a single resource
+   *
+   * @param mixed  UID string
+   * @return array Resource object as hash array
+   */
+  public function get_resource($uid)
+  {
+    return null;
+  }
+
+  /**
+   *
+   */
+  public function get_resource_owner($id)
+  {
+    return null;
+  }
+
 }
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 49f8fa7..e93d14f 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -218,8 +218,18 @@ class kolab_calendar
   public function list_events($start, $end, $search = null, $virtual = 1, $query = array())
   {
     // convert to DateTime for comparisons
-    $start = new DateTime('@'.$start);
-    $end = new DateTime('@'.$end);
+    try {
+      $start = new DateTime('@'.$start);
+    }
+    catch (Exception $e) {
+      $start = new DateTime('@0');
+    }
+    try {
+      $end = new DateTime('@'.$end);
+    }
+    catch (Exception $e) {
+      $end = new DateTime('today +10 years');
+    }
 
     // query Kolab storage
     $query[] = array('dtstart', '<=', $end);
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 7b41a31..0b23f64 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -60,6 +60,10 @@ class kolab_driver extends calendar_driver
         $this->alarm_types = array('DISPLAY');
         $this->alarm_absolute = false;
     }
+
+    if ($this->rc->config->get('calendar_resources_directory')) {
+      $this->resources = true;
+    }
   }
 
 
@@ -1283,4 +1287,104 @@ class kolab_driver extends calendar_driver
         'FFDEAD');
   }
 
+
+  private function resurces_ldap()
+  {
+    if (!isset($this->resources_dir)) {
+      $this->resources_dir = new rcube_ldap($this->rc->config->get('calendar_resources_directory'), true);
+    }
+
+    return $this->resources_dir->ready ? $this->resources_dir : null;
+  }
+
+
+  /**
+   * Fetch resource objects to be displayed for booking
+   *
+   * @param  string  Search query (optional)
+   * @return array  List of resource records available for booking
+   */
+  public function load_resources($query = null, $num = 5000)
+  {
+    if (!($ldap = $this->resurces_ldap())) {
+      return array();
+    }
+
+    // TODO: apply paging
+    $ldap->set_pagesize($num);
+
+    if (isset($query)) {
+      $results = $ldap->search('*', $query, 0, true, true);
+    }
+    else {
+      $results = $ldap->list_records();
+    }
+
+    if ($results instanceof ArrayAccess) {
+      foreach ($results as $i => $rec) {
+        $results[$i] = $this->decode_resource($rec);
+      }
+    }
+
+    return $results;
+  }
+
+  /**
+   * Return properties of a single resource
+   *
+   * @param mixed  UID string
+   * @return array Resource object as hash array
+   */
+  public function get_resource($uid)
+  {
+    $rec = null;
+
+    if ($ldap = $this->resurces_ldap()) {
+      $rec = $ldap->get_record($uid);
+
+      if (!empty($rec)) {
+        $rec = $this->decode_resource($rec);
+      }
+    }
+
+    return $rec;
+  }
+
+  /**
+   *
+   */
+  public function get_resource_owner($dn)
+  {
+    $owner = null;
+
+    if ($ldap = $this->resurces_ldap()) {
+      $owner = $ldap->get_record(rcube_ldap::dn_encode($dn), true);
+      unset($owner['_raw_attrib'], $owner['_type'], $owner['ID']);
+    }
+
+    return $owner;
+  }
+
+  /**
+   * Extract JSON-serialized attributes
+   */
+  private function decode_resource($rec)
+  {
+    if (is_array($rec['attributes']) && $rec['attributes'][0]) {
+      $attributes = array();
+
+      foreach ($rec['attributes'] as $sattr) {
+        $attr = @json_decode($sattr, true);
+        $attributes += $attr;
+      }
+
+      $rec['attributes'] = $attributes;
+    }
+
+    // remove unused cruft
+    unset($rec['_raw_attrib']);
+
+    return $rec;
+  }
+
 }
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 009e6c7..710898f 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -84,6 +84,10 @@ class calendar_ui
     $this->cal->register_handler('plugin.filedroparea', array($this, 'file_drop_area'));
     $this->cal->register_handler('plugin.attendees_list', array($this, 'attendees_list'));
     $this->cal->register_handler('plugin.attendees_form', array($this, 'attendees_form'));
+    $this->cal->register_handler('plugin.resources_form', array($this, 'resources_form'));
+    $this->cal->register_handler('plugin.resources_list', array($this, 'resources_list'));
+    $this->cal->register_handler('plugin.resources_searchform', array($this, 'resources_search_form'));
+    $this->cal->register_handler('plugin.resource_info', array($this, 'resource_info'));
     $this->cal->register_handler('plugin.attendees_freebusy_table', array($this, 'attendees_freebusy_table'));
     $this->cal->register_handler('plugin.edit_attendees_notify', array($this, 'edit_attendees_notify'));
     $this->cal->register_handler('plugin.edit_recurring_warning', array($this, 'recurring_event_warning'));
@@ -112,6 +116,7 @@ class calendar_ui
     $this->cal->include_script('calendar_ui.js');
     $this->cal->include_script('lib/js/fullcalendar.js');
     $this->cal->include_script('lib/js/jquery.miniColors.min.js');
+    $this->rc->output->include_script('treelist.js');
   }
 
   /**
@@ -191,6 +196,7 @@ class calendar_ui
       unset($prop['user_id']);
       $prop['alarms'] = $this->cal->driver->alarms;
       $prop['attendees'] = $this->cal->driver->attendees;
+      $prop['resources'] = $this->cal->driver->resources;
       $prop['freebusy'] = $this->cal->driver->freebusy;
       $prop['attachments'] = $this->cal->driver->attachments;
       $prop['undelete'] = $this->cal->driver->undelete;
@@ -732,7 +738,7 @@ class calendar_ui
   {
     $table = new html_table(array('cols' => 5, 'border' => 0, 'cellpadding' => 0, 'class' => 'rectable'));
     $table->add_header('role', $this->cal->gettext('role'));
-    $table->add_header('name', $this->cal->gettext('attendee'));
+    $table->add_header('name', $this->cal->gettext($attrib['coltitle'] ?: 'attendee'));
     $table->add_header('availability', $this->cal->gettext('availability'));
     $table->add_header('confirmstate', $this->cal->gettext('confirmstate'));
     $table->add_header('options', '');
@@ -755,7 +761,82 @@ class calendar_ui
       html::p('attendees-invitebox', html::label(null, $checkbox->show(1) . $this->cal->gettext('sendinvitations')))
       );
   }
-  
+
+  /**
+   *
+   */
+  function resources_form($attrib = array())
+  {
+    $input = new html_inputfield(array('name' => 'resource', 'id' => 'edit-resource-name', 'size' => 30));
+
+    return html::div($attrib,
+      html::div(null, $input->show() . " " .
+        html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-resource-add', 'value' => $this->cal->gettext('addresource'))) . " " .
+        html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-resource-find', 'value' => $this->cal->gettext('findresources').'...')))
+      );
+  }
+
+  /**
+   *
+   */
+  function resources_list($attrib = array())
+  {
+    $attrib += array('id' => 'calendar-resources-list');
+
+    $this->rc->output->add_gui_object('resourceslist', $attrib['id']);
+
+    return html::tag('ul', $attrib, '', html::$common_attrib);
+  }
+
+  /**
+   *
+   */
+  public function resource_info($attrib = array())
+  {
+    $attrib += array('id' => 'calendar-resources-info');
+
+    $this->rc->output->add_gui_object('resourceinfo', $attrib['id']);
+    $this->rc->output->add_gui_object('resourceownerinfo', $attrib['id'] . '-owner');
+
+    $table_attrib = array('id','class','style','width','summary','cellpadding','cellspacing','border');
+
+    return html::tag('table', $attrib,
+        html::tag('tbody', null, ''), $table_attrib) .
+
+      html::tag('table', array('id' => $attrib['id'] . '-owner', 'style' => 'display:none') + $attrib,
+        html::tag('thead', null,
+          html::tag('tr', null,
+            html::tag('td', array('colspan' => 2), Q($this->cal->gettext('resourceowner')))
+          )
+        ) .
+        html::tag('tbody', null, ''),
+        $table_attrib);
+  }
+
+  /**
+   * GUI object 'searchform' for the resource finder dialog
+   *
+   * @param array Named parameters
+   * @return string HTML code for the gui object
+   */
+  function resources_search_form($attrib)
+  {
+    $attrib += array('command' => 'search-resource', 'id' => 'rcmcalresqsearchbox', 'autocomplete' => 'off');
+    $attrib['name'] = '_q';
+
+    $input_q = new html_inputfield($attrib);
+    $out = $input_q->show();
+
+    // add form tag around text field
+    $out = $this->rc->output->form_tag(array(
+      'name' => "rcmcalresoursqsearchform",
+      'onsubmit' => rcmail_output::JS_OBJECT_NAME . ".command('" . $attrib['command'] . "'); return false",
+      'style' => "display:inline"),
+      $out);
+
+    return $out;
+  }
+
   /**
    *
    */
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index c99199e..8d68d72 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -140,6 +140,15 @@ $labels['eventupdatemailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$atten
 $labels['eventcancelsubject'] = '"$title" has been canceled';
 $labels['eventcancelmailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nThe event has been cancelled by \$organizer.\n\nPlease find attached an iCalendar file with the updated event details.";
 
+// resources
+$labels['resource'] = 'Resource';
+$labels['addresource'] = 'Book resource';
+$labels['findresources'] = 'Find resources';
+$labels['resourcedetails'] = 'Details';
+$labels['resourceavailability'] = 'Availability';
+$labels['resourceowner'] = 'Owner';
+$labels['resourceadded'] = 'The resource was added to your event';
+
 // invitation handling
 $labels['itipinvitation'] = 'Invitation to';
 $labels['itipupdate'] = 'Update of';
@@ -171,6 +180,7 @@ $labels['saveincalendar'] = 'save in';
 $labels['tabsummary'] = 'Summary';
 $labels['tabrecurrence'] = 'Recurrence';
 $labels['tabattendees'] = 'Participants';
+$labels['tabresources'] = 'Resources';
 $labels['tabattachments'] = 'Attachments';
 $labels['tabsharing'] = 'Sharing';
 
diff --git a/plugins/calendar/skins/classic/templates/eventedit.html b/plugins/calendar/skins/classic/templates/eventedit.html
index 6e1c2b3..1bc1a12 100644
--- a/plugins/calendar/skins/classic/templates/eventedit.html
+++ b/plugins/calendar/skins/classic/templates/eventedit.html
@@ -1,13 +1,13 @@
 <div id="eventedit" class="uidialog">
   <form id="eventtabs" action="#" method="post" enctype="multipart/form-data">
     <ul>
-      <li><a href="#event-tab-1"><roundcube:label name="calendar.tabsummary" /></a></li>
-      <li id="edit-tab-recurrence"><a href="#event-tab-2"><roundcube:label name="calendar.tabrecurrence" /></a></li>
-      <li id="edit-tab-attendees"><a href="#event-tab-3"><roundcube:label name="calendar.tabattendees" /></a></li>
-      <li id="edit-tab-attachments"><a href="#event-tab-4"><roundcube:label name="calendar.tabattachments" /></a></li>
+      <li><a href="#event-panel-1"><roundcube:label name="calendar.tabsummary" /></a></li>
+      <li id="edit-tab-recurrence"><a href="#event-panel-recurrence"><roundcube:label name="calendar.tabrecurrence" /></a></li>
+      <li id="edit-tab-attendees"><a href="#event-panel-attendees"><roundcube:label name="calendar.tabattendees" /></a></li>
+      <li id="edit-tab-attachments"><a href="#event-panel-attachments"><roundcube:label name="calendar.tabattachments" /></a></li>
     </ul>
     <!-- basic info -->
-    <div id="event-tab-1">
+    <div id="event-panel-1">
       <div class="event-section">
         <label for="edit-title"><roundcube:label name="calendar.title" /></label>
         <br />
@@ -65,7 +65,7 @@
       </div>
     </div>
     <!-- recurrence settings -->
-    <div id="event-tab-2">
+    <div id="event-panel-recurrence">
       <div class="event-section border-after">
         <roundcube:object name="plugin.recurrence_form" part="frequency" />
       </div>
@@ -86,13 +86,13 @@
       </div>
     </div>
     <!-- attendees list -->
-    <div id="event-tab-3">
+    <div id="event-panel-attendees">
       <roundcube:object name="plugin.attendees_list" id="edit-attendees-table" cellspacing="0" cellpadding="0" border="0" />
       <roundcube:object name="plugin.attendees_form" id="edit-attendees-form" />
       <roundcube:include file="/templates/freebusylegend.html" />
     </div>
     <!-- attachments list (with upload form) -->
-    <div id="event-tab-4">
+    <div id="event-panel-attachments">
       <div id="edit-attachments" class="attachments-list">
         <roundcube:object name="plugin.attachments_list" id="attachmentlist" deleteIcon="/images/icons/delete.png" cancelIcon="/images/icons/delete.png" loadingIcon="/images/display/loading_blue.gif" />
       </div>
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index b0120d1..7efe868 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -484,7 +484,7 @@ a.miniColors-trigger {
 .calendarmain .fc-view-table td.fc-list-header,
 #attendees-freebusy-table h3.boxtitle,
 #schedule-freebusy-times thead th,
-#edit-attendees-table thead td
+.edit-attendees-table thead td
  {
 	color: #69939e;
 	font-size: 11px;
@@ -658,34 +658,34 @@ td.topalign {
 	padding: 0.5em;
 }
 
-#edit-attendees-table {
+.edit-attendees-table {
 	width: 100%;
 	margin-top: 0.5em;
 }
 
-#edit-attendees-table td.role {
+.edit-attendees-table td.role {
 	width: 9em;
 }
 
-#edit-attendees-table td.availability,
-#edit-attendees-table td.confirmstate {
+.edit-attendees-table td.availability,
+.edit-attendees-table td.confirmstate {
 	width: 4em;
 }
 
-#edit-attendees-table td.options {
+.edit-attendees-table td.options {
 	width: 3em;
 	text-align: right;
 	padding-right: 4px;
 }
 
-#edit-attendees-table td.name {
+.edit-attendees-table td.name {
 	width: auto;
 	white-space: nowrap;
 	overflow: hidden;
 	text-overflow: ellipsis;
 }
 
-#edit-attendees-table a.deletelink {
+.edit-attendees-table a.deletelink {
 	display: block;
 	width: 17px;
 	height: 17px;
@@ -694,18 +694,20 @@ td.topalign {
 	text-indent: 1000px;
 }
 
-#edit-attendees-form {
+#edit-attendees-form,
+#edit-resources-form {
 	position: relative;
 	margin-top: 1em;
 }
 
-#edit-attendees-form #edit-attendee-schedule {
+#edit-attendees-form #edit-attendee-schedule,
+#edit-resources-form #edit-resource-find {
 	position: absolute;
 	top: 0;
 	right: 0;
 }
 
-#edit-attendees-table select.edit-attendee-role {
+.edit-attendees-table select.edit-attendee-role {
 	border: 0;
 	padding: 2px;
 	background: white;
@@ -778,34 +780,34 @@ td.topalign {
 	vertical-align: middle;
 }
 
-#edit-attendees-table tbody td.confirmstate {
+.edit-attendees-table tbody td.confirmstate {
 	overflow: hidden;
 	white-space: nowrap;
 	text-indent: -2000%;
 }
 
-#edit-attendees-table td.confirmstate span {
+.edit-attendees-table td.confirmstate span {
 	display: block;
 	width: 20px;
 	background: url(images/attendee-status.gif) 5px 0 no-repeat;
 }
 
-#edit-attendees-table td.confirmstate span.needs-action {
+.edit-attendees-table td.confirmstate span.needs-action {
 }
 
-#edit-attendees-table td.confirmstate span.accepted {
+.edit-attendees-table td.confirmstate span.accepted {
 	background-position: 5px -20px;
 }
 
-#edit-attendees-table td.confirmstate span.declined {
+.edit-attendees-table td.confirmstate span.declined {
 	background-position: 5px -40px;
 }
 
-#edit-attendees-table td.confirmstate span.tentative {
+.edit-attendees-table td.confirmstate span.tentative {
 	background-position: 5px -60px;
 }
 
-#edit-attendees-table td.confirmstate span.delegated {
+.edit-attendees-table td.confirmstate span.delegated {
 	background-position: 5px -160px;
 }
 
@@ -1042,6 +1044,88 @@ a.dropdown-link:after {
 	padding: 0.5em 1em;
 }
 
+#resource-selection {
+	position: absolute;
+	top: 0;
+	left: 8px;
+	right: 0;
+	bottom: 0;
+}
+
+#resource-selection .scroller {
+	top: 34px;
+}
+
+#resource-dialog-left {
+	position: absolute;
+	top: 10px;
+	left: 0;
+	width: 380px;
+	bottom: 10px;
+}
+
+#resource-dialog-right {
+	position: absolute;
+	top: 10px;
+	left: 392px;
+	right: 8px;
+	bottom: 10px;
+}
+
+#resource-info {
+	position: absolute;
+	top: 0;
+	left: 0;
+	right: 0;
+	height: 56%;
+}
+
+#resource-info table {
+	margin: 8px;
+	width: 97%;
+}
+
+#resource-info thead td {
+	background: none;
+	font-weight: bold;
+	font-size: 14px;
+}
+
+#resource-availability {
+	position: absolute;
+	bottom: 0;
+	left: 0;
+	right: 0;
+	height: 40%;
+}
+
+#resourcequicksearch {
+	padding: 4px;
+	background: #c7e3ef;
+}
+
+#resourcesearchbox {
+	width: 100%;
+	height: 26px;
+	-moz-box-sizing: border-box;
+	     box-sizing: border-box;
+}
+
+#resourcequicksearch .iconbutton.searchoptions {
+	position: absolute;
+	top: 5px;
+	left: 6px;
+	width: 16px;
+}
+
+.searchbox .iconbutton.reset {
+	position: absolute;
+	top: 4px;
+	right: 1px;
+}
+
+
+
 /* fullcalendar style overrides */
 
 .rcube-fc-content {
diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html
index 453d59a..574cded 100644
--- a/plugins/calendar/skins/larry/templates/calendar.html
+++ b/plugins/calendar/skins/larry/templates/calendar.html
@@ -113,6 +113,45 @@
 
 <roundcube:include file="/templates/eventedit.html" />
 
+<div id="eventresourcesdialog" class="uidialog">
+	<div id="resource-dialog-left">
+		<div id="resource-selection" class="uibox listbox">
+			<div id="resourcequicksearch">
+				<div class="searchbox">
+					<roundcube:object name="plugin.resources_searchform" id="resourcesearchbox" />
+					<a id="resourcesearchmenulink" class="iconbutton searchoptions"> </a>
+					<roundcube:button command="reset-resource-search" id="resourcesearchreset" class="iconbutton reset" title="resetsearch" content=" " />
+				</div>
+			</div>
+			<div class="scroller">
+				<roundcube:object name="plugin.resources_list" id="resources-list" class="listing treelist" />
+			</div>
+		<!--
+			<div class="boxpagenav">
+				<roundcube:button command="firstpage" type="link" class="icon firstpage disabled" classAct="icon firstpage" title="firstpage" content="|&lt;" />
+				<roundcube:button command="previouspage" type="link" class="icon prevpage disabled" classAct="icon prevpage" title="previouspage" content="&lt;" />
+				<roundcube:button command="nextpage" type="link" class="icon nextpage disabled" classAct="icon nextpage" title="nextpage" content="&gt;" />
+				<roundcube:button command="lastpage" type="link" class="icon lastpage disabled" classAct="icon lastpage" title="lastpage" content="&gt;|" />
+			</div>
+		-->
+		</div>
+	</div>
+
+	<div id="resource-dialog-right">
+		<div id="resource-info" class="uibox contentbox">
+			<h2 class="boxtitle"><roundcube:label name="calendar.resourcedetails" /></h2>
+			<div class="scroller">
+				<roundcube:object name="plugin.resource_info" id="resource-details" class="propform" />
+			</div>
+		</div>
+
+		<div id="resource-availability" class="uibox contentbox">
+			<h2 class="boxtitle"><roundcube:label name="calendar.resourceavailability" /></h2>
+			<div id="resource-freebusy-calendar"></div>
+		</div>
+	</div>
+</div>
+
 <div id="eventfreebusy" class="uidialog">
 	<roundcube:object name="plugin.attendees_freebusy_table" id="attendees-freebusy-table" cellpadding="0" />
 
@@ -205,6 +244,9 @@ $(document).ready(function(e){
 	})
 	.data('offset', $('#calendarsidebartoggle').position().left)
 	.data('sidebarwidth', $('#calendarsidebar').width() + $('#calendarsidebar').position().left);
+
+	new rcube_splitter({ id:'calresourceviewsplitter', p1:'#resource-dialog-left', p2:'#resource-dialog-right',
+		orientation:'v', relative:true, start:380, min:220, size:10, offset:-3 }).init();
 });
 
 </script>
diff --git a/plugins/calendar/skins/larry/templates/eventedit.html b/plugins/calendar/skins/larry/templates/eventedit.html
index 0ae2b77..9f2a374 100644
--- a/plugins/calendar/skins/larry/templates/eventedit.html
+++ b/plugins/calendar/skins/larry/templates/eventedit.html
@@ -1,10 +1,10 @@
 <div id="eventedit" class="uidialog uidialog-tabbed">
 	<form id="eventtabs" action="#" method="post" enctype="multipart/form-data">
 		<ul>
-			<li><a href="#event-tab-1"><roundcube:label name="calendar.tabsummary" /></a></li><li id="edit-tab-recurrence"><a href="#event-tab-2"><roundcube:label name="calendar.tabrecurrence" /></a></li><li id="edit-tab-attendees"><a href="#event-tab-3"><roundcube:label name="calendar.tabattendees" /></a></li><li id="edit-tab-attachments"><a href="#event-tab-4"><roundcube:label name="calendar.tabattachments" /></a></li>
+			<li><a href="#event-panel-summary"><roundcube:label name="calendar.tabsummary" /></a></li><li id="edit-tab-recurrence"><a href="#event-panel-recurrence"><roundcube:label name="calendar.tabrecurrence" /></a></li><li id="edit-tab-attendees"><a href="#event-panel-attendees"><roundcube:label name="calendar.tabattendees" /></a></li><li id="edit-tab-resources"><a href="#event-panel-resources"><roundcube:label name="calendar.tabresources" /></a></li><li id="edit-tab-attachments"><a href="#event-panel-attachments"><roundcube:label name="calendar.tabattachments" /></a></li>
 		</ul>
 		<!-- basic info -->
-		<div id="event-tab-1">
+		<div id="event-panel-summary">
 			<div class="event-section">
 				<label for="edit-title"><roundcube:label name="calendar.title" /></label>
 				<br />
@@ -62,7 +62,7 @@
 			</div>
 		</div>
 		<!-- recurrence settings -->
-		<div id="event-tab-2">
+		<div id="event-panel-recurrence">
 			<div class="event-section border-after">
 				<roundcube:object name="plugin.recurrence_form" part="frequency" />
 			</div>
@@ -83,20 +83,26 @@
 			</div>
 		</div>
 		<!-- attendees list -->
-		<div id="event-tab-3">
-			<roundcube:object name="plugin.attendees_list" id="edit-attendees-table" class="records-table" />
+		<div id="event-panel-attendees">
+			<roundcube:object name="plugin.attendees_list" id="edit-attendees-table" class="records-table edit-attendees-table" coltitle="attendee" />
 			<roundcube:object name="plugin.attendees_form" id="edit-attendees-form" />
 			<roundcube:include file="/templates/freebusylegend.html" />
 		</div>
+		<!-- resources list -->
+		<div id="event-panel-resources">
+			<roundcube:object name="plugin.attendees_list" id="edit-resources-table" class="records-table edit-attendees-table" coltitle="resource" />
+			<roundcube:object name="plugin.resources_form" id="edit-resources-form" />
+			<roundcube:include file="/templates/freebusylegend.html" />
+		</div>
 		<!-- attachments list (with upload form) -->
-		<div id="event-tab-4">
+		<div id="event-panel-attachments">
 			<div id="edit-attachments">
 				<roundcube:object name="plugin.attachments_list" id="attachment-list" class="attachmentslist" />
 			</div>
 			<div id="edit-attachments-form">
 				<roundcube:object name="plugin.attachments_form" id="calendar-attachment-form" attachmentFieldSize="30" />
 			</div>
-			<roundcube:object name="plugin.filedroparea" id="event-tab-4" />
+			<roundcube:object name="plugin.filedroparea" id="event-panel-attachments" />
 		</div>
 	</form>
 




More information about the commits mailing list