Commit f2acfc7494f2e7bc110b90ebe62201fdb1d23dc2

Authored by Victor Costa
1 parent ae4699f3

Use a notification component to handle success and error messages

src/app/cms/cms.component.spec.ts
@@ -9,7 +9,7 @@ describe("Components", () => { @@ -9,7 +9,7 @@ describe("Components", () => {
9 let articleServiceMock: any; 9 let articleServiceMock: any;
10 let profileServiceMock: any; 10 let profileServiceMock: any;
11 let $state: any; 11 let $state: any;
12 - let sweetAlert: any; 12 + let notification: any;
13 13
14 beforeEach(inject((_$rootScope_: ng.IRootScopeService, _$q_: ng.IQService) => { 14 beforeEach(inject((_$rootScope_: ng.IRootScopeService, _$q_: ng.IQService) => {
15 $rootScope = _$rootScope_; 15 $rootScope = _$rootScope_;
@@ -18,7 +18,7 @@ describe("Components", () => { @@ -18,7 +18,7 @@ describe("Components", () => {
18 18
19 beforeEach(() => { 19 beforeEach(() => {
20 $state = jasmine.createSpyObj("$state", ["transitionTo"]); 20 $state = jasmine.createSpyObj("$state", ["transitionTo"]);
21 - sweetAlert = jasmine.createSpyObj("SweetAlert", ["swal"]); 21 + notification = jasmine.createSpyObj("notification", ["success"]);
22 profileServiceMock = jasmine.createSpyObj("profileServiceMock", ["getCurrentProfile"]); 22 profileServiceMock = jasmine.createSpyObj("profileServiceMock", ["getCurrentProfile"]);
23 articleServiceMock = jasmine.createSpyObj("articleServiceMock", ["create"]); 23 articleServiceMock = jasmine.createSpyObj("articleServiceMock", ["create"]);
24 24
@@ -33,7 +33,7 @@ describe("Components", () => { @@ -33,7 +33,7 @@ describe("Components", () => {
33 }); 33 });
34 34
35 it("create an article in the current profile when save", done => { 35 it("create an article in the current profile when save", done => {
36 - let component: Cms = new Cms(articleServiceMock, profileServiceMock, $state, sweetAlert); 36 + let component: Cms = new Cms(articleServiceMock, profileServiceMock, $state, notification);
37 component.save(); 37 component.save();
38 $rootScope.$apply(); 38 $rootScope.$apply();
39 expect(profileServiceMock.getCurrentProfile).toHaveBeenCalled(); 39 expect(profileServiceMock.getCurrentProfile).toHaveBeenCalled();
@@ -42,11 +42,11 @@ describe("Components", () => { @@ -42,11 +42,11 @@ describe("Components", () => {
42 }); 42 });
43 43
44 it("got to the new article page and display an alert when saving sucessfully", done => { 44 it("got to the new article page and display an alert when saving sucessfully", done => {
45 - let component: Cms = new Cms(articleServiceMock, profileServiceMock, $state, sweetAlert); 45 + let component: Cms = new Cms(articleServiceMock, profileServiceMock, $state, notification);
46 component.save(); 46 component.save();
47 $rootScope.$apply(); 47 $rootScope.$apply();
48 expect($state.transitionTo).toHaveBeenCalledWith("main.profile.page", { page: "path", profile: "profile" }); 48 expect($state.transitionTo).toHaveBeenCalledWith("main.profile.page", { page: "path", profile: "profile" });
49 - expect(sweetAlert.swal).toHaveBeenCalled(); 49 + expect(notification.success).toHaveBeenCalled();
50 done(); 50 done();
51 }); 51 });
52 52
src/app/cms/cms.component.ts
@@ -2,35 +2,33 @@ import {StateConfig, Component, Inject, provide} from 'ng-forward'; @@ -2,35 +2,33 @@ import {StateConfig, Component, Inject, provide} from 'ng-forward';
2 import {Profile} from "./../models/interfaces"; 2 import {Profile} from "./../models/interfaces";
3 import {ArticleService} from "../../lib/ng-noosfero-api/http/article.service"; 3 import {ArticleService} from "../../lib/ng-noosfero-api/http/article.service";
4 import {ProfileService} from "../../lib/ng-noosfero-api/http/profile.service"; 4 import {ProfileService} from "../../lib/ng-noosfero-api/http/profile.service";
  5 +import {Notification} from "../components/notification/notification.component";
5 6
6 @Component({ 7 @Component({
7 selector: 'cms', 8 selector: 'cms',
8 templateUrl: "app/cms/cms.html", 9 templateUrl: "app/cms/cms.html",
9 providers: [ 10 providers: [
10 provide('articleService', { useClass: ArticleService }), 11 provide('articleService', { useClass: ArticleService }),
11 - provide('profileService', { useClass: ProfileService }) 12 + provide('profileService', { useClass: ProfileService }),
  13 + provide('notification', { useClass: Notification })
12 ] 14 ]
13 }) 15 })
14 -@Inject(ArticleService, ProfileService, "$state", "SweetAlert") 16 +@Inject(ArticleService, ProfileService, "$state", Notification)
15 export class Cms { 17 export class Cms {
16 18
17 article: any = {}; 19 article: any = {};
18 20
19 constructor(private articleService: ArticleService, 21 constructor(private articleService: ArticleService,
20 private profileService: ProfileService, 22 private profileService: ProfileService,
21 - private $state: ng.ui.IStateService, private SweetAlert: any) { } 23 + private $state: ng.ui.IStateService,
  24 + private notification: Notification) { }
22 25
23 save() { 26 save() {
24 this.profileService.getCurrentProfile().then((profile: Profile) => { 27 this.profileService.getCurrentProfile().then((profile: Profile) => {
25 return this.articleService.create(profile.id, this.article); 28 return this.articleService.create(profile.id, this.article);
26 }).then((response: restangular.IResponse) => { 29 }).then((response: restangular.IResponse) => {
27 this.$state.transitionTo('main.profile.page', { page: response.data.article.path, profile: response.data.article.profile.identifier }); 30 this.$state.transitionTo('main.profile.page', { page: response.data.article.path, profile: response.data.article.profile.identifier });
28 - this.SweetAlert.swal({  
29 - title: "Good job!",  
30 - text: "Article saved!",  
31 - type: "success",  
32 - timer: 1000  
33 - }); 31 + this.notification.success("Good job!", "Article saved!");
34 }); 32 });
35 } 33 }
36 34
src/app/components/notification/notification.component.spec.ts 0 → 100644
@@ -0,0 +1,67 @@ @@ -0,0 +1,67 @@
  1 +import {TestComponentBuilder, ComponentFixture} from 'ng-forward/cjs/testing/test-component-builder';
  2 +import {Pipe, Input, provide, Component} from 'ng-forward';
  3 +
  4 +import * as helpers from "../../../spec/helpers";
  5 +
  6 +import {Notification} from "./notification.component";
  7 +
  8 +const tcb = new TestComponentBuilder();
  9 +
  10 +describe("Components", () => {
  11 +
  12 + describe("Profile Image Component", () => {
  13 +
  14 + beforeEach(angular.mock.module("templates"));
  15 +
  16 + it("use the default message when call notification component without a specific message", done => {
  17 + let sweetAlert = jasmine.createSpyObj("sweetAlert", ["swal"]);
  18 + sweetAlert.swal = jasmine.createSpy("swal");
  19 +
  20 + let component: Notification = new Notification(<any>helpers.mocks.$log, <any>sweetAlert);
  21 + component.httpError(500, {});
  22 + expect(sweetAlert.swal).toHaveBeenCalledWith(jasmine.objectContaining({
  23 + text: Notification.DEFAULT_HTTP_ERROR_MESSAGE,
  24 + type: "error"
  25 + }));
  26 + done();
  27 + });
  28 +
  29 + it("use the default message when call notification component without error data", done => {
  30 + let sweetAlert = jasmine.createSpyObj("sweetAlert", ["swal"]);
  31 + sweetAlert.swal = jasmine.createSpy("swal");
  32 +
  33 + let component: Notification = new Notification(<any>helpers.mocks.$log, <any>sweetAlert);
  34 + component.httpError(500, null);
  35 + expect(sweetAlert.swal).toHaveBeenCalledWith(jasmine.objectContaining({
  36 + text: Notification.DEFAULT_HTTP_ERROR_MESSAGE,
  37 + type: "error"
  38 + }));
  39 + done();
  40 + });
  41 +
  42 + it("display a success message when call notification success", done => {
  43 + let sweetAlert = jasmine.createSpyObj("sweetAlert", ["swal"]);
  44 + sweetAlert.swal = jasmine.createSpy("swal");
  45 +
  46 + let component: Notification = new Notification(<any>helpers.mocks.$log, <any>sweetAlert);
  47 + component.success("title", "message", 1000);
  48 + expect(sweetAlert.swal).toHaveBeenCalledWith(jasmine.objectContaining({
  49 + type: "success"
  50 + }));
  51 + done();
  52 + });
  53 +
  54 + it("set the default timer in success messages", done => {
  55 + let sweetAlert = jasmine.createSpyObj("sweetAlert", ["swal"]);
  56 + sweetAlert.swal = jasmine.createSpy("swal");
  57 +
  58 + let component: Notification = new Notification(<any>helpers.mocks.$log, <any>sweetAlert);
  59 + component.success("title", "message");
  60 + expect(sweetAlert.swal).toHaveBeenCalledWith(jasmine.objectContaining({
  61 + type: "success",
  62 + timer: Notification.DEFAULT_SUCCESS_TIMER
  63 + }));
  64 + done();
  65 + });
  66 + });
  67 +});
src/app/components/notification/notification.component.ts 0 → 100644
@@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
  1 +import {Injectable, Inject} from "ng-forward";
  2 +
  3 +@Injectable()
  4 +@Inject("$log", "SweetAlert")
  5 +export class Notification {
  6 +
  7 + constructor(private $log: ng.ILogService, private SweetAlert: any) { }
  8 +
  9 + public static DEFAULT_HTTP_ERROR_MESSAGE = "Something went wrong!";
  10 + public static DEFAULT_SUCCESS_TIMER = 1000;
  11 +
  12 + httpError(status: number, data: any): boolean {
  13 + this.$log.debug(status, data);
  14 +
  15 + let message = (data || {}).message || Notification.DEFAULT_HTTP_ERROR_MESSAGE;
  16 + this.SweetAlert.swal({
  17 + title: "Oops...",
  18 + text: message,
  19 + type: "error"
  20 + });
  21 + return true; // return true to indicate that the error was already handled
  22 + }
  23 +
  24 + success(title: string, text: string, timer: number = Notification.DEFAULT_SUCCESS_TIMER) {
  25 + this.SweetAlert.swal({
  26 + title: title,
  27 + text: text,
  28 + type: "success",
  29 + timer: timer
  30 + });
  31 + }
  32 +
  33 +}
src/app/index.run.ts
1 import {Session} from "./components/auth/session"; 1 import {Session} from "./components/auth/session";
  2 +import {Notification} from "./components/notification/notification.component";
2 3
3 /** @ngInject */ 4 /** @ngInject */
4 -export function noosferoAngularRunBlock($log: ng.ILogService, Restangular: restangular.IService, Session: Session) {  
5 - Restangular.addFullRequestInterceptor((element: any, operation: string, route: string, url: string, headers: string) => { 5 +export function noosferoAngularRunBlock(
  6 + $log: ng.ILogService,
  7 + Restangular: restangular.IService,
  8 + Session: Session,
  9 + Notification: Notification
  10 +) {
  11 +
  12 + Restangular.addFullRequestInterceptor((element: any, operation: string, route: string, url: string, headers: string) => {
6 if (Session.currentUser()) { 13 if (Session.currentUser()) {
7 (<any>headers)["Private-Token"] = Session.currentUser().private_token; 14 (<any>headers)["Private-Token"] = Session.currentUser().private_token;
8 } 15 }
9 return <any>{ headers: <any>headers }; 16 return <any>{ headers: <any>headers };
10 }); 17 });
  18 + Restangular.setErrorInterceptor((response: restangular.IResponse, deferred: ng.IDeferred<any>) => {
  19 + // return false to break the promise chain and don't call catch
  20 + return !Notification.httpError(response.status, response.data);
  21 + });
11 } 22 }
12 -  
src/app/main/main.component.ts
@@ -17,6 +17,7 @@ import {DateFormat} from &quot;../components/noosfero/date-format/date-format.filter&quot; @@ -17,6 +17,7 @@ import {DateFormat} from &quot;../components/noosfero/date-format/date-format.filter&quot;
17 17
18 import {AuthService} from "./../components/auth/auth_service"; 18 import {AuthService} from "./../components/auth/auth_service";
19 import {Session} from "./../components/auth/session"; 19 import {Session} from "./../components/auth/session";
  20 +import {Notification} from "./../components/notification/notification.component";
20 21
21 22
22 import {Navbar} from "../components/navbar/navbar"; 23 import {Navbar} from "../components/navbar/navbar";
@@ -41,7 +42,7 @@ export class MainContent { @@ -41,7 +42,7 @@ export class MainContent {
41 MainBlock, RecentDocumentsBlock, Navbar, ProfileImageBlock, 42 MainBlock, RecentDocumentsBlock, Navbar, ProfileImageBlock,
42 MembersBlock, NoosferoTemplate, DateFormat, RawHTMLBlock 43 MembersBlock, NoosferoTemplate, DateFormat, RawHTMLBlock
43 ], 44 ],
44 - providers: [AuthService, Session] 45 + providers: [AuthService, Session, Notification]
45 }) 46 })
46 @StateConfig([ 47 @StateConfig([
47 { 48 {
src/app/profile/profile.component.spec.ts
@@ -7,6 +7,7 @@ describe(&quot;Components&quot;, () =&gt; { @@ -7,6 +7,7 @@ describe(&quot;Components&quot;, () =&gt; {
7 let $rootScope: ng.IRootScopeService; 7 let $rootScope: ng.IRootScopeService;
8 let $q: ng.IQService; 8 let $q: ng.IQService;
9 let profileServiceMock: any; 9 let profileServiceMock: any;
  10 + let notificationMock: any;
10 let $stateParams: any; 11 let $stateParams: any;
11 12
12 beforeEach(inject((_$rootScope_: ng.IRootScopeService, _$q_: ng.IQService) => { 13 beforeEach(inject((_$rootScope_: ng.IRootScopeService, _$q_: ng.IQService) => {
@@ -17,6 +18,7 @@ describe(&quot;Components&quot;, () =&gt; { @@ -17,6 +18,7 @@ describe(&quot;Components&quot;, () =&gt; {
17 beforeEach(() => { 18 beforeEach(() => {
18 $stateParams = jasmine.createSpyObj("$stateParams", ["profile"]); 19 $stateParams = jasmine.createSpyObj("$stateParams", ["profile"]);
19 profileServiceMock = jasmine.createSpyObj("profileServiceMock", ["setCurrentProfileByIdentifier", "getBoxes"]); 20 profileServiceMock = jasmine.createSpyObj("profileServiceMock", ["setCurrentProfileByIdentifier", "getBoxes"]);
  21 + notificationMock = jasmine.createSpyObj("notificationMock", ["httpError"]);
20 22
21 let profileResponse = $q.defer(); 23 let profileResponse = $q.defer();
22 profileResponse.resolve({ id: 1 }); 24 profileResponse.resolve({ id: 1 });
@@ -28,7 +30,7 @@ describe(&quot;Components&quot;, () =&gt; { @@ -28,7 +30,7 @@ describe(&quot;Components&quot;, () =&gt; {
28 }); 30 });
29 31
30 it("get the profile and store in profile service", done => { 32 it("get the profile and store in profile service", done => {
31 - let component: Profile = new Profile(profileServiceMock, $stateParams); 33 + let component: Profile = new Profile(profileServiceMock, $stateParams, notificationMock);
32 $rootScope.$apply(); 34 $rootScope.$apply();
33 expect(profileServiceMock.setCurrentProfileByIdentifier).toHaveBeenCalled(); 35 expect(profileServiceMock.setCurrentProfileByIdentifier).toHaveBeenCalled();
34 expect(component.profile).toEqual({ id: 1 }); 36 expect(component.profile).toEqual({ id: 1 });
@@ -36,11 +38,26 @@ describe(&quot;Components&quot;, () =&gt; { @@ -36,11 +38,26 @@ describe(&quot;Components&quot;, () =&gt; {
36 }); 38 });
37 39
38 it("get the profile boxes", done => { 40 it("get the profile boxes", done => {
39 - let component: Profile = new Profile(profileServiceMock, $stateParams); 41 + let component: Profile = new Profile(profileServiceMock, $stateParams, notificationMock);
40 $rootScope.$apply(); 42 $rootScope.$apply();
41 expect(profileServiceMock.getBoxes).toHaveBeenCalled(); 43 expect(profileServiceMock.getBoxes).toHaveBeenCalled();
42 expect(component.boxes).toEqual([{ id: 2 }]); 44 expect(component.boxes).toEqual([{ id: 2 }]);
43 done(); 45 done();
44 }); 46 });
  47 +
  48 + it("display notification error when the profile wasn't found", done => {
  49 + let profileResponse = $q.defer();
  50 + profileResponse.reject();
  51 + profileServiceMock.setCurrentProfileByIdentifier = jasmine.createSpy("setCurrentProfileByIdentifier").and.returnValue(profileResponse.promise);
  52 +
  53 + let component: Profile = new Profile(profileServiceMock, $stateParams, notificationMock);
  54 + $rootScope.$apply();
  55 +
  56 + expect(profileServiceMock.setCurrentProfileByIdentifier).toHaveBeenCalled();
  57 + expect(notificationMock.httpError).toHaveBeenCalled();
  58 + expect(component.profile).toBeUndefined();
  59 + done();
  60 + });
  61 +
45 }); 62 });
46 }); 63 });
src/app/profile/profile.component.ts
@@ -6,6 +6,7 @@ import {ContentViewer} from &quot;../content-viewer/content-viewer.component&quot;; @@ -6,6 +6,7 @@ import {ContentViewer} from &quot;../content-viewer/content-viewer.component&quot;;
6 import {ContentViewerActions} from "../content-viewer/content-viewer-actions.component"; 6 import {ContentViewerActions} from "../content-viewer/content-viewer-actions.component";
7 import {NoosferoActivities} from "../components/noosfero-activities/activities.component"; 7 import {NoosferoActivities} from "../components/noosfero-activities/activities.component";
8 import {ProfileService} from "../../lib/ng-noosfero-api/http/profile.service"; 8 import {ProfileService} from "../../lib/ng-noosfero-api/http/profile.service";
  9 +import {Notification} from "../components/notification/notification.component";
9 10
10 import * as noosferoModels from "./../models/interfaces"; 11 import * as noosferoModels from "./../models/interfaces";
11 12
@@ -13,7 +14,10 @@ import * as noosferoModels from &quot;./../models/interfaces&quot;; @@ -13,7 +14,10 @@ import * as noosferoModels from &quot;./../models/interfaces&quot;;
13 selector: 'profile', 14 selector: 'profile',
14 templateUrl: "app/profile/profile.html", 15 templateUrl: "app/profile/profile.html",
15 directives: [NoosferoActivities], 16 directives: [NoosferoActivities],
16 - providers: [provide('profileService', { useClass: ProfileService })] 17 + providers: [
  18 + provide('profileService', { useClass: ProfileService }),
  19 + provide('notification', { useClass: Notification })
  20 + ]
17 }) 21 })
18 @StateConfig([ 22 @StateConfig([
19 { 23 {
@@ -75,12 +79,14 @@ export class Profile { @@ -75,12 +79,14 @@ export class Profile {
75 boxes: noosferoModels.Box[]; 79 boxes: noosferoModels.Box[];
76 profile: noosferoModels.Profile; 80 profile: noosferoModels.Profile;
77 81
78 - constructor(profileService: ProfileService, $stateParams: ng.ui.IStateParamsService) { 82 + constructor(profileService: ProfileService, $stateParams: ng.ui.IStateParamsService, notification: Notification) {
79 profileService.setCurrentProfileByIdentifier($stateParams["profile"]).then((profile: noosferoModels.Profile) => { 83 profileService.setCurrentProfileByIdentifier($stateParams["profile"]).then((profile: noosferoModels.Profile) => {
80 this.profile = profile; 84 this.profile = profile;
81 return profileService.getBoxes(this.profile.id); 85 return profileService.getBoxes(this.profile.id);
82 }).then((response: restangular.IResponse) => { 86 }).then((response: restangular.IResponse) => {
83 this.boxes = response.data.boxes; 87 this.boxes = response.data.boxes;
  88 + }).catch(() => {
  89 + notification.httpError(404, { message: "Profile not found!" });
84 }); 90 });
85 } 91 }
86 } 92 }
src/lib/ng-noosfero-api/http/profile.service.spec.ts
@@ -24,8 +24,17 @@ describe(&quot;Services&quot;, () =&gt; { @@ -24,8 +24,17 @@ describe(&quot;Services&quot;, () =&gt; {
24 it("should return profile by its identifier", (done) => { 24 it("should return profile by its identifier", (done) => {
25 let identifier = 'profile1'; 25 let identifier = 'profile1';
26 $httpBackend.expectGET(`/api/v1/profiles?identifier=${identifier}`).respond(200, [{ name: "profile1" }]); 26 $httpBackend.expectGET(`/api/v1/profiles?identifier=${identifier}`).respond(200, [{ name: "profile1" }]);
27 - profileService.getByIdentifier(identifier).then((response: restangular.IResponse) => {  
28 - expect(response.data[0]).toEqual({ name: "profile1" }); 27 + profileService.getByIdentifier(identifier).then((profile: Profile) => {
  28 + expect(profile).toEqual({ name: "profile1" });
  29 + done();
  30 + });
  31 + $httpBackend.flush();
  32 + });
  33 +
  34 + it("should reject the promise if the profile wasn't found", (done) => {
  35 + let identifier = 'profile1';
  36 + $httpBackend.expectGET(`/api/v1/profiles?identifier=${identifier}`).respond(200, []);
  37 + profileService.getByIdentifier(identifier).catch(() => {
29 done(); 38 done();
30 }); 39 });
31 $httpBackend.flush(); 40 $httpBackend.flush();
src/lib/ng-noosfero-api/http/profile.service.ts
@@ -25,8 +25,8 @@ export class ProfileService { @@ -25,8 +25,8 @@ export class ProfileService {
25 25
26 setCurrentProfileByIdentifier(identifier: string) { 26 setCurrentProfileByIdentifier(identifier: string) {
27 this.resetCurrentProfile(); 27 this.resetCurrentProfile();
28 - return this.getByIdentifier(identifier).then((response: restangular.IResponse) => {  
29 - this.setCurrentProfile(response.data[0]); 28 + return this.getByIdentifier(identifier).then((profile: Profile) => {
  29 + this.setCurrentProfile(profile);
30 return this.getCurrentProfile(); 30 return this.getCurrentProfile();
31 }); 31 });
32 } 32 }
@@ -35,8 +35,14 @@ export class ProfileService { @@ -35,8 +35,14 @@ export class ProfileService {
35 return this.get(profileId).customGET("home_page", params); 35 return this.get(profileId).customGET("home_page", params);
36 } 36 }
37 37
38 - getByIdentifier(identifier: string): restangular.IPromise<any> {  
39 - return this.restangular.one('profiles').get({ identifier: identifier }); 38 + getByIdentifier(identifier: string): ng.IPromise<any> {
  39 + let p = this.restangular.one('profiles').get({ identifier: identifier });
  40 + return p.then((response: restangular.IResponse) => {
  41 + if (response.data.length == 0) {
  42 + return this.$q.reject(p);
  43 + }
  44 + return response.data[0];
  45 + });
40 } 46 }
41 47
42 getProfileMembers(profileId: number, params?: any): restangular.IPromise<any> { 48 getProfileMembers(profileId: number, params?: any): restangular.IPromise<any> {
src/spec/mocks.ts
@@ -60,5 +60,8 @@ export var mocks = { @@ -60,5 +60,8 @@ export var mocks = {
60 loadScript: (script?: string) => { 60 loadScript: (script?: string) => {
61 return Promise.resolve(); 61 return Promise.resolve();
62 } 62 }
  63 + },
  64 + $log: {
  65 + debug: () => { }
63 } 66 }
64 }; 67 };