Commit 68deaf35dc5d0c0dbdfc46c87b9326d4f03f2209

Authored by Carlos Purificação
1 parent 4902da4d

Initial implementation

src/app/article/article-default-view-component.spec.ts
1 import {Input, provide, Component} from 'ng-forward'; 1 import {Input, provide, Component} from 'ng-forward';
2 import {ArticleViewComponent, ArticleDefaultViewComponent} from './article-default-view.component'; 2 import {ArticleViewComponent, ArticleDefaultViewComponent} from './article-default-view.component';
  3 +import {ComponentTestHelper, createClass} from './../../spec/component-test-helper';
  4 +import {ModelEvent, ArticleEventType} from "./../shared/models/events";
3 5
4 import * as helpers from "../../spec/helpers"; 6 import * as helpers from "../../spec/helpers";
5 7
@@ -9,113 +11,83 @@ const htmlTemplate: string = '<noosfero-article [article]="ctrl.article" [profil @@ -9,113 +11,83 @@ const htmlTemplate: string = '<noosfero-article [article]="ctrl.article" [profil
9 11
10 describe("Components", () => { 12 describe("Components", () => {
11 13
12 - describe("ArticleView Component", () => {  
13 -  
14 - // the karma preprocessor html2js transform the templates html into js files which put  
15 - // the templates to the templateCache into the module templates  
16 - // we need to load the module templates here as the template for the  
17 - // component Noosfero ArtileView will be load on our tests  
18 - beforeEach(angular.mock.module("templates"));  
19 -  
20 - it("renders the default component when no specific component is found", (done: Function) => {  
21 - // Creating a container component (ArticleContainerComponent) to include  
22 - // the component under test (ArticleView)  
23 - @Component({  
24 - selector: 'test-container-component',  
25 - template: htmlTemplate,  
26 - directives: [ArticleViewComponent],  
27 - providers: [  
28 - helpers.createProviderToValue('CommentService', helpers.mocks.commentService),  
29 - helpers.provideFilters("translateFilter"),  
30 - helpers.createProviderToValue('NotificationService', helpers.mocks.notificationService),  
31 - helpers.createProviderToValue('SessionService', helpers.mocks.sessionWithCurrentUser({}))  
32 - ]  
33 - })  
34 - class ArticleContainerComponent {  
35 - article = { type: 'anyArticleType' };  
36 - profile = { name: 'profile-name' }; 14 + // the karma preprocessor html2js transform the templates html into js files which put
  15 + // the templates to the templateCache into the module templates
  16 + // we need to load the module templates here as the template for the
  17 + // component Noosfero ArtileView will be load on our tests
  18 + beforeEach(angular.mock.module("templates"));
  19 +
  20 + describe("Article Default View Component", () => {
  21 + let helper: ComponentTestHelper<ArticleDefaultViewComponent>;
  22 + const defaultViewTemplate: string = '<noosfero-default-article [article]="ctrl.article" [profile]="ctrl.profile"></noosfero-default-article>';
  23 + let articleService: any = helpers.mocks.articleService;
  24 + let article = <noosfero.Article>{
  25 + id: 1,
  26 + profile: {
  27 + identifier: "1"
37 } 28 }
38 -  
39 - helpers.createComponentFromClass(ArticleContainerComponent).then((fixture) => {  
40 - // and here we can inspect and run the test assertions  
41 -  
42 - // gets the children component of ArticleContainerComponent  
43 - let articleView: ArticleViewComponent = fixture.debugElement.componentViewChildren[0].componentInstance;  
44 -  
45 - // and checks if the article View rendered was the Default Article View  
46 - expect(articleView.constructor.prototype).toEqual(ArticleDefaultViewComponent.prototype);  
47 -  
48 - // done needs to be called (it isn't really needed, as we can read in  
49 - // here (https://github.com/ngUpgraders/ng-forward/blob/master/API.md#createasync)  
50 - // because createAsync in ng-forward is not really async, but as the intention  
51 - // here is write tests in angular 2 ways, this is recommended  
52 - done(); 29 + };
  30 + let state = <ng.ui.IStateService>jasmine.createSpyObj("state", ["go", "transitionTo"]);
  31 + let providers = [
  32 + provide('$state', { useValue: state }),
  33 + provide('ArticleService', { useValue: articleService })
  34 + ].concat(helpers.provideFilters("translateFilter"));
  35 +
  36 + /**
  37 + * The beforeEach procedure will initialize the helper and parse
  38 + * the component according to the given providers. Unfortunetly, in
  39 + * this mode, the providers and properties given to the construtor
  40 + * can't be overriden.
  41 + */
  42 + beforeEach((done) => {
  43 + // Create the component bed for the test. Optionally, this could be done
  44 + // in each test if one needs customization of these parameters per test
  45 + let cls = createClass({
  46 + template: defaultViewTemplate,
  47 + directives: [ArticleDefaultViewComponent],
  48 + providers: providers,
  49 + properties: {
  50 + article: article
  51 + }
53 }); 52 });
54 - 53 + helper = new ComponentTestHelper<ArticleDefaultViewComponent>(cls, done);
55 }); 54 });
56 55
57 - it("receives the article and profile as inputs", (done: Function) => {  
58 -  
59 - // Creating a container component (ArticleContainerComponent) to include  
60 - // the component under test (ArticleView)  
61 - @Component({  
62 - selector: 'test-container-component',  
63 - template: htmlTemplate,  
64 - directives: [ArticleViewComponent],  
65 - providers: [  
66 - helpers.createProviderToValue('CommentService', helpers.mocks.commentService),  
67 - helpers.provideFilters("translateFilter"),  
68 - helpers.createProviderToValue('NotificationService', helpers.mocks.notificationService),  
69 - helpers.createProviderToValue('SessionService', helpers.mocks.sessionWithCurrentUser({}))  
70 - ]  
71 - })  
72 - class ArticleContainerComponent {  
73 - article = { type: 'anyArticleType' };  
74 - profile = { name: 'profile-name' };  
75 - }  
76 -  
77 - // uses the TestComponentBuilder instance to initialize the component  
78 - helpers.createComponentFromClass(ArticleContainerComponent).then((fixture) => {  
79 - // and here we can inspect and run the test assertions  
80 - let articleView: ArticleViewComponent = fixture.debugElement.componentViewChildren[0].componentInstance; 56 + function getArticle() {
  57 + return this.article;
  58 + }
81 59
82 - // assure the article object inside the ArticleView matches  
83 - // the provided through the parent component  
84 - expect(articleView.article.type).toEqual("anyArticleType");  
85 - expect(articleView.profile.name).toEqual("profile-name");  
86 -  
87 - // done needs to be called (it isn't really needed, as we can read in  
88 - // here (https://github.com/ngUpgraders/ng-forward/blob/master/API.md#createasync)  
89 - // because createAsync in ng-forward is not really async, but as the intention  
90 - // here is write tests in angular 2 ways, this is recommended  
91 - done();  
92 - }); 60 + it("it should delete article when delete is activated", () => {
  61 + expect(helper.component.article).toEqual(article);
  62 + // Spy the state service
  63 + doDeleteArticle();
  64 + expect(state.transitionTo).toHaveBeenCalled();
93 }); 65 });
94 66
95 -  
96 - it("renders a article view which matches to the article type", done => {  
97 - // NoosferoTinyMceArticle component created to check if it will be used  
98 - // when a article with type 'TinyMceArticle' is provided to the noosfero-article (ArticleView)  
99 - // *** Important *** - the selector is what ng-forward uses to define the name of the directive provider  
100 - @Component({ selector: 'noosfero-tiny-mce-article', template: "<h1>TinyMceArticle</h1>" })  
101 - class TinyMceArticleView {  
102 - @Input() article: any;  
103 - @Input() profile: any;  
104 - }  
105 -  
106 - // Creating a container component (ArticleContainerComponent) to include our NoosferoTinyMceArticle  
107 - @Component({ selector: 'test-container-component', template: htmlTemplate, directives: [ArticleViewComponent, TinyMceArticleView] })  
108 - class CustomArticleType {  
109 - article = { type: 'TinyMceArticle' };  
110 - profile = { name: 'profile-name' };  
111 - }  
112 - helpers.createComponentFromClass(CustomArticleType).then(fixture => {  
113 - let myComponent: CustomArticleType = fixture.componentInstance;  
114 - expect(myComponent.article.type).toEqual("TinyMceArticle");  
115 - expect(fixture.debugElement.componentViewChildren[0].text()).toEqual("TinyMceArticle");  
116 - done(); 67 + /**
  68 + * Execute the delete method on the target component
  69 + */
  70 + function doDeleteArticle() {
  71 + // Create a mock for the ArticleService removeArticle method
  72 + spyOn(helper.component.articleService, 'removeArticle').and.callFake(function(param: noosfero.Article) {
  73 + return {
  74 + catch: () => {}
  75 + };
117 }); 76 });
118 - });  
119 - 77 + helper.component.delete();
  78 + expect(articleService.removeArticle).toHaveBeenCalled();
  79 + // After the component delete method execution, fire the
  80 + // ArticleEvent.removed event
  81 + simulateRemovedEvent();
  82 + }
  83 +
  84 + /**
  85 + * Simulate the ArticleService ArticleEvent.removed event
  86 + */
  87 + function simulateRemovedEvent() {
  88 + let event: ModelEvent = ModelEvent.event(ArticleEventType.removed);
  89 + helper.component.articleService.notifyArticleRemovedListeners(article);
  90 + }
120 }); 91 });
  92 +
121 }); 93 });
src/app/article/article-default-view.component.ts
@@ -4,6 +4,8 @@ import {CommentsComponent} from &quot;./comment/comments.component&quot;; @@ -4,6 +4,8 @@ import {CommentsComponent} from &quot;./comment/comments.component&quot;;
4 import {MacroDirective} from "./macro/macro.directive"; 4 import {MacroDirective} from "./macro/macro.directive";
5 import {ArticleToolbarHotspotComponent} from "../hotspot/article-toolbar-hotspot.component"; 5 import {ArticleToolbarHotspotComponent} from "../hotspot/article-toolbar-hotspot.component";
6 import {ArticleContentHotspotComponent} from "../hotspot/article-content-hotspot.component"; 6 import {ArticleContentHotspotComponent} from "../hotspot/article-content-hotspot.component";
  7 +import {ArticleService} from "./../../lib/ng-noosfero-api/http/article.service";
  8 +import {ModelEvent, ArticleEventType} from "./../shared/models/events";
7 9
8 /** 10 /**
9 * @ngdoc controller 11 * @ngdoc controller
@@ -16,11 +18,30 @@ import {ArticleContentHotspotComponent} from &quot;../hotspot/article-content-hotspot @@ -16,11 +18,30 @@ import {ArticleContentHotspotComponent} from &quot;../hotspot/article-content-hotspot
16 selector: 'noosfero-default-article', 18 selector: 'noosfero-default-article',
17 templateUrl: 'app/article/article.html' 19 templateUrl: 'app/article/article.html'
18 }) 20 })
  21 +@Inject("$state", ArticleService)
19 export class ArticleDefaultViewComponent { 22 export class ArticleDefaultViewComponent {
20 23
21 @Input() article: noosfero.Article; 24 @Input() article: noosfero.Article;
22 @Input() profile: noosfero.Profile; 25 @Input() profile: noosfero.Profile;
23 26
  27 + constructor(private $state: ng.ui.IStateService, public articleService: ArticleService) {
  28 + // Subscribe to the Article Removed Event
  29 + let event = ModelEvent.event(ArticleEventType.removed);
  30 + this.articleService.subscribe(event, (article: noosfero.Article) => {
  31 + if (this.article.parent) {
  32 + this.$state.transitionTo('main.profile.page', { page: this.article.parent.path, profile: this.article.profile.identifier });
  33 + } else {
  34 + this.$state.transitionTo('main.profile.info', { profile: this.article.profile.identifier });
  35 + }
  36 + });
  37 + }
  38 +
  39 + delete() {
  40 + this.articleService.removeArticle(this.article).catch((cause: any) => {
  41 + throw new Error(`Problem removing the article: ${cause}`);
  42 + });
  43 + }
  44 +
24 } 45 }
25 46
26 /** 47 /**
@@ -60,4 +81,5 @@ export class ArticleViewComponent { @@ -60,4 +81,5 @@ export class ArticleViewComponent {
60 private $injector: ng.auto.IInjectorService, 81 private $injector: ng.auto.IInjectorService,
61 private $compile: ng.ICompileService) { 82 private $compile: ng.ICompileService) {
62 } 83 }
  84 +
63 } 85 }
src/app/article/article-view-component.spec.ts 0 → 100644
@@ -0,0 +1,124 @@ @@ -0,0 +1,124 @@
  1 +import {Input, provide, Component} from 'ng-forward';
  2 +import {ArticleViewComponent, ArticleDefaultViewComponent} from './article-default-view.component';
  3 +
  4 +import * as helpers from "../../spec/helpers";
  5 +
  6 +// this htmlTemplate will be re-used between the container components in this spec file
  7 +const htmlTemplate: string = '<noosfero-article [article]="ctrl.article" [profile]="ctrl.profile"></noosfero-article>';
  8 +
  9 +
  10 +describe("Components", () => {
  11 +
  12 + // the karma preprocessor html2js transform the templates html into js files which put
  13 + // the templates to the templateCache into the module templates
  14 + // we need to load the module templates here as the template for the
  15 + // component Noosfero ArtileView will be load on our tests
  16 + beforeEach(angular.mock.module("templates"));
  17 +
  18 + describe("ArticleView Component", () => {
  19 + let state = <ng.ui.IStateService>jasmine.createSpyObj("state", ["go", "transitionTo"]);
  20 + it("renders the default component when no specific component is found", (done: Function) => {
  21 + // Creating a container component (ArticleContainerComponent) to include
  22 + // the component under test (ArticleView)
  23 + @Component({
  24 + selector: 'test-container-component',
  25 + template: htmlTemplate,
  26 + directives: [ArticleViewComponent],
  27 + providers: [
  28 + helpers.createProviderToValue('CommentService', helpers.mocks.commentService),
  29 + helpers.provideFilters("translateFilter"),
  30 + helpers.createProviderToValue('NotificationService', helpers.mocks.notificationService),
  31 + helpers.createProviderToValue('SessionService', helpers.mocks.sessionWithCurrentUser({})),
  32 + helpers.createProviderToValue('ArticleService', helpers.mocks.articleService),
  33 + helpers.createProviderToValue('$state', state)
  34 + ]
  35 + })
  36 + class ArticleContainerComponent {
  37 + article = { type: 'anyArticleType' };
  38 + profile = { name: 'profile-name' };
  39 + }
  40 +
  41 + helpers.createComponentFromClass(ArticleContainerComponent).then((fixture) => {
  42 + // and here we can inspect and run the test assertions
  43 +
  44 + // gets the children component of ArticleContainerComponent
  45 + let articleView: ArticleViewComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
  46 +
  47 + // and checks if the article View rendered was the Default Article View
  48 + expect(articleView.constructor.prototype).toEqual(ArticleDefaultViewComponent.prototype);
  49 +
  50 + // done needs to be called (it isn't really needed, as we can read in
  51 + // here (https://github.com/ngUpgraders/ng-forward/blob/master/API.md#createasync)
  52 + // because createAsync in ng-forward is not really async, but as the intention
  53 + // here is write tests in angular 2 ways, this is recommended
  54 + done();
  55 + });
  56 + });
  57 +
  58 + it("receives the article and profile as inputs", (done: Function) => {
  59 +
  60 + // Creating a container component (ArticleContainerComponent) to include
  61 + // the component under test (ArticleView)
  62 + @Component({
  63 + selector: 'test-container-component',
  64 + template: htmlTemplate,
  65 + directives: [ArticleViewComponent],
  66 + providers: [
  67 + helpers.createProviderToValue('CommentService', helpers.mocks.commentService),
  68 + helpers.provideFilters("translateFilter"),
  69 + helpers.createProviderToValue('NotificationService', helpers.mocks.notificationService),
  70 + helpers.createProviderToValue('SessionService', helpers.mocks.sessionWithCurrentUser({})),
  71 + helpers.createProviderToValue('ArticleService', helpers.mocks.articleService),
  72 + helpers.createProviderToValue('$state', state)
  73 + ]
  74 + })
  75 + class ArticleContainerComponent {
  76 + article = { type: 'anyArticleType' };
  77 + profile = { name: 'profile-name' };
  78 + }
  79 +
  80 + // uses the TestComponentBuilder instance to initialize the component
  81 + helpers.createComponentFromClass(ArticleContainerComponent).then((fixture) => {
  82 + // and here we can inspect and run the test assertions
  83 + let articleView: ArticleViewComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
  84 +
  85 + // assure the article object inside the ArticleView matches
  86 + // the provided through the parent component
  87 + expect(articleView.article.type).toEqual("anyArticleType");
  88 + expect(articleView.profile.name).toEqual("profile-name");
  89 +
  90 + // done needs to be called (it isn't really needed, as we can read in
  91 + // here (https://github.com/ngUpgraders/ng-forward/blob/master/API.md#createasync)
  92 + // because createAsync in ng-forward is not really async, but as the intention
  93 + // here is write tests in angular 2 ways, this is recommended
  94 + done();
  95 + });
  96 + });
  97 +
  98 +
  99 + it("renders a article view which matches to the article type", done => {
  100 + // NoosferoTinyMceArticle component created to check if it will be used
  101 + // when a article with type 'TinyMceArticle' is provided to the noosfero-article (ArticleView)
  102 + // *** Important *** - the selector is what ng-forward uses to define the name of the directive provider
  103 + @Component({ selector: 'noosfero-tiny-mce-article', template: "<h1>TinyMceArticle</h1>" })
  104 + class TinyMceArticleView {
  105 + @Input() article: any;
  106 + @Input() profile: any;
  107 + }
  108 +
  109 + // Creating a container component (ArticleContainerComponent) to include our NoosferoTinyMceArticle
  110 + @Component({ selector: 'test-container-component', template: htmlTemplate, directives: [ArticleViewComponent, TinyMceArticleView] })
  111 + class CustomArticleType {
  112 + article = { type: 'TinyMceArticle' };
  113 + profile = { name: 'profile-name' };
  114 + }
  115 + helpers.createComponentFromClass(CustomArticleType).then(fixture => {
  116 + let myComponent: CustomArticleType = fixture.componentInstance;
  117 + expect(myComponent.article.type).toEqual("TinyMceArticle");
  118 + expect(fixture.debugElement.componentViewChildren[0].text()).toEqual("TinyMceArticle");
  119 + done();
  120 + });
  121 + });
  122 +
  123 + });
  124 +});
src/app/article/article.html
@@ -7,6 +7,9 @@ @@ -7,6 +7,9 @@
7 <a href="#" class="btn btn-default btn-xs" ui-sref="main.cmsEdit({profile: ctrl.profile.identifier, id: ctrl.article.id})"> 7 <a href="#" class="btn btn-default btn-xs" ui-sref="main.cmsEdit({profile: ctrl.profile.identifier, id: ctrl.article.id})">
8 <i class="fa fa-pencil-square-o fa-fw fa-lg"></i> {{"article.actions.edit" | translate}} 8 <i class="fa fa-pencil-square-o fa-fw fa-lg"></i> {{"article.actions.edit" | translate}}
9 </a> 9 </a>
  10 + <a href="#" class="btn btn-default btn-xs" ng-click="ctrl.delete()">
  11 + <i class="fa fa-trash-o fa-fw fa-lg" ng-click="ctrl.delete()"></i> {{"article.actions.delete" | translate}}
  12 + </a>
10 <noosfero-hotspot-article-toolbar [article]="ctrl.article"></noosfero-hotspot-article-toolbar> 13 <noosfero-hotspot-article-toolbar [article]="ctrl.article"></noosfero-hotspot-article-toolbar>
11 <div class="page-info pull-right small text-muted"> 14 <div class="page-info pull-right small text-muted">
12 <span class="time"> 15 <span class="time">
src/app/shared/models/events.spec.ts 0 → 100644
@@ -0,0 +1,71 @@ @@ -0,0 +1,71 @@
  1 +import { EventEmitter } from "ng-forward";
  2 +import {ModelEvent, ArticleEventType} from "./events";
  3 +import {HashMap} from "./../utils/hashmap";
  4 +import {ArrayUtils} from "./../utils/arrays";
  5 +
  6 +describe("Events", () => {
  7 +
  8 + describe("Event Type Tests", () => {
  9 +
  10 + it("verify event type is correctly created", (done) => {
  11 + let eventType1 = ArticleEventType.removed;
  12 + expect(eventType1.type).toBe("removed");
  13 + expect(ArrayUtils.arraysEqual(eventType1.types, ["added", "removed"])).toBeTruthy();
  14 + done();
  15 + });
  16 +
  17 + it("different event types of same type should be equal", (done) => {
  18 + let eventType1 = ArticleEventType.removed;
  19 + let eventType2 = ArticleEventType.removed;
  20 + expect(eventType1).toBe(eventType2);
  21 + expect(eventType1 === eventType2).toBeTruthy();
  22 + done();
  23 + });
  24 +
  25 + it("different events types of different types should not be equal", (done) => {
  26 + let eventType1 = ArticleEventType.removed;
  27 + let eventType2 = ArticleEventType.added;
  28 + expect(eventType1).not.toBe(eventType2);
  29 + expect(eventType1 === eventType2).not.toBeTruthy();
  30 + done();
  31 + });
  32 +
  33 + it("different events of same type should be equal", (done) => {
  34 + let event1 = ModelEvent.event(ArticleEventType.added);
  35 + let event2 = ModelEvent.event(ArticleEventType.added);
  36 + expect(event1.equals(event2)).toBeTruthy();
  37 + done();
  38 + });
  39 +
  40 + });
  41 +
  42 + describe("Event HashMap Tests", () => {
  43 + let events: HashMap<ModelEvent, EventEmitter<noosfero.Article>>;
  44 + beforeEach((done) => {
  45 + events = new HashMap<ModelEvent, EventEmitter<noosfero.Article>>();
  46 + done();
  47 + });
  48 +
  49 + it("verify event HashMap contains the correct event", () => {
  50 + let expected = new EventEmitter<noosfero.Article>();
  51 + events.put(ModelEvent.event(ArticleEventType.added), expected);
  52 + let actual = events.get(ModelEvent.event(ArticleEventType.added));
  53 + expect(expected === actual).toBeTruthy();
  54 + });
  55 +
  56 + it("verify event HashMap does not contain the wrong event", () => {
  57 + events.put(ModelEvent.event(ArticleEventType.added), new EventEmitter<noosfero.Article>());
  58 + let actual = events.get(ModelEvent.event(ArticleEventType.removed));
  59 + expect(actual).not.toBeTruthy();
  60 + });
  61 +
  62 + it("verify HashMap has been cleared", () => {
  63 + events.put(ModelEvent.event(ArticleEventType.added), new EventEmitter<noosfero.Article>());
  64 + events.clear();
  65 + let actual = events.get(ModelEvent.event(ArticleEventType.added));
  66 + expect(actual).not.toBeTruthy();
  67 + });
  68 +
  69 + });
  70 +
  71 +});
src/app/shared/models/events.ts 0 → 100644
@@ -0,0 +1,62 @@ @@ -0,0 +1,62 @@
  1 +import {ArrayUtils} from "./../utils/arrays";
  2 +
  3 +export abstract class EventType {
  4 + /**
  5 + * All possible types for the event
  6 + */
  7 + types = new Array<string>();
  8 +
  9 + /**
  10 + * The event type
  11 + */
  12 + type: string;
  13 +
  14 + constructor(types: Array<string>, type: string) {
  15 + this.types = types;
  16 + this.type = type;
  17 + }
  18 +
  19 + equals(other: EventType) {
  20 + return ArrayUtils.arraysEqual(this.types, other.types) && this.type === other.type;
  21 + }
  22 +
  23 +}
  24 +
  25 +export class ArticleEventType extends EventType {
  26 + static types = ["added", "removed"];
  27 + static removed: ArticleEventType = new ArticleEventType("removed");
  28 + static added: ArticleEventType = new ArticleEventType("added");
  29 +
  30 + constructor(type: string) {
  31 + super(ArticleEventType.types, type);
  32 + }
  33 +}
  34 +
  35 +/**
  36 + * A model event have a type (ModelEventType), and optionally with a model element.
  37 + * Therefore, it is possible to have events related to specific model elements.
  38 + * If the model element element is not provided, then the event is a generic event
  39 + * of the given type.
  40 + */
  41 +export class ModelEvent {
  42 + private type: EventType;
  43 + private id: number;
  44 +
  45 + static event(type: EventType, model?: noosfero.RestModel): ModelEvent {
  46 + if (model) {
  47 + return new ModelEvent(type, model.id);
  48 + } else {
  49 + return new ModelEvent(type);
  50 + }
  51 + }
  52 +
  53 + constructor(type: EventType, id?: number) {
  54 + this.type = type;
  55 + this.id = id;
  56 + }
  57 +
  58 + equals(other: ModelEvent) {
  59 + return other.type.equals(this.type) && other.id === this.id;
  60 + }
  61 +
  62 +}
