diff --git a/platform/commonUI/notification/src/NotificationService.js b/platform/commonUI/notification/src/NotificationService.js index 30e746cc42..46950440da 100644 --- a/platform/commonUI/notification/src/NotificationService.js +++ b/platform/commonUI/notification/src/NotificationService.js @@ -23,13 +23,61 @@ /** * This bundle implements the notification service, which can be used to - * show banner notifications to the user. + * show banner notifications to the user. Banner notifications + * are used to inform users of events in a non-intrusive way. As + * much as possible, notifications share a model with blocking + * dialogs so that the same information can be provided in a dialog + * and then minimized to a banner notification if needed. + * * @namespace platform/commonUI/dialog */ define( ["./MessageSeverity"], function (MessageSeverity) { "use strict"; + + /** + * A representation of a user action. Actions are provided to + * dialogs and notifications and are shown as buttons. + * + * @typedef {object} NotificationAction + * @property {string} label the label to appear on the button for + * this action + * @property {function} action a callback function to be invoked + * when the button is clicked + */ + + /** + * A representation of a banner notification. Banner notifications + * are used to inform users of events in a non-intrusive way. As + * much as possible, notifications share a model with blocking + * dialogs so that the same information can be provided in a dialog + * and then minimized to a banner notification if needed. + * + * @typedef {object} Notification + * @property {string} title The title of the message + * @property {number} progress The completion status of a task + * represented numerically + * @property {MessageSeverity} messageSeverity The importance of the + * message (eg. error, success) + * @property {boolean} unknownProgress a boolean indicating that the + * progress of the underlying task is unknown. This will result in a + * visually distinct progress bar. + * @property {boolean | number} autoDismiss If truthy, dialog will + * be automatically minimized or dismissed (depending on severity). + * Additionally, if the provided value is a number, it will be used + * as the delay period before being dismissed. + * @property {NotificationAction} primaryAction the default user + * response to + * this message. Will be represented as a button with the provided + * label and action. May be used by banner notifications to display + * only the most important option to users. + * @property {NotificationAction[]} additionalActions any additional + * actions + * that the user can take. Will be represented as additional buttons + * that may or may not be available from a banner. + */ + /** * The notification service is responsible for informing the user of * events via the use of banner notifications. @@ -41,69 +89,51 @@ define( this.$timeout = $timeout; this.DEFAULT_AUTO_DISMISS = DEFAULT_AUTO_DISMISS; - /** - * Exposes the current "active" notification. This is a - * notification that is of current highest importance that has - * not been dismissed. The deinition of what is of highest - * importance might be a little nuanced and require tweaking. - * For example, if an important error message is visible and a - * success message is triggered, it may be desirable to - * temporarily show the success message and then auto-dismiss it. - * @type {{notification: undefined}} + /* + * A context in which to hold the active notification and a + * handle to its timeout. */ this.active = { }; } - /** - var model = { - title: string, - progress: number, - severity: MessageSeverity, - unknownProgress: boolean, - minimized: boolean, - autoDismiss: boolean | number, - actions: { - label: string, - action: function - } - } - */ /** - * Possibly refactor this out to a provider? - * @constructor + * Returns the notification that is currently visible in the banner area + * @returns {Notification} */ - function Notification (model) { - this.model = model; - } - - Notification.prototype.minimize = function (setValue) { - if (typeof setValue !== undefined){ - this.model.minimized = setValue; - } else { - return this.model.minimized; - } - }; - NotificationService.prototype.getActiveNotification = function (){ return this.active.notification; } /** - * model = { - * - * } - * @param model + * A convenience method for success notifications. Notifications + * created via this method will be auto-dismissed after a default + * wait period + * @param {Notification} notification The notification to display */ - NotificationService.prototype.notify = function (model) { - var notification = new Notification(model), - that=this; + NotificationService.prototype.success = function (notification) { + notification.autoDismiss = notification.autoDismiss || true; + NotificationService.prototype.notify(notification); + } + + /** + * Notifies the user of an event. If there is a banner notification + * already active, then it will be dismissed or minimized automatically, + * and the provided notification displayed in its place. + * + * @param {Notification} notification The notification to display + */ + NotificationService.prototype.notify = function (notification) { + /*var notification = new Notification(model), + that=this; */ + var that = this; + this.notifications.push(notification); /* Check if there is already an active (ie. visible) notification */ if (!this.active.notification){ - setActiveNotification.call(this, notification); + this.setActiveNotification(notification); } else if (!this.active.timeout){ /* @@ -122,28 +152,42 @@ define( }; - function setActiveNotification (notification) { - var that = this; - this.active.notification = notification; - /* - If autoDismiss has been specified, setup a timeout to - dismiss the dialog. + /** + * Used internally by the NotificationService + * @private + */ + NotificationService.prototype.setActiveNotification = + function (notification) { - If there are other notifications pending in the queue, set this - one to auto-dismiss - */ - if (notification.model.autoDismiss - || selectNextNotification.call(this)) { - var timeout = isNaN(notification.model.autoDismiss) ? - this.DEFAULT_AUTO_DISMISS : notification.model.autoDismiss; + var that = this; + this.active.notification = notification; + /* + If autoDismiss has been specified, setup a timeout to + dismiss the dialog. - this.active.timeout = this.$timeout(function () { - that.dismissOrMinimize(notification); - }, timeout); - } - } + If there are other notifications pending in the queue, set this + one to auto-dismiss + */ + if (notification && (notification.autoDismiss + || this.selectNextNotification())) { + var timeout = isNaN(notification.autoDismiss) ? + this.DEFAULT_AUTO_DISMISS : + notification.autoDismiss; - function selectNextNotification () { + this.active.timeout = this.$timeout(function () { + that.dismissOrMinimize(notification); + }, timeout); + } else { + delete this.active.timeout; + } + }; + + /** + * Used internally by the NotificationService + * + * @private + */ + NotificationService.prototype.selectNextNotification = function () { /* Loop through the notifications queue and find the first one that has not already been minimized (manually or otherwise). @@ -151,7 +195,7 @@ define( for (var i=0; i< this.notifications.length; i++) { var notification = this.notifications[i]; - if (!notification.model.minimized + if (!notification.minimized && notification!= this.activeNotification) { return notification; @@ -162,8 +206,9 @@ define( /** * Minimize a notification. The notification will still be available * from the notification list. Typically notifications with a - * severity of SUCCESS should not be minimized, but rather - * dismissed. + * severity of 'success' should not be minimized, but rather + * dismissed. If you're not sure which is appropriate, + * use {@link NotificationService#dismissOrMinimize} * @see dismiss * @see dismissOrMinimize * @param notification @@ -172,19 +217,17 @@ define( //Check this is a known notification var index = this.notifications.indexOf(notification); if (index >= 0) { - notification.minimize(true); - delete this.active.notification; - delete this.active.timeout; - setActiveNotification.call(this, selectNextNotification.call(this)); + notification.minimized=true; + this.setActiveNotification(this.selectNextNotification()); } - } + }; /** - * Completely remove a notification. This will dismiss it from the + * Completely removes a notification. This will dismiss it from the * message banner and remove it from the list of notifications. - * Typically only notifications with a severity of SUCCESS should be + * Typically only notifications with a severity of success should be * dismissed. If you're not sure whether to dismiss or minimize a - * notification, use the dismissOrMinimize method. + * notification, use {@link NotificationService#dismissOrMinimize}. * dismiss * @see dismissOrMinimize * @param notification The notification to dismiss @@ -194,11 +237,7 @@ define( var index = this.notifications.indexOf(notification); if (index >= 0) { this.notifications.splice(index, 1); - - delete this.active.notification; - delete this.active.timeout; - - setActiveNotification.call(this, selectNextNotification.call(this)); + this.setActiveNotification(this.selectNextNotification()); } } @@ -210,7 +249,7 @@ define( * @param notification */ NotificationService.prototype.dismissOrMinimize = function (notification){ - if (notification.model.severity > MessageSeverity.SUCCESS){ + if (notification.severity > MessageSeverity.SUCCESS){ this.minimize(notification); } else { this.dismiss(notification); diff --git a/platform/commonUI/notification/test/NotificationServiceSpec.js b/platform/commonUI/notification/test/NotificationServiceSpec.js index c7a5fee234..c54ba08e38 100644 --- a/platform/commonUI/notification/test/NotificationServiceSpec.js +++ b/platform/commonUI/notification/test/NotificationServiceSpec.js @@ -30,24 +30,18 @@ define( var notificationService, mockTimeout, mockAutoDismiss, - successModel = { - title: "Mock Success Notification", - severity: MessageSeverity.SUCCESS - }, - errorModel = { - title: "Mock Error Notification", - severity: MessageSeverity.ERROR - }; + successModel, + errorModel; /** * 1) Calling .notify results in a new notification being created - * with the provided model and set to the active notification + * with the provided model and set to the active notification. DONE * * 2) Calling .notify with autoDismiss results in a SUCCESS notification - * becoming dismissed after timeout has elapsed + * becoming dismissed after timeout has elapsed DONE * * 3) Calling .notify with autoDismiss results in an ERROR notification - * being MINIMIZED after a timeout has elapsed + * being MINIMIZED after a timeout has elapsed DONE * * 4) Calling .notify with an active success notification results in that * notification being auto-dismissed, and the new notification becoming @@ -84,17 +78,49 @@ define( mockAutoDismiss = 0; notificationService = new NotificationService( mockTimeout, mockAutoDismiss); + successModel = { + title: "Mock Success Notification", + severity: MessageSeverity.SUCCESS + }; + errorModel = { + title: "Mock Error Notification", + severity: MessageSeverity.ERROR + }; }); - it("Calls the notification service with a new notification, making" + + it("gets a new success notification, making" + " the notification active", function() { var activeNotification; notificationService.notify(successModel); activeNotification = notificationService.getActiveNotification(); - expect(activeNotification.model).toBe(successModel); + expect(activeNotification).toBe(successModel); }); - describe(" called with multiple notifications", function(){ + it("gets a new success notification with" + + " numerical auto-dismiss specified. ", function() { + var activeNotification; + successModel.autoDismiss = 1000; + notificationService.notify(successModel); + activeNotification = notificationService.getActiveNotification(); + expect(activeNotification).toBe(successModel); + mockTimeout.mostRecentCall.args[0](); + activeNotification = notificationService.getActiveNotification(); + expect(activeNotification).toBeUndefined(); + }); + + it("gets a new notification with" + + " boolean auto-dismiss specified. ", function() { + var activeNotification; + successModel.autoDismiss = true; + notificationService.notify(successModel); + activeNotification = notificationService.getActiveNotification(); + expect(activeNotification).toBe(successModel); + mockTimeout.mostRecentCall.args[0](); + activeNotification = notificationService.getActiveNotification(); + expect(activeNotification).toBeUndefined(); + }); + + describe(" gets called with multiple notifications", function(){ it("auto-dismisses the previously active notification, making" + " the new notification active", function() { var activeNotification; @@ -103,14 +129,14 @@ define( activeNotification = notificationService.getActiveNotification(); //Initially expect the active notification to be success - expect(activeNotification.model).toBe(successModel); + expect(activeNotification).toBe(successModel); //Then notify of an error notificationService.notify(errorModel); //But it should be auto-dismissed and replaced with the // error notification mockTimeout.mostRecentCall.args[0](); activeNotification = notificationService.getActiveNotification(); - expect(activeNotification.model).toBe(errorModel); + expect(activeNotification).toBe(errorModel); }); it("auto-dismisses an active success notification, removing" + " it completely", function() { @@ -125,9 +151,9 @@ define( }); it("auto-minimizes an active error notification", function() { var activeNotification; - //First pre-load with a success message + //First pre-load with an error message notificationService.notify(errorModel); - //Then notify of an error + //Then notify of success notificationService.notify(successModel); expect(notificationService.notifications.length).toEqual(2); //Mock the auto-minimize @@ -137,8 +163,42 @@ define( expect(notificationService.notifications.length).toEqual(2); activeNotification = notificationService.getActiveNotification(); - expect(activeNotification.model).toBe(successModel); + expect(activeNotification).toBe(successModel); expect(errorModel.minimized).toEqual(true); + }); + it("auto-minimizes errors when a number of them arrive in" + + " short succession ", function() { + var activeNotification; + var error2 = { + title: "Second Mock Error Notification", + severity: MessageSeverity.ERROR + } + var error3 = { + title: "Third Mock Error Notification", + severity: MessageSeverity.ERROR + } + //First pre-load with a success message + notificationService.notify(errorModel); + //Then notify of a third error + notificationService.notify(error2); + notificationService.notify(error3); + expect(notificationService.notifications.length).toEqual(3); + //Mock the auto-minimize + mockTimeout.mostRecentCall.args[0](); + //Previous error message should be minimized, not + // dismissed + expect(notificationService.notifications.length).toEqual(3); + activeNotification = + notificationService.getActiveNotification(); + expect(activeNotification).toBe(error2); + expect(errorModel.minimized).toEqual(true); + + //Mock the second auto-minimize + mockTimeout.mostRecentCall.args[0](); + activeNotification = + notificationService.getActiveNotification(); + expect(activeNotification).toBe(error3); + expect(error2.minimized).toEqual(true); }); });