Angular alert/messaging service

Rewrite existing Horizon alerts in Angular. To use,
inject 'toastService' as dependency, and call service
methods, e.g.

toastService.add('success', 'User successfully updated.').

Refactor patch to follow.

Partially Implements: blueprint launch-instance-redesign
Co-Authored-By: Cindy Lu <clu@us.ibm.com>

Change-Id: I7d3f0897d1064830865fa9077342b6575c3e9ff7
This commit is contained in:
Cindy Lu 2015-02-04 17:28:48 -08:00
parent 3ef270b940
commit 6dfa100d77
7 changed files with 328 additions and 2 deletions

View File

@ -0,0 +1,6 @@
<div ng-repeat="toast in toast.get()">
<div class="alert alert-{$ ::toast.type $}">
<a class="close" ng-click="toast.close($index)"><span class="fa fa-times"></span></a>
<div><strong>{$ toast.typeMsg $}: </strong>{$ toast.msg $}</div>
</div>
</div>

161
horizon/static/angular/toast/toast.js vendored Normal file
View File

@ -0,0 +1,161 @@
/*
* Copyright 2015 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use_strict';
/**
* @ngdoc overview
* @name hz.widget.toast
* description
*
* # hz.widget.toast
*
* The `hz.widget.toast` module provides pop-up notifications to Horizon.
* A toast is a short text message triggered by a user action to provide
* real-time information. Toasts do not disrupt the page's behavior and
* typically auto-expire and fade. Also, toasts do not accept any user
* interaction.
*
*
* | Components |
* |--------------------------------------------------------------------------|
* | {@link hz.widget.toast.factory:toastService `toastService`} |
* | {@link hz.widget.toast.directive:toast `toast`} |
*
*/
angular.module('hz.widget.toast', [])
/**
* @ngdoc service
* @name toastService
*
* @description
* This service can be used to display user messages, toasts, in Horizon.
* To create a new toast, inject the 'toastService' module into your
* current module. Then, use the service methods.
*
* For example to add a 'success' message:
* toastService.add('success', 'User successfully created.');
*
* All actions (add, clearAll, etc.) taken on the data are automatically
* sync-ed with the HTML.
*/
.factory('toastService', function() {
var toasts = [];
var service = {};
/**
* Helper method used to remove all the toasts matching the 'type'
* passed in.
*/
function clear(type) {
for (var i = toasts.length - 1; i >= 0; i--) {
if (toasts[i].type === type) {
toasts.splice(i, 1);
}
}
}
/**
* There are 5 types of toasts, which are based off Bootstrap alerts.
*/
service.types = {
danger: gettext('Danger'),
warning: gettext('Warning'),
info: gettext('Info'),
success: gettext('Success'),
error: gettext('Error')
};
/**
* Create a toast object and push it to the toasts array.
*/
service.add = function(type, msg) {
var toast = {
type: type === 'error' ? 'danger' : type,
typeMsg: this.types[type],
msg: msg,
close: function(index) {
toasts.splice(index, 1);
}
};
toasts.push(toast);
};
/**
* Remove a single toast.
*/
service.close = function(index) {
toasts.splice(index, 1);
};
/**
* Return all toasts.
*/
service.get = function() {
return toasts;
};
/**
* Remove all toasts.
*/
service.clearAll = function() {
toasts = [];
};
/**
* Remove all toasts of type 'danger.'
*/
service.clearErrors = function() {
clear('danger');
};
/**
* Remove all toasts of type 'success.'
*/
service.clearSuccesses = function() {
clear('success');
};
return service;
})
/**
* @ngdoc directive
* @name hz.widget.toast.directive:toast
*
* @description
* The `toast` directive allows you to place the toasts wherever you
* want in your layout. Currently styling is pulled from Bootstrap alerts.
*
* @restrict EA
* @scope true
*
*/
.directive('toast', ['toastService', 'basePath', function(toastService, path) {
return {
restrict: 'EA',
templateUrl: path + 'toast/toast.html',
scope: {},
link: function(scope) {
scope.toast = toastService;
}
};
}]);
})();

View File

