Merge pull request #3392 from ulferts/feature/wp_error_notifications

Feature/wp error notifications
pull/3423/head
Jan Sandbrink 9 years ago
commit 38d3619b00
  1. 18
      app/assets/stylesheets/layout/_work_package.sass
  2. 1
      app/views/layouts/angular.html.erb
  3. 2
      app/views/layouts/base.html.erb
  4. 45
      frontend/app/services/api-notifications-service.js
  5. 6
      frontend/app/services/index.js
  6. 5
      frontend/app/services/notifications-service.js
  7. 25
      frontend/app/services/work-package-service.js
  8. 8
      frontend/app/templates/components/flash_message.html
  9. 2
      frontend/app/time_entries/controllers/index.js
  10. 12
      frontend/app/time_entries/controllers/time-entries-controller.js
  11. 6
      frontend/app/ui_components/index.js
  12. 30
      frontend/app/work_packages/controllers/dialogs/save.js
  13. 14
      frontend/app/work_packages/controllers/dialogs/settings.js
  14. 16
      frontend/app/work_packages/controllers/dialogs/share.js
  15. 6
      frontend/app/work_packages/controllers/index.js
  16. 4
      frontend/app/work_packages/controllers/menus/index.js
  17. 30
      frontend/app/work_packages/controllers/menus/settings-dropdown-menu-controller.js
  18. 15
      frontend/app/work_packages/controllers/work-package-details-controller.js
  19. 9
      frontend/app/work_packages/controllers/work-packages-list-controller.js
  20. 4
      frontend/app/work_packages/directives/inplace_editor/inplace-editor-edit-pane-directive.js
  21. 20
      frontend/app/work_packages/helpers/api-helper.js
  22. 2
      frontend/app/work_packages/helpers/index.js
  23. 10
      frontend/app/work_packages/view_models/children-relations-handler.js
  24. 19
      frontend/app/work_packages/view_models/common-relations-handler.js
  25. 17
      frontend/app/work_packages/view_models/index.js
  26. 6
      frontend/app/work_packages/view_models/parent-relations-handler.js
  27. 1
      frontend/public/index.html
  28. 91
      frontend/tests/unit/tests/services/api-notifications-service-test.js
  29. 29
      frontend/tests/unit/tests/services/notifications-service-test.js
  30. 126
      frontend/tests/unit/tests/ui_components/flash-message-directive-test.js
  31. 20
      frontend/tests/unit/tests/work_packages/controllers/dialogs/settings-test.js
  32. 69
      frontend/tests/unit/tests/work_packages/helpers/api-helper-test.js
  33. 6
      spec/features/menu_items/query_menu_item_spec.rb
  34. 43
      spec/features/page_objects/notification.rb

