diff --git a/core/client/app/components/gh-alert.js b/core/client/app/components/gh-alert.js index e5fbad5c28d..d10842701b3 100644 --- a/core/client/app/components/gh-alert.js +++ b/core/client/app/components/gh-alert.js @@ -7,10 +7,9 @@ export default Ember.Component.extend({ notifications: Ember.inject.service(), - typeClass: Ember.computed(function () { + typeClass: Ember.computed('message.type', function () { var classes = '', - message = this.get('message'), - type = Ember.get(message, 'type'), + type = this.get('message.type'), typeMapping; typeMapping = { diff --git a/core/client/app/components/gh-notification.js b/core/client/app/components/gh-notification.js index 55cb48b7318..525d252db15 100644 --- a/core/client/app/components/gh-notification.js +++ b/core/client/app/components/gh-notification.js @@ -9,10 +9,9 @@ export default Ember.Component.extend({ notifications: Ember.inject.service(), - typeClass: Ember.computed(function () { + typeClass: Ember.computed('message.type', function () { var classes = '', - message = this.get('message'), - type = Ember.get(message, 'type'), + type = this.get('message.type'), typeMapping; typeMapping = { diff --git a/core/client/app/components/gh-user-invited.js b/core/client/app/components/gh-user-invited.js index 17b90ae9594..b120e9169e7 100644 --- a/core/client/app/components/gh-user-invited.js +++ b/core/client/app/components/gh-user-invited.js @@ -27,13 +27,14 @@ export default Ember.Component.extend({ // If sending the invitation email fails, the API will still return a status of 201 // but the user's status in the response object will be 'invited-pending'. if (result.users[0].status === 'invited-pending') { - notifications.showAlert('Invitation email was not sent. Please try resending.', {type: 'error'}); + notifications.showAlert('Invitation email was not sent. Please try resending.', {type: 'error', key: 'invite.resend.not-sent'}); } else { user.set('status', result.users[0].status); notifications.showNotification(notificationText); + notifications.closeAlerts('invite.resend'); } }).catch(function (error) { - notifications.showAPIError(error); + notifications.showAPIError(error, {key: 'invite.resend'}); }).finally(function () { self.set('isSending', false); }); @@ -50,15 +51,15 @@ export default Ember.Component.extend({ if (user.get('invited')) { user.destroyRecord().then(function () { var notificationText = 'Invitation revoked. (' + email + ')'; - notifications.showNotification(notificationText); + notifications.closeAlerts('invite.revoke'); }).catch(function (error) { - notifications.showAPIError(error); + notifications.showAPIError(error, {key: 'invite.revoke'}); }); } else { // if the user is no longer marked as "invited", then show a warning and reload the route self.sendAction('reload'); - notifications.showAlert('This user has already accepted the invitation.', {type: 'error', delayed: true}); + notifications.showAlert('This user has already accepted the invitation.', {type: 'error', delayed: true, key: 'invite.revoke.already-accepted'}); } }); } diff --git a/core/client/app/controllers/modals/delete-all.js b/core/client/app/controllers/modals/delete-all.js index dc19efda5c0..16cbe61a7fd 100644 --- a/core/client/app/controllers/modals/delete-all.js +++ b/core/client/app/controllers/modals/delete-all.js @@ -12,11 +12,11 @@ export default Ember.Controller.extend({ ajax(this.get('ghostPaths.url').api('db'), { type: 'DELETE' }).then(function () { - self.get('notifications').showAlert('All content deleted from database.', {type: 'success'}); + self.get('notifications').showAlert('All content deleted from database.', {type: 'success', key: 'all-content.delete.success'}); self.store.unloadAll('post'); self.store.unloadAll('tag'); }).catch(function (response) { - self.get('notifications').showAPIError(response); + self.get('notifications').showAPIError(response, {key: 'all-content.delete'}); }); }, diff --git a/core/client/app/controllers/modals/delete-post.js b/core/client/app/controllers/modals/delete-post.js index 8b876281225..b12b62d7913 100644 --- a/core/client/app/controllers/modals/delete-post.js +++ b/core/client/app/controllers/modals/delete-post.js @@ -14,9 +14,10 @@ export default Ember.Controller.extend({ model.destroyRecord().then(function () { self.get('dropdown').closeDropdowns(); + self.get('notifications').closeAlerts('post.delete'); self.transitionToRoute('posts.index'); }, function () { - self.get('notifications').showAlert('Your post could not be deleted. Please try again.', {type: 'error'}); + self.get('notifications').showAlert('Your post could not be deleted. Please try again.', {type: 'error', key: 'post.delete.failed'}); }); }, diff --git a/core/client/app/controllers/modals/delete-tag.js b/core/client/app/controllers/modals/delete-tag.js index f94908c4b34..5381daad199 100644 --- a/core/client/app/controllers/modals/delete-tag.js +++ b/core/client/app/controllers/modals/delete-tag.js @@ -15,7 +15,7 @@ export default Ember.Controller.extend({ this.send('closeMenus'); tag.destroyRecord().catch(function (error) { - self.get('notifications').showAPIError(error); + self.get('notifications').showAPIError(error, {key: 'tag.delete'}); }); }, diff --git a/core/client/app/controllers/modals/delete-user.js b/core/client/app/controllers/modals/delete-user.js index 06728700e40..b657e0f17b7 100644 --- a/core/client/app/controllers/modals/delete-user.js +++ b/core/client/app/controllers/modals/delete-user.js @@ -29,10 +29,11 @@ export default Ember.Controller.extend({ user = this.get('model'); user.destroyRecord().then(function () { + self.get('notifications').closeAlerts('user.delete'); self.store.unloadAll('post'); self.transitionToRoute('team'); }, function () { - self.get('notifications').showAlert('The user could not be deleted. Please try again.', {type: 'error'}); + self.get('notifications').showAlert('The user could not be deleted. Please try again.', {type: 'error', key: 'user.delete.failed'}); }); }, diff --git a/core/client/app/controllers/modals/invite-new-user.js b/core/client/app/controllers/modals/invite-new-user.js index 34be7bbbe91..5866d32a0bd 100644 --- a/core/client/app/controllers/modals/invite-new-user.js +++ b/core/client/app/controllers/modals/invite-new-user.js @@ -58,9 +58,9 @@ export default Ember.Controller.extend(ValidationEngine, { if (invitedUser) { if (invitedUser.get('status') === 'invited' || invitedUser.get('status') === 'invited-pending') { - self.get('notifications').showAlert('A user with that email address was already invited.', {type: 'warn'}); + self.get('notifications').showAlert('A user with that email address was already invited.', {type: 'warn', key: 'invite.send.already-invited'}); } else { - self.get('notifications').showAlert('A user with that email address already exists.', {type: 'warn'}); + self.get('notifications').showAlert('A user with that email address already exists.', {type: 'warn', key: 'invite.send.user-exists'}); } } else { newUser = self.store.createRecord('user', { @@ -75,8 +75,9 @@ export default Ember.Controller.extend(ValidationEngine, { // If sending the invitation email fails, the API will still return a status of 201 // but the user's status in the response object will be 'invited-pending'. if (newUser.get('status') === 'invited-pending') { - self.get('notifications').showAlert('Invitation email was not sent. Please try resending.', {type: 'error'}); + self.get('notifications').showAlert('Invitation email was not sent. Please try resending.', {type: 'error', key: 'invite.send.failed'}); } else { + self.get('notifications').closeAlerts('invite.send'); self.get('notifications').showNotification(notificationText); } }).catch(function (errors) { @@ -86,9 +87,9 @@ export default Ember.Controller.extend(ValidationEngine, { // want to use inline-validations here and only show an // alert if we have an actual error if (errors) { - self.get('notifications').showErrors(errors); + self.get('notifications').showErrors(errors, {key: 'invite.send'}); } else if (validationErrors) { - self.get('notifications').showAlert(validationErrors.toString(), {type: 'error'}); + self.get('notifications').showAlert(validationErrors.toString(), {type: 'error', key: 'invite.send.validation-error'}); } }).finally(function () { self.get('errors').clear(); diff --git a/core/client/app/controllers/modals/transfer-owner.js b/core/client/app/controllers/modals/transfer-owner.js index d1dd749edae..cdd98bc744b 100644 --- a/core/client/app/controllers/modals/transfer-owner.js +++ b/core/client/app/controllers/modals/transfer-owner.js @@ -33,9 +33,9 @@ export default Ember.Controller.extend({ }); } - self.get('notifications').showAlert('Ownership successfully transferred to ' + user.get('name'), {type: 'success'}); + self.get('notifications').showAlert('Ownership successfully transferred to ' + user.get('name'), {type: 'success', key: 'owner.transfer.success'}); }).catch(function (error) { - self.get('notifications').showAPIError(error); + self.get('notifications').showAPIError(error, {key: 'owner.transfer'}); }); }, diff --git a/core/client/app/controllers/modals/upload.js b/core/client/app/controllers/modals/upload.js index f11abae874f..2e423f46a74 100644 --- a/core/client/app/controllers/modals/upload.js +++ b/core/client/app/controllers/modals/upload.js @@ -12,7 +12,7 @@ export default Ember.Controller.extend({ this.get('model').save().then(function (model) { return model; }).catch(function (err) { - notifications.showAPIError(err); + notifications.showAPIError(err, {key: 'image.upload'}); }); }, diff --git a/core/client/app/controllers/reset.js b/core/client/app/controllers/reset.js index 1be35b61a2f..b66c3eaf8d7 100644 --- a/core/client/app/controllers/reset.js +++ b/core/client/app/controllers/reset.js @@ -45,13 +45,13 @@ export default Ember.Controller.extend(ValidationEngine, { } }).then(function (resp) { self.toggleProperty('submitting'); - self.get('notifications').showAlert(resp.passwordreset[0].message, {type: 'warn', delayed: true}); + self.get('notifications').showAlert(resp.passwordreset[0].message, {type: 'warn', delayed: true, key: 'password.reset'}); self.get('session').authenticate('ghost-authenticator:oauth2-password-grant', { identification: self.get('email'), password: credentials.newPassword }); }).catch(function (response) { - self.get('notifications').showAPIError(response); + self.get('notifications').showAPIError(response, {key: 'password.reset'}); self.toggleProperty('submitting'); }); }).catch(function () { diff --git a/core/client/app/controllers/settings/code-injection.js b/core/client/app/controllers/settings/code-injection.js index a197c4e1fbb..beebb21531b 100644 --- a/core/client/app/controllers/settings/code-injection.js +++ b/core/client/app/controllers/settings/code-injection.js @@ -8,7 +8,7 @@ export default Ember.Controller.extend(SettingsSaveMixin, { var notifications = this.get('notifications'); return this.get('model').save().catch(function (error) { - notifications.showAPIError(error); + notifications.showAPIError(error, {key: 'code-injection.save'}); }); } }); diff --git a/core/client/app/controllers/settings/general.js b/core/client/app/controllers/settings/general.js index 9420924de86..37b0e3c9b49 100644 --- a/core/client/app/controllers/settings/general.js +++ b/core/client/app/controllers/settings/general.js @@ -74,7 +74,7 @@ export default Ember.Controller.extend(SettingsSaveMixin, { return model; }).catch(function (error) { if (error) { - notifications.showAPIError(error); + notifications.showAPIError(error, {key: 'settings.save'}); } }); }, diff --git a/core/client/app/controllers/settings/labs.js b/core/client/app/controllers/settings/labs.js index d899fa23376..ad70797d511 100644 --- a/core/client/app/controllers/settings/labs.js +++ b/core/client/app/controllers/settings/labs.js @@ -54,12 +54,13 @@ export default Ember.Controller.extend({ self.set('session.user', self.store.findRecord('user', currentUserId)); // TODO: keep as notification, add link to view content notifications.showNotification('Import successful.'); + notifications.closeAlerts('import.upload'); }).catch(function (response) { if (response && response.jqXHR && response.jqXHR.responseJSON && response.jqXHR.responseJSON.errors) { self.set('importErrors', response.jqXHR.responseJSON.errors); } - notifications.showAlert('Import Failed', {type: 'error'}); + notifications.showAlert('Import Failed', {type: 'error', key: 'import.upload.failed'}); }).finally(function () { self.set('uploadButtonText', 'Import'); }); @@ -86,13 +87,13 @@ export default Ember.Controller.extend({ ajax(this.get('ghostPaths.url').api('mail', 'test'), { type: 'POST' }).then(function () { - notifications.showAlert('Check your email for the test message.', {type: 'info'}); + notifications.showAlert('Check your email for the test message.', {type: 'info', key: 'test-email.send.success'}); self.toggleProperty('submitting'); }).catch(function (error) { if (typeof error.jqXHR !== 'undefined') { - notifications.showAPIError(error); + notifications.showAPIError(error, {key: 'test-email.send'}); } else { - notifications.showErrors(error); + notifications.showErrors(error, {key: 'test-email.send'}); } self.toggleProperty('submitting'); }); diff --git a/core/client/app/controllers/settings/tags.js b/core/client/app/controllers/settings/tags.js index 45a746452e0..fd36f481d8f 100644 --- a/core/client/app/controllers/settings/tags.js +++ b/core/client/app/controllers/settings/tags.js @@ -50,7 +50,7 @@ export default Ember.Controller.extend(SettingsMenuMixin, { activeTag.save().catch(function (error) { if (error) { - self.notifications.showAPIError(error); + self.notifications.showAPIError(error, {key: 'tag.save'}); } }); }, diff --git a/core/client/app/controllers/setup/three.js b/core/client/app/controllers/setup/three.js index dbe3e2bc6f4..6da7dc94fd6 100644 --- a/core/client/app/controllers/setup/three.js +++ b/core/client/app/controllers/setup/three.js @@ -175,13 +175,13 @@ export default Ember.Controller.extend({ invitationsString = erroredEmails.length > 1 ? ' invitations: ' : ' invitation: '; message = 'Failed to send ' + erroredEmails.length + invitationsString; message += erroredEmails.join(', '); - notifications.showAlert(message, {type: 'error', delayed: successCount > 0}); + notifications.showAlert(message, {type: 'error', delayed: successCount > 0, key: 'signup.send-invitations.failed'}); } if (successCount > 0) { // pluralize invitationsString = successCount > 1 ? 'invitations' : 'invitation'; - notifications.showAlert(successCount + ' ' + invitationsString + ' sent!', {type: 'success', delayed: true}); + notifications.showAlert(successCount + ' ' + invitationsString + ' sent!', {type: 'success', delayed: true, key: 'signup.send-invitations.success'}); } self.send('loadServerNotifications'); self.toggleProperty('submitting'); diff --git a/core/client/app/controllers/setup/two.js b/core/client/app/controllers/setup/two.js index 8d7c92f4ad3..33ad593e426 100644 --- a/core/client/app/controllers/setup/two.js +++ b/core/client/app/controllers/setup/two.js @@ -99,7 +99,7 @@ export default Ember.Controller.extend(ValidationEngine, { self.transitionToRoute('setup.three'); }).catch(function (resp) { self.toggleProperty('submitting'); - notifications.showAPIError(resp); + notifications.showAPIError(resp, {key: 'setup.blog-details'}); }); } else { self.toggleProperty('submitting'); @@ -111,7 +111,7 @@ export default Ember.Controller.extend(ValidationEngine, { if (resp && resp.jqXHR && resp.jqXHR.responseJSON && resp.jqXHR.responseJSON.errors) { self.set('flowErrors', resp.jqXHR.responseJSON.errors[0].message); } else { - notifications.showAPIError(resp); + notifications.showAPIError(resp, {key: 'setup.blog-details'}); } }); }).catch(function () { diff --git a/core/client/app/controllers/signin.js b/core/client/app/controllers/signin.js index 6cde209caac..5349ea849b1 100644 --- a/core/client/app/controllers/signin.js +++ b/core/client/app/controllers/signin.js @@ -58,7 +58,7 @@ export default Ember.Controller.extend(ValidationEngine, { self.send('authenticate'); }).catch(function (error) { if (error) { - self.get('notifications').showAPIError(error); + self.get('notifications').showAPIError(error, {key: 'signin.authenticate'}); } else { self.set('flowErrors', 'Please fill out the form to sign in.'); } @@ -86,7 +86,7 @@ export default Ember.Controller.extend(ValidationEngine, { } }).then(function () { self.toggleProperty('submitting'); - notifications.showAlert('Please check your email for instructions.', {type: 'info'}); + notifications.showAlert('Please check your email for instructions.', {type: 'info', key: 'forgot-password.send.success'}); }).catch(function (resp) { self.toggleProperty('submitting'); if (resp && resp.jqXHR && resp.jqXHR.responseJSON && resp.jqXHR.responseJSON.errors) { @@ -98,7 +98,7 @@ export default Ember.Controller.extend(ValidationEngine, { self.get('model.errors').add('identification', ''); } } else { - notifications.showAPIError(resp, {defaultErrorText: 'There was a problem with the reset, please try again.'}); + notifications.showAPIError(resp, {defaultErrorText: 'There was a problem with the reset, please try again.', key: 'forgot-password.send'}); } }); }).catch(function () { diff --git a/core/client/app/controllers/signup.js b/core/client/app/controllers/signup.js index 039fa1069ce..465e6694e9b 100644 --- a/core/client/app/controllers/signup.js +++ b/core/client/app/controllers/signup.js @@ -74,14 +74,14 @@ export default Ember.Controller.extend(ValidationEngine, { self.sendImage(); } }).catch(function (resp) { - notifications.showAPIError(resp); + notifications.showAPIError(resp, {key: 'signup.complete'}); }); }).catch(function (resp) { self.toggleProperty('submitting'); if (resp && resp.jqXHR && resp.jqXHR.responseJSON && resp.jqXHR.responseJSON.errors) { self.set('flowErrors', resp.jqXHR.responseJSON.errors[0].message); } else { - notifications.showAPIError(resp); + notifications.showAPIError(resp, {key: 'signup.complete'}); } }); }).catch(function () { diff --git a/core/client/app/controllers/team/user.js b/core/client/app/controllers/team/user.js index 0a2db1d0e97..603f9a12538 100644 --- a/core/client/app/controllers/team/user.js +++ b/core/client/app/controllers/team/user.js @@ -125,11 +125,12 @@ export default Ember.Controller.extend(ValidationEngine, { } self.toggleProperty('submitting'); + self.get('notifications').closeAlerts('user.update'); return model; }).catch(function (errors) { if (errors) { - self.get('notifications').showErrors(errors); + self.get('notifications').showErrors(errors, {key: 'user.update'}); } self.toggleProperty('submitting'); @@ -151,15 +152,15 @@ export default Ember.Controller.extend(ValidationEngine, { ne2Password: '' }); - self.get('notifications').showAlert('Password updated.', {type: 'success'}); + self.get('notifications').showAlert('Password updated.', {type: 'success', key: 'user.change-password.success'}); return model; }).catch(function (errors) { - self.get('notifications').showAPIError(errors); + self.get('notifications').showAPIError(errors, {key: 'user.change-password'}); }); } else { // TODO: switch to in-line validation - self.get('notifications').showErrors(user.get('passwordValidationErrors')); + self.get('notifications').showErrors(user.get('passwordValidationErrors'), {key: 'user.change-password'}); } }, diff --git a/core/client/app/mixins/editor-base-controller.js b/core/client/app/mixins/editor-base-controller.js index f49fd085661..88dd9e8805f 100644 --- a/core/client/app/mixins/editor-base-controller.js +++ b/core/client/app/mixins/editor-base-controller.js @@ -246,7 +246,7 @@ export default Ember.Mixin.create({ message += '
' + error; - notifications.showAlert(message.htmlSafe(), {type: 'error', delayed: delay}); + notifications.showAlert(message.htmlSafe(), {type: 'error', delayed: delay, key: 'post.save'}); }, actions: { diff --git a/core/client/app/mixins/pagination-route.js b/core/client/app/mixins/pagination-route.js index 9a95eb9dc60..e4882fb232b 100644 --- a/core/client/app/mixins/pagination-route.js +++ b/core/client/app/mixins/pagination-route.js @@ -37,7 +37,7 @@ export default Ember.Mixin.create({ message += '.'; } - this.get('notifications').showAlert(message, {type: 'error'}); + this.get('notifications').showAlert(message, {type: 'error', key: 'pagination.load.failed'}); }, loadFirstPage: function () { diff --git a/core/client/app/routes/application.js b/core/client/app/routes/application.js index 636e72cebe9..9c28628fffa 100644 --- a/core/client/app/routes/application.js +++ b/core/client/app/routes/application.js @@ -52,6 +52,7 @@ export default Ember.Route.extend(ApplicationRouteMixin, ShortcutsRoute, { }, signedIn: function () { + this.get('notifications').clearAll(); this.send('loadServerNotifications', true); }, @@ -67,7 +68,7 @@ export default Ember.Route.extend(ApplicationRouteMixin, ShortcutsRoute, { }); } else { // Connection errors don't return proper status message, only req.body - this.get('notifications').showAlert('There was a problem on the server.', {type: 'error'}); + this.get('notifications').showAlert('There was a problem on the server.', {type: 'error', key: 'session.authenticate.failed'}); } }, @@ -92,7 +93,7 @@ export default Ember.Route.extend(ApplicationRouteMixin, ShortcutsRoute, { }, sessionInvalidationFailed: function (error) { - this.get('notifications').showAlert(error.message, {type: 'error'}); + this.get('notifications').showAlert(error.message, {type: 'error', key: 'session.invalidate.failed'}); }, openModal: function (modalName, model, type) { diff --git a/core/client/app/routes/reset.js b/core/client/app/routes/reset.js index e2e98a6d121..df13e9beb87 100644 --- a/core/client/app/routes/reset.js +++ b/core/client/app/routes/reset.js @@ -9,7 +9,7 @@ export default Ember.Route.extend(styleBody, { beforeModel: function () { if (this.get('session').isAuthenticated) { - this.get('notifications').showAlert('You can\'t reset your password while you\'re signed in.', {type: 'warn', delayed: true}); + this.get('notifications').showAlert('You can\'t reset your password while you\'re signed in.', {type: 'warn', delayed: true, key: 'password.reset.signed-in'}); this.transitionTo(Configuration.routeAfterAuthentication); } }, diff --git a/core/client/app/routes/signout.js b/core/client/app/routes/signout.js index 12e0925d6df..d543c257af8 100644 --- a/core/client/app/routes/signout.js +++ b/core/client/app/routes/signout.js @@ -10,7 +10,7 @@ export default AuthenticatedRoute.extend(styleBody, { notifications: Ember.inject.service(), afterModel: function (model, transition) { - this.get('notifications').closeAll(); + this.get('notifications').clearAll(); if (Ember.canInvoke(transition, 'send')) { transition.send('invalidateSession'); transition.abort(); diff --git a/core/client/app/routes/signup.js b/core/client/app/routes/signup.js index b7a900c746f..223382771b5 100644 --- a/core/client/app/routes/signup.js +++ b/core/client/app/routes/signup.js @@ -12,7 +12,7 @@ export default Ember.Route.extend(styleBody, { beforeModel: function () { if (this.get('session').isAuthenticated) { - this.get('notifications').showAlert('You need to sign out to register as a new user.', {type: 'warn', delayed: true}); + this.get('notifications').showAlert('You need to sign out to register as a new user.', {type: 'warn', delayed: true, key: 'signup.create.already-authenticated'}); this.transitionTo(Configuration.routeAfterAuthentication); } }, @@ -26,7 +26,7 @@ export default Ember.Route.extend(styleBody, { return new Ember.RSVP.Promise(function (resolve) { if (!re.test(params.token)) { - self.get('notifications').showAlert('Invalid token.', {type: 'error', delayed: true}); + self.get('notifications').showAlert('Invalid token.', {type: 'error', delayed: true, key: 'signup.create.invalid-token'}); return resolve(self.transitionTo('signin')); } @@ -47,7 +47,7 @@ export default Ember.Route.extend(styleBody, { } }).then(function (response) { if (response && response.invitation && response.invitation[0].valid === false) { - self.get('notifications').showAlert('The invitation does not exist or is no longer valid.', {type: 'warn', delayed: true}); + self.get('notifications').showAlert('The invitation does not exist or is no longer valid.', {type: 'warn', delayed: true, key: 'signup.create.invalid-invitation'}); return resolve(self.transitionTo('signin')); } diff --git a/core/client/app/services/notifications.js b/core/client/app/services/notifications.js index f178ee36ca6..eeba85c4daf 100644 --- a/core/client/app/services/notifications.js +++ b/core/client/app/services/notifications.js @@ -1,5 +1,14 @@ import Ember from 'ember'; +// Notification keys take the form of "noun.verb.message", eg: +// +// "invite.resend.api-error" +// "user.invite.already-invited" +// +// The "noun.verb" part will be used as the "key base" in duplicate checks +// to avoid stacking of multiple error messages whilst leaving enough +// specificity to re-use keys for i18n lookups + export default Ember.Service.extend({ delayedNotifications: Ember.A(), content: Ember.A(), @@ -24,6 +33,11 @@ export default Ember.Service.extend({ Ember.set(message, 'status', 'notification'); } + // close existing duplicate alerts/notifications to avoid stacking + if (Ember.get(message, 'key')) { + this._removeItems(Ember.get(message, 'status'), Ember.get(message, 'key')); + } + if (!delayed) { this.get('content').pushObject(message); } else { @@ -37,7 +51,8 @@ export default Ember.Service.extend({ this.handleNotification({ message: message, status: 'alert', - type: options.type + type: options.type, + key: options.key }, options.delayed); }, @@ -46,31 +61,43 @@ export default Ember.Service.extend({ if (!options.doNotCloseNotifications) { this.closeNotifications(); + } else { + // TODO: this should be removed along with showErrors + options.key = undefined; } this.handleNotification({ message: message, status: 'notification', - type: options.type + type: options.type, + key: options.key }, options.delayed); }, // TODO: review whether this can be removed once no longer used by validations showErrors: function (errors, options) { options = options || {}; + options.type = options.type || 'error'; + // TODO: getting keys from the server would be useful here (necessary for i18n) + options.key = (options.key && `${options.key}.api-error`) || 'api-error'; if (!options.doNotCloseNotifications) { this.closeNotifications(); } + // ensure all errors that are passed in get shown + options.doNotCloseNotifications = true; + for (var i = 0; i < errors.length; i += 1) { - this.showNotification(errors[i].message || errors[i], {type: 'error', doNotCloseNotifications: true}); + this.showNotification(errors[i].message || errors[i], options); } }, showAPIError: function (resp, options) { options = options || {}; options.type = options.type || 'error'; + // TODO: getting keys from the server would be useful here (necessary for i18n) + options.key = (options.key && `${options.key}.api-error`) || 'api-error'; if (!options.doNotCloseNotifications) { this.closeNotifications(); @@ -85,7 +112,7 @@ export default Ember.Service.extend({ } else if (resp && resp.jqXHR && resp.jqXHR.responseJSON && resp.jqXHR.responseJSON.message) { this.showAlert(resp.jqXHR.responseJSON.message, options); } else { - this.showAlert(options.defaultErrorText, {type: options.type, doNotCloseNotifications: true}); + this.showAlert(options.defaultErrorText, options); } }, @@ -111,11 +138,40 @@ export default Ember.Service.extend({ } }, - closeNotifications: function () { - this.set('content', this.get('content').rejectBy('status', 'notification')); + closeNotifications: function (key) { + this._removeItems('notification', key); + }, + + closeAlerts: function (key) { + this._removeItems('alert', key); }, - closeAll: function () { + clearAll: function () { this.get('content').clear(); + }, + + _removeItems: function (status, key) { + if (key) { + let keyBase = this._getKeyBase(key), + // TODO: keys should only have . special char but we should + // probably use a better regexp escaping function/polyfill + escapedKeyBase = keyBase.replace('.', '\\.'), + keyRegex = new RegExp(`^${escapedKeyBase}`); + + this.set('content', this.get('content').reject(function (item) { + let itemKey = Ember.get(item, 'key'), + itemStatus = Ember.get(item, 'status'); + + return itemStatus === status && (itemKey && itemKey.match(keyRegex)); + })); + } else { + this.set('content', this.get('content').rejectBy('status', status)); + } + }, + + // take a key and return the first two elements, eg: + // "invite.revoke.failed" => "invite.revoke" + _getKeyBase: function (key) { + return key.split('.').slice(0, 2).join('.'); } }); diff --git a/core/client/tests/integration/components/gh-alert-test.js b/core/client/tests/integration/components/gh-alert-test.js new file mode 100644 index 00000000000..aa976aa4a4c --- /dev/null +++ b/core/client/tests/integration/components/gh-alert-test.js @@ -0,0 +1,46 @@ +/* jshint expr:true */ +import { expect } from 'chai'; +import { + describeComponent, + it +} from 'ember-mocha'; +import hbs from 'htmlbars-inline-precompile'; + +describeComponent( + 'gh-alert', + 'Integration: Component: gh-alert', + { + integration: true + }, + function () { + it('renders', function () { + this.set('message', {message: 'Test message', type: 'success'}); + + this.render(hbs`{{gh-alert message=message}}`); + + expect(this.$('article.gh-alert')).to.have.length(1); + let $alert = this.$('.gh-alert'); + + expect($alert.text()).to.match(/Test message/); + }); + + it('maps message types to CSS classes', function () { + this.set('message', {message: 'Test message', type: 'success'}); + + this.render(hbs`{{gh-alert message=message}}`); + let $alert = this.$('.gh-alert'); + + this.set('message.type', 'success'); + expect($alert.hasClass('gh-alert-green'), 'success class isn\'t green').to.be.true; + + this.set('message.type', 'error'); + expect($alert.hasClass('gh-alert-red'), 'success class isn\'t red').to.be.true; + + this.set('message.type', 'warn'); + expect($alert.hasClass('gh-alert-yellow'), 'success class isn\'t yellow').to.be.true; + + this.set('message.type', 'info'); + expect($alert.hasClass('gh-alert-blue'), 'success class isn\'t blue').to.be.true; + }); + } +); diff --git a/core/client/tests/integration/components/gh-alerts-test.js b/core/client/tests/integration/components/gh-alerts-test.js new file mode 100644 index 00000000000..958eb42acb3 --- /dev/null +++ b/core/client/tests/integration/components/gh-alerts-test.js @@ -0,0 +1,56 @@ +/* jshint expr:true */ +import { expect } from 'chai'; +import { + describeComponent, + it +} from 'ember-mocha'; +import hbs from 'htmlbars-inline-precompile'; +import Ember from 'ember'; + +const {run} = Ember, + notificationsStub = Ember.Service.extend({ + alerts: Ember.A() + }); + +describeComponent( + 'gh-alerts', + 'Integration: Component: gh-alerts', + { + integration: true + }, + function () { + beforeEach(function () { + this.register('service:notifications', notificationsStub); + this.inject.service('notifications', {as: 'notifications'}); + + this.set('notifications.alerts', [ + {message: 'First', type: 'error'}, + {message: 'Second', type: 'warn'} + ]); + }); + + it('renders', function () { + this.render(hbs`{{gh-alerts}}`); + expect(this.$('.gh-alerts').length).to.equal(1); + expect(this.$('.gh-alerts').children().length).to.equal(2); + + this.set('notifications.alerts', Ember.A()); + expect(this.$('.gh-alerts').children().length).to.equal(0); + }); + + it('triggers "notify" action when message count changes', function () { + let expectedCount = 0; + + // test double for notify action + this.set('notify', (count) => expect(count).to.equal(expectedCount)); + + this.render(hbs`{{gh-alerts notify=(action notify)}}`); + + expectedCount = 3; + this.get('notifications.alerts').pushObject({message: 'Third', type: 'success'}); + + expectedCount = 0; + this.set('notifications.alerts', Ember.A()); + }); + } +); diff --git a/core/client/tests/integration/components/gh-notification-test.js b/core/client/tests/integration/components/gh-notification-test.js new file mode 100644 index 00000000000..37e59d7d0fc --- /dev/null +++ b/core/client/tests/integration/components/gh-notification-test.js @@ -0,0 +1,44 @@ +/* jshint expr:true */ +import { expect } from 'chai'; +import { + describeComponent, + it +} from 'ember-mocha'; +import hbs from 'htmlbars-inline-precompile'; + +describeComponent( + 'gh-notification', + 'Integration: Component: gh-notification', + { + integration: true + }, + function () { + it('renders', function () { + this.set('message', {message: 'Test message', type: 'success'}); + + this.render(hbs`{{gh-notification message=message}}`); + + expect(this.$('article.gh-notification')).to.have.length(1); + let $notification = this.$('.gh-notification'); + + expect($notification.hasClass('gh-notification-passive')).to.be.true; + expect($notification.text()).to.match(/Test message/); + }); + + it('maps message types to CSS classes', function () { + this.set('message', {message: 'Test message', type: 'success'}); + + this.render(hbs`{{gh-notification message=message}}`); + let $notification = this.$('.gh-notification'); + + this.set('message.type', 'success'); + expect($notification.hasClass('gh-notification-green'), 'success class isn\'t green').to.be.true; + + this.set('message.type', 'error'); + expect($notification.hasClass('gh-notification-red'), 'success class isn\'t red').to.be.true; + + this.set('message.type', 'warn'); + expect($notification.hasClass('gh-notification-yellow'), 'success class isn\'t yellow').to.be.true; + }); + } +); diff --git a/core/client/tests/integration/components/gh-notifications-test.js b/core/client/tests/integration/components/gh-notifications-test.js new file mode 100644 index 00000000000..843ea999d74 --- /dev/null +++ b/core/client/tests/integration/components/gh-notifications-test.js @@ -0,0 +1,42 @@ +/* jshint expr:true */ +import { expect } from 'chai'; +import { + describeComponent, + it +} from 'ember-mocha'; +import hbs from 'htmlbars-inline-precompile'; +import Ember from 'ember'; + +const {run} = Ember, + notificationsStub = Ember.Service.extend({ + notifications: Ember.A() + }); + +describeComponent( + 'gh-notifications', + 'Integration: Component: gh-notifications', + { + integration: true + }, + function () { + beforeEach(function () { + this.register('service:notifications', notificationsStub); + this.inject.service('notifications', {as: 'notifications'}); + + this.set('notifications.notifications', [ + {message: 'First', type: 'error'}, + {message: 'Second', type: 'warn'} + ]); + }); + + it('renders', function () { + this.render(hbs`{{gh-notifications}}`); + expect(this.$('.gh-notifications').length).to.equal(1); + + expect(this.$('.gh-notifications').children().length).to.equal(2); + + this.set('notifications.notifications', Ember.A()); + expect(this.$('.gh-notifications').children().length).to.equal(0); + }); + } +); diff --git a/core/client/tests/unit/components/gh-alert-test.js b/core/client/tests/unit/components/gh-alert-test.js index 31286a2b91b..541b4a71ab2 100644 --- a/core/client/tests/unit/components/gh-alert-test.js +++ b/core/client/tests/unit/components/gh-alert-test.js @@ -16,46 +16,6 @@ describeComponent( // needs: ['component:foo', 'helper:bar'] }, function () { - it('renders', function () { - // creates the component instance - var component = this.subject(); - expect(component._state).to.equal('preRender'); - - component.set('message', {message: 'Test message', type: 'success'}); - - // renders the component on the page - this.render(); - expect(component._state).to.equal('inDOM'); - - expect(this.$().prop('tagName')).to.equal('ARTICLE'); - expect(this.$().hasClass('gh-alert')).to.be.true; - expect(this.$().text()).to.match(/Test message/); - }); - - it('maps success alert type to correct class', function () { - var component = this.subject(); - component.set('message', {message: 'Test message', type: 'success'}); - expect(this.$().hasClass('gh-alert-green')).to.be.true; - }); - - it('maps error alert type to correct class', function () { - var component = this.subject(); - component.set('message', {message: 'Test message', type: 'error'}); - expect(this.$().hasClass('gh-alert-red')).to.be.true; - }); - - it('maps warn alert type to correct class', function () { - var component = this.subject(); - component.set('message', {message: 'Test message', type: 'warn'}); - expect(this.$().hasClass('gh-alert-yellow')).to.be.true; - }); - - it('maps info alert type to correct class', function () { - var component = this.subject(); - component.set('message', {message: 'Test message', type: 'info'}); - expect(this.$().hasClass('gh-alert-blue')).to.be.true; - }); - it('closes notification through notifications service', function () { var component = this.subject(), notifications = {}, diff --git a/core/client/tests/unit/components/gh-alerts-test.js b/core/client/tests/unit/components/gh-alerts-test.js deleted file mode 100644 index 41d4df02b0f..00000000000 --- a/core/client/tests/unit/components/gh-alerts-test.js +++ /dev/null @@ -1,65 +0,0 @@ -/* jshint expr:true */ -import Ember from 'ember'; -import { expect } from 'chai'; -import { - describeComponent, - it -} -from 'ember-mocha'; -import sinon from 'sinon'; - -describeComponent( - 'gh-alerts', - 'Unit: Component: gh-alerts', - { - unit: true, - // specify the other units that are required for this test - needs: ['component:gh-alert'] - }, - function () { - beforeEach(function () { - // Stub the notifications service - var notifications = Ember.Object.create(); - notifications.alerts = Ember.A(); - notifications.alerts.pushObject({message: 'First', type: 'error'}); - notifications.alerts.pushObject({message: 'Second', type: 'warn'}); - - this.subject().set('notifications', notifications); - }); - - it('renders', function () { - // creates the component instance - var component = this.subject(); - expect(component._state).to.equal('preRender'); - - // renders the component on the page - this.render(); - expect(component._state).to.equal('inDOM'); - - expect(this.$().prop('tagName')).to.equal('ASIDE'); - expect(this.$().hasClass('gh-alerts')).to.be.true; - expect(this.$().children().length).to.equal(2); - - Ember.run(function () { - component.set('notifications.alerts', Ember.A()); - }); - - expect(this.$().children().length).to.equal(0); - }); - - it('triggers "notify" action when message count changes', function () { - var component = this.subject(); - - component.sendAction = sinon.spy(); - - component.get('notifications.alerts') - .pushObject({message: 'New alert', type: 'info'}); - - expect(component.sendAction.calledWith('notify', 3)).to.be.true; - - component.set('notifications.alerts', Ember.A()); - - expect(component.sendAction.calledWith('notify', 0)).to.be.true; - }); - } -); diff --git a/core/client/tests/unit/components/gh-notification-test.js b/core/client/tests/unit/components/gh-notification-test.js index 6db8e125c20..1dc05784567 100644 --- a/core/client/tests/unit/components/gh-notification-test.js +++ b/core/client/tests/unit/components/gh-notification-test.js @@ -16,40 +16,6 @@ describeComponent( // needs: ['component:foo', 'helper:bar'] }, function () { - it('renders', function () { - // creates the component instance - var component = this.subject(); - expect(component._state).to.equal('preRender'); - - component.set('message', {message: 'Test message', type: 'success'}); - - // renders the component on the page - this.render(); - expect(component._state).to.equal('inDOM'); - - expect(this.$().prop('tagName')).to.equal('ARTICLE'); - expect(this.$().is('.gh-notification, .gh-notification-passive')).to.be.true; - expect(this.$().text()).to.match(/Test message/); - }); - - it('maps success alert type to correct class', function () { - var component = this.subject(); - component.set('message', {message: 'Test message', type: 'success'}); - expect(this.$().hasClass('gh-notification-green')).to.be.true; - }); - - it('maps error alert type to correct class', function () { - var component = this.subject(); - component.set('message', {message: 'Test message', type: 'error'}); - expect(this.$().hasClass('gh-notification-red')).to.be.true; - }); - - it('maps warn alert type to correct class', function () { - var component = this.subject(); - component.set('message', {message: 'Test message', type: 'warn'}); - expect(this.$().hasClass('gh-notification-yellow')).to.be.true; - }); - it('closes notification through notifications service', function () { var component = this.subject(), notifications = {}, diff --git a/core/client/tests/unit/components/gh-notifications-test.js b/core/client/tests/unit/components/gh-notifications-test.js deleted file mode 100644 index b5855a6cb43..00000000000 --- a/core/client/tests/unit/components/gh-notifications-test.js +++ /dev/null @@ -1,47 +0,0 @@ -/* jshint expr:true */ -import Ember from 'ember'; -import { expect } from 'chai'; -import { - describeComponent, - it -} -from 'ember-mocha'; - -describeComponent( - 'gh-notifications', - 'Unit: Component: gh-notifications', { - unit: true, - needs: ['component:gh-notification'] - }, - function () { - beforeEach(function () { - // Stub the notifications service - var notifications = Ember.Object.create(); - notifications.notifications = Ember.A(); - notifications.notifications.pushObject({message: 'First', type: 'error'}); - notifications.notifications.pushObject({message: 'Second', type: 'warn'}); - - this.subject().set('notifications', notifications); - }); - - it('renders', function () { - // creates the component instance - var component = this.subject(); - expect(component._state).to.equal('preRender'); - - // renders the component on the page - this.render(); - expect(component._state).to.equal('inDOM'); - - expect(this.$().prop('tagName')).to.equal('ASIDE'); - expect(this.$().hasClass('gh-notifications')).to.be.true; - expect(this.$().children().length).to.equal(2); - - Ember.run(function () { - component.set('notifications.notifications', Ember.A()); - }); - - expect(this.$().children().length).to.equal(0); - }); - } -); diff --git a/core/client/tests/unit/services/notifications-test.js b/core/client/tests/unit/services/notifications-test.js index f46bfda647f..8a9f1553318 100644 --- a/core/client/tests/unit/services/notifications-test.js +++ b/core/client/tests/unit/services/notifications-test.js @@ -7,6 +7,8 @@ import { it } from 'ember-mocha'; +const {run, get} = Ember; + describeModule( 'service:notifications', 'Unit: Service: notifications', @@ -23,16 +25,17 @@ describeModule( it('filters alerts/notifications', function () { var notifications = this.subject(); - notifications.set('content', [ - {message: 'Alert', status: 'alert'}, - {message: 'Notification', status: 'notification'} - ]); + // wrapped in run-loop to enure alerts/notifications CPs are updated + run(() => { + notifications.showAlert('Alert'); + notifications.showNotification('Notification'); + }); - expect(notifications.get('alerts')) - .to.deep.equal([{message: 'Alert', status: 'alert'}]); + expect(notifications.get('alerts.length')).to.equal(1); + expect(notifications.get('alerts.firstObject.message')).to.equal('Alert'); - expect(notifications.get('notifications')) - .to.deep.equal([{message: 'Notification', status: 'notification'}]); + expect(notifications.get('notifications.length')).to.equal(1); + expect(notifications.get('notifications.firstObject.message')).to.equal('Notification'); }); it('#handleNotification deals with DS.Notification notifications', function () { @@ -61,71 +64,106 @@ describeModule( it('#showAlert adds POJO alerts', function () { var notifications = this.subject(); - notifications.showAlert('Test Alert', {type: 'error'}); + run(() => { + notifications.showAlert('Test Alert', {type: 'error'}); + }); expect(notifications.get('alerts')) - .to.deep.include({message: 'Test Alert', status: 'alert', type: 'error'}); + .to.deep.include({message: 'Test Alert', status: 'alert', type: 'error', key: undefined}); }); it('#showAlert adds delayed notifications', function () { var notifications = this.subject(); - notifications.showNotification('Test Alert', {type: 'error', delayed: true}); + run(() => { + notifications.showNotification('Test Alert', {type: 'error', delayed: true}); + }); expect(notifications.get('delayedNotifications')) - .to.deep.include({message: 'Test Alert', status: 'notification', type: 'error'}); + .to.deep.include({message: 'Test Alert', status: 'notification', type: 'error', key: undefined}); + }); + + // in order to cater for complex keys that are suitable for i18n + // we split on the second period and treat the resulting base as + // the key for duplicate checking + it('#showAlert clears duplicates', function () { + var notifications = this.subject(); + + run(() => { + notifications.showAlert('Kept'); + notifications.showAlert('Duplicate', {key: 'duplicate.key.fail'}); + }); + + expect(notifications.get('alerts.length')).to.equal(2); + + run(() => { + notifications.showAlert('Duplicate with new message', {key: 'duplicate.key.success'}); + }); + + expect(notifications.get('alerts.length')).to.equal(2); + expect(notifications.get('alerts.lastObject.message')).to.equal('Duplicate with new message'); }); it('#showNotification adds POJO notifications', function () { var notifications = this.subject(); - notifications.showNotification('Test Notification', {type: 'success'}); + run(() => { + notifications.showNotification('Test Notification', {type: 'success'}); + }); expect(notifications.get('notifications')) - .to.deep.include({message: 'Test Notification', status: 'notification', type: 'success'}); + .to.deep.include({message: 'Test Notification', status: 'notification', type: 'success', key: undefined}); }); it('#showNotification adds delayed notifications', function () { var notifications = this.subject(); - notifications.showNotification('Test Notification', {delayed: true}); + run(() => { + notifications.showNotification('Test Notification', {delayed: true}); + }); expect(notifications.get('delayedNotifications')) - .to.deep.include({message: 'Test Notification', status: 'notification', type: undefined}); + .to.deep.include({message: 'Test Notification', status: 'notification', type: undefined, key: undefined}); }); it('#showNotification clears existing notifications', function () { var notifications = this.subject(); - notifications.showNotification('First'); - notifications.showNotification('Second'); + run(() => { + notifications.showNotification('First'); + notifications.showNotification('Second'); + }); - expect(notifications.get('content.length')).to.equal(1); - expect(notifications.get('content')) - .to.deep.equal([{message: 'Second', status: 'notification', type: undefined}]); + expect(notifications.get('notifications.length')).to.equal(1); + expect(notifications.get('notifications')) + .to.deep.equal([{message: 'Second', status: 'notification', type: undefined, key: undefined}]); }); it('#showNotification keeps existing notifications if doNotCloseNotifications option passed', function () { var notifications = this.subject(); - notifications.showNotification('First'); - notifications.showNotification('Second', {doNotCloseNotifications: true}); + run(() => { + notifications.showNotification('First'); + notifications.showNotification('Second', {doNotCloseNotifications: true}); + }); - expect(notifications.get('content.length')).to.equal(2); + expect(notifications.get('notifications.length')).to.equal(2); }); // TODO: review whether this can be removed once it's no longer used by validations it('#showErrors adds multiple notifications', function () { var notifications = this.subject(); - notifications.showErrors([ - {message: 'First'}, - {message: 'Second'} - ]); + run(() => { + notifications.showErrors([ + {message: 'First'}, + {message: 'Second'} + ]); + }); - expect(notifications.get('content')).to.deep.equal([ - {message: 'First', status: 'notification', type: 'error'}, - {message: 'Second', status: 'notification', type: 'error'} + expect(notifications.get('notifications')).to.deep.equal([ + {message: 'First', status: 'notification', type: 'error', key: undefined}, + {message: 'Second', status: 'notification', type: 'error', key: undefined} ]); }); @@ -133,11 +171,15 @@ describeModule( var notifications = this.subject(), resp = {jqXHR: {responseJSON: {error: 'Single error'}}}; - notifications.showAPIError(resp); + run(() => { + notifications.showAPIError(resp); + }); - expect(notifications.get('content')).to.deep.equal([ - {message: 'Single error', status: 'alert', type: 'error'} - ]); + let notification = notifications.get('alerts.firstObject'); + expect(get(notification, 'message')).to.equal('Single error'); + expect(get(notification, 'status')).to.equal('alert'); + expect(get(notification, 'type')).to.equal('error'); + expect(get(notification, 'key')).to.equal('api-error'); }); // used to display validation errors returned from the server @@ -145,11 +187,13 @@ describeModule( var notifications = this.subject(), resp = {jqXHR: {responseJSON: {errors: ['First error', 'Second error']}}}; - notifications.showAPIError(resp); + run(() => { + notifications.showAPIError(resp); + }); - expect(notifications.get('content')).to.deep.equal([ - {message: 'First error', status: 'notification', type: 'error'}, - {message: 'Second error', status: 'notification', type: 'error'} + expect(notifications.get('notifications')).to.deep.equal([ + {message: 'First error', status: 'notification', type: 'error', key: undefined}, + {message: 'Second error', status: 'notification', type: 'error', key: undefined} ]); }); @@ -157,43 +201,70 @@ describeModule( var notifications = this.subject(), resp = {jqXHR: {responseJSON: {message: 'Single message'}}}; - notifications.showAPIError(resp); + run(() => { + notifications.showAPIError(resp); + }); - expect(notifications.get('content')).to.deep.equal([ - {message: 'Single message', status: 'alert', type: 'error'} - ]); + let notification = notifications.get('alerts.firstObject'); + expect(get(notification, 'message')).to.equal('Single message'); + expect(get(notification, 'status')).to.equal('alert'); + expect(get(notification, 'type')).to.equal('error'); + expect(get(notification, 'key')).to.equal('api-error'); }); it('#showAPIError displays default error text if response has no error/message', function () { var notifications = this.subject(), resp = {}; - notifications.showAPIError(resp); + run(() => { notifications.showAPIError(resp); }); expect(notifications.get('content')).to.deep.equal([ - {message: 'There was a problem on the server, please try again.', status: 'alert', type: 'error'} + {message: 'There was a problem on the server, please try again.', status: 'alert', type: 'error', key: 'api-error'} ]); notifications.set('content', Ember.A()); - notifications.showAPIError(resp, {defaultErrorText: 'Overridden default'}); + run(() => { + notifications.showAPIError(resp, {defaultErrorText: 'Overridden default'}); + }); expect(notifications.get('content')).to.deep.equal([ - {message: 'Overridden default', status: 'alert', type: 'error'} + {message: 'Overridden default', status: 'alert', type: 'error', key: 'api-error'} ]); }); - it('#displayDelayed moves delayed notifications into content', function () { + it('#showAPIError sets correct key when passed a base key', function () { var notifications = this.subject(); - notifications.showNotification('First', {delayed: true}); - notifications.showNotification('Second', {delayed: true}); - notifications.showNotification('Third', {delayed: false}); + run(() => { + notifications.showAPIError('Test', {key: 'test.alert'}); + }); - notifications.displayDelayed(); + expect(notifications.get('alerts.firstObject.key')).to.equal('test.alert.api-error'); + }); - expect(notifications.get('content')).to.deep.equal([ - {message: 'Third', status: 'notification', type: undefined}, - {message: 'First', status: 'notification', type: undefined}, - {message: 'Second', status: 'notification', type: undefined} + it('#showAPIError sets correct key when not passed a key', function () { + var notifications = this.subject(); + + run(() => { + notifications.showAPIError('Test'); + }); + + expect(notifications.get('alerts.firstObject.key')).to.equal('api-error'); + }); + + it('#displayDelayed moves delayed notifications into content', function () { + var notifications = this.subject(); + + run(() => { + notifications.showNotification('First', {delayed: true}); + notifications.showNotification('Second', {delayed: true}); + notifications.showNotification('Third', {delayed: false}); + notifications.displayDelayed(); + }); + + expect(notifications.get('notifications')).to.deep.equal([ + {message: 'Third', status: 'notification', type: undefined, key: undefined}, + {message: 'First', status: 'notification', type: undefined, key: undefined}, + {message: 'Second', status: 'notification', type: undefined, key: undefined} ]); }); @@ -201,12 +272,16 @@ describeModule( var notification = {message: 'Close test', status: 'notification'}, notifications = this.subject(); - notifications.handleNotification(notification); + run(() => { + notifications.handleNotification(notification); + }); expect(notifications.get('notifications')) .to.include(notification); - notifications.closeNotification(notification); + run(() => { + notifications.closeNotification(notification); + }); expect(notifications.get('notifications')) .to.not.include(notification); @@ -226,40 +301,59 @@ describeModule( }; sinon.spy(notification, 'save'); - notifications.handleNotification(notification); + run(() => { notifications.handleNotification(notification); }); + expect(notifications.get('alerts')).to.include(notification); - notifications.closeNotification(notification); + run(() => { notifications.closeNotification(notification); }); expect(notification.deleteRecord.calledOnce).to.be.true; expect(notification.save.calledOnce).to.be.true; - // wrap in runloop so filter updates - Ember.run.next(function () { - expect(notifications.get('alerts')).to.not.include(notification); - }); + expect(notifications.get('alerts')).to.not.include(notification); }); it('#closeNotifications only removes notifications', function () { var notifications = this.subject(); - notifications.showAlert('First alert'); - notifications.showNotification('First notification'); - notifications.showNotification('Second notification', {doNotCloseNotifications: true}); + run(() => { + notifications.showAlert('First alert'); + notifications.showNotification('First notification'); + notifications.showNotification('Second notification', {doNotCloseNotifications: true}); + }); - expect(notifications.get('alerts.length')).to.equal(1); - expect(notifications.get('notifications.length')).to.equal(2); + expect(notifications.get('alerts.length'), 'alerts count').to.equal(1); + expect(notifications.get('notifications.length'), 'notifications count').to.equal(2); + + run(() => { notifications.closeNotifications(); }); - notifications.closeNotifications(); + expect(notifications.get('alerts.length'), 'alerts count').to.equal(1); + expect(notifications.get('notifications.length'), 'notifications count').to.equal(0); + }); + + it('#closeNotifications only closes notifications with specified key', function () { + var notifications = this.subject(); - // wrap in runloop so filter updates - Ember.run.next(function () { - expect(notifications.get('alerts.length')).to.equal(1); - expect(notifications.get('notifications.length')).to.equal(1); + run(() => { + notifications.showAlert('First alert'); + // using handleNotification as showNotification will auto-prune + // duplicates and keys will be removed if doNotCloseNotifications + // is true + notifications.handleNotification({message: 'First notification', key: 'test.close', status: 'notification'}); + notifications.handleNotification({message: 'Second notification', key: 'test.keep', status: 'notification'}); + notifications.handleNotification({message: 'Third notification', key: 'test.close', status: 'notification'}); }); + + run(() => { + notifications.closeNotifications('test.close'); + }); + + expect(notifications.get('notifications.length'), 'notifications count').to.equal(1); + expect(notifications.get('notifications.firstObject.message'), 'notification message').to.equal('Second notification'); + expect(notifications.get('alerts.length'), 'alerts count').to.equal(1); }); - it('#closeAll removes everything without deletion', function () { + it('#clearAll removes everything without deletion', function () { var notifications = this.subject(), notificationModel = Ember.Object.create({message: 'model'}); @@ -276,11 +370,43 @@ describeModule( notifications.handleNotification(notificationModel); notifications.handleNotification({message: 'pojo'}); - notifications.closeAll(); + notifications.clearAll(); expect(notifications.get('content')).to.be.empty; expect(notificationModel.deleteRecord.called).to.be.false; expect(notificationModel.save.called).to.be.false; }); + + it('#closeAlerts only removes alerts', function () { + var notifications = this.subject(); + + notifications.showNotification('First notification'); + notifications.showAlert('First alert'); + notifications.showAlert('Second alert'); + + run(() => { + notifications.closeAlerts(); + }); + + expect(notifications.get('alerts.length')).to.equal(0); + expect(notifications.get('notifications.length')).to.equal(1); + }); + + it('#closeAlerts closes only alerts with specified key', function () { + var notifications = this.subject(); + + notifications.showNotification('First notification'); + notifications.showAlert('First alert', {key: 'test.close'}); + notifications.showAlert('Second alert', {key: 'test.keep'}); + notifications.showAlert('Third alert', {key: 'test.close'}); + + run(() => { + notifications.closeAlerts('test.close'); + }); + + expect(notifications.get('alerts.length')).to.equal(1); + expect(notifications.get('alerts.firstObject.message')).to.equal('Second alert'); + expect(notifications.get('notifications.length')).to.equal(1); + }); } );