diff --git a/SoObjects/Appointments/SOGoComponentOccurence.m b/SoObjects/Appointments/SOGoComponentOccurence.m index f81a73f5a..c8a16030c 100644 --- a/SoObjects/Appointments/SOGoComponentOccurence.m +++ b/SoObjects/Appointments/SOGoComponentOccurence.m @@ -1,6 +1,6 @@ /* SOGoComponentOccurence.m - this file is part of SOGo * - * Copyright (C) 2008-2014 Inverse inc. + * Copyright (C) 2008-2016 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -211,6 +211,13 @@ return [container saveComponent: newObject]; } +- (NSException *) saveComponent: (iCalRepeatableEntityObject *) newObject + force: (BOOL) forceSave +{ + return [container saveComponent: newObject + force: forceSave]; +} + #warning most of SOGoCalendarComponent and SOGoComponentOccurence share the same external interface... \ they should be siblings or SOGoComponentOccurence the parent class of SOGoCalendarComponent... - (NSException *) changeParticipationStatus: (NSString *) newStatus diff --git a/UI/Scheduler/UIxAppointmentActions.m b/UI/Scheduler/UIxAppointmentActions.m index 7568dde44..a96129b52 100644 --- a/UI/Scheduler/UIxAppointmentActions.m +++ b/UI/Scheduler/UIxAppointmentActions.m @@ -22,6 +22,7 @@ #import #import #import +#import #import @@ -68,6 +69,7 @@ SOGoAppointmentFolder *targetCalendar, *sourceCalendar; SOGoAppointmentFolders *folders; BOOL forceSave; + id error; rq = [context request]; params = [[rq contentAsString] objectFromJSONString]; @@ -76,7 +78,7 @@ startDelta = [params objectForKey: @"start"]; durationDelta = [params objectForKey: @"duration"]; destionationCalendar = [params objectForKey: @"destination"]; - forceSave = NO; + forceSave = [[params objectForKey: @"ignoreConflicts"] boolValue]; if (daysDelta || startDelta || durationDelta) { @@ -150,8 +152,11 @@ if ([ex respondsToSelector: @selector(httpStatus)]) httpStatus = [ex httpStatus]; + error = [[ex reason] objectFromJSONString]; + if (error == nil) + error = [ex reason]; jsonResponse = [NSDictionary dictionaryWithObjectsAndKeys: - [ex reason], @"message", + error, @"message", nil]; response = [self responseWithStatus: httpStatus diff --git a/UI/Scheduler/UIxAppointmentEditor.m b/UI/Scheduler/UIxAppointmentEditor.m index 8cc46110c..507f71890 100644 --- a/UI/Scheduler/UIxAppointmentEditor.m +++ b/UI/Scheduler/UIxAppointmentEditor.m @@ -467,6 +467,7 @@ SOGoAppointmentObject *co; SoSecurityManager *sm; WORequest *request; + id error; unsigned int httpStatus; BOOL forceSave; @@ -490,7 +491,7 @@ else { [self setAttributes: params]; - forceSave = NO; + forceSave = [[params objectForKey: @"ignoreConflicts"] boolValue]; if ([event hasRecurrenceRules]) [self _adjustRecurrentRules]; @@ -546,9 +547,11 @@ if ([ex respondsToSelector: @selector(httpStatus)]) httpStatus = [ex httpStatus]; + error = [[ex reason] objectFromJSONString]; + if (error == nil) + error = [ex reason]; jsonResponse = [NSDictionary dictionaryWithObjectsAndKeys: - [ex reason], @"message", - nil]; + error, @"message", nil]; } else { diff --git a/UI/Templates/SchedulerUI/UIxAppointmentEditorTemplate.wox b/UI/Templates/SchedulerUI/UIxAppointmentEditorTemplate.wox index 134b25b54..79666f52a 100644 --- a/UI/Templates/SchedulerUI/UIxAppointmentEditorTemplate.wox +++ b/UI/Templates/SchedulerUI/UIxAppointmentEditorTemplate.wox @@ -275,7 +275,7 @@ - + @@ -287,6 +287,48 @@ + + + +
+ + close + +
+ +
+ person {{editor.attendeeConflictError.attendee_name}} ({{editor.attendeeConflictError.attendee_email}}) +
+
+ + schedule +
+ +
{{conflict.startDate}} trending_flat
+
+
+ +
{{conflict.endDate}}
+
+
+ + + + + + + + + + + diff --git a/UI/Templates/SchedulerUI/UIxCalMainView.wox b/UI/Templates/SchedulerUI/UIxCalMainView.wox index d448c3d4e..d248c9486 100644 --- a/UI/Templates/SchedulerUI/UIxCalMainView.wox +++ b/UI/Templates/SchedulerUI/UIxCalMainView.wox @@ -737,4 +737,39 @@ + + + diff --git a/UI/WebServerResources/js/Common/navController.js b/UI/WebServerResources/js/Common/navController.js index a6f78b5d8..934de0961 100644 --- a/UI/WebServerResources/js/Common/navController.js +++ b/UI/WebServerResources/js/Common/navController.js @@ -66,7 +66,7 @@ function onHttpError(event, response) { var message; - if (response.data && response.data.message) + if (response.data && response.data.message && angular.isString(response.data.message)) message = response.data.message; else if (response.status) message = response.statusText; @@ -85,7 +85,7 @@ position: 'top right' }); else - console.debug('untrap error'); + $log.debug('untrap error'); } // Listen to HTTP errors broadcasted from HTTP interceptor diff --git a/UI/WebServerResources/js/Scheduler/CalendarListController.js b/UI/WebServerResources/js/Scheduler/CalendarListController.js index 93793a05c..644371def 100644 --- a/UI/WebServerResources/js/Scheduler/CalendarListController.js +++ b/UI/WebServerResources/js/Scheduler/CalendarListController.js @@ -162,8 +162,7 @@ component.setDelta(coordinates.duration * 15); newComponent(null, component).finally(function() { $timeout(function() { - Component.$ghost.pointerHandler = null; - Component.$ghost.component = null; + Component.$resetGhost(); }); }); } @@ -186,8 +185,11 @@ // Immediately perform the adjustments component.$adjust(params).then(function() { $rootScope.$emit('calendars:list'); + }, function(response) { + onComponentAdjustError(response, component, params); + }).finally(function() { $timeout(function() { - Component.$ghost = {}; + Component.$resetGhost(); }); }); else if (component.occurrenceId) { @@ -199,7 +201,7 @@ params: params }, template: [ - '', + '', ' ', '

' + l('editRepeatingItem') + '

', '
', @@ -214,7 +216,7 @@ $rootScope.$emit('calendars:list'); }).finally(function() { $timeout(function() { - Component.$ghost = {}; + Component.$resetGhost(); }); }); } @@ -226,13 +228,58 @@ RecurrentComponentDialogController.$inject = ['$scope', '$mdDialog', 'component', 'params']; function RecurrentComponentDialogController($scope, $mdDialog, component, params) { $scope.updateThisOccurrence = function() { - component.$adjust(params).then($mdDialog.hide, $mdDialog.cancel); + component.$adjust(params).then($mdDialog.hide, function(response) { + $mdDialog.cancel().then(function() { + onComponentAdjustError(response, component, params); + }); + }); }; $scope.updateAllOccurrences = function() { delete component.occurrenceId; - component.$adjust(params).then($mdDialog.hide, $mdDialog.cancel); + component.$adjust(params).then($mdDialog.hide, function(response) { + $mdDialog.cancel().then(function() { + onComponentAdjustError(response, component, params); + }); + }); }; } + + function onComponentAdjustError(response, component, params) { + if (response.status == 403 && + response.data && response.data.message && angular.isObject(response.data.message)) { + $mdDialog.show({ + parent: angular.element(document.body), + clickOutsideToClose: false, + escapeToClose: false, + templateUrl: 'UIxAttendeeConflictDialog', + controller: AttendeeConflictDialogController, + controllerAs: '$AttendeeConflictDialogController', + locals: { + component: component, + params: params, + conflictError: response.data.message + } + }).then(function() { + $rootScope.$emit('calendars:list'); + }); + } + } + + /** + * @ngInject + */ + AttendeeConflictDialogController.$inject = ['$scope', '$mdDialog', 'component', 'params', 'conflictError']; + function AttendeeConflictDialogController($scope, $mdDialog, component, params, conflictError) { + var vm = this; + + vm.conflictError = conflictError; + vm.cancel = $mdDialog.cancel; + vm.save = save; + + function save() { + component.$adjust(angular.extend({ ignoreConflicts: true }, params)).then($mdDialog.hide); + } + } } function filter(filterpopup) { diff --git a/UI/WebServerResources/js/Scheduler/Component.service.js b/UI/WebServerResources/js/Scheduler/Component.service.js index 7390fbe84..a6ab49487 100644 --- a/UI/WebServerResources/js/Scheduler/Component.service.js +++ b/UI/WebServerResources/js/Scheduler/Component.service.js @@ -414,6 +414,15 @@ }); }; + /** + * @function $resetGhost + * @desc Prepare the ghost object for the next drag by resetting appropriate attributes + */ + Component.$resetGhost = function() { + this.$ghost.pointerHandler = null; + this.$ghost.component = null; + }; + /** * @function $parseDate * @desc Parse a date string with format YYYY-MM-DDTHH:MM @@ -1035,8 +1044,9 @@ * @function $save * @memberof Component.prototype * @desc Save the component to the server. + * @param {object} extraAttributes - additional attributes to send to the server */ - Component.prototype.$save = function() { + Component.prototype.$save = function(extraAttributes) { var _this = this, options, path, component, date, dlp; component = this.$omit(); @@ -1106,6 +1116,8 @@ if (this.occurrenceId) path.push(this.occurrenceId); + angular.extend(component, extraAttributes); + return Component.$$resource.save(path.join('/'), component, options) .then(function(data) { // Make a copy of the data for an eventual reset diff --git a/UI/WebServerResources/js/Scheduler/ComponentController.js b/UI/WebServerResources/js/Scheduler/ComponentController.js index 3bc62fdfd..fc5f3df36 100644 --- a/UI/WebServerResources/js/Scheduler/ComponentController.js +++ b/UI/WebServerResources/js/Scheduler/ComponentController.js @@ -162,6 +162,7 @@ vm.addAttachUrl = addAttachUrl; vm.cancel = cancel; vm.save = save; + vm.attendeeConflictError = false; vm.attendeesEditor = { days: getDays(), hours: getHours() @@ -217,15 +218,18 @@ } } - function save(form) { + function save(form, options) { if (form.$valid) { - vm.component.$save() + vm.component.$save(options) .then(function(data) { $rootScope.$emit('calendars:list'); $mdDialog.hide(); Alarm.getAlarms(); - }, function(data, status) { - $log.debug('failed'); + }, function(response) { + if (response.status == 403 && + response.data && response.data.message && angular.isObject(response.data.message)) { + vm.attendeeConflictError = response.data.message; + } }); } }