diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index a4bf1abd..cd8567df 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; @@ -143,6 +144,10 @@ 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('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... @@ -210,16 +215,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; } /** @@ -285,13 +284,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('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'))); @@ -1908,6 +1908,103 @@ 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); + $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 ****/ /** diff --git a/plugins/calendar/calendar_base.js b/plugins/calendar/calendar_base.js index adcbdb40..37552ce6 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 2c846e74..a63263da 100644 --- a/plugins/calendar/calendar_ui.js +++ b/plugins/calendar/calendar_ui.js @@ -47,6 +47,12 @@ 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 resource_owners = {}; + var resources_events_source = { url:null, editable:false }; var freebusy_ui = { workinhoursonly:false, needsupdate:false }; var freebusy_data = {}; var current_view = null; @@ -103,6 +109,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) @@ -276,7 +287,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; @@ -304,9 +315,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) { @@ -332,19 +343,33 @@ function rcube_calendar_ui(settings) // list event attendees if (calendar.attendees && event.attendees) { - var data, dispname, organizer = false, rsvp = false, line, morelink, html = '',overflow = ''; + // 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, 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 = '' + dispname + ''; + tooltip = data.email; + dispname = '' + dispname + ''; 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(); } - line = '' + dispname + ' '; + 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 = '' + dispname + ' '; if (morelink) overflow += line; else @@ -360,7 +385,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) { @@ -371,7 +396,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; }) } @@ -427,6 +452,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) { @@ -573,6 +612,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')](); @@ -760,6 +800,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')[(rcmail.env.calendar_resources?'show':'hide')](); $('#edit-tab-attachments')[(calendar.attachments?'show':'hide')](); // activate the first tab @@ -777,6 +818,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(); @@ -879,7 +923,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')) { @@ -1312,7 +1356,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), @@ -1421,9 +1465,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*$/, ''), ','); @@ -1446,9 +1491,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 { @@ -1458,19 +1502,24 @@ 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 = '' + dispname + ''; + dispname = '' + dispname + ''; // role selection var organizer = data.role == 'ORGANIZER'; @@ -1480,8 +1529,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']; @@ -1496,20 +1547,27 @@ function rcube_calendar_ui(settings) // delete icon var icon = rcmail.env.deleteicon ? '' : rcmail.gettext('delete'); var dellink = '' + icon + ''; - + 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 = '' + select + '' + '' + dispname + '' + - '' + - '' + Q(data.status || '') + '' + + '' + + '' + Q(data.status || '') + '' + '' + (organizer || readonly ? '' : dellink) + ''; - + + var table = rcmail.env.calendar_resources && data.cutype == 'RESOURCE' ? resources_list : attendees_list; var 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; }); + tr.find('a.mailtolink').click(event_attendee_click); // select organizer identity if (data.identity_id) @@ -1521,16 +1579,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; }; @@ -1540,11 +1599,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', @@ -1566,7 +1625,313 @@ 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(search) + { + var $dialog = $('#eventresourcesdialog'); + + 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(); + }, + resize: function(e) { + var container = $(rcmail.gui_objects.resourceinfocalendar) + container.fullCalendar('option', 'height', container.height() + 4); + }, + buttons: buttons, + 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)); + + // set search query + $('#resourcesearchbox').val(search || ''); + + // 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 && $("#eventedit").is(':visible') ? true : false); + } + else { + 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); + } + }); + + // 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'); + + // 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(); + } + 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 + 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($('').addClass(k) + .append('' + Q(ucfirst(rcmail.get_label(k, 'calendar'))) + '') + .append('' + text2html(render_attrib(attribs[k])) + '') + ); + } + + $(rcmail.gui_objects.resourceownerinfo).hide(); + $(rcmail.gui_objects.resourceinfocalendar).fullCalendar('removeEventSource', resources_events_source); + + if (resource.owner) { + // 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); + } + } + + // 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); + } + }; + + // callback from server for resource listing + var resource_data_load = function(data) + { + var resources_tree = {}; + + // store data by ID + $.each(data, function(i, rec) { + resources_data[rec.ID] = rec; + + // assign parent-relations + if (rec.members) { + $.each(rec.members, function(j, m){ + resources_tree[m] = rec.ID; + }); + } + }); + + // 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() != '') + resource_search(); + else // ...or render full list + 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 = $('').attr('href', '#') + .attr('rel', rec.ID) + .html(Q(rec.name)); + + resources_treelist.insert({ id:rec.ID, 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) { + // cache this! + resource_owners[data.ID] = data; + + var table = $(rcmail.gui_objects.resourceownerinfo).find('tbody').html(''); + + for (var k in data) { + if (k == 'event' || k == 'ID') + continue; + + table.append($('').addClass(k) + .append('' + Q(ucfirst(rcmail.get_label(k, 'calendar'))) + '') + .append('' + text2html(data[k]) + '') + ); + } + + 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 || String(rec.email).toLowerCase() == q) { + dataset.push(rec.ID); + } + } + + 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) { @@ -1706,13 +2071,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'), @@ -1724,13 +2084,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) @@ -1838,7 +2208,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'], @@ -1963,6 +2333,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(); }, @@ -2056,6 +2429,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(); @@ -2117,7 +2493,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 }); } @@ -2133,6 +2509,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(); @@ -2255,6 +2634,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 ***/ @@ -2404,6 +2789,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 = []; @@ -2475,6 +2865,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)); @@ -2803,8 +3196,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); @@ -2889,16 +3283,39 @@ 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(){ $('#edit-attendees-donotify, #edit-attendees-invite').prop('checked', this.checked); @@ -2981,6 +3398,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(); }); @@ -2988,6 +3410,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/config.inc.php.dist b/plugins/calendar/config.inc.php.dist index 9a472a7a..f09d30f0 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 fb29740d..20f9f164 100644 --- a/plugins/calendar/drivers/calendar_driver.php +++ b/plugins/calendar/drivers/calendar_driver.php @@ -529,23 +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; - } - } diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php index 2019a671..129d30f4 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 7912df86..c3a18ac9 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(); 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 00000000..c377393d --- /dev/null +++ b/plugins/calendar/drivers/ldap/resources_driver_ldap.php @@ -0,0 +1,150 @@ + + * + * Copyright (C) 2014, Kolab Systems AG + * + * 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 . + */ + +/** + * LDAP-based resource directory implementation + */ +class resources_driver_ldap extends resources_driver +{ + private $rc; + 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), true); + + 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; + } + + // 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']); + + 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 00000000..c51e9226 --- /dev/null +++ b/plugins/calendar/drivers/resources_driver.php @@ -0,0 +1,114 @@ + + * + * Copyright (C) 2014, Kolab Systems AG + * + * 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 . + */ + + +/** + * Interface definition for a resources directory driver classe + */ +abstract class resources_driver +{ + protected$cal; + + /** + * Default constructor + */ + function __construct($cal) + { + $this->cal = $cal; + } + + /** + * 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; + } + + /** + * 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 b6c0e787..3e6b2236 100644 --- a/plugins/calendar/lib/calendar_ui.php +++ b/plugins/calendar/lib/calendar_ui.php @@ -84,6 +84,11 @@ 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.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')); @@ -112,6 +117,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'); } /** @@ -740,7 +746,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', ''); @@ -763,7 +769,97 @@ 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'); + + // 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, + 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); + } + + /** + * + */ + 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 + * @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 990838ee..de8e7dd6 100644 --- a/plugins/calendar/localization/en_US.inc +++ b/plugins/calendar/localization/en_US.inc @@ -123,6 +123,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'; @@ -155,10 +157,20 @@ $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'; $labels['tabattendees'] = 'Participants'; +$labels['tabresources'] = 'Resources'; $labels['tabattachments'] = 'Attachments'; $labels['tabsharing'] = 'Sharing'; diff --git a/plugins/calendar/skins/classic/calendar.css b/plugins/calendar/skins/classic/calendar.css index e9bde94b..346d61f4 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 { @@ -621,7 +624,7 @@ td.topalign { border: 1px solid #C2D071; } -#edit-attendees-table { +.edit-attendees-table { width: 100%; display: table; table-layout: fixed; @@ -629,49 +632,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; @@ -744,35 +749,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 { @@ -836,10 +841,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; } @@ -1085,12 +1094,159 @@ 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: 48%; + border: 1px solid #999; + background-color: #F9F9F9; + overflow: auto; +} + +#resource-availability { + top: auto; + bottom: 0; + height: 49%; + overflow: hidden; +} + +#resource-info .boxtitle, +#resource-availability .boxtitle { + 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; +} + +#resources-list li ul div.treetoggle { + left: 23px !important; +} + +#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; @@ -1169,7 +1325,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); @@ -1272,6 +1428,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; @@ -1285,6 +1442,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 4c561e4b..fd3b9266 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 da779993..fa93afcc 100644 --- a/plugins/calendar/skins/classic/templates/calendar.html +++ b/plugins/calendar/skins/classic/templates/calendar.html @@ -13,6 +13,14 @@
+
+ + + + + +
+
@@ -159,14 +194,6 @@
-
- - - - - -
-
diff --git a/plugins/calendar/skins/classic/templates/eventedit.html b/plugins/calendar/skins/classic/templates/eventedit.html index 03e47e6a..3bc4a488 100644 --- a/plugins/calendar/skins/classic/templates/eventedit.html +++ b/plugins/calendar/skins/classic/templates/eventedit.html @@ -1,13 +1,14 @@
    -
  • -
  • -
  • -
  • +
  • +
  • +
  • +
  • +
