From 5e19a889c2bed84f5fa3e55fe0dc37fb5bb18e65 Mon Sep 17 00:00:00 2001 From: Francis Lachapelle Date: Wed, 5 Aug 2015 16:44:25 -0400 Subject: [PATCH] (feat) Handle invitations in appointment viewer --- .../English.lproj/Localizable.strings | 5 +- UI/Scheduler/UIxAppointmentEditor.m | 150 ++++++++------ UI/Scheduler/UIxCalMainView.m | 64 +++++- UI/Scheduler/UIxComponentEditor.h | 2 + UI/Scheduler/UIxComponentEditor.m | 79 ++++---- .../UIxAppointmentViewTemplate.wox | 188 ++++++++++++------ .../js/Common/User.service.js | 1 + .../js/Contacts/AddressBook.service.js | 51 ++++- .../js/Contacts/Card.service.js | 3 + .../js/Scheduler/CalendarListController.js | 2 +- .../js/Scheduler/Component.service.js | 82 ++++++++ .../js/Scheduler/ComponentController.js | 78 +++++--- .../scss/components/content/content.scss | 5 + .../scss/components/list/list.scss | 1 + .../scss/core/base_styles/_base_style.scss | 6 + 15 files changed, 518 insertions(+), 199 deletions(-) diff --git a/UI/Scheduler/English.lproj/Localizable.strings b/UI/Scheduler/English.lproj/Localizable.strings index 1b9c8f347..1a024b446 100644 --- a/UI/Scheduler/English.lproj/Localizable.strings +++ b/UI/Scheduler/English.lproj/Localizable.strings @@ -200,7 +200,7 @@ "General" = "General"; "Reply" = "Reply"; "Created by" = "Created by"; - +"You are invited to participate" = "You are invited to participate"; "Target:" = "Target:"; @@ -431,6 +431,7 @@ validate_endbeforestart = "The end date that you entered occurs before the st "New Event" = "New Event"; "New Task" = "New Task"; "Edit" = "Edit"; +"Update" = "Update"; "Delete" = "Delete"; "Go to Today" = "Go to Today"; "Day View" = "Day View"; @@ -523,6 +524,8 @@ vtodo_class2 = "(Confidential task)"; "button_allOccurrences" = "All occurences"; "Edit This Occurrence" = "Edit This Occurrence"; "Edit All Occurrences" = "Edit All Occurrences"; +"Update This Occurrence" = "Update This Occurrence"; +"Update All Occurrences" = "Update All Occurrences"; /* Properties dialog */ "Name" = "Name"; diff --git a/UI/Scheduler/UIxAppointmentEditor.m b/UI/Scheduler/UIxAppointmentEditor.m index 7ed3a349a..d65839803 100644 --- a/UI/Scheduler/UIxAppointmentEditor.m +++ b/UI/Scheduler/UIxAppointmentEditor.m @@ -237,55 +237,77 @@ } } -// -// -// +/** + * @api {post} /so/:username/Calendar/:calendarId/:appointmentId/rsvpAppointment Set participation state + * @apiVersion 1.0.0 + * @apiName PostEventRsvp + * @apiGroup Calendar + * @apiDescription Set the participation state of an attendee. + * @apiExample {curl} Example usage: + * curl -i http://localhost/SOGo/so/sogo1/Calendar/personal/71B6-54904400-1-7C308500.ics/rsvpAppointment \ + * -H 'Content-Type: application/json' \ + * -d '{ "reply": 1, \ + * "alarm": { { "quantity": 15, "unit": "MINUTES", "action": "display", "reference": "BEFORE", "relation": "START" } }' + * + * @apiParam {Number} reply 0 if needs-action, 1 if accepted, 2 if declined, 3 if tentative, 4 if delegated + * @apiParam {String} [delegatedTo] Email address of delegated attendee + * @apiParam {Object[]} [alarm] Set an alarm for the attendee + * @apiParam {String} alarm.action Either display or email + * @apiParam {Number} alarm.quantity Quantity of units + * @apiParam {String} alarm.unit Either MINUTES, HOURS, or DAYS + * @apiParam {String} alarm.reference Either BEFORE or AFTER + * @apiParam {String} alarm.relation Either START or END + * @apiParam {Boolean} [alarm.attendees] Alert attendees by email if 1 and action is email + * @apiParam {Boolean} [alarm.organizer] Alert organizer by email if 1 and action is email + */ - (id ) rsvpAction { iCalPerson *delegatedAttendee; - NSDictionary *message; + NSDictionary *params, *jsonResponse; WOResponse *response; WORequest *request; iCalAlarm *anAlarm; + NSException *ex; NSString *status; + id alarm; - int replyList, reminderList; + int replyList; request = [context request]; - message = [[request contentAsString] objectFromJSONString]; + params = [[request contentAsString] objectFromJSONString]; delegatedAttendee = nil; anAlarm = nil; status = nil; - replyList = [[message objectForKey: @"replyList"] intValue]; + replyList = [[params objectForKey: @"reply"] intValue]; switch (replyList) { - case 0: + case iCalPersonPartStatAccepted: status = @"ACCEPTED"; break; - case 1: + case iCalPersonPartStatDeclined: status = @"DECLINED"; break; - case 2: + case iCalPersonPartStatNeedsAction: status = @"NEEDS-ACTION"; break; - case 3: + case iCalPersonPartStatTentative: status = @"TENTATIVE"; break; - case 4: + case iCalPersonPartStatDelegated: default: { NSString *delegatedEmail, *delegatedUid; SOGoUser *user; status = @"DELEGATED"; - delegatedEmail = [[message objectForKey: @"delegatedTo"] stringByTrimmingSpaces]; + delegatedEmail = [[params objectForKey: @"delegatedTo"] stringByTrimmingSpaces]; if ([delegatedEmail length]) { @@ -308,60 +330,58 @@ [NSString stringWithFormat: @"mailto:%@", [[user allEmails] objectAtIndex: 0]]]; } else - return [NSException exceptionWithHTTPStatus: 400 - reason: @"missing 'to' parameter"]; + { + jsonResponse = [NSDictionary dictionaryWithObjectsAndKeys: + @"failure", @"status", + @"missing 'delegatedTo' parameter", @"message", + nil]; + return [self responseWithStatus: 400 + andString: [jsonResponse jsonRepresentation]]; + } } break; } - // Extract the user alarm, if any - reminderList = [[message objectForKey: @"reminderList"] intValue]; + // Set an alarm for the user + alarm = [params objectForKey: @"alarm"]; + if ([alarm isKindOfClass: [NSDictionary class]]) + { + NSString *reminderAction, *reminderUnit, *reminderQuantity, *reminderReference, *reminderRelation; + BOOL reminderEmailAttendees, reminderEmailOrganizer; - if ([[message objectForKey: @"reminderList"] isEqualToString: @"WONoSelectionString"] || reminderList == 5 || reminderList == 10 || reminderList == 14) - { - // No selection, wipe alarm which will be done in changeParticipationStatus... - } - else if (reminderList == 15) - { - // Custom + reminderAction = [alarm objectForKey: @"action"]; + reminderUnit = [alarm objectForKey: @"unit"]; + reminderQuantity = [alarm objectForKey: @"quantity"]; + reminderReference = [alarm objectForKey: @"reference"]; + reminderRelation = [alarm objectForKey: @"relation"]; + reminderEmailAttendees = [[alarm objectForKey: @"attendees"] boolValue]; + reminderEmailOrganizer = [[alarm objectForKey: @"organizer"] boolValue]; anAlarm = [iCalAlarm alarmForEvent: [self event] owner: [[self clientObject] ownerInContext: context] - action: [message objectForKey: @"reminderAction"] - unit: [message objectForKey: @"reminderUnit"] - quantity: [message objectForKey: @"reminderQuantity"] - reference: [message objectForKey: @"reminderReference"] - reminderRelation: [message objectForKey: @"reminderRelation"] - emailAttendees: [[message objectForKey: @"reminderEmailAttendees"] boolValue] - emailOrganizer: [[message objectForKey: @"reminderEmailOrganizer"] boolValue]]; + action: reminderAction + unit: reminderUnit + quantity: reminderQuantity + reference: reminderReference + reminderRelation: reminderRelation + emailAttendees: reminderEmailAttendees + emailOrganizer: reminderEmailOrganizer]; + } + + ex = [[self clientObject] changeParticipationStatus: status + withDelegate: delegatedAttendee + alarm: anAlarm]; + + if (ex) + { + jsonResponse = [NSDictionary dictionaryWithObjectsAndKeys: + [ex reason], @"message", + nil]; + response = [self responseWithStatus: [ex httpStatus] + andString: [jsonResponse jsonRepresentation]]; } else - { - // Standard - NSString *aValue; - - aValue = [[UIxComponentEditor reminderValues] objectAtIndex: reminderList]; - - // Predefined alarm - if ([aValue length]) - { - iCalTrigger *aTrigger; - - anAlarm = [[[iCalAlarm alloc] init] autorelease]; - aTrigger = [iCalTrigger elementWithTag: @"TRIGGER"]; - [aTrigger setValueType: @"DURATION"]; - [anAlarm setTrigger: aTrigger]; - [anAlarm setAction: @"DISPLAY"]; - [aTrigger setSingleValue: aValue forKey: @""]; - } - } - - response = (WOResponse *)[[self clientObject] changeParticipationStatus: status - withDelegate: delegatedAttendee - alarm: anAlarm]; - - if (!response) response = [self responseWith204]; - + return response; } @@ -413,9 +433,7 @@ * @apiParam {String} alarm.reference Either BEFORE or AFTER * @apiParam {String} alarm.relation Either START or END * @apiParam {Boolean} [alarm.attendees] Alert attendees by email if true and action is email - * @apiParam {Object} [alarm.organizer] Alert organizer at this email address if action is email - * @apiParam {String} [alarm.organizer.name] Attendee's name - * @apiParam {String} alarm.organizer.email Attendee's email address + * @apiParam {Boolean} [alarm.organizer] Alert organizer by email if true and action is email * * @apiParam {_} ... _Save in [iCalRepeatbleEntityObject+SOGo setAttributes:inContext:]_ * @@ -561,6 +579,8 @@ * @apiSuccess (Success 200) {String} localizedEndDate Formatted end date * @apiSuccess (Success 200) {String} [localizedEndTime] Formatted end time * @apiSuccess (Success 200) {Number} isReadOnly 1 if event is read-only + * @apiSuccess (Success 200) {Number} userHasRSVP 1 if owner is invited + * @apiSuccess (Success 200) {Number} [reply] 0 if needs-action, 1 if accepted, 2 if declined, 3 if tentative, 4 if delegated * @apiSuccess (Success 200) {Object[]} [attachUrls] Attached URLs * @apiSuccess (Success 200) {String} attachUrls.value URL * @@ -572,10 +592,8 @@ * @apiSuccess (Success 200) {String} alarm.unit Either MINUTES, HOURS, or DAYS * @apiSuccess (Success 200) {String} alarm.reference Either BEFORE or AFTER * @apiSuccess (Success 200) {String} alarm.relation Either START or END - * @apiSuccess (Success 200) {Object[]} [alarm.attendees] List of attendees - * @apiSuccess (Success 200) {String} [alarm.attendees.name] Attendee's name - * @apiSuccess (Success 200) {String} alarm.attendees.email Attendee's email address - * @apiSuccess (Success 200) {String} [alarm.attendees.uid] System user ID + * @apiSuccess (Success 200) {Boolean} alarm.attendees Alert attendees by email if true and action is email + * @apiSuccess (Success 200) {Boolean} alarm.organizer Alert organizer by email if true and action is email * * @apiSuccess {_} ... _From [iCalEvent+SOGo attributes]_ * @@ -687,6 +705,7 @@ [componentCalendar nameInContainer], @"pid", [componentCalendar displayName], @"calendar", [NSNumber numberWithBool: [self isReadOnly]], @"isReadOnly", + [NSNumber numberWithBool: [self userHasRSVP]], @"userHasRSVP", [dateFormatter formattedDate: eventStartDate], @"localizedStartDate", [dateFormatter formattedDate: eventEndDate], @"localizedEndDate", [self alarm], @"alarm", @@ -711,6 +730,9 @@ [data setObject: [dateFormatter formattedTime: eventEndDate] forKey: @"localizedEndTime"]; } + if ([self userHasRSVP]) + [data setObject: [self reply] forKey: @"reply"]; + // Add attributes from iCalEvent+SOGo, iCalEntityObject+SOGo and iCalRepeatableEntityObject+SOGo [data addEntriesFromDictionary: [event attributesInContext: context]]; diff --git a/UI/Scheduler/UIxCalMainView.m b/UI/Scheduler/UIxCalMainView.m index 6a9839584..fad816823 100644 --- a/UI/Scheduler/UIxCalMainView.m +++ b/UI/Scheduler/UIxCalMainView.m @@ -30,6 +30,8 @@ #import #import +#import + #import #import #import @@ -329,9 +331,67 @@ @end +/* Component Viewer, parent class of Appointment Viewer and Task Viewer */ + +@interface UIxComponentViewTemplate : UIxComponent +{ + id item; +} +@end + +@implementation UIxComponentViewTemplate + +- (id) init +{ + if ((self = [super init])) + { + item = nil; + } + + return self; +} + +- (void) dealloc +{ + [item release]; + [super dealloc]; +} + +- (void) setItem: (id) _item +{ + ASSIGN (item, _item); +} + +- (id) item +{ + return item; +} + +- (NSArray *) replyList +{ + return [NSArray arrayWithObjects: + [NSNumber numberWithInt: iCalPersonPartStatAccepted], + [NSNumber numberWithInt: iCalPersonPartStatDeclined], + [NSNumber numberWithInt: iCalPersonPartStatNeedsAction], + [NSNumber numberWithInt: iCalPersonPartStatTentative], + [NSNumber numberWithInt: iCalPersonPartStatDelegated], + nil]; +} + +- (NSString *) itemReplyText +{ + NSString *word; + + word = [iCalPerson descriptionForParticipationStatus: [item intValue]]; + + return [self labelForKey: [NSString stringWithFormat: @"partStat_%@", word]]; +} + +@end + /* Appointment Viewer */ -@interface UIxAppointmentViewTemplate : UIxComponent +@interface UIxAppointmentViewTemplate : UIxComponentViewTemplate @end @implementation UIxAppointmentViewTemplate @@ -339,7 +399,7 @@ /* Task Viewer */ -@interface UIxTaskViewTemplate : UIxComponent +@interface UIxTaskViewTemplate : UIxComponentViewTemplate @end @implementation UIxTaskViewTemplate diff --git a/UI/Scheduler/UIxComponentEditor.h b/UI/Scheduler/UIxComponentEditor.h index 238afebca..73c57f40e 100644 --- a/UI/Scheduler/UIxComponentEditor.h +++ b/UI/Scheduler/UIxComponentEditor.h @@ -34,6 +34,8 @@ } - (BOOL) isReadOnly; +- (BOOL) userHasRSVP; +- (NSNumber *) reply; - (BOOL) isChildOccurrence; - (void) setAttributes: (NSDictionary *) attributes; diff --git a/UI/Scheduler/UIxComponentEditor.m b/UI/Scheduler/UIxComponentEditor.m index e1eb457a1..d68d54167 100644 --- a/UI/Scheduler/UIxComponentEditor.m +++ b/UI/Scheduler/UIxComponentEditor.m @@ -75,6 +75,10 @@ #import "UIxComponentEditor.h" #import "UIxDatePicker.h" +#define componentReadableWritable 0 +#define componentOwnerIsInvited 1 +#define componentReadableOnly 2 + static NSArray *reminderItems = nil; static NSArray *reminderValues = nil; @@ -317,14 +321,21 @@ static NSArray *reminderValues = nil; // nil]; //} // -//- (NSNumber *) reply -//{ -// iCalPersonPartStat participationStatus; -// -// participationStatus = [ownerAsAttendee participationStatus]; -// -// return [NSNumber numberWithInt: participationStatus]; -//} +- (NSNumber *) reply +{ + NSString *owner, *ownerEmail; + SOGoUserManager *um; + iCalPerson *ownerAsAttendee; + iCalPersonPartStat participationStatus; + + um = [SOGoUserManager sharedUserManager]; + owner = [componentCalendar ownerInContext: context]; + ownerEmail = [um getEmailForUID: owner]; + ownerAsAttendee = [component findAttendeeWithEmail: (id)ownerEmail]; + participationStatus = [ownerAsAttendee participationStatus]; + + return [NSNumber numberWithInt: participationStatus]; +} ///* priorities */ // @@ -798,7 +809,7 @@ static NSArray *reminderValues = nil; iCalPerson *ownerAttendee; int rc; - rc = 0; + rc = componentReadableWritable; sm = [SoSecurityManager sharedSecurityManager]; if (![sm validatePermission: SOGoCalendarPerm_ModifyComponent @@ -813,42 +824,42 @@ static NSArray *reminderValues = nil; ownerAttendee = [component userAsAttendee: ownerUser]; if ([[ownerAttendee rsvp] isEqualToString: @"true"] && ![component userIsOrganizer: ownerUser]) - rc = 1; + rc = componentOwnerIsInvited; else - rc = 2; + rc = componentReadableOnly; } else - rc = 2; // not invited, just RO + rc = componentReadableOnly; return rc; } - (int) getEventRWType { - SOGoContentObject *clientObject; - SOGoUser *ownerUser; - int rc; + SOGoContentObject *clientObject; + SOGoUser *ownerUser; + int rc; - clientObject = [self clientObject]; - ownerUser = [SOGoUser userWithLogin: [clientObject ownerInContext: context]]; - if ([componentCalendar isKindOfClass: [SOGoWebAppointmentFolder class]]) - rc = 2; - else - { - if ([ownerUser isEqual: [context activeUser]]) - rc = [self ownerIsAttendee: ownerUser - andClientObject: clientObject]; - else - rc = [self delegateIsAttendee: ownerUser - andClientObject: clientObject]; - } + clientObject = [self clientObject]; + ownerUser = [SOGoUser userWithLogin: [clientObject ownerInContext: context]]; + if ([clientObject isKindOfClass: [SOGoWebAppointmentFolder class]]) + rc = componentReadableOnly; + else + { + if ([ownerUser isEqual: [context activeUser]]) + rc = [self ownerIsAttendee: ownerUser + andClientObject: clientObject]; + else + rc = [self delegateIsAttendee: ownerUser + andClientObject: clientObject]; + } - return rc; + return rc; } - (BOOL) isReadOnly { - return [self getEventRWType] != 0; + return [self getEventRWType] != componentReadableWritable; } // //- (NSString *) emailAlarmsEnabled @@ -862,10 +873,10 @@ static NSArray *reminderValues = nil; // : @"false"); //} -//- (BOOL) userHasRSVP -//{ -// return ([self getEventRWType] == 1); -//} +- (BOOL) userHasRSVP +{ + return ([self getEventRWType] == componentOwnerIsInvited); +} //- (unsigned int) firstDayOfWeek //{ diff --git a/UI/Templates/SchedulerUI/UIxAppointmentViewTemplate.wox b/UI/Templates/SchedulerUI/UIxAppointmentViewTemplate.wox index 737ba59f4..63f7b000b 100644 --- a/UI/Templates/SchedulerUI/UIxAppointmentViewTemplate.wox +++ b/UI/Templates/SchedulerUI/UIxAppointmentViewTemplate.wox @@ -4,21 +4,20 @@ xmlns:var="http://www.skyrix.com/od/binding" xmlns:const="http://www.skyrix.com/od/constant" xmlns:label="OGo:label"> - -
- + +
event -

