Compare View

switch
from
...
to
 
Commits (10)
Showing 35 changed files   Show diff stats
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';
3 4
4 import * as helpers from "../../spec/helpers"; 5 import * as helpers from "../../spec/helpers";
5 6
@@ -9,113 +10,82 @@ const htmlTemplate: string = '<noosfero-article [article]="ctrl.article" [profil @@ -9,113 +10,82 @@ const htmlTemplate: string = '<noosfero-article [article]="ctrl.article" [profil
9 10
10 describe("Components", () => { 11 describe("Components", () => {
11 12
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' }; 13 + // the karma preprocessor html2js transform the templates html into js files which put
  14 + // the templates to the templateCache into the module templates
  15 + // we need to load the module templates here as the template for the
  16 + // component Noosfero ArtileView will be load on our tests
  17 + beforeEach(angular.mock.module("templates"));
  18 +
  19 + describe("Article Default View Component", () => {
  20 + let helper: ComponentTestHelper<ArticleDefaultViewComponent>;
  21 + const defaultViewTemplate: string = '<noosfero-default-article [article]="ctrl.article" [profile]="ctrl.profile"></noosfero-default-article>';
  22 + let articleService: any = helpers.mocks.articleService;
  23 + let article = <noosfero.Article>{
  24 + id: 1,
  25 + profile: {
  26 + identifier: "1"
37 } 27 }
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(); 28 + };
  29 + let state = <ng.ui.IStateService>jasmine.createSpyObj("state", ["go", "transitionTo"]);
  30 + let providers = [
  31 + provide('$state', { useValue: state }),
  32 + provide('ArticleService', { useValue: articleService })
  33 + ].concat(helpers.provideFilters("translateFilter"));
  34 +
  35 + /**
  36 + * The beforeEach procedure will initialize the helper and parse
  37 + * the component according to the given providers. Unfortunetly, in
  38 + * this mode, the providers and properties given to the construtor
  39 + * can't be overriden.
  40 + */
  41 + beforeEach((done) => {
  42 + // Create the component bed for the test. Optionally, this could be done
  43 + // in each test if one needs customization of these parameters per test
  44 + let cls = createClass({
  45 + template: defaultViewTemplate,
  46 + directives: [ArticleDefaultViewComponent],
  47 + providers: providers,
  48 + properties: {
  49 + article: article
  50 + }
53 }); 51 });
54 - 52 + helper = new ComponentTestHelper<ArticleDefaultViewComponent>(cls, done);
55 }); 53 });
56 54
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; 55 + function getArticle() {
  56 + return this.article;
  57 + }
81 58
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 - }); 59 + it("it should delete article when delete is activated", () => {
  60 + expect(helper.component.article).toEqual(article);
  61 + // Spy the state service
  62 + doDeleteArticle();
  63 + expect(state.transitionTo).toHaveBeenCalled();
93 }); 64 });
94 65
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(); 66 + /**
  67 + * Execute the delete method on the target component
  68 + */
  69 + function doDeleteArticle() {
  70 + // Create a mock for the ArticleService removeArticle method
  71 + spyOn(helper.component.articleService, 'removeArticle').and.callFake(function(param: noosfero.Article) {
  72 + return {
  73 + catch: () => {}
  74 + };
117 }); 75 });
118 - });  
119 - 76 + helper.component.delete();
  77 + expect(articleService.removeArticle).toHaveBeenCalled();
  78 + // After the component delete method execution, fire the
  79 + // ArticleEvent.removed event
  80 + simulateRemovedEvent();
  81 + }
  82 +
  83 + /**
  84 + * Simulate the ArticleService ArticleEvent.removed event
  85 + */
  86 + function simulateRemovedEvent() {
  87 + helper.component.articleService["notifyArticleRemovedListeners"](article);
  88 + }
120 }); 89 });
  90 +