-
+

@@ -65,7 +66,7 @@
-
+
@@ -89,13 +90,19 @@
-
- +
+
+ +
+ + + +
-
+
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css index c9e7a8cb..d2ade1a1 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 { @@ -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; @@ -683,34 +683,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; @@ -719,18 +719,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; @@ -803,35 +805,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; + background: url(images/attendee-status.png) 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 { @@ -863,7 +865,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; } @@ -891,10 +893,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; } @@ -1067,6 +1073,122 @@ 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: 48%; +} + +#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: 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 { + 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 { @@ -1472,7 +1594,7 @@ div.calendar-invitebox .rsvp-status.tentative, 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 { @@ -1484,7 +1606,7 @@ div.calendar-invitebox .rsvp-status.tentative { } div.calendar-invitebox .rsvp-status.delegated { - background-position: 2px -160px; + background-position: 2px -180px; } /* iTIP attend reply page */ 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 60c5d957..00000000 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 00000000..59b44930 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 453d59a6..036a4fcc 100644 --- a/plugins/calendar/skins/larry/templates/calendar.html +++ b/plugins/calendar/skins/larry/templates/calendar.html @@ -113,6 +113,41 @@ +
+
+
+
+ +
+
+ +
+
+
+ +
+
+

+
+ +
+
+ +
+

+ +
+ + +
+
+
+
+
@@ -151,6 +186,7 @@ +
@@ -205,6 +241,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(); }); diff --git a/plugins/calendar/skins/larry/templates/eventedit.html b/plugins/calendar/skins/larry/templates/eventedit.html index 4af08f33..95be8eab 100644 --- a/plugins/calendar/skins/larry/templates/eventedit.html +++ b/plugins/calendar/skins/larry/templates/eventedit.html @@ -1,10 +1,10 @@
    -
  • +
-
+

@@ -62,7 +62,7 @@
-
+
@@ -86,20 +86,26 @@
-
- +
+
+ +
+ + + +
-
+
- +
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js index 8e5b24b5..745d6763 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