src/app/shared/models/index.ts
1 /* Module Index Entry - generated using the script npm run generate-index */ 1 /* Module Index Entry - generated using the script npm run generate-index */
2 export * from "./interfaces"; 2 export * from "./interfaces";
  3 +export * from "./events";
src/app/shared/models/interfaces.ts
@@ -6,7 +6,6 @@ export interface UserResponse { @@ -6,7 +6,6 @@ export interface UserResponse {
6 user: noosfero.User; 6 user: noosfero.User;
7 } 7 }
8 8
9 -  
10 export interface INoosferoLocalStorage extends angular.storage.ILocalStorageService { 9 export interface INoosferoLocalStorage extends angular.storage.ILocalStorageService {
11 currentUser: noosfero.User; 10 currentUser: noosfero.User;
12 } 11 }
src/app/shared/utils/arrays.ts 0 → 100644
@@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
  1 +export class ArrayUtils {
  2 +
  3 + /**
  4 + * Returns if two arrays are equal
  5 + */
  6 + static arraysEqual(a: any, b: any): boolean {
  7 + if (a === b) return true;
  8 + if (a == null || b == null) return false;
  9 + if (a.length !== b.length) return false;
  10 +
  11 + // If you don't care about the order of the elements inside
  12 + // the array, you should sort both arrays here.
  13 + for (let i = 0; i < a.length; ++i) {
  14 + if (a[i] !== b[i]) return false;
  15 + }
  16 + return true;
  17 + }
  18 +
  19 +}