121 }); 91 });
src/app/article/article-default-view.component.ts
@@ -4,6 +4,7 @@ import {CommentsComponent} from &quot;./comment/comments.component&quot;; @@ -4,6 +4,7 @@ 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";
7 8
8 /** 9 /**
9 * @ngdoc controller 10 * @ngdoc controller
@@ -16,11 +17,29 @@ import {ArticleContentHotspotComponent} from &quot;../hotspot/article-content-hotspot @@ -16,11 +17,29 @@ import {ArticleContentHotspotComponent} from &quot;../hotspot/article-content-hotspot
16 selector: 'noosfero-default-article', 17 selector: 'noosfero-default-article',
17 templateUrl: 'app/article/article.html' 18 templateUrl: 'app/article/article.html'
18 }) 19 })
  20 +@Inject("$state", ArticleService)
19 export class ArticleDefaultViewComponent { 21 export class ArticleDefaultViewComponent {
20 22
21 @Input() article: noosfero.Article; 23 @Input() article: noosfero.Article;
22 @Input() profile: noosfero.Profile; 24 @Input() profile: noosfero.Profile;
23 25
  26 + constructor(private $state: ng.ui.IStateService, public articleService: ArticleService) {
  27 + // Subscribe to the Article Removed Event
  28 + this.articleService.subscribeToArticleRemoved((article: noosfero.Article) => {
  29 + if (this.article.parent) {
  30 + this.$state.transitionTo('main.profile.page', { page: this.article.parent.path, profile: this.article.profile.identifier });
  31 + } else {
  32 + this.$state.transitionTo('main.profile.info', { profile: this.article.profile.identifier });
  33 + }
  34 + });
  35 + }
  36 +
  37 + delete() {
  38 + this.articleService.removeArticle(this.article).catch((cause: any) => {
  39 + throw new Error(`Problem removing the article: ${cause}`);
  40 + });
  41 + }
  42 +
24 } 43 }
25 44
26 /** 45 /**
@@ -60,4 +79,5 @@ export class ArticleViewComponent { @@ -60,4 +79,5 @@ export class ArticleViewComponent {
60 private $injector: ng.auto.IInjectorService, 79 private $injector: ng.auto.IInjectorService,
61 private $compile: ng.ICompileService) { 80 private $compile: ng.ICompileService) {
62 } 81 }
  82 +
63 } 83 }
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/article/comment/comment.component.spec.ts
@@ -8,6 +8,8 @@ describe(&quot;Components&quot;, () =&gt; { @@ -8,6 +8,8 @@ describe(&quot;Components&quot;, () =&gt; {
8 describe("Comment Component", () => { 8 describe("Comment Component", () => {
9 9
10 let properties: any; 10 let properties: any;
  11 + let notificationService = helpers.mocks.notificationService;
  12 + let commentService = jasmine.createSpyObj("commentService", ["removeFromArticle"]);
11 13
12 beforeEach(angular.mock.module("templates")); 14 beforeEach(angular.mock.module("templates"));
13 beforeEach(() => { 15 beforeEach(() => {
@@ -18,7 +20,10 @@ describe(&quot;Components&quot;, () =&gt; { @@ -18,7 +20,10 @@ describe(&quot;Components&quot;, () =&gt; {
18 }); 20 });
19 21
20 function createComponent() { 22 function createComponent() {
21 - let providers = helpers.provideFilters("translateFilter"); 23 + let providers = [
  24 + helpers.createProviderToValue('NotificationService', notificationService),
  25 + helpers.createProviderToValue("CommentService", commentService)
  26 + ].concat(helpers.provideFilters("translateFilter"));
22 27
23 @Component({ selector: 'test-container-component', directives: [CommentComponent], template: htmlTemplate, providers: providers }) 28 @Component({ selector: 'test-container-component', directives: [CommentComponent], template: htmlTemplate, providers: providers })
24 class ContainerComponent { 29 class ContainerComponent {
@@ -74,5 +79,36 @@ describe(&quot;Components&quot;, () =&gt; { @@ -74,5 +79,36 @@ describe(&quot;Components&quot;, () =&gt; {
74 done(); 79 done();
75 }); 80 });
76 }); 81 });
  82 +
  83 + it("does not show the Remove button if user is not allowed to remove", done => {
  84 + createComponent().then(fixture => {
  85 + let component: CommentComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
  86 + component.allowRemove = () => false;
  87 + fixture.detectChanges();
  88 + expect(fixture.debugElement.queryAll("a.action.remove").length).toEqual(0);
  89 + done();
  90 + });
  91 + });
  92 +
  93 + it("shows the Remove button if user is allowed to remove", done => {
  94 + createComponent().then(fixture => {
  95 + let component: CommentComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
  96 + component.allowRemove = () => true;
  97 + fixture.detectChanges();
  98 + expect(fixture.debugElement.queryAll("a.action.remove").length).toEqual(1);
  99 + done();
  100 + });
  101 + });
  102 +
  103 + it("call comment service to remove comment", done => {
  104 + notificationService.confirmation = (params: any, func: Function) => { func(); };
  105 + commentService.removeFromArticle = jasmine.createSpy("removeFromArticle").and.returnValue(Promise.resolve());
  106 + createComponent().then(fixture => {
  107 + let component = fixture.debugElement.componentViewChildren[0].componentInstance;
  108 + component.remove();
  109 + expect(commentService.removeFromArticle).toHaveBeenCalled();
  110 + done();
  111 + });
  112 + });
77 }); 113 });
78 }); 114 });
src/app/article/comment/comment.component.ts
1 import { Inject, Input, Component, Output, EventEmitter } from 'ng-forward'; 1 import { Inject, Input, Component, Output, EventEmitter } from 'ng-forward';
2 import { PostCommentComponent } from "./post-comment/post-comment.component"; 2 import { PostCommentComponent } from "./post-comment/post-comment.component";
  3 +import { CommentService } from "../../../lib/ng-noosfero-api/http/comment.service";
  4 +import { NotificationService } from "../../shared/services/notification.service";
3 5
4 @Component({ 6 @Component({
5 selector: 'noosfero-comment', 7 selector: 'noosfero-comment',
  8 + outputs: ['commentRemoved'],
6 templateUrl: 'app/article/comment/comment.html' 9 templateUrl: 'app/article/comment/comment.html'
7 }) 10 })
  11 +@Inject(CommentService, NotificationService)
8 export class CommentComponent { 12 export class CommentComponent {
9 13
10 @Input() comment: noosfero.CommentViewModel; 14 @Input() comment: noosfero.CommentViewModel;
11 @Input() article: noosfero.Article; 15 @Input() article: noosfero.Article;
12 @Input() displayActions = true; 16 @Input() displayActions = true;
13 @Input() displayReplies = true; 17 @Input() displayReplies = true;
  18 + @Output() commentRemoved: EventEmitter<Comment> = new EventEmitter<Comment>();
14 19
15 showReply() { 20 showReply() {
16 return this.comment && this.comment.__show_reply === true; 21 return this.comment && this.comment.__show_reply === true;
17 } 22 }
18 23
19 - constructor() {  
20 - }  
21 - 24 + constructor(private commentService: CommentService,
  25 + private notificationService: NotificationService) { }
22 26
23 reply() { 27 reply() {
24 this.comment.__show_reply = !this.comment.__show_reply; 28 this.comment.__show_reply = !this.comment.__show_reply;
25 } 29 }
  30 +
  31 + allowRemove() {
  32 + return true;
  33 + }
  34 +
  35 + remove() {
  36 + this.notificationService.confirmation({ title: "comment.remove.confirmation.title", message: "comment.remove.confirmation.message" }, () => {
  37 + this.commentService.removeFromArticle(this.article, this.comment).then((result: noosfero.RestResult<noosfero.Comment>) => {
  38 + this.commentRemoved.next(this.comment);
  39 + this.notificationService.success({ title: "comment.remove.success.title", message: "comment.remove.success.message" });
  40 + });
  41 + });
  42 + }
26 } 43 }
src/app/article/comment/comment.html
@@ -18,9 +18,14 @@ @@ -18,9 +18,14 @@
18 <div class="title">{{ctrl.comment.title}}</div> 18 <div class="title">{{ctrl.comment.title}}</div>
19 <div class="body">{{ctrl.comment.body}}</div> 19 <div class="body">{{ctrl.comment.body}}</div>
20 <div class="actions" ng-if="ctrl.displayActions"> 20 <div class="actions" ng-if="ctrl.displayActions">
21 - <a href="#" (click)="ctrl.reply()" class="small text-muted reply" ng-if="ctrl.article.accept_comments"> 21 + <a href="#" (click)="ctrl.reply()" class="action small text-muted reply" ng-if="ctrl.article.accept_comments">
  22 + <span class="bullet-separator">•</span>
22 {{"comment.reply" | translate}} 23 {{"comment.reply" | translate}}
23 </a> 24 </a>
  25 + <a href="#" (click)="ctrl.remove()" class="action small text-muted remove" ng-if="ctrl.allowRemove()">
  26 + <span class="bullet-separator">•</span>
  27 + {{"comment.remove" | translate}}
  28 + </a>
24 </div> 29 </div>
25 </div> 30 </div>
26 <noosfero-comments [show-form]="ctrl.showReply()" [article]="ctrl.article" [parent]="ctrl.comment" ng-if="ctrl.displayReplies"></noosfero-comments> 31 <noosfero-comments [show-form]="ctrl.showReply()" [article]="ctrl.article" [parent]="ctrl.comment" ng-if="ctrl.displayReplies"></noosfero-comments>
src/app/article/comment/comment.scss
@@ -10,6 +10,22 @@ @@ -10,6 +10,22 @@
10 .title { 10 .title {
11 font-weight: bold; 11 font-weight: bold;
12 } 12 }
  13 + .actions {
  14 + .action {
  15 + text-decoration: none;
  16 + &:first-child {
  17 + .bullet-separator {
  18 + display: none;
  19 + }
  20 + }
  21 + .bullet-separator {
  22 + font-size: 8px;
  23 + vertical-align: middle;
  24 + margin: 3px;
  25 + color: #B6C2CA;
  26 + }
  27 + }
  28 + }
13 .media-left { 29 .media-left {
14 min-width: 40px; 30 min-width: 40px;
15 } 31 }
src/app/article/comment/comments.component.spec.ts
@@ -83,5 +83,26 @@ describe(&quot;Components&quot;, () =&gt; { @@ -83,5 +83,26 @@ describe(&quot;Components&quot;, () =&gt; {
83 }); 83 });
84 }); 84 });
85 85
  86 + it("remove comment when calling commentRemoved", done => {
  87 + createComponent().then(fixture => {
  88 + let component: CommentsComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
  89 + let comment = { id: 1 };
  90 + component.comments = <any>[comment];
  91 + component.commentRemoved(<any>comment);
  92 + expect(component.comments).toEqual([]);
  93 + done();
  94 + });
  95 + });
  96 +
  97 + it("do nothing when call commentRemoved with a comment that doesn't belongs to the comments list", done => {
  98 + createComponent().then(fixture => {
  99 + let component: CommentsComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
  100 + let comment = { id: 1 };
  101 + component.comments = <any>[comment];
  102 + component.commentRemoved(<any>{ id: 2 });
  103 + expect(component.comments).toEqual([comment]);
  104 + done();
  105 + });
  106 + });
86 }); 107 });
87 }); 108 });
src/app/article/comment/comments.component.ts
@@ -37,6 +37,13 @@ export class CommentsComponent { @@ -37,6 +37,13 @@ export class CommentsComponent {
37 this.resetShowReply(); 37 this.resetShowReply();
38 } 38 }
39 39
  40 + commentRemoved(comment: noosfero.Comment): void {
  41 + let index = this.comments.indexOf(comment, 0);
  42 + if (index >= 0) {
  43 + this.comments.splice(index, 1);
  44 + }
  45 + }
  46 +
40 private resetShowReply() { 47 private resetShowReply() {
41 this.comments.forEach((comment: noosfero.CommentViewModel) => { 48 this.comments.forEach((comment: noosfero.CommentViewModel) => {
42 comment.__show_reply = false; 49 comment.__show_reply = false;
src/app/article/comment/comments.html
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 <noosfero-post-comment (comment-saved)="ctrl.commentAdded($event.detail)" ng-if="ctrl.showForm" [article]="ctrl.article" [parent]="ctrl.parent" [comment]="ctrl.newComment"></noosfero-post-comment> 2 <noosfero-post-comment (comment-saved)="ctrl.commentAdded($event.detail)" ng-if="ctrl.showForm" [article]="ctrl.article" [parent]="ctrl.parent" [comment]="ctrl.newComment"></noosfero-post-comment>
3 3
4 <div class="comments-list"> 4 <div class="comments-list">
5 - <noosfero-comment ng-repeat="comment in ctrl.comments | orderBy: 'created_at':true" [comment]="comment" [article]="ctrl.article"></noosfero-comment> 5 + <noosfero-comment (comment-removed)="ctrl.commentRemoved($event.detail)" ng-repeat="comment in ctrl.comments | orderBy: 'created_at':true" [comment]="comment" [article]="ctrl.article"></noosfero-comment>
6 </div> 6 </div>
7 <button type="button" ng-if="ctrl.displayMore()" class="more-comments btn btn-default btn-block" ng-click="ctrl.loadNextPage()">{{"comment.pagination.more" | translate}}</button> 7 <button type="button" ng-if="ctrl.displayMore()" class="more-comments btn btn-default btn-block" ng-click="ctrl.loadNextPage()">{{"comment.pagination.more" | translate}}</button>
8 </div> 8 </div>
src/app/main/main.component.ts
@@ -16,6 +16,7 @@ import {RecentDocumentsBlockComponent} from &quot;../layout/blocks/recent-documents/r @@ -16,6 +16,7 @@ import {RecentDocumentsBlockComponent} from &quot;../layout/blocks/recent-documents/r
16 import {ProfileImageBlockComponent} from "../layout/blocks/profile-image/profile-image-block.component"; 16 import {ProfileImageBlockComponent} from "../layout/blocks/profile-image/profile-image-block.component";
17 import {RawHTMLBlockComponent} from "../layout/blocks/raw-html/raw-html-block.component"; 17 import {RawHTMLBlockComponent} from "../layout/blocks/raw-html/raw-html-block.component";
18 import {StatisticsBlockComponent} from "../layout/blocks/statistics/statistics-block.component"; 18 import {StatisticsBlockComponent} from "../layout/blocks/statistics/statistics-block.component";
  19 +import {ProfileWallComponent} from "../profile/wall/profile-wall.component";
19 20
20 import {MembersBlockComponent} from "../layout/blocks/members/members-block.component"; 21 import {MembersBlockComponent} from "../layout/blocks/members/members-block.component";
21 import {CommunitiesBlockComponent} from "../layout/blocks/communities/communities-block.component"; 22 import {CommunitiesBlockComponent} from "../layout/blocks/communities/communities-block.component";
@@ -99,7 +100,7 @@ export class EnvironmentContent { @@ -99,7 +100,7 @@ export class EnvironmentContent {
99 LinkListBlockComponent, CommunitiesBlockComponent, HtmlEditorComponent, 100 LinkListBlockComponent, CommunitiesBlockComponent, HtmlEditorComponent,
100 MainBlockComponent, RecentDocumentsBlockComponent, Navbar, SidebarComponent, ProfileImageBlockComponent, 101 MainBlockComponent, RecentDocumentsBlockComponent, Navbar, SidebarComponent, ProfileImageBlockComponent,
101 MembersBlockComponent, NoosferoTemplate, DateFormat, RawHTMLBlockComponent, StatisticsBlockComponent, 102 MembersBlockComponent, NoosferoTemplate, DateFormat, RawHTMLBlockComponent, StatisticsBlockComponent,
102 - LoginBlockComponent 103 + LoginBlockComponent, ProfileWallComponent
103 ].concat(plugins.mainComponents).concat(plugins.hotspots), 104 ].concat(plugins.mainComponents).concat(plugins.hotspots),
104 105
105 providers: [AuthService, SessionService, NotificationService, BodyStateClassesService] 106 providers: [AuthService, SessionService, NotificationService, BodyStateClassesService]
src/app/profile/info/profile-info.component.ts
@@ -13,8 +13,6 @@ import {TranslateProfile} from &quot;../../shared/pipes/translate-profile.filter&quot;; @@ -13,8 +13,6 @@ import {TranslateProfile} from &quot;../../shared/pipes/translate-profile.filter&quot;;
13 @Inject(ProfileService) 13 @Inject(ProfileService)
14 @Inject("amDateFormatFilter") 14 @Inject("amDateFormatFilter")
15 export class ProfileInfoComponent { 15 export class ProfileInfoComponent {
16 -  
17 - activities: any;  
18 profile: noosfero.Profile; 16 profile: noosfero.Profile;
19 17
20 constructor(private profileService: ProfileService, private amDateFormatFilter: any) { 18 constructor(private profileService: ProfileService, private amDateFormatFilter: any) {
@@ -24,9 +22,6 @@ export class ProfileInfoComponent { @@ -24,9 +22,6 @@ export class ProfileInfoComponent {
24 init() { 22 init() {
25 this.profileService.getCurrentProfile().then((profile: noosfero.Profile) => { 23 this.profileService.getCurrentProfile().then((profile: noosfero.Profile) => {
26 this.profile = profile; 24 this.profile = profile;
27 - return this.profileService.getActivities(<number>this.profile.id);  
28 - }).then((response: restangular.IResponse) => {  
29 - this.activities = response.data.activities;  
30 }); 25 });
31 } 26 }
32 } 27 }
src/app/profile/info/profile-info.html
1 <div class="profile-wall"> 1 <div class="profile-wall">
2 -  
3 <div class="col-lg-3 col-md-4 col-sm-4"> 2 <div class="col-lg-3 col-md-4 col-sm-4">
4 <div class="main-box clearfix"> 3 <div class="main-box clearfix">
5 <header class="main-box-header clearfix"> 4 <header class="main-box-header clearfix">
@@ -16,15 +15,6 @@ @@ -16,15 +15,6 @@
16 </div> 15 </div>
17 16
18 <div class="col-lg-9 col-md-8 col-sm-8"> 17 <div class="col-lg-9 col-md-8 col-sm-8">
19 - <div class="main-box clearfix">  
20 - <uib-tabset active="active">  
21 - <uib-tab index="0" heading="{{ 'activities.title' | translate }}">  
22 - <noosfero-activities [activities]="vm.activities"></noosfero-activities>  
23 - </uib-tab>  
24 - <uib-tab index="0" heading="{{ 'profile.about' | translate }}">  
25 - <profile-data [profile]="vm.profile"></profile-data>  
26 - </uib-tab>  
27 - </uib-tabset>  
28 - </div> 18 + <profile-wall [profile]="vm.profile"></profile-wall>
29 </div> 19 </div>
30 </div> 20 </div>
src/app/profile/wall/index.ts 0 → 100644
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 +export * from "./profile-wall.component";
0 \ No newline at end of file 2 \ No newline at end of file
src/app/profile/wall/profile-wall.component.ts 0 → 100644
@@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
  1 +import {Component, Inject, Input, provide} from 'ng-forward';
  2 +import {ProfileService} from "../../../lib/ng-noosfero-api/http/profile.service";
  3 +import {TranslateProfile} from "../../shared/pipes/translate-profile.filter";
  4 +
  5 +@Component({
  6 + selector: 'profile-wall',
  7 + templateUrl: "app/profile/wall/profile-wall.html",
  8 + providers: [provide('profileService', { useClass: ProfileService })],
  9 + pipes: [TranslateProfile]
  10 +})
  11 +@Inject(ProfileService)
  12 +export class ProfileWallComponent {
  13 + @Input() profile: noosfero.Profile;
  14 + activities: noosfero.Activity[];
  15 +
  16 + constructor(private profileService: ProfileService) {
  17 + }
  18 +
  19 + ngOnInit() {
  20 + this.profileService.getActivities(<number>this.profile.id)
  21 + .then((response: restangular.IResponse) => {
  22 + this.activities = <noosfero.Activity[]>response.data.activities;
  23 + });
  24 + }
  25 +}
src/app/profile/wall/profile-wall.html 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  1 +<h1>Name: {{ctrl.profile.name}}</h1>
  2 +<div class="main-box clearfix">
  3 + <uib-tabset active="active">
  4 + <uib-tab index="0" heading="{{ 'activities.title' | translate }}">
  5 + <noosfero-activities [activities]="ctrl.activities"></noosfero-activities>
  6 + </uib-tab>
  7 + <uib-tab index="0" heading="{{ 'profile.about' | translate }}">
  8 + <profile-data [profile]="ctrl.profile"></profile-data>
  9 + </uib-tab>
  10 + </uib-tabset>
  11 +</div>
0 \ No newline at end of file 12 \ No newline at end of file
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/services/notification.service.spec.ts
@@ -20,10 +20,10 @@ describe(&quot;Components&quot;, () =&gt; { @@ -20,10 +20,10 @@ describe(&quot;Components&quot;, () =&gt; {
20 let component: NotificationService = new NotificationService(<any>helpers.mocks.$log, <any>sweetAlert, <any>helpers.mocks.translatorService); 20 let component: NotificationService = new NotificationService(<any>helpers.mocks.$log, <any>sweetAlert, <any>helpers.mocks.translatorService);
21 component.error({ message: "message", title: "title" }); 21 component.error({ message: "message", title: "title" });
22 expect(sweetAlert.swal).toHaveBeenCalledWith(jasmine.objectContaining({ 22 expect(sweetAlert.swal).toHaveBeenCalledWith(jasmine.objectContaining({
23 - text: "message",  
24 title: "title", 23 title: "title",
  24 + text: "message",
25 type: "error" 25 type: "error"
26 - })); 26 + }), null);
27 done(); 27 done();
28 }); 28 });
29 29
@@ -36,7 +36,7 @@ describe(&quot;Components&quot;, () =&gt; { @@ -36,7 +36,7 @@ describe(&quot;Components&quot;, () =&gt; {
36 expect(sweetAlert.swal).toHaveBeenCalledWith(jasmine.objectContaining({ 36 expect(sweetAlert.swal).toHaveBeenCalledWith(jasmine.objectContaining({
37 text: NotificationService.DEFAULT_ERROR_MESSAGE, 37 text: NotificationService.DEFAULT_ERROR_MESSAGE,
38 type: "error" 38 type: "error"
39 - })); 39 + }), null);
40 done(); 40 done();
41 }); 41 });
42 42
@@ -48,7 +48,7 @@ describe(&quot;Components&quot;, () =&gt; { @@ -48,7 +48,7 @@ describe(&quot;Components&quot;, () =&gt; {
48 component.success({ title: "title", message: "message", timer: 1000 }); 48 component.success({ title: "title", message: "message", timer: 1000 });
49 expect(sweetAlert.swal).toHaveBeenCalledWith(jasmine.objectContaining({ 49 expect(sweetAlert.swal).toHaveBeenCalledWith(jasmine.objectContaining({
50 type: "success" 50 type: "success"
51 - })); 51 + }), null);
52 done(); 52 done();
53 }); 53 });
54 54
@@ -60,7 +60,7 @@ describe(&quot;Components&quot;, () =&gt; { @@ -60,7 +60,7 @@ describe(&quot;Components&quot;, () =&gt; {
60 component.httpError(500, {}); 60 component.httpError(500, {});
61 expect(sweetAlert.swal).toHaveBeenCalledWith(jasmine.objectContaining({ 61 expect(sweetAlert.swal).toHaveBeenCalledWith(jasmine.objectContaining({
62 text: "notification.http_error.500.message" 62 text: "notification.http_error.500.message"
63 - })); 63 + }), null);
64 done(); 64 done();
65 }); 65 });
66 66
@@ -73,7 +73,22 @@ describe(&quot;Components&quot;, () =&gt; { @@ -73,7 +73,22 @@ describe(&quot;Components&quot;, () =&gt; {
73 expect(sweetAlert.swal).toHaveBeenCalledWith(jasmine.objectContaining({ 73 expect(sweetAlert.swal).toHaveBeenCalledWith(jasmine.objectContaining({
74 type: "success", 74 type: "success",
75 timer: NotificationService.DEFAULT_SUCCESS_TIMER 75 timer: NotificationService.DEFAULT_SUCCESS_TIMER
76 - })); 76 + }), null);
  77 + done();
  78 + });
  79 +
  80 + it("display a confirmation dialog when call confirmation method", done => {
  81 + let sweetAlert = jasmine.createSpyObj("sweetAlert", ["swal"]);
  82 + sweetAlert.swal = jasmine.createSpy("swal");
  83 +
  84 + let component: NotificationService = new NotificationService(<any>helpers.mocks.$log, <any>sweetAlert, <any>helpers.mocks.translatorService);
  85 + let func = () => { };
  86 + component.confirmation({ title: "title", message: "message" }, func);
  87 + expect(sweetAlert.swal).toHaveBeenCalledWith(jasmine.objectContaining({
  88 + title: "title",
  89 + text: "message",
  90 + type: "warning"
  91 + }), jasmine.any(Function));
77 done(); 92 done();
78 }); 93 });
79 }); 94 });
src/app/shared/services/notification.service.ts
@@ -36,15 +36,23 @@ export class NotificationService { @@ -36,15 +36,23 @@ export class NotificationService {
36 this.showMessage({ title: title, text: message, timer: timer }); 36 this.showMessage({ title: title, text: message, timer: timer });
37 } 37 }
38 38
39 - private showMessage({title, text, type = "success", timer = null, showConfirmButton = true}) { 39 + confirmation({title, message, showCancelButton = true, type = "warning"}, confirmationFunction: Function) {
  40 + this.showMessage({ title: title, text: message, showCancelButton: showCancelButton, type: type, closeOnConfirm: false }, confirmationFunction);
  41 + }
  42 +
  43 + private showMessage({title, text, type = "success", timer = null, showConfirmButton = true, showCancelButton = false, closeOnConfirm = true}, confirmationFunction: Function = null) {
40 this.$log.debug("Notification message:", title, text, type, this.translatorService.currentLanguage()); 44 this.$log.debug("Notification message:", title, text, type, this.translatorService.currentLanguage());
41 this.SweetAlert.swal({ 45 this.SweetAlert.swal({
42 title: this.translatorService.translate(title), 46 title: this.translatorService.translate(title),
43 text: this.translatorService.translate(text), 47 text: this.translatorService.translate(text),
44 type: type, 48 type: type,
45 timer: timer, 49 timer: timer,
46 - showConfirmButton: showConfirmButton  
47 - }); 50 + showConfirmButton: showConfirmButton,
  51 + showCancelButton: showCancelButton,
  52 + closeOnConfirm: closeOnConfirm
  53 + }, confirmationFunction ? (isConfirm: boolean) => {
  54 + if (isConfirm) confirmationFunction();
  55 + } : null);
48 } 56 }
49 57
50 } 58 }
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
@@ -35,8 +35,14 @@ @@ -35,8 +35,14 @@
35 "comment.pagination.more": "More", 35 "comment.pagination.more": "More",
36 "comment.post.success.title": "Good job!", 36 "comment.post.success.title": "Good job!",
37 "comment.post.success.message": "Comment saved!", 37 "comment.post.success.message": "Comment saved!",
  38 + "comment.remove.success.title": "Good job!",
  39 + "comment.remove.success.message": "Comment removed!",
  40 + "comment.remove.confirmation.title": "Are you sure?",
  41 + "comment.remove.confirmation.message": "You will not be able to recover this comment!",
38 "comment.reply": "reply", 42 "comment.reply": "reply",
  43 + "comment.remove": "remove",
39 "article.actions.edit": "Edit", 44 "article.actions.edit": "Edit",
  45 + "article.actions.delete": "Delete",
40 "article.actions.read_more": "Read More", 46 "article.actions.read_more": "Read More",
41 "article.basic_editor.title": "Title", 47 "article.basic_editor.title": "Title",
42 "article.basic_editor.body": "Body", 48 "article.basic_editor.body": "Body",
src/languages/pt.json
@@ -35,8 +35,14 @@ @@ -35,8 +35,14 @@
35 "comment.pagination.more": "Mais", 35 "comment.pagination.more": "Mais",
36 "comment.post.success.title": "Bom trabalho!", 36 "comment.post.success.title": "Bom trabalho!",
37 "comment.post.success.message": "Comentário salvo com sucesso!", 37 "comment.post.success.message": "Comentário salvo com sucesso!",
  38 + "comment.remove.success.title": "Bom trabalho!",
  39 + "comment.remove.success.message": "Comentário removido com sucesso!",
  40 + "comment.remove.confirmation.title": "Tem certeza?",
  41 + "comment.remove.confirmation.message": "Você não poderá recuperar o comentário removido!",
38 "comment.reply": "responder", 42 "comment.reply": "responder",
  43 + "comment.remove": "remover",
39 "article.actions.edit": "Editar", 44 "article.actions.edit": "Editar",
  45 + "article.actions.delete": "Excluir",
40 "article.actions.read_more": "Ler mais", 46 "article.actions.read_more": "Ler mais",
41 "article.basic_editor.title": "Título", 47 "article.basic_editor.title": "Título",
42 "article.basic_editor.body": "Corpo", 48 "article.basic_editor.body": "Corpo",
src/lib/ng-noosfero-api/http/article.service.spec.ts
@@ -20,6 +20,15 @@ describe(&quot;Services&quot;, () =&gt; { @@ -20,6 +20,15 @@ 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 + $httpBackend.verifyNoOutstandingExpectation();
  29 + done();
  30 + });
  31 +
23 it("should return article children", (done) => { 32 it("should return article children", (done) => {
24 let articleId = 1; 33 let articleId = 1;
25 $httpBackend.expectGET(`/api/v1/articles/${articleId}/children`).respond(200, { articles: [{ name: "article1" }] }); 34 $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";
4 5
5 @Injectable() 6 @Injectable()
6 @Inject("Restangular", "$q", "$log", ProfileService) 7 @Inject("Restangular", "$q", "$log", ProfileService)
7 -  
8 export class ArticleService extends RestangularService<noosfero.Article> { 8 export class ArticleService extends RestangularService<noosfero.Article> {
9 9
  10 + private articleRemoved: EventEmitter<noosfero.Article> = new EventEmitter<noosfero.Article>();
  11 +
10 constructor(Restangular: restangular.IService, $q: ng.IQService, $log: ng.ILogService, protected profileService: ProfileService) { 12 constructor(Restangular: restangular.IService, $q: ng.IQService, $log: ng.ILogService, protected profileService: ProfileService) {
11 super(Restangular, $q, $log); 13 super(Restangular, $q, $log);
12 } 14 }
@@ -22,6 +24,31 @@ export class ArticleService extends RestangularService&lt;noosfero.Article&gt; { @@ -22,6 +24,31 @@ export class ArticleService extends RestangularService&lt;noosfero.Article&gt; {
22 }; 24 };
23 } 25 }
24 26
  27 + removeArticle(article: noosfero.Article) {
  28 + let restRequest: ng.IPromise<noosfero.RestResult<noosfero.Article>> = this.remove(article);
  29 + let deferred = this.$q.defer<noosfero.RestResult<noosfero.Article>>();
  30 + restRequest.then((result: any) => {
  31 + this.notifyArticleRemovedListeners(article);
  32 + }).catch(this.getHandleErrorFunction(deferred));
  33 + return deferred.promise;
  34 + }
  35 +
  36 + /**
  37 + * Notify listeners that this article has been removed
  38 + */
  39 + private notifyArticleRemovedListeners(article: noosfero.Article) {
  40 + // let listener = this.events.get(this.removed);
  41 + // listener.next(article);
  42 + this.articleRemoved.next(article);
  43 + }
  44 +
  45 + /**
  46 + * subscribes to the ArticleRemoved event emitter
  47 + */
  48 + subscribeToArticleRemoved(fn: Function) {
  49 + this.articleRemoved.subscribe(fn);
  50 + }
  51 +