+
- visibility_off - vpn_key + visibility_off + vpn_key - {{viewer.component.summary}} + {{::editor.component.summary}} - star -

- + star +
+ close @@ -26,104 +25,177 @@ - - + + {{$chip}} - + place -

{{viewer.component.location}}

+

{{::editor.component.location}}

event -

{{viewer.component.calendar}}

+

{{editor.component.calendar}}

- + access_time
-
+

-

- {{viewer.component.localizedStartDate}} - {{viewer.component.localizedStartTime}} -

+

{{::editor.component.localizedStartDate}}

+

{{::editor.component.localizedStartTime}}

-
+

-

- {{viewer.component.localizedEndDate}} - {{viewer.component.localizedEndTime}} -

+

{{::editor.component.localizedEndDate}}

+

{{::editor.component.localizedEndTime}}

- + event_available

- + send

- + link -

{{url.value}}

+

{{url.value}}

- + mode_comment -

{{viewer.component.comment}}

+

{{::editor.component.comment}}

- + repeat -

+

- - alarm -

+ + alarm_on +

- - -
- - - - -
{{$chip.name}}
-
{{$chip.name}}
- -
-
+ + people +
+ +
+ + + +
{{$chip.name}}
+
{{$chip.name}}
+
+
+
+ +
+ + + + +
{{$chip.name}}
+
{{$chip.name}}
+ +
+
+
+ + + + + + + insert_invitation + + + + + + + + + + + + + + + {{card.$$fullname}} {{card.$$email}} + + + + + + +