0 \ No newline at end of file 20 \ No newline at end of file
src/app/shared/utils/hashmap.ts 0 → 100644
@@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
  1 +export class HashMap<K extends EqualsObject, V> {
  2 + private values: Array<HashItem<K, V>> = new Array();
  3 +
  4 + get(key: K): V {
  5 + let found = this.values.find( function(value: HashItem<K, V>) {
  6 + return value.key.equals(key);
  7 + });
  8 + if (found) {
  9 + return found.value;
  10 + }
  11 + return null;
  12 + }
  13 +
  14 + put(key: K, value: V): void {
  15 + this.values.push(new HashItem(key, value));
  16 + }
  17 +
  18 + clear() {
  19 + this.values = new Array();
  20 + }
  21 +}
  22 +
  23 +export class HashItem<K, V> {
  24 + key: K;
  25 + value: V;
  26 + constructor(key: K, value: V) {
  27 + this.key = key;
  28 + this.value = value;
  29 + }
  30 +}
  31 +
  32 +export abstract class EqualsObject {
  33 +
  34 + abstract equals(other: any): boolean;
  35 +
  36 +}
0 \ No newline at end of file 37 \ No newline at end of file
src/app/shared/utils/index.ts 0 → 100644
@@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
  1 +/* Module Index Entry - generated using the script npm run generate-index */
  2 +export * from "./hashmap";
  3 +export * from "./arrays";