25 updateArticle(article: noosfero.Article) { 52 updateArticle(article: noosfero.Article) {
26 let headers = { 53 let headers = {
27 'Content-Type': 'application/json' 54 'Content-Type': 'application/json'
@@ -103,3 +130,4 @@ export class ArticleService extends RestangularService&lt;noosfero.Article&gt; { @@ -103,3 +130,4 @@ export class ArticleService extends RestangularService&lt;noosfero.Article&gt; {
103 130
104 131
105 } 132 }
  133 +
src/lib/ng-noosfero-api/http/comment.service.spec.ts
@@ -40,6 +40,17 @@ describe(&quot;Services&quot;, () =&gt; { @@ -40,6 +40,17 @@ describe(&quot;Services&quot;, () =&gt; {
40 }); 40 });
41 $httpBackend.flush(); 41 $httpBackend.flush();
42 }); 42 });
  43 +
  44 + it("should remove a comment from an article", (done) => {
  45 + let articleId = 1;
  46 + let comment: noosfero.Comment = <any>{ id: 2 };
  47 + $httpBackend.expectDELETE(`/api/v1/articles/${articleId}/comments/${comment.id}`).respond(200, { comment: { id: 2 } });
  48 + commentService.removeFromArticle(<noosfero.Article>{ id: articleId }, comment).then((result: noosfero.RestResult<noosfero.Comment>) => {
  49 + expect(result.data).toEqual({ id: 2 });
  50 + done();
  51 + });
  52 + $httpBackend.flush();
  53 + });