@ -32,31 +32,21 @@
#content
$flash-margins-padding: 10px
// HACK: workaround to ensure correct height applied to child elements
// the dom looks like this
// HACK: workaround to ensure correct height applied to child elements.
// The dom looks like this:
// flash (generated from rails - if it was generated when rendering the page)
// flash (generated from angular - exists always but is hidden with ng-hide if not needed)
// div[ui-view] (main content we want to adjust the height for)
// div style="clear:both;" (is pushed to overflow - of no relevance)
//
// There can be at most two flash messages visible on the page.
// We need to adjust the hight of the div[ui-view] depending on the amount of flash messages.
//
// This makes use of more specific rules overwriting less specific ones.
// Per default, the height is always 100%
.flash + div[ui-view]
& > div[ui-view]
height: 100%
// If there is only one flash message shown:
// Subtract the height of the flash message
.flash:not(.ng-hide) ~ div[ui-view]
.flash ~ div[ui-view]
height: calc(100% - #{($content-flash-height + $flash-margins-padding)})
// If there are two flash messages shown:
// Subtract the height of the two flash messages
.flash:not(.ng-hide) ~ .flash:not(.ng-hide) ~ div[ui-view]
height: calc(100% - #{2 * ($content-flash-height + $flash-margins-padding)})
// HACK: workaround to ensure correct height applied to child elements
#work-packages-index
height: 100%

@ -122,7 +122,6 @@ See doc/COPYRIGHT.rdoc for more details.
ng-class="{ 'hidden-navigation': !showNavigation }">
<h1 class="hidden-for-sighted"><%= l(:label_content) %></h1>
<%= render_flash_messages %>
<flash-message></flash-message>
<div ui-view></div>
<%= call_hook :view_layouts_base_content %>
<div style="clear:both;">&nbsp;</div>

@ -118,7 +118,7 @@ See doc/COPYRIGHT.rdoc for more details.
ng-class="{ 'hidden-navigation': !showNavigation }">
<h1 class="hidden-for-sighted"><%= l(:label_content) %></h1>
<%= render_flash_messages %>
<flash-message></flash-message>
<notifications></notifications>
<!-- Action menu -->
<%= render :partial => 'layouts/action_menu' %>
<%= yield %>

@ -26,37 +26,26 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
// TODO move to UI components
module.exports = function($rootScope, $timeout, ConfigurationService) {
module.exports = function(NotificationsService, ApiHelper) {
'use strict';
return {
restrict: 'E',
replace: true,
scope: {},
templateUrl: '/templates/components/flash_message.html',
link: function(scope, element, attrs) {
$rootScope.$on('flashMessage', function(event, message) {
scope.message = message;
scope.flashType = 'notice';
scope.flashId = 'flash-notice';
var addError = function(error) {
var messages = ApiHelper.getErrorMessages(error);
var fadeOutTime = attrs.fadeOutTime || 3000;
if (messages.length > 1) {
NotificationsService.addError('', messages);
}
else {
NotificationsService.addError(messages[0]);
}
};
if (message.isError) {
scope.flashType = "errorExplanation";
scope.flashId = "errorExplanation";
}
var addSuccess = function(text) {
NotificationsService.addSuccess(text);
};
// not using $timeout to allow capybara to not wait until timeout is done with
// scope apply
if (!ConfigurationService.accessibilityModeEnabled() && !message.isPermanent) {
setTimeout(function() {
scope.$apply(function() {
scope.message = undefined;
});
}, fadeOutTime);
}
});
}
return {
addError: addError,
addSuccess: addSuccess
};
};

@ -103,6 +103,7 @@ angular.module('openproject.services')
'AuthorisationService',
'EditableFieldsState',
'WorkPackageFieldService',
'NotificationsService',
require('./work-package-service')
])
.service('NotificationsService', [
@ -110,4 +111,9 @@ angular.module('openproject.services')
'$rootScope',
require('./notifications-service.js')
])
.service('ApiNotificationsService', [
'NotificationsService',
'ApiHelper',
require('./api-notifications-service.js')
])
.service('ConversionService', require('./conversion-service.js'));

@ -42,12 +42,9 @@ module.exports = function(I18n, $rootScope) {
return _.extend(createNotification(message), { type: 'warning' });
},
createErrorNotification = function(message, errors) {
if(!errors) {
throw new Error('Cannot create an error notification without errors!');
}
return _.extend(createNotification(message), {
type: 'error',
errors: errors
errors: errors || []
});
},
createWorkPackageUploadNotification = function(message, uploads) {

@ -38,7 +38,8 @@ module.exports = function($http,
$q,
AuthorisationService,
EditableFieldsState,
WorkPackageFieldService
WorkPackageFieldService,
NotificationsService
) {
var workPackage;
@ -298,11 +299,9 @@ module.exports = function($http,
return response.data;
},
function(failedResponse) {
$rootScope.$emit('flashMessage', {
isError: true,
isPermanent: true,
text: I18n.t('js.work_packages.query.errors.unretrievable_query')
});
NotificationsService.addError(
I18n.t('js.work_packages.query.errors.unretrievable_query')
);
}
);
},
@ -320,18 +319,16 @@ module.exports = function($http,
if (defaultHandling) {
promise.success(function(data, status) {
// TODO wire up to API and process API response
$rootScope.$emit('flashMessage', {
isError: false,
text: I18n.t('js.work_packages.message_successful_bulk_delete')
});
NotificationsService.addSuccess(
I18n.t('js.work_packages.message_successful_bulk_delete')
);
$rootScope.$emit('workPackagesRefreshRequired');
})
.error(function(data, status) {
// TODO wire up to API and processs API response
$rootScope.$emit('flashMessage', {
isError: true,
text: I18n.t('js.work_packages.message_error_during_bulk_delete')
});
NotificationsService.addError(
I18n.t('js.work_packages.message_error_during_bulk_delete')
);
});
}

@ -1,8 +0,0 @@
<div ng-show="message !== undefined"
style="display: block;"
class="flash"
ng-class="['icon', 'icon-'+flashType, flashType]"
id="{{flashId}}"
role="alert"
ng-bind="message.text">
</div>

@ -28,6 +28,6 @@
angular.module('openproject.timeEntries.controllers')
.controller('TimeEntriesController', ['$scope', '$http', 'PathHelper',
'SortService', 'PaginationService',
'SortService', 'PaginationService', 'NotificationsService',
require('./time-entries-controller')
]);

@ -26,7 +26,7 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
module.exports = function($scope, $http, PathHelper, SortService, PaginationService) {
module.exports = function($scope, $http, PathHelper, SortService, PaginationService, NotificationsService) {
$scope.PathHelper = PathHelper;
$scope.timeEntries = gon.timeEntries;
$scope.totalEntryCount = gon.total_count;
@ -57,11 +57,11 @@ module.exports = function($scope, $http, PathHelper, SortService, PaginationServ
$scope.deleteTimeEntry = function(id) {
if (window.confirm(I18n.t('js.text_are_you_sure'))) {
$http['delete'](PathHelper.timeEntryPath(id))
.success(function(data, status, headers, config) {
.success(function(data) {
var index = 0;
for (var i = 0; i < $scope.timeEntries.length; i++) {
if ($scope.timeEntries[i].id == id) {
if ($scope.timeEntries[i].id === id) {
index = i;
break;
}
@ -69,10 +69,10 @@ module.exports = function($scope, $http, PathHelper, SortService, PaginationServ
$scope.timeEntries.splice(index, 1);
$scope.$emit('flashMessage', data);
NotificationsService.addSuccess(data.text);
})
.error(function(data, status, headers, config) {
$scope.$emit('flashMessage', data);
.error(function(data) {
NotificationsService.addError(data.text);
});
}
};

@ -49,12 +49,6 @@ angular.module('openproject.uiComponents')
.constant('ENTER_KEY', 13)
.directive('executeOnEnter', ['ENTER_KEY', require(
'./execute-on-enter-directive')])
.directive('flashMessage', [
'$rootScope',
'$timeout',
'ConfigurationService',
require('./flash-message-directive')
])
.directive('expandableSearch', ['ENTER_KEY', require('./expandable-search')])
.directive('focus', ['FocusHelper', require('./focus-directive')])
.constant('FOCUSABLE_SELECTOR', 'a, button, :input, [tabindex], select')

@ -26,7 +26,14 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
module.exports = function($scope, saveModal, QueryService, AuthorisationService, $state) {
module.exports = function(
$scope,
saveModal,
QueryService,
AuthorisationService,
$state,
NotificationsService
) {
this.name = 'Save';
this.closeMe = saveModal.deactivate;
@ -34,14 +41,23 @@ module.exports = function($scope, saveModal, QueryService, AuthorisationService,
$scope.saveQueryAs = function(name) {
QueryService.saveQueryAs(name)
.then(function(data){
// push query id to URL without reinitializing work-packages-list-controller
if (data.query) {
$state.go('work-packages.list', { query_id: data.query.id, query: null }, { notify: false });
AuthorisationService.initModelAuth("query", data.query._links);
if (data.status.isError){
NotificationsService.addError(data.status.text);
}
else {
// push query id to URL without reinitializing work-packages-list-controller
if (data.query) {
$state.go('work-packages.list',
{ query_id: data.query.id, query: null },
{ notify: false });
AuthorisationService.initModelAuth('query', data.query._links);
}
saveModal.deactivate();
saveModal.deactivate();
$scope.$emit('flashMessage', data.status);
NotificationsService.addSuccess(data.status.text);
}
});
};
};

@ -26,7 +26,15 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
module.exports = function($scope, settingsModal, QueryService, AuthorisationService, $rootScope, QUERY_MENU_ITEM_TYPE) {
module.exports = function(
$scope,
settingsModal,
QueryService,
AuthorisationService,
$rootScope,
QUERY_MENU_ITEM_TYPE,
NotificationsService
) {
var query = QueryService.getQuery();
@ -43,7 +51,7 @@ module.exports = function($scope, settingsModal, QueryService, AuthorisationServ
})
.then(function(data) {
settingsModal.deactivate();
$scope.$emit('flashMessage', data.status);
NotificationsService.addSuccess(data.status.text);
$rootScope.$broadcast('openproject.layout.renameQueryMenuItem', {
itemType: QUERY_MENU_ITEM_TYPE,
@ -52,7 +60,7 @@ module.exports = function($scope, settingsModal, QueryService, AuthorisationServ
});
if(data.query) {
AuthorisationService.initModelAuth("query", data.query._links);
AuthorisationService.initModelAuth('query', data.query._links);
}
});
};

@ -26,7 +26,15 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
module.exports = function($scope, shareModal, QueryService, AuthorisationService, queryMenuItemFactory, PathHelper) {
module.exports = function(
$scope,
shareModal,
QueryService,
AuthorisationService,
queryMenuItemFactory,
PathHelper,
NotificationsService
) {
this.name = 'Share';
this.closeMe = shareModal.deactivate;
@ -38,7 +46,7 @@ module.exports = function($scope, shareModal, QueryService, AuthorisationService
function closeAndReport(message) {
shareModal.deactivate();
$scope.$emit('flashMessage', message);
NotificationsService.addSuccess(message.text);
}
$scope.cannot = AuthorisationService.cannot;
@ -49,11 +57,11 @@ module.exports = function($scope, shareModal, QueryService, AuthorisationService
.then(function(data){
messageObject = data.status;
if(data.query) {
AuthorisationService.initModelAuth("query", data.query._links);
AuthorisationService.initModelAuth('query', data.query._links);
}
})
.then(function(data){
if($scope.query.starred != $scope.shareSettings.starred){
if($scope.query.starred !== $scope.shareSettings.starred){
QueryService.toggleQueryStarred($scope.query)
.then(function(data){
closeAndReport(data.status || messageObject);

@ -90,7 +90,7 @@ angular.module('openproject.workPackages.controllers')
'CommonRelationsHandler',
'ChildrenRelationsHandler',
'ParentRelationsHandler',
'EditableFieldsState',
'NotificationsService',
require('./work-package-details-controller')
])
.controller('WorkPackageNewController', [
@ -134,6 +134,7 @@ angular.module('openproject.workPackages.controllers')
'PathHelper',
'Query',
'OPERATORS_AND_LABELS_BY_FILTER_TYPE',
'NotificationsService',
require('./work-packages-list-controller')
])
.factory('columnsModal', ['btfModal', function(btfModal) {
@ -197,6 +198,7 @@ angular.module('openproject.workPackages.controllers')
'QueryService',
'AuthorisationService',
'$state',
'NotificationsService',
require('./dialogs/save')
])
.factory('settingsModal', ['btfModal', function(btfModal) {
@ -214,6 +216,7 @@ angular.module('openproject.workPackages.controllers')
'AuthorisationService',
'$rootScope',
'QUERY_MENU_ITEM_TYPE',
'NotificationsService',
require('./dialogs/settings')
])
.factory('shareModal', ['btfModal', function(btfModal) {
@ -231,6 +234,7 @@ angular.module('openproject.workPackages.controllers')
'AuthorisationService',
'queryMenuItemFactory',
'PathHelper',
'NotificationsService',
require('./dialogs/share')
])
.factory('sortingModal', ['btfModal', function(btfModal) {

@ -73,7 +73,9 @@ angular.module('openproject.workPackages')
'AuthorisationService',
'$window',
'$state',
'$timeout', require('./settings-dropdown-menu-controller')
'$timeout',
'NotificationsService',
require('./settings-dropdown-menu-controller')
])
.factory('TasksDropdownMenu', [
'ngContextMenu',

@ -31,7 +31,8 @@ module.exports = function(
exportModal, saveModal, settingsModal,
shareModal, sortingModal, groupingModal,
QueryService, AuthorisationService,
$window, $state, $timeout) {
$window, $state, $timeout,
NotificationsService) {
$scope.$watch('query.displaySums', function(newValue) {
$timeout(function() {
$scope.displaySumsLabel = (newValue) ? I18n.t('js.toolbar.settings.hide_sums')
@ -53,10 +54,15 @@ module.exports = function(
if( allowQueryAction(event, 'update') ) {
QueryService.saveQuery()
.then(function(data){
$scope.$emit('flashMessage', data.status);
$state.go('work-packages.list',
{ 'query_id': $scope.query.id, 'query_props': null },
{ notify: false });
if (data.status.isError) {
NotificationsService.addError(data.status.text);
}
else {
NotificationsService.addSuccess(data.status.text);
$state.go('work-packages.list',
{ 'query_id': $scope.query.id, 'query_props': null },
{ notify: false });
}
});
}
}
@ -67,11 +73,15 @@ module.exports = function(
if( allowQueryAction(event, 'delete') && preventNewQueryAction(event) && deleteConfirmed() ){
QueryService.deleteQuery()
.then(function(data){
settingsModal.deactivate();
$scope.$emit('flashMessage', data.status);
$state.go('work-packages.list',
{ 'query_id': null, 'query_props': null },
{ reload: true });
if (data.status.isError) {
NotificationsService.addError(data.status.text);
}
else {
NotificationsService.addSuccess(data.status.text);
$state.go('work-packages.list',
{ 'query_id': null, 'query_props': null },
{ reload: true });
}
});
}
};

@ -42,7 +42,8 @@ module.exports = function($scope,
WorkPackageService,
CommonRelationsHandler,
ChildrenRelationsHandler,
ParentRelationsHandler
ParentRelationsHandler,
NotificationsService
) {
$scope.$on('$stateChangeSuccess', function(event, toState){
latestTab.registerState(toState.name);
@ -75,14 +76,16 @@ module.exports = function($scope,
$scope.$emit('workPackgeLoaded');
function outputMessage(message, isError) {
$scope.$emit('flashMessage', {
isError: !!isError,
text: message
});
if (!!isError) {
NotificationsService.addError(message);
}
else {
NotificationsService.addSuccess(message);
}
}
function outputError(error) {
outputMessage(error.message, true);
NotificationsService.addError(error.message);
}
$scope.outputMessage = outputMessage; // expose to child controllers

@ -30,7 +30,7 @@ module.exports = function($scope, $rootScope, $state, $location, latestTab,
I18n, WorkPackagesTableService,
WorkPackageService, ProjectService, QueryService, PaginationService,
AuthorisationService, UrlParamsHelper, PathHelper, Query,
OPERATORS_AND_LABELS_BY_FILTER_TYPE) {
OPERATORS_AND_LABELS_BY_FILTER_TYPE, NotificationsService) {
// Setup
function initialSetup() {
@ -84,10 +84,9 @@ module.exports = function($scope, $rootScope, $state, $location, latestTab,
return WorkPackageService.getWorkPackages($scope.projectIdentifier, queryFromParams, PaginationService.getPaginationOptions());
} catch(e) {
$scope.$emit('flashMessage', {
isError: true,
text: I18n.t('js.work_packages.query.errors.unretrievable_query')
});
NotificationsService.addError(
I18n.t('js.work_packages.query.errors.unretrievable_query')
);
clearUrlQueryParams();
return WorkPackageService.getWorkPackages($scope.projectIdentifier);

@ -42,7 +42,7 @@ module.exports = function(
if (_.isEmpty(_.keys(errors))) {
return;
}
var errorMessages = _.map(errors);
var errorMessages = _.flatten(_.map(errors), true);
NotificationsService.addError(I18n.t('js.label_validation_error'), errorMessages);
};
@ -157,7 +157,7 @@ module.exports = function(
function setFailure(e) {
afterError();
EditableFieldsState.errors = {
'_common': ApiHelper.getErrorMessage(e)
'_common': ApiHelper.getErrorMessages(e)
};
showErrors();
}

@ -28,20 +28,12 @@
module.exports = function() {
var ApiHelper = {
handleError: function(scope, error) {
scope.$emit('flashMessage', {
isError: true,
text: ApiHelper.getErrorMessage(error)
});
},
getErrorMessage: function(error) {
if(error.status == 500) {
return error.statusText;
getErrorMessages: function(error) {
if(error.status === 500) {
return [error.statusText];
} else {
var response = JSON.parse(error.responseText);
var messages = [];
var message;
if (ApiHelper.isMultiErrorMessage(response)) {
angular.forEach(response._embedded.errors, function(error) {
@ -51,14 +43,12 @@ module.exports = function() {
messages.push(response.message);
}
message = messages.join(' ');
return message;
return messages;
}
},
isMultiErrorMessage: function(error) {
return error.errorIdentifier == 'urn:openproject-org:api:v3:errors:MultipleErrors';
return error.errorIdentifier === 'urn:openproject-org:api:v3:errors:MultipleErrors';
}
};

@ -27,7 +27,7 @@
//++
angular.module('openproject.workPackages.helpers')
.factory('ApiHelper', require('./api-helper'))
.factory('ApiHelper', ['NotificationsService', require('./api-helper')])
.factory('FiltersHelper', ['I18n', require('./filters-helper')])
.constant('ACTIVE_USER_STATUSES', ['active', 'registered'])
.factory('UsersHelper', ['ACTIVE_USER_STATUSES', require('./users-helper')])

@ -26,11 +26,15 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
module.exports = function(PathHelper, CommonRelationsHandler, WorkPackageService) {
module.exports = function(
CommonRelationsHandler,
WorkPackageService,
ApiNotificationsService
) {
function ChildrenRelationsHandler(workPackage, children) {
var handler = new CommonRelationsHandler(workPackage, children, undefined);
handler.type = "child";
handler.type = 'child';
handler.applyCustomExtensions = undefined;
handler.canAddRelation = function() { return !!this.workPackage.links.addChild; };
@ -55,7 +59,7 @@ module.exports = function(PathHelper, CommonRelationsHandler, WorkPackageService
scope.updateFocus(index);
scope.$emit('workPackageRefreshRequired');
}, function(error) {
ApiHelper.handleError(scope, error);
ApiNotificationsService.addError(error);
});
};

@ -26,7 +26,11 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
module.exports = function($timeout, WorkPackageService, ApiHelper, PathHelper, MAX_AUTOCOMPLETER_ADDITION_ITERATIONS) {
module.exports = function(
$timeout,
WorkPackageService,
ApiNotificationsService
) {
function CommonRelationsHandler(workPackage,
relations,
relationsId) {
@ -34,7 +38,7 @@ module.exports = function($timeout, WorkPackageService, ApiHelper, PathHelper, M
this.relations = relations;
this.relationsId = relationsId;
this.type = "relation";
this.type = 'relation';
this.isSingletonRelation = false;
}
@ -56,12 +60,15 @@ module.exports = function($timeout, WorkPackageService, ApiHelper, PathHelper, M
},
addRelation: function(scope) {
WorkPackageService.addWorkPackageRelation(this.workPackage, scope.relationToAddId, this.relationsId).then(function(relation) {
WorkPackageService.addWorkPackageRelation(this.workPackage,
scope.relationToAddId,
this.relationsId)
.then(function() {
scope.relationToAddId = '';
scope.updateFocus(-1);
scope.$emit('workPackageRefreshRequired');
}, function(error) {
ApiHelper.handleError(scope, error);
ApiNotificationsService.addError(error);
});
},
@ -74,7 +81,7 @@ module.exports = function($timeout, WorkPackageService, ApiHelper, PathHelper, M
scope.updateFocus(index);
scope.$emit('workPackageRefreshRequired');
}, function(error) {
ApiHelper.handleError(scope, error);
ApiNotificationsService.addError(scope, error);
});
},
@ -89,7 +96,7 @@ module.exports = function($timeout, WorkPackageService, ApiHelper, PathHelper, M
getRelatedWorkPackage: function(workPackage, relation) {
var self = workPackage.links.self.href;
if (relation.links.relatedTo.href == self) {
if (relation.links.relatedTo.href === self) {
return relation.links.relatedFrom.fetch();
} else {
return relation.links.relatedTo.fetch();

@ -27,20 +27,21 @@
//++
angular.module('openproject.viewModels')
.constant('MAX_AUTOCOMPLETER_ADDITION_ITERATIONS', 3)
.factory('CommonRelationsHandler', [
'$timeout',
'WorkPackageService',
'ApiHelper',
'PathHelper',
'MAX_AUTOCOMPLETER_ADDITION_ITERATIONS', require(
'./common-relations-handler')
'ApiNotificationsService',
require('./common-relations-handler')
])
.factory('ChildrenRelationsHandler', ['PathHelper', 'CommonRelationsHandler',
.factory('ChildrenRelationsHandler', [
'CommonRelationsHandler',
'WorkPackageService',
'ApiNotificationsService',
require('./children-relations-handler')
])
.factory('ParentRelationsHandler', ['CommonRelationsHandler',
'WorkPackageService', 'ApiHelper',
.factory('ParentRelationsHandler', [
'CommonRelationsHandler',
'WorkPackageService',
'ApiNotificationsService',
require('./parent-relations-handler')
]);

@ -26,7 +26,7 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
module.exports = function(CommonRelationsHandler, WorkPackageService, ApiHelper) {
module.exports = function(CommonRelationsHandler, WorkPackageService, ApiNotificationsService) {
function ParentRelationsHandler(workPackage, parents, relationsId) {
var relations = parents.filter(function(parent) {
return parent.props.id !== workPackage.props.id;
@ -52,7 +52,7 @@ module.exports = function(CommonRelationsHandler, WorkPackageService, ApiHelper)
scope.updateFocus(-1);
scope.$emit('workPackageRefreshRequired');
}, function(error) {
ApiHelper.handleError(scope, error);
ApiNotificationsService.addError(error);
});
};
handler.removeRelation = function(scope) {
@ -69,7 +69,7 @@ module.exports = function(CommonRelationsHandler, WorkPackageService, ApiHelper)
scope.updateFocus(index);
scope.$emit('workPackageRefreshRequired');
}, function(error) {
ApiHelper.handleError(scope, error);
ApiNotificationsService.addError(error);
});
};

@ -85,7 +85,6 @@ See doc/COPYRIGHT.rdoc for more details.
<h1 class="hidden-for-sighted">Content</h1>
<flash-message></flash-message>
<div ui-view></div>
<div style="clear:both;">&nbsp;</div>

@ -0,0 +1,91 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
describe('NotificationsService', function() {
'use strict';
var ApiNotificationsService,
NotificationsService,
ApiHelper;
beforeEach(module('openproject.services'));
beforeEach(inject(function(_ApiNotificationsService_, _NotificationsService_, _ApiHelper_){
ApiNotificationsService = _ApiNotificationsService_;
NotificationsService = _NotificationsService_;
ApiHelper = _ApiHelper_;
}));
describe('#addError', function() {
var error = {},
messages = [];
beforeEach(function() {
sinon.spy(NotificationsService, 'addError');
});
describe('with only one error message', function() {
beforeEach(function() {
messages = ['Oh my - Error'];
ApiHelper.getErrorMessages = sinon.stub().returns(messages);
});
it('adds the error to the notification service', function() {
ApiNotificationsService.addError(error);
expect(NotificationsService.addError).to.have.been.calledWith('Oh my - Error');
});
});
describe('with multiple error messages', function() {
beforeEach(function() {
messages = ['Oh my - Error', 'Hey you - Error'];
ApiHelper.getErrorMessages = sinon.stub().returns(messages);
});
it('adds the error to the notification service', function() {
ApiNotificationsService.addError(error);
expect(NotificationsService.addError).to.have.been.calledWith('', messages);
});
});
});
describe('#addSuccess', function() {
var message = 'Great success';
beforeEach(function() {
sinon.spy(NotificationsService, 'addSuccess');
});
it('delegates to NotificationService', function() {
ApiNotificationsService.addSuccess(message);
expect(NotificationsService.addSuccess).to.have.been.calledWith(message);
});
});
});

@ -48,18 +48,6 @@ describe('NotificationsService', function() {
expect(notification).to.eql({ message: 'warning!', type: 'warning' });
});
it('should throw an Error if trying to create an error without errors', function() {
expect(function() {
NotificationsService.addError('error!');
}).to.throw(Error);
});
it('should throw an Error if trying to create an upload without uploads', function() {
expect(function() {
NotificationsService.addWorkPackageUpload('themUploads');
}).to.throw(Error);
});
it('should be able to create error messages with errors', function() {
var notification = NotificationsService.addError('a super cereal error', ['fooo', 'baarr']);
expect(notification).to.eql({
@ -69,7 +57,16 @@ describe('NotificationsService', function() {
});
});
it('should be able to create error messages with errors', function() {
it('should be able to create error messages with only a message', function() {
var notification = NotificationsService.addError('a super cereal error');
expect(notification).to.eql({
message: 'a super cereal error',
errors: [],
type: 'error'
});
});
it('should be able to create upload messages with uploads', function() {
var notification = NotificationsService.addWorkPackageUpload('uploading...', [0, 1, 2]);
expect(notification).to.eql({
message: 'uploading...',
@ -77,4 +74,10 @@ describe('NotificationsService', function() {
uploads: [0, 1, 2]
});
});
it('should throw an Error if trying to create an upload without uploads', function() {
expect(function() {
NotificationsService.addWorkPackageUpload('themUploads');
}).to.throw(Error);
});
});

@ -1,126 +0,0 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
/*jshint expr: true*/
describe('flashMessage Directive', function() {
var compile, element, rootScope, scope;
beforeEach(angular.mock.module('openproject.uiComponents', function($provide) {
var configurationService = {};
configurationService.accessibilityModeEnabled = sinon.stub().returns(true);
$provide.constant('ConfigurationService', configurationService);
}));
beforeEach(module('openproject.templates'));
beforeEach(inject(function($rootScope, $compile) {
var html = '<flash-message></flash-message>';
element = angular.element(html);
rootScope = $rootScope;
scope = $rootScope.$new();
compile = function() {
$compile(element)(scope);
scope.$digest();
};
}));
context('with no message', function() {
beforeEach(function() {
compile();
});
it('should render no message initially', function() {
expect(element.text()).to.be.equal('');
});
it('should be hidden', function() {
expect(element.hasClass('ng-hide')).to.be.true;
});
});
context('with flash messages', function() {
beforeEach(function() {
compile();
});
describe('info message', function() {
var message = {
text: 'für deine Informationen',
isError: false
};
beforeEach(function() {
rootScope.$emit('flashMessage', message);
scope.$apply();
});
it('should render message', function() {
expect(element.text().trim()).to.equal('für deine Informationen');
});
it('should be visible', function() {
expect(element.hasClass('ng-hide')).to.be.false;
});
it('should style as an info message', function() {
expect(element.attr('class').split(' ')).to
.include.members(['flash', 'icon-notice', 'notice']);
});
});
describe('error message', function() {
var message = {
text: '¡Alerta! WARNING! Achtung!',
isError: true
};
beforeEach(function() {
rootScope.$emit('flashMessage', message);
scope.$apply();
});
it('should render message', function() {
expect(element.text().trim()).to.equal('¡Alerta! WARNING! Achtung!');
});
it('should be visible', function() {
expect(element.hasClass('ng-hide')).to.be.false;
});
it('should style as an error message', function() {
expect(element.attr('class').split(' ')).to
.include.members(['flash', 'icon-errorExplanation', 'errorExplanation']);
});
});
});
});

@ -29,7 +29,7 @@
/*jshint expr: true*/
describe('SettingsModalController', function() {
var scope, $q, defer, settingsModal, QueryService;
var scope, $q, defer, settingsModal, QueryService, NotificationsService;
var ctrl, buildController;
beforeEach(module('openproject.workPackages.controllers'));
@ -51,12 +51,16 @@ describe('SettingsModalController', function() {
};
settingsModal = { deactivate: angular.noop };
NotificationsService = {
addSuccess: function() {}
};
buildController = function() {
ctrl = $controller("SettingsModalController", {
$scope: scope,
settingsModal: settingsModal,
QueryService: QueryService
QueryService: QueryService,
NotificationsService: NotificationsService
});
};
}));
@ -68,9 +72,14 @@ describe('SettingsModalController', function() {
sinon.spy(scope, "$emit");
sinon.spy(settingsModal, "deactivate");
sinon.spy(QueryService, "updateHighlightName");
sinon.spy(NotificationsService, 'addSuccess');
scope.updateQuery();
defer.resolve({status: "Query updated!"});
defer.resolve({
status: {
text: 'Query updated!'
}
});
scope.$digest();
});
@ -79,9 +88,8 @@ describe('SettingsModalController', function() {
expect(settingsModal.deactivate).to.have.been.called;
});
it('should update the flash message', function() {
expect(scope.$emit).to.have.been.calledWith("flashMessage",
"Query updated!");
it('should notfify success', function() {
expect(NotificationsService.addSuccess).to.have.been.calledWith('Query updated!');
});
it ('should update the query menu name', function() {

@ -47,53 +47,56 @@ describe('API helper', function() {
return error;
}
describe('500', function() {
var error = createErrorObject(500, "Internal Server Error");
describe('.getErrorMessages', function() {
describe('500', function() {
var error = createErrorObject(500, 'Internal Server Error');
it('should return status text', function() {
expect(ApiHelper.getErrorMessage(error)).to.eq(error.statusText);
it('should return status text in an array', function() {
expect(ApiHelper.getErrorMessages(error)).to.eql([error.statusText]);
});
});
});
describe('other codes', function() {
function createApiErrorObject(errorIdentifier, message, multiple) {
var apiError = {};
describe('other codes', function() {
function createApiErrorObject(errorIdentifier, message, multiple) {
var apiError = {};
apiError._type = 'Error';
apiError.errorIdentifier = (multiple) ? 'urn:openproject-org:api:v3:errors:MultipleErrors' : errorIdentifier;
apiError.message = message;
apiError._type = 'Error';
apiError.errorIdentifier = (multiple) ?
'urn:openproject-org:api:v3:errors:MultipleErrors' :
errorIdentifier;
apiError.message = message;
if (multiple) {
apiError._embedded = { errors: [] };
if (multiple) {
apiError._embedded = { errors: [] };
for (var x=0; x < 2; x++) {
apiError._embedded.errors.push(createApiErrorObject(errorIdentifier, message));
for (var x=0; x < 2; x++) {
apiError._embedded.errors.push(createApiErrorObject(errorIdentifier, message));
}
}
}
return apiError;
}
return apiError;
}
describe('single error', function() {
var apiError = createApiErrorObject('NotFound', 'Not found.');
var error = createErrorObject(404, null, JSON.stringify(apiError));
var expectedResult = 'Not found.';
describe('single error', function() {
var apiError = createApiErrorObject('NotFound', 'Not found.');
var error = createErrorObject(404, null, JSON.stringify(apiError));
var expectedResult = 'Not found.';
it('should return api error message', function() {
expect(ApiHelper.getErrorMessage(error)).to.eq(expectedResult);
it('should return api error message in an arry', function() {
expect(ApiHelper.getErrorMessages(error)).to.eql([expectedResult]);
});
});
});
describe('multiple errors', function() {
var errorMessage = 'This is an error message.';
var apiError = createApiErrorObject('PropertyIsReadOnly', errorMessage, true);
var error = createErrorObject(404, null, JSON.stringify(apiError));
describe('multiple errors', function() {
var errorMessage = 'This is an error message.';
var apiError = createApiErrorObject('PropertyIsReadOnly', errorMessage, true);
var error = createErrorObject(404, null, JSON.stringify(apiError));
it('should return concatenated api error messages', function() {
var messages = [];
var expectedResult = errorMessage + ' ' + errorMessage;
it('should return an array of messages', function() {
var expectedResult = [errorMessage, errorMessage];
expect(ApiHelper.getErrorMessage(error)).to.eq(expectedResult);
expect(ApiHelper.getErrorMessages(error)).to.eql(expectedResult);
});
});
});
});

@ -27,6 +27,7 @@
#++
require 'spec_helper'
require 'features/page_objects/notification'
require 'features/work_packages/shared_contexts'
require 'features/work_packages/work_packages_page'
@ -34,6 +35,7 @@ feature 'Query menu items' do
let(:user) { FactoryGirl.create :admin }
let(:project) { FactoryGirl.create :project }
let(:work_packages_page) { WorkPackagesPage.new(project) }
let(:notification) { PageObjects::Notifications.new(page) }
def visit_index_page(query)
work_packages_page.select_query(query)
@ -72,7 +74,7 @@ feature 'Query menu items' do
check 'show_in_menu'
click_on 'Save'
expect(page).to have_selector('.flash', text: 'Successful update')
notification.expect_success('Successful update')
expect(page).to have_selector('a', text: query.name)
end
@ -103,7 +105,7 @@ feature 'Query menu items' do
end
it 'displaying a success message', js: true do
expect(page).to have_selector('.flash', text: 'Successful update')
notification.expect_success('Successful update')
end
it 'is renaming and reordering the list', js: true do

@ -0,0 +1,43 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
module PageObjects
class Notifications
include RSpec::Matchers
attr_accessor :page
def initialize(page)
@page = page
end
def expect_success(message)
expect(page).to have_selector('.notification-box.-success', text: message)
end
end
end
Loading…
Cancel
Save