diff --git a/NEWS b/NEWS index 588ed76f9..4c027c916 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,8 @@ Enhancements - [web] create card from sender or recipient address (#3002, #4610) + - [web] updated Angular to version 1.7.7 + - [web] restored support for next/previous slot suggestion in attendees editor - [core] baseDN now accept dynamic domain values (#3685 - sponsored by iRedMail) - [core] we now handle optional and non-required attendee states diff --git a/UI/Templates/SchedulerUI/UIxAppointmentEditorTemplate.wox b/UI/Templates/SchedulerUI/UIxAppointmentEditorTemplate.wox index 51d8caed9..f73cedcb5 100644 --- a/UI/Templates/SchedulerUI/UIxAppointmentEditorTemplate.wox +++ b/UI/Templates/SchedulerUI/UIxAppointmentEditorTemplate.wox @@ -130,6 +130,7 @@
@@ -295,7 +296,7 @@
- + @@ -336,7 +337,7 @@ - + diff --git a/UI/Templates/SchedulerUI/UIxAttendeesEditor.wox b/UI/Templates/SchedulerUI/UIxAttendeesEditor.wox index 4eb75b0e1..e0883ed82 100644 --- a/UI/Templates/SchedulerUI/UIxAttendeesEditor.wox +++ b/UI/Templates/SchedulerUI/UIxAttendeesEditor.wox @@ -10,7 +10,23 @@ var dayEndHour = ; var timeFormat = ''; - +
+ + + + + + + chevron_left + chevron_right +
+
@@ -24,7 +40,7 @@ person -
+
{{editor.component.organizer.name}}
{{editor.component.organizer.email}}
@@ -40,69 +56,72 @@ person -
+
{{currentAttendee.name}}
{{currentAttendee.email}}
- - - - -
-
{{day.stringWithSeparator}}
-
-
{{hour}}
+ + + + + +
+
{{day.stringWithSeparator}}
+
+
{{hour}}
+
-
- - - -
-
-
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+ + + + +
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- -
- - -
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
- - + + + + +
-
+
diff --git a/UI/WebServerResources/js/Common/utils.js b/UI/WebServerResources/js/Common/utils.js index 46cf65b33..8b6b61f88 100644 --- a/UI/WebServerResources/js/Common/utils.js +++ b/UI/WebServerResources/js/Common/utils.js @@ -260,6 +260,14 @@ String.prototype.parseDate = function(localeProvider, format) { return new Date(NaN); }; +Date.prototype.clone = function() { + var newDate = new Date(); + + newDate.setTime(this.getTime()); + + return newDate; +}; + Date.prototype.daysUpTo = function(otherDate) { var days = []; diff --git a/UI/WebServerResources/js/Scheduler/Attendees.service.js b/UI/WebServerResources/js/Scheduler/Attendees.service.js new file mode 100644 index 000000000..d7ef8c277 --- /dev/null +++ b/UI/WebServerResources/js/Scheduler/Attendees.service.js @@ -0,0 +1,647 @@ +/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ + +(function() { + 'use strict'; + + /** + * @name Attendees + * @constructor + * @param {object} component - a Component object instance + */ + function Attendees(component) { + this.component = component; + if (this.component.attendees) { + _.forEach(this.component.attendees, function(attendee) { + attendee.image = Attendees.$gravatar(attendee.email, 32); + }); + } + this.workDaysOnly = true; + this.slotStartTimeLimit = new Date(); + this.slotStartTimeLimit.setMinutes(0); + this.slotStartTimeLimit.setHours(Attendees.dayStartHour); + this.slotEndTimeLimit = new Date(); + this.slotEndTimeLimit.setMinutes(0); + this.slotEndTimeLimit.setHours(Attendees.dayEndHour); + this.$days = []; + this.updateFreeBusyCoverage(); + this.updateFreeBusy(); + } + + /** + * @memberof Attendees + * @desc The factory we'll use to register with Angular + * @returns the Attendees constructor + */ + Attendees.$factory = ['$q', '$timeout', '$log', 'sgSettings', 'Preferences', 'User', 'Card', 'Gravatar', 'Resource', function($q, $timeout, $log, Settings, Preferences, User, Card, Gravatar, Resource) { + angular.extend(Attendees, { + $q: $q, + $timeout: $timeout, + $log: $log, + $settings: Settings, + $User: User, + $Preferences: Preferences, + $Card: Card, + $gravatar: Gravatar, + $$resource: new Resource(Settings.activeUser('folderURL') + 'Calendar', Settings.activeUser()) + }); + + Attendees.dayStartHour = parseInt(Preferences.defaults.SOGoDayStartTime.split(':')[0]); + Attendees.dayEndHour = parseInt(Preferences.defaults.SOGoDayEndTime.split(':')[0]); + + return Attendees; // return constructor + }]; + + /** + * @module SOGo.SchedulerUI + * @desc Factory registration of Attendees in Angular module. + */ + try { + angular.module('SOGo.SchedulerUI'); + } + catch(e) { + angular.module('SOGo.SchedulerUI', ['SOGo.Common']); + } + angular.module('SOGo.SchedulerUI') + .factory('Attendees', Attendees.$factory); + + + /** + * @function timeToQuarters + * @memberof Attendees + * @param {date} dateTime - a Date object instance + * @desc Return the number of quarters matching the time + * @returns the number of quarters + */ + Attendees.timeToQuarters = function(dateTime) { + return dateTime.getHours() * 4 + Math.ceil(dateTime.getMinutes()/15); + }; + + /** + * @function getLength + * @memberof Attendees.prototype + * @returns the number of attendees + */ + Attendees.prototype.getLength = function() { + return this.component.attendees ? this.component.attendees.length : 0; + }; + + /** + * @function initOrganizer + * @memberof Attendees.prototype + * @desc Extend instance with organizer including her freebusy information. + * @param {object} calendar - Calendar instance associated to current component + */ + Attendees.prototype.initOrganizer = function(calendar) { + var _this = this, promise; + if (calendar && calendar.isSubscription) { + promise = Attendees.$User.$filter(calendar.owner).then(function(results) { + var owner = results[0]; + _this.component.organizer = { + uid: owner.uid, + name: owner.cn, + email: owner.c_email + }; + }); + } + else { + this.component.organizer = { + uid: Attendees.$settings.activeUser('login'), + name: Attendees.$settings.activeUser('identification'), + email: Attendees.$settings.activeUser('email') + }; + promise = Attendees.$q.when(); + } + // Fetch organizer's freebusy + promise.then(function() { + _this.updateFreeBusyAttendee(_this.component.organizer); + }); + }; + + /** + * @function add + * @memberof Attendees.prototype + * @desc Add an attendee and fetch his freebusy info. + * @param {Object} card - an Card object instance to be added to the attendees list + */ + Attendees.prototype.add = function(card, options) { + var _this = this, attendee, list, url, params; + if (card) { + if (!this.component.attendees || (options && options.organizerCalendar)) { + // No attendee yet; initialize the organizer + this.initOrganizer(options? options.organizerCalendar : undefined); + } + if (card.$isList({expandable: true})) { + // Decompose list members + list = Attendees.$Card.$find(card.container, card.c_name); + list.$id().then(function(listId) { + _.forEach(list.refs, function(ref) { + attendee = { + name: ref.c_cn, + email: ref.$preferredEmail(options? options.partial : undefined), + role: 'req-participant', + partstat: 'needs-action', + uid: ref.c_uid, + $avatarIcon: 'person', + }; + if (!_.find(_this.component.attendees, function(o) { + return o.email == attendee.email; + })) { + // Contact is not already an attendee, add it + attendee.image = Attendees.$gravatar(attendee.email, 32); + if (_this.component.attendees) + _this.component.attendees.push(attendee); + else + _this.component.attendees = [attendee]; + _this.updateFreeBusyAttendee(attendee); + } + }); + }); + } + else { + // Single contact + attendee = { + uid: card.c_uid, + domain: card.c_domain, + isMSExchange: card.ismsexchange, + name: card.c_cn, + email: card.$preferredEmail(), + role: 'req-participant', + partstat: 'needs-action', + $avatarIcon: card.$avatarIcon + }; + if (!_.find(this.attendees, function(o) { + return o.email == attendee.email; + })) { + attendee.image = Attendees.$gravatar(attendee.email, 32); + if (this.component.attendees) + this.component.attendees.push(attendee); + else + this.component.attendees = [attendee]; + this.updateFreeBusyAttendee(attendee); + } + } + } + }; + + /** + * @function hasAttendee + * @memberof Attendees.prototype + * @desc Verify if one of the email addresses of a Card instance matches an attendee. + * @param {Object} card - an Card object instance + * @returns true if the Card matches an attendee + */ + Attendees.prototype.hasAttendee = function(card) { + var attendee = _.find(this.component.attendees, function(attendee) { + return _.find(card.emails, function(email) { + return email.value == attendee.email; + }); + }); + return angular.isDefined(attendee); + }; + + /** + * @function remove + * @memberof Attendees.prototype + * @desc Remove an attendee from the component. + * @param {Object} attendee - an object literal defining an attendee + */ + Attendees.prototype.remove = function(attendee) { + var index = _.findIndex(this.component.attendees, function(currentAttendee) { + return currentAttendee.email == attendee.email; + }); + this.component.attendees.splice(index, 1); + }; + + /** + * @function updateFreeBusyCoverage + * @memberof Attendees.prototype + * @desc Build a 15-minute-based representation of the component's period. + * @returns an object literal hashed by days and hours and arrays of four 1's and 0's + */ + Attendees.prototype.updateFreeBusyCoverage = function() { + var _this = this, freebusy = {}; + var roundedStart, roundedEnd, startQuarter, endQuarter; + + if (this.component.start && this.component.end) { + roundedStart = new Date(this.component.start.getTime()); + roundedEnd = new Date(this.component.end.getTime()); + if (this.component.isAllDay) { + roundedStart.setHours(Attendees.dayStartHour); + roundedStart.setMinutes(0); + roundedEnd.setHours(Attendees.dayEndHour); + roundedEnd.setMinutes(0); + startQuarter = endQuarter = 0; + } + else { + startQuarter = parseInt(roundedStart.getMinutes()/15 + 0.5); + endQuarter = parseInt(roundedEnd.getMinutes()/15 + 0.5); + } + roundedStart.setMinutes(15*startQuarter); + roundedEnd.setMinutes(15*endQuarter); + + _.forEach(roundedStart.daysUpTo(roundedEnd), function(date, index) { + var currentDay = date.getDate(), + dayKey = date.getDayString(), + hourKey; + if (dayKey === roundedStart.getDayString()) { + hourKey = date.getHours().toString(); + freebusy[dayKey] = {}; + freebusy[dayKey][hourKey] = []; + while (startQuarter > 0) { + freebusy[dayKey][hourKey].push(0); + startQuarter--; + } + } + else { + date = date.beginOfDay(); + freebusy[dayKey] = {}; + } + while (date.getTime() < roundedEnd.getTime() && + date.getDate() == currentDay) { + hourKey = date.getHours().toString(); + if (angular.isUndefined(freebusy[dayKey][hourKey])) + freebusy[dayKey][hourKey] = []; + freebusy[dayKey][hourKey].push(1); + date.addMinutes(15); + } + }); + this.freebusy = freebusy; + } + }; + + /** + * @function coversFreeBusy + * @memberof Attendees.prototype + * @desc Check if a specific quarter matches the component's period. + * @returns true if the quarter covers the component's period + */ + Attendees.prototype.coversFreeBusy = function(day, hour, quarter) { + var b = (this.freebusy && + angular.isDefined(this.freebusy[day]) && + angular.isDefined(this.freebusy[day][hour]) && + this.freebusy[day][hour][quarter] == 1); + return b; + }; + + /** + * @function getDays + * @memberof Attendees.prototype + * @desc Define a period of one week before and one week after the component's period or a reference date. + * @param refDate - a Date object + * @returns an array of objects representing the days + */ + Attendees.prototype.getDays = function(refDate) { + var _this = this, sd, ed, formatFcn; + + if (refDate) { + sd = refDate; + ed = new Date(refDate.getTime()); + ed.addMinutes(this.component.delta); + } + else { + sd = this.component.start; + ed = this.component.end; + } + + if (this.$days.length === 0 || + _.findIndex(this.$days, ['getDayString', sd.getDayString()]) < 0 || + _.findIndex(this.$days, ['getDayString', ed.getDayString()]) < 0) { + sd = sd.beginOfDay().addDays(-7); + ed = ed.beginOfDay().addDays(7); + formatFcn = Attendees.$Preferences.$mdDateLocaleProvider.formatDate; + this.$days.splice(0, this.$days.length); + _.forEach(sd.daysUpTo(ed), function(date) { + date.$dateFormat = Attendees.$Preferences.defaults.SOGoLongDateFormat; + _this.$days.push({ + stringWithSeparator: formatFcn(date), + getDayString: date.getDayString() + }); + }); + } + + return this.$days; + }; + + /** + * @function updateFreeBusy + * @memberof Attendees.prototype + * @desc Fetch the freebusy information of the organizer and all attendees. + * @returns a promise of the all HTTP operations + */ + Attendees.prototype.updateFreeBusy = function(refDate) { + var _this = this, promises = []; + + if (this.getLength() > 0) { + if (this.component.organizer) { + promises.push(this.updateFreeBusyAttendee(this.component.organizer, refDate)); + } + _.forEach(_.filter(this.component.attendees, 'uid'), function(attendee) { + promises.push(_this.updateFreeBusyAttendee(attendee, refDate)); + }); + } + + return Attendees.$q.all(promises); + }; + + /** + * @function updateFreeBusyAttendee + * @memberof Attendees.prototype + * @desc Update the freebusy information for the component's period for a specific attendee. + * @param {Object} card - an Card object instance of the attendee + * @returns a promise of the HTTP operation if the information was not cached + */ + Attendees.prototype.updateFreeBusyAttendee = function(attendee, refDate) { + var promise, resource, uid, sd, ed, params, days; + + if (attendee.uid) { + uid = attendee.uid; + if (attendee.domain) + uid += '@' + attendee.domain; + days = _.map(this.getDays(refDate), 'getDayString'); + params = + { + sday: days[0], + eday: days[days.length - 1] + }; + + if (attendee.isMSExchange) { + // Attendee is not a local user, but her freebusy data is available from an external MS Exchange server; + // we query /SOGo/so//freebusy.ifb/ajaxRead?uid= + resource = Attendees.$$resource.userResource(); + params.uid = uid; + } + else { + // Attendee is a user; + // web query /SOGo/so//freebusy.ifb/ajaxRead + resource = Attendees.$$resource.userResource(uid); + } + + if (angular.isUndefined(attendee.freebusy)) + attendee.freebusy = {}; + + if (_.intersection(_.keys(attendee.freebusy), days).length !== days.length) { + // Fetch FreeBusy information + promise = resource.fetch('freebusy.ifb', 'ajaxRead', params).then(function(data) { + _.forEach(days, function(day) { + var hour; + + if (angular.isUndefined(attendee.freebusy[day])) + attendee.freebusy[day] = {}; + + if (angular.isUndefined(data[day])) + data[day] = {}; + + for (var i = 0; i <= 23; i++) { + hour = i.toString(); + if (data[day][hour]) + attendee.freebusy[day][hour] = [ + data[day][hour]["0"], + data[day][hour]["15"], + data[day][hour]["30"], + data[day][hour]["45"] + ]; + else + attendee.freebusy[day][hour] = [0, 0, 0, 0]; + } + }); + }); + } + else { + promise = Attendees.$q.when(); + } + + return promise; + } + }; + + + /** + * @function forwardFindDate + * @memberof Attendees.prototype + * @desc Find the next slot for which all attendees are available whitin the reference day + * @param {date} currentStart - the reference day + * @returns a date object or null if no slot were found + */ + Attendees.prototype.forwardFindDate = function(currentStart) { + var foundDate = null; + var maxOffset = this.endLimit - this.duration; + var offset = 0; + + if (this.firstStep) { + offset = Math.floor(this.start.getHours() * 4 + this.start.getMinutes() / 15) + 1; + this.firstStep = false; + } + else { + offset = this.currentEntries.indexOf(0); + } + if (offset > -1 && offset < this.startLimit) { + offset = this.startLimit; + } + + while (!foundDate && offset > -1 && offset <= maxOffset) { + var testDuration = 0; + while (this.currentEntries[offset] === 0 && testDuration < this.duration) { + testDuration++; + offset++; + } + if (testDuration == this.duration) { + foundDate = new Date(); + var foundTime = (currentStart.getTime() + (offset - testDuration) * 900000); + foundDate.setTime(foundTime); + } + else { + offset = this.currentEntries.indexOf(0, offset + 1); + } + } + + return foundDate; + }; + + /** + * @function forwardAdjustCurrentStart + * @memberof Attendees.prototype + * @desc Adjust a date to the next non-weekend day + * @param {date} currentStart - the reference day + */ + Attendees.prototype.forwardAdjustCurrentStart = function (currentStart) { + var day = currentStart.getDay(); + if (day === 0) { + currentStart.addDays(1); + } + else if (day === 6) { + currentStart.addDays(2); + } + }; + + /** + * @function backwardFindDate + * @memberof Attendees.prototype + * @desc Find the previous slot for which all attendees are available whitin the reference day + * @param {date} currentStart - the reference day + * @returns a date object or null if no slot were found + */ + Attendees.prototype.backwardFindDate = function (currentStart) { + var foundDate = null; + var maxOffset = this.endLimit - this.duration; + var offset; + if (this.firstStep) { + offset = Math.floor(this.start.getHours() * 4 + this.start.getMinutes() / 15) - 1; + this.firstStep = false; + } + else { + offset = this.currentEntries.lastIndexOf(0); + } + if (offset > maxOffset) { + offset = maxOffset; + } + while (!foundDate && offset >= this.startLimit) { + var testDuration = 0; + var testOffset = offset; + while (this.currentEntries[testOffset] === 0 && testDuration < this.duration) { + testDuration++; + testOffset++; + } + if (testDuration == this.duration) { + foundDate = new Date(); + var foundTime = (currentStart.getTime() + offset * 900000); + foundDate.setTime(foundTime); + } + else { + offset = this.currentEntries.lastIndexOf(0, offset - 1); + } + } + Attendees.$log.debug(['found = ' + foundDate, offset]); + return foundDate; + }; + + /** + * @function backwardAdjustCurrentStart + * @memberof Attendees.prototype + * @desc Adjust a date to the previous non-weekend day + * @param {date} currentStart - the reference day + */ + Attendees.prototype.backwardAdjustCurrentStart = function (currentStart) { + var day = currentStart.getDay(); + if (day == 0) { + currentStart.addDays(-2); + } + else if (day == 6) { + currentStart.addDays(-1); + } + }; + + /** + * @function findSlot + * @memberof Attendees.prototype + * @desc Find the next or previous slot when all attendees are available. + * @param {number} direction - the search direction (1 or -1) + */ + Attendees.prototype.findSlot = function(direction) { + var _this = this, currentStart; + + this.direction = direction; + this.firstStep = true; + + if (direction > 0) { + this.findDate = this.forwardFindDate; + this.adjustCurrentStart = this.forwardAdjustCurrentStart; + } + else { + this.findDate = this.backwardFindDate; + this.adjustCurrentStart = this.backwardAdjustCurrentStart; + } + + if (this.component.isAllDay) { + // Event lasts all day within limits + this.start = this.component.start.clone(); + this.start.setHours(Attendees.dayStartHour); + this.start.setMinutes(0); + this.start.setSeconds(0); + + this.end = this.component.end.clone(); + this.end.setHours(Attendees.dayEndHour); + this.end.setMinutes(0); + this.end.setSeconds(0); + + this.startLimit = Attendees.dayStartHour * 4; // from user's defaults + this.endLimit = Attendees.dayEndHour * 4; // from user's defaults + + this.duration = (Attendees.dayEndHour - Attendees.dayStartHour) * 4; + } + else { + // Event can be outside limits + this.start = this.component.start; + this.end = this.component.end; + + this.startLimit = Attendees.timeToQuarters(this.slotStartTimeLimit); // from time picker + this.endLimit = Attendees.timeToQuarters(this.slotEndTimeLimit); // from time picker + + this.duration = Math.ceil((this.end.getTime() - this.start.getTime()) / 900000); + } + + currentStart = this.component.start.clone(); + currentStart.setHours(0, 0, 0, 0); + + if (this.workDaysOnly) { + this.adjustCurrentStart(currentStart); + } + + // Start a recursive search + return this.step(currentStart).then(function (foundDate) { + _this.component.start = new Date(foundDate.getTime()); + _this.component.end = new Date(_this.component.start.getTime()); + _this.component.end.addMinutes(_this.component.delta); + _this.updateFreeBusyCoverage(); + return foundDate; + }); + }; + + /** + * @function mergeFreebusy + * @memberof Attendees.prototype + * @desc Merge freebusy information of organizer and all attendees for a referene date. + * @param {date) start - the reference date + */ + Attendees.prototype.mergeFreebusy = function(start) { + var _this = this; + var startDay = start.getDayString(); + + return this.updateFreeBusy(start).then(function () { + var i, j, attendee, attendeeEntries; + _this.currentEntries = _.flatMap(_this.component.organizer.freebusy[startDay]); + for (i = 0; i < _this.component.attendees.length; i++) { + attendee = _this.component.attendees[i]; + attendeeEntries = _.flatMap(attendee.freebusy[startDay]); + for (j = 0; j < _this.currentEntries.length; j++) { + _this.currentEntries[j] += attendeeEntries[j]; + } + } + }); + }; + + /** + * @function step + * @memberof Attendees.prototype + * @desc Recursively search for the next available slot, one day a the time. + * @param {date) currentStart - the starting day + */ + Attendees.prototype.step = function(currentStart) { + var _this = this; + // var currentStartDay = currentStart.getDayString(); + return this.mergeFreebusy(currentStart).then(function () { + var foundDate = _this.findDate(currentStart); + if (foundDate) { + return foundDate; + } + else { + currentStart.addDays(_this.direction > 0 ? 1 : -1); + currentStart.setHours(0, 0, 0, 0); + if (_this.workDaysOnly) { + _this.adjustCurrentStart(currentStart); + } + return _this.step(currentStart); + } + }); + }; + +})(); diff --git a/UI/WebServerResources/js/Scheduler/Component.service.js b/UI/WebServerResources/js/Scheduler/Component.service.js index a479a6e3a..c7008049d 100644 --- a/UI/WebServerResources/js/Scheduler/Component.service.js +++ b/UI/WebServerResources/js/Scheduler/Component.service.js @@ -31,7 +31,7 @@ * @desc The factory we'll use to register with Angular * @returns the Component constructor */ - Component.$factory = ['$q', '$timeout', '$log', '$rootScope', 'sgSettings', 'sgComponent_STATUS', 'Preferences', 'User', 'Card', 'Gravatar', 'Resource', function($q, $timeout, $log, $rootScope, Settings, Component_STATUS, Preferences, User, Card, Gravatar, Resource) { + Component.$factory = ['$q', '$timeout', '$log', '$rootScope', 'sgSettings', 'sgComponent_STATUS', 'Attendees', 'Preferences', 'User', 'Card', 'Resource', function($q, $timeout, $log, $rootScope, Settings, Component_STATUS, Attendees, Preferences, User, Card, Resource) { angular.extend(Component, { STATUS: Component_STATUS, $q: $q, @@ -41,8 +41,8 @@ $settings: Settings, $User: User, $Preferences: Preferences, + $Attendees: Attendees, $Card: Card, - $gravatar: Gravatar, $$resource: new Resource(Settings.activeUser('folderURL') + 'Calendar', Settings.activeUser()), timeFormat: "%H:%M", // Filter parameters common to events and tasks @@ -629,51 +629,20 @@ // this.organizer.$image = Component.$gravatar(this.organizer.email, 32); // } - if (this.attendees) { - _.forEach(this.attendees, function(attendee) { - attendee.image = Component.$gravatar(attendee.email, 32); - }); - } - - // Load freebusy of attendees - this.updateFreeBusy(); - this.selected = false; }; - /** - * @function initOrganizer + * @function init * @memberof Component.prototype - * @desc Extend instance with organizer including her freebusy information. - * @param {object} calendar - Calendar instance associated to current component + * @desc Extend instance with required attributes and new data. + * @param {object} data - attributes of component */ - Component.prototype.initOrganizer = function(calendar) { - var _this = this, promise; - if (calendar && calendar.isSubscription) { - promise = Component.$User.$filter(calendar.owner).then(function(results) { - var owner = results[0]; - _this.organizer = { - uid: owner.uid, - name: owner.cn, - email: owner.c_email - }; - }); - } - else { - this.organizer = { - uid: Component.$settings.activeUser('login'), - name: Component.$settings.activeUser('identification'), - email: Component.$settings.activeUser('email') - }; - promise = Component.$q.when(); - } - // Fetch organizer's freebusy - promise.then(function() { - _this.updateFreeBusyAttendee(_this.organizer); - }); + Component.prototype.initAttendees = function() { + this.$attendees = new Component.$Attendees(this); }; + /** * @function hasCustomRepeat * @memberof Component.prototype @@ -783,85 +752,6 @@ } }; - /** - * @function coversFreeBusy - * @memberof Component.prototype - * @desc Check if a specific quarter matches the component's period - * @returns true if the quarter covers the component's period - */ - Component.prototype.coversFreeBusy = function(day, hour, quarter) { - var b = (angular.isDefined(this.freebusy[day]) && - angular.isDefined(this.freebusy[day][hour]) && - this.freebusy[day][hour][quarter] == 1); - return b; - }; - - /** - * @function updateFreeBusyCoverage - * @memberof Component.prototype - * @desc Build a 15-minute-based representation of the component's period. - * @returns an object literal hashed by days and hours and arrays of four 1's and 0's - */ - Component.prototype.updateFreeBusyCoverage = function() { - var _this = this, freebusy = {}; - - if (this.start && this.end) { - var roundedStart = new Date(this.start.getTime()), - roundedEnd = new Date(this.end.getTime()), - startQuarter = parseInt(roundedStart.getMinutes()/15 + 0.5), - endQuarter = parseInt(roundedEnd.getMinutes()/15 + 0.5); - roundedStart.setMinutes(15*startQuarter); - roundedEnd.setMinutes(15*endQuarter); - - _.forEach(roundedStart.daysUpTo(roundedEnd), function(date, index) { - var currentDay = date.getDate(), - dayKey = date.getDayString(), - hourKey; - if (dayKey == _this.start.getDayString()) { - hourKey = date.getHours().toString(); - freebusy[dayKey] = {}; - freebusy[dayKey][hourKey] = []; - while (startQuarter > 0) { - freebusy[dayKey][hourKey].push(0); - startQuarter--; - } - } - else { - date = date.beginOfDay(); - freebusy[dayKey] = {}; - } - while (date.getTime() < _this.end.getTime() && - date.getDate() == currentDay) { - hourKey = date.getHours().toString(); - if (angular.isUndefined(freebusy[dayKey][hourKey])) - freebusy[dayKey][hourKey] = []; - freebusy[dayKey][hourKey].push(1); - date.addMinutes(15); - } - }); - return freebusy; - } - }; - - /** - * @function updateFreeBusy - * @memberof Component.prototype - * @desc Update the freebusy coverage representation and the attendees freebusy information - */ - Component.prototype.updateFreeBusy = function() { - var _this = this; - - this.freebusy = this.updateFreeBusyCoverage(); - - if (this.attendees) { - if (this.organizer) - this.updateFreeBusyAttendee(this.organizer); - _.forEach(this.attendees, function(attendee) { - _this.updateFreeBusyAttendee(attendee); - }); - } - }; - /** * @function setDelta * @memberof Component.prototype @@ -875,70 +765,6 @@ this.end.addMinutes(this.delta); }; - /** - * @function updateFreeBusyAttendee - * @memberof Component.prototype - * @desc Update the freebusy information for the component's period for a specific attendee. - * @param {Object} card - an Card object instance of the attendee - */ - Component.prototype.updateFreeBusyAttendee = function(attendee) { - var resource, uid, params, days; - - if (attendee.uid) { - uid = attendee.uid; - if (attendee.domain) - uid += '@' + attendee.domain; - params = - { - sday: this.start.getDayString(), - eday: this.end.getDayString() - }; - - if (attendee.isMSExchange) { - // Attendee is not a local user, but her freebusy data is available from an external MS Exchange server; - // we query /SOGo/so//freebusy.ifb/ajaxRead?uid= - resource = Component.$$resource.userResource(); - params.uid = uid; - } - else { - // Attendee is a user; - // web query /SOGo/so//freebusy.ifb/ajaxRead - resource = Component.$$resource.userResource(uid); - } - - days = _.map(this.start.daysUpTo(this.end), function(day) { return day.getDayString(); }); - - if (angular.isUndefined(attendee.freebusy)) - attendee.freebusy = {}; - - // Fetch FreeBusy information - resource.fetch('freebusy.ifb', 'ajaxRead', params).then(function(data) { - _.forEach(days, function(day) { - var hour; - - if (angular.isUndefined(attendee.freebusy[day])) - attendee.freebusy[day] = {}; - - if (angular.isUndefined(data[day])) - data[day] = {}; - - for (var i = 0; i <= 23; i++) { - hour = i.toString(); - if (data[day][hour]) - attendee.freebusy[day][hour] = [ - data[day][hour]["0"], - data[day][hour]["15"], - data[day][hour]["30"], - data[day][hour]["45"] - ]; - else - attendee.freebusy[day][hour] = [0, 0, 0, 0]; - } - }); - }); - } - }; - /** * @function getClassName * @memberof Component.prototype @@ -952,101 +778,6 @@ return base + '-folder' + (this.destinationCalendar || this.c_folder || this.pid); }; - /** - * @function addAttendee - * @memberof Component.prototype - * @desc Add an attendee and fetch his freebusy info. - * @param {Object} card - an Card object instance to be added to the attendees list - */ - Component.prototype.addAttendee = function(card, options) { - var _this = this, attendee, list, url, params; - if (card) { - if (!this.attendees || (options && options.organizerCalendar)) { - // No attendee yet; initialize the organizer - this.initOrganizer(options? options.organizerCalendar : undefined); - } - if (card.$isList({expandable: true})) { - // Decompose list members - list = Component.$Card.$find(card.container, card.c_name); - list.$id().then(function(listId) { - _.forEach(list.refs, function(ref) { - attendee = { - name: ref.c_cn, - email: ref.$preferredEmail(options? options.partial : undefined), - role: 'req-participant', - partstat: 'needs-action', - uid: ref.c_uid, - $avatarIcon: 'person', - }; - if (!_.find(_this.attendees, function(o) { - return o.email == attendee.email; - })) { - // Contact is not already an attendee, add it - attendee.image = Component.$gravatar(attendee.email, 32); - if (_this.attendees) - _this.attendees.push(attendee); - else - _this.attendees = [attendee]; - _this.updateFreeBusyAttendee(attendee); - } - }); - }); - } - else { - // Single contact - attendee = { - uid: card.c_uid, - domain: card.c_domain, - isMSExchange: card.ismsexchange, - name: card.c_cn, - email: card.$preferredEmail(), - role: 'req-participant', - partstat: 'needs-action', - $avatarIcon: card.$avatarIcon - }; - if (!_.find(this.attendees, function(o) { - return o.email == attendee.email; - })) { - attendee.image = Component.$gravatar(attendee.email, 32); - if (this.attendees) - this.attendees.push(attendee); - else - this.attendees = [attendee]; - this.updateFreeBusyAttendee(attendee); - } - } - } - }; - - /** - * @function hasAttendee - * @memberof Component.prototype - * @desc Verify if one of the email addresses of a Card instance matches an attendee. - * @param {Object} card - an Card object instance - * @returns true if the Card matches an attendee - */ - Component.prototype.hasAttendee = function(card) { - var attendee = _.find(this.attendees, function(attendee) { - return _.find(card.emails, function(email) { - return email.value == attendee.email; - }); - }); - return angular.isDefined(attendee); - }; - - /** - * @function deleteAttendee - * @memberof Component.prototype - * @desc Remove an attendee from the component - * @param {Object} attendee - an object literal defining an attendee - */ - Component.prototype.deleteAttendee = function(attendee) { - var index = _.findIndex(this.attendees, function(currentAttendee) { - return currentAttendee.email == attendee.email; - }); - this.attendees.splice(index, 1); - }; - /** * @function canRemindAttendeesByEmail * @memberof Component.prototype diff --git a/UI/WebServerResources/js/Scheduler/ComponentController.js b/UI/WebServerResources/js/Scheduler/ComponentController.js index 2b0550542..0dd0ee88c 100644 --- a/UI/WebServerResources/js/Scheduler/ComponentController.js +++ b/UI/WebServerResources/js/Scheduler/ComponentController.js @@ -205,21 +205,27 @@ /** * @ngInject */ - ComponentEditorController.$inject = ['$rootScope', '$scope', '$log', '$timeout', '$mdDialog', 'sgFocus', 'User', 'CalendarSettings', 'Calendar', 'Component', 'AddressBook', 'Card', 'Alarm', 'stateComponent']; - function ComponentEditorController($rootScope, $scope, $log, $timeout, $mdDialog, focus, User, CalendarSettings, Calendar, Component, AddressBook, Card, Alarm, stateComponent) { + ComponentEditorController.$inject = ['$rootScope', '$scope', '$log', '$timeout', '$element', '$mdDialog', 'sgFocus', 'User', 'CalendarSettings', 'Calendar', 'Component', 'Attendees', 'AddressBook', 'Card', 'Alarm', 'stateComponent']; + function ComponentEditorController($rootScope, $scope, $log, $timeout, $element, $mdDialog, focus, User, CalendarSettings, Calendar, Component, Attendees, AddressBook, Card, Alarm, stateComponent) { var vm = this, component, oldStartDate, oldEndDate, oldDueDate; this.$onInit = function () { + stateComponent.initAttendees(); this.service = Calendar; this.component = stateComponent; this.categories = {}; + this.updateFreeBusyCoverage = + angular.bind(this.component.$attendees, this.component.$attendees.updateFreeBusyCoverage); + this.coversFreeBusy = + angular.bind(this.component.$attendees, this.component.$attendees.coversFreeBusy); this.showRecurrenceEditor = this.component.$hasCustomRepeat; this.showAttendeesEditor = this.component.attendees && this.component.attendees.length; //this.searchText = null; this.attendeeConflictError = false; this.attendeesEditor = { - days: getDays(), - hours: getHours() + days: this.component.$attendees.$days, + hours: getHours(), + containerElement: $element[0].querySelector('#freebusy') }; if (this.component.start) @@ -297,7 +303,7 @@ card.charCodeAt(i) == 44 || // , card.charCodeAt(i) == 59) && // ; emailRE.test(address)) { - this.component.addAttendee(createCard(address), options); + this.component.$attendees.add(createCard(address), options); address = ''; } else { @@ -305,21 +311,43 @@ } } if (address) - this.component.addAttendee(createCard(address), options); + this.component.$attendees.add(createCard(address), options); } else { - this.component.addAttendee(card, options); + this.component.$attendees.add(card, options); this.showAttendeesEditor |= initOrganizer; } + + $timeout(scrollToStart); }; + function scrollToStart() { + var dayElement = $element[0].querySelector('#freebusy_day_' + vm.component.start.getDayString()); + var scrollLeft = dayElement.offsetLeft - vm.attendeesEditor.containerElement.offsetLeft; + vm.attendeesEditor.containerElement.scrollLeft = scrollLeft; + } + this.removeAttendee = function (attendee, form) { - this.component.deleteAttendee(attendee); - if (this.component.attendees.length === 0) + this.component.$attendees.remove(attendee); + if (this.component.$attendees.getLength() === 0) this.showAttendeesEditor = false; form.$setDirty(); }; + this.nextSlot = function () { + findSlot(1); + }; + + this.previousSlot = function () { + findSlot(-1); + }; + + function findSlot(direction) { + vm.component.$attendees.findSlot(direction).then(function () { + $timeout(scrollToStart); + }); + } + this.priorityLevel = function () { if (this.component && this.component.priority) { if (this.component.priority > 5) @@ -393,18 +421,6 @@ form.$setDirty(); }; - function getDays() { - var days = []; - - if (vm.component.start && vm.component.end) - days = vm.component.start.daysUpTo(vm.component.end); - - return _.map(days, function(date) { - return { stringWithSeparator: date.stringWithSeparator(), - getDayString: date.getDayString() }; - }); - } - function getHours() { var hours = []; for (var i = 0; i <= 23; i++) { @@ -486,8 +502,9 @@ }; function updateFreeBusy() { - vm.attendeesEditor.days = getDays(); - vm.component.updateFreeBusy(); + vm.component.$attendees.updateFreeBusyCoverage(); + vm.component.$attendees.updateFreeBusy(); + scrollToStart(); } } diff --git a/UI/WebServerResources/scss/views/SchedulerUI.scss b/UI/WebServerResources/scss/views/SchedulerUI.scss index 62bbdf8b8..155a70106 100644 --- a/UI/WebServerResources/scss/views/SchedulerUI.scss +++ b/UI/WebServerResources/scss/views/SchedulerUI.scss @@ -713,6 +713,7 @@ $quarter_height: 10px; md-content { background-color: transparent; display: table-row; + scroll-behavior: smooth; } md-list { display: table-cell;