43 }); 54 });
44 55
45 56
src/lib/ng-noosfero-api/http/comment.service.ts
@@ -31,4 +31,9 @@ export class CommentService extends RestangularService&lt;noosfero.Comment&gt; { @@ -31,4 +31,9 @@ export class CommentService extends RestangularService&lt;noosfero.Comment&gt; {
31 let articleElement = this.articleService.getElement(<number>article.id); 31 let articleElement = this.articleService.getElement(<number>article.id);
32 return this.create(comment, articleElement, null, { 'Content-Type': 'application/json' }, false); 32 return this.create(comment, articleElement, null, { 'Content-Type': 'application/json' }, false);
33 } 33 }
  34 +
  35 + removeFromArticle(article: noosfero.Article, comment: noosfero.Comment): ng.IPromise<noosfero.RestResult<noosfero.Comment>> {
  36 + let articleElement = this.articleService.getElement(<number>article.id);
  37 + return this.remove(comment, articleElement);
  38 + }
34 } 39 }
src/lib/ng-noosfero-api/interfaces/article.ts
@@ -13,5 +13,6 @@ namespace noosfero { @@ -13,5 +13,6 @@ namespace noosfero {
13 setting: any; 13 setting: any;
14 start_date: string; 14 start_date: string;
15 end_date: string; 15 end_date: string;
  16 + accept_comments: boolean;
16 } 17 }
17 } 18 }
src/plugins/comment_paragraph/allow-comment/allow-comment.component.spec.ts
@@ -57,7 +57,7 @@ describe(&quot;Components&quot;, () =&gt; { @@ -57,7 +57,7 @@ describe(&quot;Components&quot;, () =&gt; {
57 }); 57 });
58 58
59 it('display button to side comments', () => { 59 it('display button to side comments', () => {
60 - expect(helper.all(".paragraph .actions a").length).toEqual(1); 60 + expect(helper.all(".paragraph .paragraph-actions a").length).toEqual(1);
61 }); 61 });
62 62
63 it('set display to true when click in show paragraph', () => { 63 it('set display to true when click in show paragraph', () => {
@@ -74,5 +74,17 @@ describe(&quot;Components&quot;, () =&gt; { @@ -74,5 +74,17 @@ describe(&quot;Components&quot;, () =&gt; {
74 functionToggleCommentParagraph({ id: 2 }); 74 functionToggleCommentParagraph({ id: 2 });
75 expect(helper.component.article.id).toEqual(2); 75 expect(helper.component.article.id).toEqual(2);
76 }); 76 });
  77 +
  78 + it('not display button to side comments when comments was closed and there is no comment paragraph', () => {
  79 + helper.component.article.accept_comments = false;
  80 + helper.component.commentsCount = 0;
  81 + expect(helper.component.isActivated()).toBeFalsy();
  82 + });
  83 +
  84 + it('display button to side comments when comments was closed and there is some comments to display', () => {
  85 + helper.component.article.accept_comments = false;
  86 + helper.component.commentsCount = 2;
  87 + expect(helper.component.isActivated()).toBeTruthy();
  88 + });
77 }); 89 });
78 }); 90 });
src/plugins/comment_paragraph/allow-comment/allow-comment.component.ts
@@ -32,7 +32,9 @@ export class AllowCommentComponent { @@ -32,7 +32,9 @@ export class AllowCommentComponent {
32 } 32 }
33 33
34 isActivated() { 34 isActivated() {
35 - return this.article && this.article.setting && this.article.setting.comment_paragraph_plugin_activate; 35 + return this.article && this.article.setting &&
  36 + this.article.setting.comment_paragraph_plugin_activate &&
  37 + (this.article.accept_comments || this.commentsCount > 0);
36 } 38 }
37 39
38 showParagraphComments() { 40 showParagraphComments() {
src/plugins/comment_paragraph/allow-comment/allow-comment.html
1 <div class="paragraph" ng-class="{'active' : ctrl.display}"> 1 <div class="paragraph" ng-class="{'active' : ctrl.display}">
2 <div class="paragraph-content" ng-bind-html="ctrl.content" ng-class="{'active' : ctrl.display}"></div> 2 <div class="paragraph-content" ng-bind-html="ctrl.content" ng-class="{'active' : ctrl.display}"></div>
3 - <div ng-if="ctrl.isActivated()" class="actions"> 3 + <div ng-if="ctrl.isActivated()" class="paragraph-actions">
4 <a href="#" popover-placement="right-top" popover-trigger="none" 4 <a href="#" popover-placement="right-top" popover-trigger="none"
5 uib-popover-template="'plugins/comment_paragraph/allow-comment/popover.html'" 5 uib-popover-template="'plugins/comment_paragraph/allow-comment/popover.html'"
6 (click)="ctrl.showParagraphComments()" popover-is-open="ctrl.display"> 6 (click)="ctrl.showParagraphComments()" popover-is-open="ctrl.display">
src/plugins/comment_paragraph/allow-comment/allow-comment.scss
@@ -15,7 +15,7 @@ comment-paragraph-plugin-allow-comment { @@ -15,7 +15,7 @@ comment-paragraph-plugin-allow-comment {
15 width: 95%; 15 width: 95%;
16 display: inline-block; 16 display: inline-block;
17 } 17 }
18 - .actions { 18 + .paragraph-actions {
19 width: 3%; 19 width: 3%;
20 display: inline-block; 20 display: inline-block;
21 vertical-align: top; 21 vertical-align: top;
src/spec/mocks.ts
@@ -76,6 +76,32 @@ export var mocks: any = { @@ -76,6 +76,32 @@ export var mocks: any = {
76 isAuthenticated: () => { } 76 isAuthenticated: () => { }
77 }, 77 },
78 articleService: { 78 articleService: {
  79 + articleRemovedFn: null,
  80 + subscribeToArticleRemoved: (fn: Function) => {
  81 + mocks.articleService.articleRemovedFn = fn;
  82 + },
  83 + articleRemoved:
  84 + {
  85 + subscribe: (fn: Function) => {
  86 + mocks.articleService.articleRemovedFn = fn;
  87 + },
  88 + next: (param: any) => {
  89 + mocks.articleService.articleRemovedFn(param);
  90 + }
  91 + }
  92 + ,
  93 + removeArticle: (article: noosfero.Article) => {
  94 + return {
  95 + catch: (func?: Function) => {
  96 + }
  97 + };
  98 + },
  99 + notifyArticleRemovedListeners: (article: noosfero.Article) => {
  100 + mocks.articleService.articleRemoved.next(article);
  101 + },
  102 + subscribe: (eventType: any, fn: Function) => {
  103 + mocks.articleService.articleRemoved.subscribe(fn);
  104 + },
79 getByProfile: (profileId: number, params?: any) => { 105 getByProfile: (profileId: number, params?: any) => {
80 return { 106 return {
81 then: (func?: Function) => { 107 then: (func?: Function) => {