diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 840a2ed2..4975f7c2 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -294,7 +294,7 @@ class calendar extends rcube_plugin
$this->ui->init_templates();
$this->rc->output->add_label('lowest','low','normal','high','highest','delete','cancel','uploading','noemailwarning','close');
- $this->rc->output->add_label('libcalendaring.itipaccepted','libcalendaring.itiptentative','libcalendaring.itipdeclined','libcalendaring.itipdelegated');
+ $this->rc->output->add_label('libcalendaring.itipaccepted','libcalendaring.itiptentative','libcalendaring.itipdeclined','libcalendaring.itipdelegated','libcalendaring.expandattendeegroup','libcalendaring.expandattendeegroupnodata');
// initialize attendees autocompletion
rcube_autocomplete_init();
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index c41bd962..1b3547da 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -914,11 +914,11 @@ function rcube_calendar_ui(settings)
},
buttons: buttons,
minWidth: 500,
- width: 580
+ width: 600
}).append(editform.show()); // adding form content AFTERWARDS massively speeds up opening on IE6
// set dialog size according to form content
- me.dialog_resize($dialog.get(0), editform.height() + (bw.ie ? 20 : 0), 530);
+ me.dialog_resize($dialog.get(0), editform.height() + (bw.ie ? 20 : 0), 550);
title.select();
@@ -1877,7 +1877,7 @@ function rcube_calendar_ui(settings)
};
// add the given attendee to the list
- var add_attendee = function(data, readonly)
+ var add_attendee = function(data, readonly, before)
{
if (!me.selected_event)
return false;
@@ -1931,6 +1931,12 @@ function rcube_calendar_ui(settings)
else if (data['delegated-from'])
tooltip = rcmail.gettext('delegatedfrom', 'calendar') + data['delegated-from'];
+ // add expand button for groups
+ if (data.cutype == 'GROUP') {
+ dispname += ' ' +
+ rcmail.gettext('expandattendeegroup','libcalendaring') + '';
+ }
+
var html = '
' + select + ' | ' +
'' + dispname + ' | ' +
' | ' +
@@ -1941,11 +1947,16 @@ function rcube_calendar_ui(settings)
var table = rcmail.env.calendar_resources && data.cutype == 'RESOURCE' ? resources_list : attendees_list;
var tr = $('')
.addClass(String(data.role).toLowerCase())
- .html(html)
- .appendTo(table);
+ .html(html);
+
+ if (before)
+ tr.insertBefore(before)
+ else
+ tr.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(event_attendee_click);
+ tr.find('a.expandlink').click(data, function(e) { me.expand_attendee_group(e, add_attendee, remove_attendee); });
tr.find('input.edit-attendee-reply').click(function() {
var enabled = $('#edit-attendees-invite:checked').length || $('input.edit-attendee-reply:checked').length;
$('p.attendees-commentbox')[enabled ? 'show' : 'hide']();
@@ -3755,12 +3766,15 @@ function rcube_calendar_ui(settings)
};
}
rcmail.init_address_input_events($('#edit-attendee-name'), ac_props);
- rcmail.addEventListener('autocomplete_insert', function(e){
+ rcmail.addEventListener('autocomplete_insert', function(e) {
+ var success = false;
if (e.field.name == 'participant') {
- $('#edit-attendee-add').click();
+ success = add_attendees(e.insert, { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:(e.data && e.data.type == 'group' ? 'GROUP' : 'INDIVIDUAL') });
}
else if (e.field.name == 'resource' && e.data && e.data.email) {
- add_attendee($.extend(e.data, { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'RESOURCE' }));
+ success = add_attendee($.extend(e.data, { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'RESOURCE' }));
+ }
+ if (e.field && success) {
e.field.value = '';
}
});
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 85a7aa5a..0700455a 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -955,31 +955,31 @@ td.topalign {
width: 95%;
}
-.edit-attendees-table {
+#eventedit .edit-attendees-table {
width: 100%;
margin-top: 0.5em;
}
-.edit-attendees-table th.role,
-.edit-attendees-table td.role {
+#eventedit .edit-attendees-table th.role,
+#eventedit .edit-attendees-table td.role {
width: 9em;
}
-.edit-attendees-table th.availability,
-.edit-attendees-table td.availability,
-.edit-attendees-table th.confirmstate,
-.edit-attendees-table td.confirmstate {
+#eventedit .edit-attendees-table th.availability,
+#eventedit .edit-attendees-table td.availability,
+#eventedit .edit-attendees-table th.confirmstate,
+#eventedit .edit-attendees-table td.confirmstate {
width: 4em;
}
-.edit-attendees-table th.options,
-.edit-attendees-table td.options {
+#eventedit .edit-attendees-table th.options,
+#eventedit .edit-attendees-table td.options {
width: 16px;
padding: 2px 4px;
}
-.edit-attendees-table th.sendmail,
-.edit-attendees-table td.sendmail {
+#eventedit .edit-attendees-table th.sendmail,
+#eventedit .edit-attendees-table td.sendmail {
width: 44px;
padding: 2px;
}
@@ -998,23 +998,24 @@ td.topalign {
background: url(images/sendinvitation.png) 1px 0 no-repeat;
}
-.edit-attendees-table tbody tr:last-child td {
+#eventedit .edit-attendees-table tbody tr:last-child td {
border-bottom: 0;
}
-.edit-attendees-table th.name,
-.edit-attendees-table td.name {
+#eventedit .edit-attendees-table th.name,
+#eventedit .edit-attendees-table td.name {
width: auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+ position: relative;
}
-.edit-attendees-table td.name select {
+#eventedit .edit-attendees-table td.name select {
width: 100%;
}
-.edit-attendees-table a.deletelink {
+#eventedit .edit-attendees-table a.deletelink {
display: inline-block;
width: 17px;
height: 17px;
@@ -1023,6 +1024,14 @@ td.topalign {
text-indent: 1000px;
}
+#eventedit .edit-attendees-table a.expandlink {
+ position: absolute;
+ top: 4px;
+ right: 6px;
+ width: 16px;
+ height: 16px;
+}
+
#edit-attendees-form,
#edit-resources-form {
position: relative;
diff --git a/plugins/libcalendaring/libcalendaring.js b/plugins/libcalendaring/libcalendaring.js
index 6f20ab7c..39b34ae7 100644
--- a/plugins/libcalendaring/libcalendaring.js
+++ b/plugins/libcalendaring/libcalendaring.js
@@ -33,6 +33,7 @@ function rcube_libcalendaring(settings)
this.alarm_dialog = null;
this.snooze_popup = null;
this.dismiss_link = null;
+ this.group2expand = {};
// abort if env isn't set
if (!settings || !settings.date_format)
@@ -729,6 +730,64 @@ function rcube_libcalendaring(settings)
$.each(listitems, function(idx, item) { mylist.append(item); });
};
+
+ /***** Attendee form handling *****/
+
+ // expand the given contact group into individual event/task attendees
+ this.expand_attendee_group = function(e, add, remove)
+ {
+ var id = (e.data ? e.data.email : null) || $(e.target).attr('data-email'),
+ role_select = $(e.target).closest('tr').find('select.edit-attendee-role option:selected');
+
+ this.group2expand[id] = { link: e.target, data: $.extend({}, e.data || {}), adder: add, remover: remove }
+
+ // copy group role from the according form element
+ if (role_select.length) {
+ this.group2expand[id].data.role = role_select.val();
+ }
+
+ // register callback handler
+ if (!this._expand_attendee_listener) {
+ this._expand_attendee_listener = this.expand_attendee_callback;
+ rcmail.addEventListener('plugin.expand_attendee_callback', function(result) {
+ me._expand_attendee_listener(result);
+ });
+ }
+
+ rcmail.http_post('libcal/plugin.expand_attendee_group', { id: id, data: e.data || {} }, rcmail.set_busy(true, 'loading'));
+ };
+
+ // callback from server to expand an attendee group
+ this.expand_attendee_callback = function(result)
+ {
+ var attendee, id = result.id,
+ data = this.group2expand[id],
+ row = $(data.link).closest('tr');
+
+ // replace group entry with all members returned by the server
+ if (data && data.adder && result.members && result.members.length) {
+ for (var i=0; i < result.members.length; i++) {
+ attendee = result.members[i];
+ attendee.role = data.data.role;
+ attendee.cutype = 'INDIVIDUAL';
+ attendee.status = 'NEEDS-ACTION';
+ data.adder(attendee, null, row);
+ }
+
+ if (data.remover) {
+ data.remover(data.link, id)
+ }
+ else {
+ row.remove();
+ }
+
+ delete this.group2expand[id];
+ }
+ else {
+ rcmail.display_message(result.error || rcmail.gettext('expandattendeegroupnodata','libcalendaring'), 'error');
+ }
+ };
+
}
////// static methods
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index 6ca548ef..52a81347 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -106,6 +106,7 @@ class libcalendaring extends rcube_plugin
// add hook to display alarms
$this->add_hook('refresh', array($this, 'refresh'));
$this->register_action('plugin.alarms', array($this, 'alarms_action'));
+ $this->register_action('plugin.expand_attendee_group', array($this, 'expand_attendee_group'));
}
// proceed initialization in startup hook
@@ -1359,6 +1360,61 @@ class libcalendaring extends rcube_plugin
}
+ /********* Attendee handling functions *********/
+
+ /**
+ * Handler for attendee group expansion requests
+ */
+ public function expand_attendee_group()
+ {
+ $id = rcube_utils::get_input_value('id', rcube_utils::INPUT_POST);
+ $data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);
+ $result = array('id' => $id, 'members' => array());
+ $maxnum = 500;
+
+ // iterate over all autocomplete address books (we don't know the source of the group)
+ foreach ((array)$this->rc->config->get('autocomplete_addressbooks', 'sql') as $abook_id) {
+ if (($abook = $this->rc->get_address_book($abook_id)) && $abook->groups) {
+ foreach ($abook->list_groups($data['name'], 1) as $group) {
+ // this is the matching group to expand
+ if (in_array($data['email'], (array)$group['email'])) {
+ $abook->set_pagesize($maxnum);
+ $abook->set_group($group['ID']);
+
+ // get all members
+ $res = $abook->list_records($this->rc->config->get('contactlist_fields'));
+
+ // handle errors (e.g. sizelimit, timelimit)
+ if ($abook->get_error()) {
+ $result['error'] = $this->rc->gettext('expandattendeegrouperror', 'libcalendaring');
+ $res = false;
+ }
+ // check for maximum number of members (we don't wanna bloat the UI too much)
+ else if ($res->count > $maxnum) {
+ $result['error'] = $this->rc->gettext('expandattendeegroupsizelimit', 'libcalendaring');
+ $res = false;
+ }
+
+ while ($res && ($member = $res->iterate())) {
+ $emails = (array)$abook->get_col_values('email', $member, true);
+ if (!empty($emails) && ($email = array_shift($emails))) {
+ $result['members'][] = array(
+ 'email' => $email,
+ 'name' => rcube_addressbook::compose_list_name($member),
+ );
+ }
+ }
+
+ break 2;
+ }
+ }
+ }
+ }
+
+ $this->rc->output->command('plugin.expand_attendee_callback', $result);
+ }
+
+
/********* Static utility functions *********/
/**
diff --git a/plugins/libcalendaring/localization/en_US.inc b/plugins/libcalendaring/localization/en_US.inc
index 5ad34ed5..82fb7a8d 100644
--- a/plugins/libcalendaring/localization/en_US.inc
+++ b/plugins/libcalendaring/localization/en_US.inc
@@ -140,3 +140,9 @@ $labels['declinedeleteconfirm'] = 'Do you also want to delete this declined obje
$labels['savingdata'] = 'Saving data...';
+// attendees labels
+$labels['expandattendeegroup'] = 'Substitute with group members';
+$labels['expandattendeegroupnodata'] = 'Unable to substitute this group. No members found.';
+$labels['expandattendeegrouperror'] = 'Unable to substitute this group. It might contain too many members.';
+$labels['expandattendeegroupsizelimit'] = 'This group contains too many members for substituting.';
+
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index 0b2ed3dd..0ffaf966 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -918,6 +918,7 @@ a.morelink:hover {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+ position: relative;
}
#taskedit .edit-attendees-table td.name select {
@@ -933,6 +934,14 @@ a.morelink:hover {
text-indent: 1000px;
}
+#taskedit .edit-attendees-table a.expandlink {
+ position: absolute;
+ top: 4px;
+ right: 6px;
+ width: 16px;
+ height: 16px;
+}
+
#edit-attendees-form {
position: relative;
margin-top: 15px;
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 5e0dc8eb..4c07937c 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -615,22 +615,22 @@ function rcube_tasklist_ui(settings)
};
}
rcmail.init_address_input_events($('#edit-attendee-name'), ac_props);
- 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 = '';
-// }
+ rcmail.addEventListener('autocomplete_insert', function(e) {
+ var success = false;
+ if (e.field.name == 'participant') {
+ success = add_attendees(e.insert, { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:(e.data && e.data.type == 'group' ? 'GROUP' : 'INDIVIDUAL') });
+ }
+ if (e.field && success) {
+ e.field.value = '';
+ }
});
- $('#edit-attendee-add').click(function(){
- var input = $('#edit-attendee-name');
- rcmail.ksearch_blur();
- if (add_attendees(input.val(), { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'INDIVIDUAL' })) {
- input.val('');
- }
+ $('#edit-attendee-add').click(function() {
+ var input = $('#edit-attendee-name');
+ rcmail.ksearch_blur();
+ if (add_attendees(input.val(), { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'INDIVIDUAL' })) {
+ input.val('');
+ }
});
// handle change of "send invitations" checkbox
@@ -1595,7 +1595,7 @@ function rcube_tasklist_ui(settings)
};
// add the given attendee to the list
- var add_attendee = function(data, readonly)
+ var add_attendee = function(data, readonly, before)
{
if (!me.selected_task)
return false;
@@ -1624,6 +1624,12 @@ function rcube_tasklist_ui(settings)
else if (data['delegated-from'])
tooltip = rcmail.gettext('delegatedfrom', 'tasklist') + data['delegated-from'];
+ // add expand button for groups
+ if (data.cutype == 'GROUP') {
+ dispname += ' ' +
+ rcmail.gettext('expandattendeegroup','libcalendaring') + '';
+ }
+
var html = '' + dispname + ' | ' +
'' + Q(data.status || '') + ' | ' +
(data.cutype != 'RESOURCE' ? '' + (readonly || !invbox ? '' : invbox) + ' | ' : '') +
@@ -1631,11 +1637,16 @@ function rcube_tasklist_ui(settings)
var tr = $('
')
.addClass(String(data.role).toLowerCase())
- .html(html)
- .appendTo(attendees_list);
+ .html(html);
+
+ if (before)
+ tr.insertBefore(before)
+ else
+ tr.appendTo(attendees_list);
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(task_attendee_click);
+ tr.find('a.expandlink').click(data, function(e) { me.expand_attendee_group(e, add_attendee, remove_attendee); });
tr.find('input.edit-attendee-reply').click(function() {
var enabled = $('#edit-attendees-invite:checked').length || $('input.edit-attendee-reply:checked').length;
$('p.attendees-commentbox')[enabled ? 'show' : 'hide']();
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 7894b102..a5731289 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -1147,7 +1147,7 @@ class tasklist extends rcube_plugin
$this->rc->output->set_env('autocomplete_threads', (int)$this->rc->config->get('autocomplete_threads', 0));
$this->rc->output->set_env('autocomplete_max', (int)$this->rc->config->get('autocomplete_max', 15));
$this->rc->output->set_env('autocomplete_min_length', $this->rc->config->get('autocomplete_min_length'));
- $this->rc->output->add_label('autocompletechars', 'autocompletemore');
+ $this->rc->output->add_label('autocompletechars', 'autocompletemore', 'libcalendaring.expandattendeegroup', 'libcalendaring.expandattendeegroupnodata');
$this->rc->output->set_pagetitle($this->gettext('navtitle'));
$this->rc->output->send('tasklist.mainview');