+
+
+
+ +
-
- +
+ - +
+
+ - +
- - +
+ + + +
+
+ + + + + + +
+ diff --git a/UI/WebServerResources/js/Common/User.service.js b/UI/WebServerResources/js/Common/User.service.js index 18d39ad67..476adaf40 100644 --- a/UI/WebServerResources/js/Common/User.service.js +++ b/UI/WebServerResources/js/Common/User.service.js @@ -38,6 +38,7 @@ * @memberof User * @desc Search for users that match a string. * @param {string} search - a string used to performed the search + * @param {object[]} excludedUsers - a list of User objects that must be excluded from the results * @return a promise of an array of matching User objects */ User.$filter = function(search, excludedUsers) { diff --git a/UI/WebServerResources/js/Contacts/AddressBook.service.js b/UI/WebServerResources/js/Contacts/AddressBook.service.js index 1c8027509..6c5b302e8 100644 --- a/UI/WebServerResources/js/Contacts/AddressBook.service.js +++ b/UI/WebServerResources/js/Contacts/AddressBook.service.js @@ -67,19 +67,58 @@ * @desc Search for cards among all addressbooks matching some criterias. * @param {string} search - the search string to match * @param {object} [options] - additional options to the query (excludeGroups and excludeLists) + * @param {object[]} excludedCards - a list of Card objects that must be excluded from the results * @returns a collection of Cards instances */ - AddressBook.$filterAll = function(search, options) { + AddressBook.$filterAll = function(search, options, excludedCards) { var params = {search: search}; + if (!search) { + // No query specified + AddressBook.$cards = []; + return AddressBook.$q.when(AddressBook.$cards); + } + if (angular.isUndefined(AddressBook.$cards)) { + // First session query + AddressBook.$cards = []; + } + else if (AddressBook.$query == search) { + // Query hasn't changed + return AddressBook.$q.when(AddressBook.$cards); + } + AddressBook.$query = search; + angular.extend(params, options); + return AddressBook.$$resource.fetch(null, 'allContactSearch', params).then(function(response) { - var results = []; - angular.forEach(response.contacts, function(data) { - var card = new AddressBook.$Card(data); - results.push(card); + var results, card, index, + compareIds = function(data) { + return this.id == data.id; + }; + if (excludedCards) { + // Remove excluded cards from results + results = _.filter(response.contacts, function(data) { + return _.isUndefined(_.find(excludedCards, compareIds, data)); + }); + } + else { + results = response.contacts; + } + // Remove cards that no longer match the search query + for (index = AddressBook.$cards.length - 1; index >= 0; index--) { + card = AddressBook.$cards[index]; + if (_.isUndefined(_.find(results, compareIds, card))) { + AddressBook.$cards.splice(index, 1); + } + } + // Add new cards matching the search query + _.each(results, function(data, index) { + if (_.isUndefined(_.find(AddressBook.$cards, compareIds, data))) { + var card = new AddressBook.$Card(data, search); + AddressBook.$cards.splice(index, 0, card); + } }); - return results; + return AddressBook.$cards; }); }; diff --git a/UI/WebServerResources/js/Contacts/Card.service.js b/UI/WebServerResources/js/Contacts/Card.service.js index 130891c11..291fd166e 100644 --- a/UI/WebServerResources/js/Contacts/Card.service.js +++ b/UI/WebServerResources/js/Contacts/Card.service.js @@ -133,6 +133,9 @@ if (!this.$$image) this.$$image = this.image || Card.$gravatar(this.$preferredEmail(partial), 32); this.selected = false; + + // An empty attribute to trick md-autocomplete when adding attendees from the appointment editor + this.empty = ' '; }; /** diff --git a/UI/WebServerResources/js/Scheduler/CalendarListController.js b/UI/WebServerResources/js/Scheduler/CalendarListController.js index bc7092b58..bda28a808 100644 --- a/UI/WebServerResources/js/Scheduler/CalendarListController.js +++ b/UI/WebServerResources/js/Scheduler/CalendarListController.js @@ -68,7 +68,7 @@ escapeToClose: true, templateUrl: templateUrl, controller: 'ComponentController', - controllerAs: 'viewer', + controllerAs: 'editor', locals: { stateComponent: component } diff --git a/UI/WebServerResources/js/Scheduler/Component.service.js b/UI/WebServerResources/js/Scheduler/Component.service.js index 059be10df..3913b9d26 100644 --- a/UI/WebServerResources/js/Scheduler/Component.service.js +++ b/UI/WebServerResources/js/Scheduler/Component.service.js @@ -311,6 +311,7 @@ this.due = new Date(this.dueDate.substring(0,10) + ' ' + this.dueDate.substring(11,16)); // Parse recurrence rule definition and initialize default values + this.$isRecurrent = angular.isDefined(data.repeat); if (this.repeat.days) { var byDayMask = _.find(this.repeat.days, function(o) { return angular.isDefined(o.occurrence); @@ -368,6 +369,10 @@ // Allow the component to be moved to a different calendar this.destinationCalendar = this.pid; + if (this.organizer && this.organizer.email) { + this.organizer.$image = Component.$gravatar(this.organizer.email, 32); + } + // Load freebusy of attendees this.freebusy = this.updateFreeBusyCoverage(); @@ -394,6 +399,56 @@ return b; }; + /** + * @function isEditable + * @memberof Component.prototype + * @desc Check if the component is editable and not an occurrence of a recurrent component + * @returns true or false + */ + Component.prototype.isEditable = function() { + return (!this.occurrenceId && !this.isReadOnly); + }; + + /** + * @function isEditableOccurrence + * @memberof Component.prototype + * @desc Check if the component is editable and an occurrence of a recurrent component + * @returns true or false + */ + Component.prototype.isEditableOccurrence = function() { + return (this.occurrenceId && !this.isReadOnly); + }; + + /** + * @function isInvitation + * @memberof Component.prototype + * @desc Check if the component an invitation and not an occurrence of a recurrent component + * @returns true or false + */ + Component.prototype.isInvitation = function() { + return (!this.occurrenceId && this.userHasRSVP); + }; + + /** + * @function isInvitationOccurrence + * @memberof Component.prototype + * @desc Check if the component an invitation and an occurrence of a recurrent component + * @returns true or false + */ + Component.prototype.isInvitationOccurrence = function() { + return (this.occurrenceId && this.userHasRSVP); + }; + + /** + * @function isReadOnly + * @memberof Component.prototype + * @desc Check if the component is not editable and not an invitation + * @returns true or false + */ + Component.prototype.isReadOnly = function() { + return (this.isReadOnly && !this.userHasRSVP); + }; + /** * @function enablePercentComplete * @memberof Component.prototype @@ -581,6 +636,7 @@ */ Component.prototype.canRemindAttendeesByEmail = function() { return this.alarm.action == 'email' && + !this.isReadOnly && this.attendees && this.attendees.length > 0; }; @@ -635,6 +691,32 @@ this.$shadowData = this.$omit(true); }; + /** + * @function reply + * @memberof Component.prototype + * @desc Reply to an invitation. + * @returns a promise of the HTTP operation + */ + Component.prototype.$reply = function() { + var _this = this, data, path = [this.pid, this.id]; + + if (this.occurrenceId) + path.push(this.occurrenceId); + + data = { + reply: this.reply, + delegatedTo: this.delegatedTo, + alarm: this.$hasAlarm? this.alarm : {} + }; + + return Component.$$resource.save(path.join('/'), data, { action: 'rsvpAppointment' }) + .then(function(data) { + // Make a copy of the data for an eventual reset + _this.$shadowData = _this.$omit(true); + return data; + }); + }; + /** * @function $save * @memberof Component.prototype diff --git a/UI/WebServerResources/js/Scheduler/ComponentController.js b/UI/WebServerResources/js/Scheduler/ComponentController.js index 70a584a54..7ab0db0e9 100644 --- a/UI/WebServerResources/js/Scheduler/ComponentController.js +++ b/UI/WebServerResources/js/Scheduler/ComponentController.js @@ -6,20 +6,24 @@ /** * @ngInject */ - ComponentController.$inject = ['$mdDialog', 'Calendar', 'stateComponent']; - function ComponentController($mdDialog, Calendar, stateComponent) { + ComponentController.$inject = ['$rootScope', '$mdDialog', 'Calendar', 'AddressBook', 'Alarm', 'stateComponent']; + function ComponentController($rootScope, $mdDialog, Calendar, AddressBook, Alarm, stateComponent) { var vm = this, component; vm.component = stateComponent; vm.close = close; + vm.cardFilter = cardFilter; vm.edit = edit; vm.editAllOccurrences = editAllOccurrences; + vm.reply = reply; + vm.replyAllOccurrences = replyAllOccurrences; // Load all attributes of component if (angular.isUndefined(vm.component.$futureComponentData)) { component = Calendar.$get(vm.component.c_folder).$getComponent(vm.component.c_name, vm.component.c_recurrence_id); component.$futureComponentData.then(function() { vm.component = component; + vm.organizer = [vm.component.organizer]; }); } @@ -27,12 +31,10 @@ $mdDialog.hide(); } - function editAllOccurrences() { - component = Calendar.$get(vm.component.pid).$getComponent(vm.component.id); - component.$futureComponentData.then(function() { - vm.component = component; - edit(); - }); + // Autocomplete cards for attendees + function cardFilter($query) { + AddressBook.$filterAll($query); + return AddressBook.$cards; } function edit() { @@ -54,6 +56,38 @@ }); }); } + + function editAllOccurrences() { + component = Calendar.$get(vm.component.pid).$getComponent(vm.component.id); + component.$futureComponentData.then(function() { + vm.component = component; + edit(); + }); + } + + function reply(component) { + var c = component || vm.component; + + c.$reply().then(function() { + $rootScope.$broadcast('calendars:list'); + $mdDialog.hide(); + Alarm.getAlarms(); + }); + } + + function replyAllOccurrences() { + // Retrieve master event + component = Calendar.$get(vm.component.pid).$getComponent(vm.component.id); + component.$futureComponentData.then(function() { + // Propagate the participant status and alarm to the master event + component.reply = vm.component.reply; + component.delegatedTo = vm.component.delegatedTo; + component.$hasAlarm = vm.component.$hasAlarm; + component.alarm = vm.component.alarm; + // Send reply to the server + reply(component); + }); + } } /** @@ -71,7 +105,6 @@ vm.showAttendeesEditor = angular.isDefined(vm.component.attendees); vm.toggleAttendeesEditor = toggleAttendeesEditor; vm.cardFilter = cardFilter; - vm.cardResults = []; vm.addAttendee = addAttendee; vm.addAttachUrl = addAttachUrl; vm.cancel = cancel; @@ -119,29 +152,8 @@ // Autocomplete cards for attendees function cardFilter($query) { - var index, indexResult, card; - if ($query) { - AddressBook.$filterAll($query).then(function(results) { - var compareIds = function(result) { - return this.id == result.id; - }; - // Remove cards that no longer match the search query - for (index = vm.cardResults.length - 1; index >= 0; index--) { - card = vm.cardResults[index]; - indexResult = _.findIndex(results, compareIds, card); - if (indexResult >= 0) - results.splice(indexResult, 1); - else - vm.cardResults.splice(index, 1); - } - _.each(results, function(card) { - // Add cards matching the search query but not already in the list of attendees - if (!vm.component.hasAttendee(card)) - vm.cardResults.push(card); - }); - }); - } - return vm.cardResults; + AddressBook.$filterAll($query); + return AddressBook.$cards; } function addAttendee(card) { @@ -202,7 +214,7 @@ } angular - .module('SOGo.SchedulerUI') + .module('SOGo.SchedulerUI') .controller('ComponentController', ComponentController) .controller('ComponentEditorController', ComponentEditorController); })(); diff --git a/UI/WebServerResources/scss/components/content/content.scss b/UI/WebServerResources/scss/components/content/content.scss index 24f88f894..437f55544 100644 --- a/UI/WebServerResources/scss/components/content/content.scss +++ b/UI/WebServerResources/scss/components/content/content.scss @@ -21,4 +21,9 @@ md-content { border-left: $baseline-grid solid sg-color($sogoGreen, 100); margin-left: ($baseline-grid / 2); padding-left: $baseline-grid; +} + +.sg-inset { + // Inspired from + margin-left: $baseline-grid * 10; } \ No newline at end of file diff --git a/UI/WebServerResources/scss/components/list/list.scss b/UI/WebServerResources/scss/components/list/list.scss index a456b2e9b..6f88cf028 100644 --- a/UI/WebServerResources/scss/components/list/list.scss +++ b/UI/WebServerResources/scss/components/list/list.scss @@ -26,6 +26,7 @@ md-list-item { .md-list-item-inner { flex-grow: 1; // use all column space } + md-icon, .md-button md-icon { margin-left: 0; } diff --git a/UI/WebServerResources/scss/core/base_styles/_base_style.scss b/UI/WebServerResources/scss/core/base_styles/_base_style.scss index 50371c4de..93807f1f7 100644 --- a/UI/WebServerResources/scss/core/base_styles/_base_style.scss +++ b/UI/WebServerResources/scss/core/base_styles/_base_style.scss @@ -11,9 +11,15 @@ main { .sg-padded { padding-left: $mg; padding-right: $mg; + &--left { + padding-left: $mg; + } &--right { padding-right: $mg; } + &--top { + padding-top: $mg; + } } .sg-logo {