src/languages/en.json
@@ -37,6 +37,7 @@ @@ -37,6 +37,7 @@
37 "comment.post.success.message": "Comment saved!", 37 "comment.post.success.message": "Comment saved!",
38 "comment.reply": "reply", 38 "comment.reply": "reply",
39 "article.actions.edit": "Edit", 39 "article.actions.edit": "Edit",
  40 + "article.actions.delete": "Delete",
40 "article.actions.read_more": "Read More", 41 "article.actions.read_more": "Read More",
41 "article.basic_editor.title": "Title", 42 "article.basic_editor.title": "Title",
42 "article.basic_editor.body": "Body", 43 "article.basic_editor.body": "Body",
src/languages/pt.json
@@ -37,6 +37,7 @@ @@ -37,6 +37,7 @@
37 "comment.post.success.message": "Comentário salvo com sucesso!", 37 "comment.post.success.message": "Comentário salvo com sucesso!",
38 "comment.reply": "responder", 38 "comment.reply": "responder",
39 "article.actions.edit": "Editar", 39 "article.actions.edit": "Editar",
  40 + "article.actions.delete": "Excluir",
40 "article.actions.read_more": "Ler mais", 41 "article.actions.read_more": "Ler mais",
41 "article.basic_editor.title": "Título", 42 "article.basic_editor.title": "Título",
42 "article.basic_editor.body": "Corpo", 43 "article.basic_editor.body": "Corpo",
src/lib/ng-noosfero-api/http/article.service.spec.ts
@@ -20,6 +20,14 @@ describe(&quot;Services&quot;, () =&gt; { @@ -20,6 +20,14 @@ describe(&quot;Services&quot;, () =&gt; {
20 20
21 describe("Succesfull requests", () => { 21 describe("Succesfull requests", () => {
22 22
  23 + it("should remove article", (done) => {
  24 + let articleId = 1;
  25 + $httpBackend.expectDELETE(`/api/v1/articles/${articleId}`).respond(200, { success: "true" });
  26 + articleService.removeArticle(<noosfero.Article>{id: articleId});
  27 + $httpBackend.flush();
  28 + done();
  29 + });
  30 +
23 it("should return article children", (done) => { 31 it("should return article children", (done) => {
24 let articleId = 1; 32 let articleId = 1;
25 $httpBackend.expectGET(`/api/v1/articles/${articleId}/children`).respond(200, { articles: [{ name: "article1" }] }); 33 $httpBackend.expectGET(`/api/v1/articles/${articleId}/children`).respond(200, { articles: [{ name: "article1" }] });
src/lib/ng-noosfero-api/http/article.service.ts
1 -import { Injectable, Inject } from "ng-forward"; 1 +import { Injectable, Inject, EventEmitter } from "ng-forward";
2 import {RestangularService} from "./restangular_service"; 2 import {RestangularService} from "./restangular_service";
3 import {ProfileService} from "./profile.service"; 3 import {ProfileService} from "./profile.service";
  4 +import {NoosferoRootScope} from "./../../../app/shared/models/interfaces";
  5 +import {ModelEvent, ArticleEventType} from "./../../../app/shared/models/events";
  6 +import {HashMap} from "./../../../app/shared/utils/hashmap";
4 7
5 @Injectable() 8 @Injectable()
6 @Inject("Restangular", "$q", "$log", ProfileService) 9 @Inject("Restangular", "$q", "$log", ProfileService)
7 -  
8 export class ArticleService extends RestangularService<noosfero.Article> { 10 export class ArticleService extends RestangularService<noosfero.Article> {
9 11
  12 + private events: HashMap<ModelEvent, EventEmitter<noosfero.Article>> = new HashMap<ModelEvent, EventEmitter<noosfero.Article>>();
  13 +
  14 + // This event is not tyed to any specific model element.
  15 + private removed: ModelEvent = ModelEvent.event(ArticleEventType.removed);
  16 +
10 constructor(Restangular: restangular.IService, $q: ng.IQService, $log: ng.ILogService, protected profileService: ProfileService) { 17 constructor(Restangular: restangular.IService, $q: ng.IQService, $log: ng.ILogService, protected profileService: ProfileService) {
11 super(Restangular, $q, $log); 18 super(Restangular, $q, $log);
  19 + this.events.put(this.removed, new EventEmitter<noosfero.Article>());
12 } 20 }
13 21
14 getResourcePath() { 22 getResourcePath() {
@@ -22,6 +30,36 @@ export class ArticleService extends RestangularService&lt;noosfero.Article&gt; { @@ -22,6 +30,36 @@ export class ArticleService extends RestangularService&lt;noosfero.Article&gt; {
22 }; 30 };
23 } 31 }
24 32
  33 + removeArticle(article: noosfero.Article) {
  34 + let restRequest: ng.IPromise<noosfero.RestResult<noosfero.Article>> = this.remove(article);
  35 + let deferred = this.$q.defer<noosfero.RestResult<noosfero.Article>>();
  36 + restRequest.then((result: any) => {
  37 + this.notifyArticleRemovedListeners(article);
  38 + }).catch(this.getHandleErrorFunction(deferred));
  39 + return deferred.promise;
  40 + }
  41 +
  42 + /**
  43 + * Notify listeners that this article has been removed
  44 + */
  45 + notifyArticleRemovedListeners(article: noosfero.Article) {
  46 + let listener = this.events.get(this.removed);
  47 + listener.next(article);
  48 + }
  49 +
  50 + /**
  51 + * Subscribe a listener a given article event
  52 + */
  53 + subscribe(eventToSubscribe: ModelEvent, fn: Function) {
  54 + // Find the requested event in map
  55 + let event: EventEmitter<noosfero.Article> = this.events.get(eventToSubscribe);
  56 + if (event) {
  57 + event.subscribe(fn);
  58 + } else {
  59 + throw new Error(`The event: ${eventToSubscribe} not exists`);
  60 + }
  61 + }
  62 +
25 updateArticle(article: noosfero.Article) { 63 updateArticle(article: noosfero.Article) {
26 let headers = { 64 let headers = {
27 'Content-Type': 'application/json' 65 'Content-Type': 'application/json'
@@ -103,3 +141,4 @@ export class ArticleService extends RestangularService&lt;noosfero.Article&gt; { @@ -103,3 +141,4 @@ export class ArticleService extends RestangularService&lt;noosfero.Article&gt; {
103 141
104 142
105 } 143 }
  144 +
src/spec/mocks.ts
@@ -76,6 +76,29 @@ export var mocks: any = { @@ -76,6 +76,29 @@ export var mocks: any = {
76 isAuthenticated: () => { } 76 isAuthenticated: () => { }
77 }, 77 },
78 articleService: { 78 articleService: {
  79 + events: [
  80 + {
  81 + event: Function,
  82 + subscribe: (fn: Function) => {
  83 + mocks.articleService.events[0].event = fn;
  84 + },
  85 + next: (param: any) => {
  86 + mocks.articleService.events[0].event(param);
  87 + }
  88 + }
  89 + ],
  90 + removeArticle: (article: noosfero.Article) => {
  91 + return {
  92 + catch: (func?: Function) => {
  93 + }
  94 + };
  95 + },
  96 + notifyArticleRemovedListeners: (article: noosfero.Article) => {
  97 + mocks.articleService.events[0].next(article);
  98 + },
  99 + subscribe: (eventType: any, fn: Function) => {
  100 + mocks.articleService.events[0].subscribe(fn);
  101 + },
79 getByProfile: (profileId: number, params?: any) => { 102 getByProfile: (profileId: number, params?: any) => {
80 return { 103 return {
81 then: (func?: Function) => { 104 then: (func?: Function) => {