@ -0,0 +1,153 @@
/*
* Copyright 2015 IBM Corp
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function(){
'use strict';
describe('hz.widget.toast module', function() {
it('should have been defined', function () {
expect(angular.module('hz.widget.toast')).toBeDefined();
});
});
describe('toast factory', function() {
var $compile,
$scope,
$element,
service;
var successMsg = "I am success.";
var dangerMsg = "I am danger.";
var infoMsg = "I am info.";
beforeEach(module('templates'));
beforeEach(module('hz'));
beforeEach(module('hz.widgets'));
beforeEach(module('hz.widget.toast'));
beforeEach(inject(function ($injector, toastService) {
service = toastService;
$scope = $injector.get('$rootScope').$new();
$compile = $injector.get('$compile');
}));
it('should create different toasts', function() {
service.add('danger', dangerMsg);
expect(service.get().length).toBe(1);
expect(service.get()[0].type).toBe('danger');
service.add('success', successMsg);
expect(service.get().length).toBe(2);
expect(service.get()[1].type).toBe('success');
service.add('info', infoMsg);
expect(service.get().length).toBe(3);
expect(service.get()[2].msg).toBe(infoMsg);
});
it('should provide a function to clear all toasts', function() {
service.add('success', successMsg);
service.add('success', successMsg);
service.add('info', infoMsg);
expect(service.get().length).toBe(3);
service.clearAll();
expect(service.get().length).toBe(0);
});
it('should provide a function to clear all error toasts', function() {
service.add('danger', dangerMsg);
service.add('success', successMsg);
service.add('danger', dangerMsg);
expect(service.get().length).toBe(3);
service.clearErrors();
expect(service.get().length).toBe(1);
expect(service.get()[0].type).toBe('success');
});
it('should provide a function to clear all success toasts', function() {
service.add('success', successMsg);
service.add('success', successMsg);
service.add('info', infoMsg);
expect(service.get().length).toBe(3);
service.clearSuccesses();
expect(service.get().length).toBe(1);
expect(service.get()[0].type).toBe('info');
});
it('should provide a function to clear a specific toast', function() {
service.add('success', successMsg);
service.add('info', infoMsg);
service.close(1);
expect(service.get().length).toBe(1);
expect(service.get()[0].type).not.toEqual('info');
});
});
describe('toast directive', function () {
var $compile,
$scope,
$element,
service;
var successMsg = "I am success.";
var dangerMsg = "I am danger.";
var infoMsg = "I am info.";
function toasts() {
return $element.find('.alert');
}
beforeEach(module('templates'));
beforeEach(module('hz'));
beforeEach(module('hz.widgets'));
beforeEach(module('hz.widget.toast'));
beforeEach(inject(function ($injector, toastService) {
$scope = $injector.get('$rootScope').$new();
$compile = $injector.get('$compile');
service = toastService;
var markup = '<toast></toast>';
$element = $compile(markup)($scope);
$scope.$digest();
}));
it('should create toasts using ng-repeat', function() {
service.add('danger', dangerMsg);
service.add('success', successMsg);
service.add('info', infoMsg);
$scope.$apply();
expect(toasts().length).toBe(3);
});
it('should have the proper classes for different toasts types', function() {
service.add('danger', dangerMsg);
service.add('success', successMsg);
service.add('info', infoMsg);
$scope.$apply();
expect(toasts().length).toBe(3);
expect(toasts().eq(0).hasClass('alert-danger'));
expect(toasts().eq(1).hasClass('alert-success'));
expect(toasts().eq(2).hasClass('alert-info'));
});
it('should be possible to remove a toast by clicking close', function() {
service.add('success', successMsg);
$scope.$apply();
expect(toasts().length).toBe(1);
toasts().eq(0).find('.close').click();
$scope.$apply();
expect(toasts().length).toBe(0);
});
});
})();

View File

@ -15,7 +15,8 @@
'hz.widget.action-list',
'hz.widget.metadata-tree',
'hz.widget.metadata-display',
'hz.framework.validators'
'hz.framework.validators',
'hz.widget.toast'
])
.constant('basePath', '/static/angular/');

View File

@ -1,5 +1,6 @@
{% load i18n %}
<div class="messages">
<toast></toast>
{% for message in messages %}
{% if "info" in message.tags %}
<div class="alert alert-info alert-dismissable fade in">
@ -26,4 +27,4 @@
</div>
{% endif %}
{% endfor %}
</div>
</div>

View File

@ -58,6 +58,7 @@
<script src='{{ STATIC_URL }}angular/magic-search/magic-search.js'></script>
<script src='{{ STATIC_URL }}angular/validators/validators.js'></script>
<script src='{{ STATIC_URL }}angular/workflow/workflow.js'></script>
<script src='{{ STATIC_URL }}angular/toast/toast.js'></script>
<script src='{{ STATIC_URL }}horizon/lib/jquery/jquery.quicksearch.js'></script>
<script src="{{ STATIC_URL }}horizon/lib/jquery/jquery.tablesorter.js"></script>

View File

@ -44,6 +44,7 @@ class ServicesTests(test.JasmineTests):
'angular/wizard/wizard.js',
'angular/workflow/workflow.js',
'angular/metadata-display/metadata-display.js',
'angular/toast/toast.js',
'horizon/js/angular/filters/filters.js',
]
specs = [
@ -66,6 +67,7 @@ class ServicesTests(test.JasmineTests):
'angular/workflow/workflow.spec.js',
'angular/metadata-tree/metadata-tree.spec.js',
'angular/metadata-display/metadata-display.spec.js',
'angular/toast/toast.spec.js',
'horizon/js/angular/filters/filters.spec.js',
]
externalTemplates = [
@ -83,4 +85,5 @@ class ServicesTests(test.JasmineTests):
'angular/metadata-tree/metadata-tree.html',
'angular/metadata-tree/metadata-tree-item.html',
'angular/metadata-display/metadata-display.html',
'angular/toast/toast.html',
]