Commit 4d6c0e819e3163beff9d84fa379ae843f9e089e0

Authored by Ábner Oliveira
2 parents 2417cffc 1eeaf5bc

Merge branch 'remove-comments' into 'master'

Add a button to remove comments from articles

See merge request !21
src/app/article/comment/comment.component.spec.ts
... ... @@ -8,6 +8,8 @@ describe("Components", () => {
8 8 describe("Comment Component", () => {
9 9  
10 10 let properties: any;
  11 + let notificationService = helpers.mocks.notificationService;
  12 + let commentService = jasmine.createSpyObj("commentService", ["removeFromArticle"]);
11 13  
12 14 beforeEach(angular.mock.module("templates"));
13 15 beforeEach(() => {
... ... @@ -18,7 +20,10 @@ describe("Components", () => {
18 20 });
19 21  
20 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 28 @Component({ selector: 'test-container-component', directives: [CommentComponent], template: htmlTemplate, providers: providers })
24 29 class ContainerComponent {
... ... @@ -74,5 +79,36 @@ describe("Components", () => {
74 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 1 import { Inject, Input, Component, Output, EventEmitter } from 'ng-forward';
2 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 6 @Component({
5 7 selector: 'noosfero-comment',
  8 + outputs: ['commentRemoved'],
6 9 templateUrl: 'app/article/comment/comment.html'
7 10 })
  11 +@Inject(CommentService, NotificationService)
8 12 export class CommentComponent {
9 13  
10 14 @Input() comment: noosfero.CommentViewModel;
11 15 @Input() article: noosfero.Article;
12 16 @Input() displayActions = true;
13 17 @Input() displayReplies = true;
  18 + @Output() commentRemoved: EventEmitter<Comment> = new EventEmitter<Comment>();
14 19  
15 20 showReply() {
16 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 27 reply() {
24 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 18 <div class="title">{{ctrl.comment.title}}</div>
19 19 <div class="body">{{ctrl.comment.body}}</div>
20 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 23 {{"comment.reply" | translate}}
23 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 29 </div>
25 30 </div>
26 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 10 .title {
11 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 29 .media-left {
14 30 min-width: 40px;
15 31 }
... ...
src/app/article/comment/comments.component.spec.ts
... ... @@ -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 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 47 private resetShowReply() {
41 48 this.comments.forEach((comment: noosfero.CommentViewModel) => {
42 49 comment.__show_reply = false;
... ...
src/app/article/comment/comments.html
... ... @@ -2,7 +2,7 @@
2 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 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 6 </div>
7 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 8 </div>
... ...
src/app/shared/services/notification.service.spec.ts
... ... @@ -20,10 +20,10 @@ describe(&quot;Components&quot;, () =&gt; {
20 20 let component: NotificationService = new NotificationService(<any>helpers.mocks.$log, <any>sweetAlert, <any>helpers.mocks.translatorService);
21 21 component.error({ message: "message", title: "title" });
22 22 expect(sweetAlert.swal).toHaveBeenCalledWith(jasmine.objectContaining({
23   - text: "message",
24 23 title: "title",
  24 + text: "message",
25 25 type: "error"
26   - }));
  26 + }), null);
27 27 done();
28 28 });
29 29  
... ... @@ -36,7 +36,7 @@ describe(&quot;Components&quot;, () =&gt; {
36 36 expect(sweetAlert.swal).toHaveBeenCalledWith(jasmine.objectContaining({
37 37 text: NotificationService.DEFAULT_ERROR_MESSAGE,
38 38 type: "error"
39   - }));
  39 + }), null);
40 40 done();
41 41 });
42 42  
... ... @@ -48,7 +48,7 @@ describe(&quot;Components&quot;, () =&gt; {
48 48 component.success({ title: "title", message: "message", timer: 1000 });
49 49 expect(sweetAlert.swal).toHaveBeenCalledWith(jasmine.objectContaining({
50 50 type: "success"
51   - }));
  51 + }), null);
52 52 done();
53 53 });
54 54  
... ... @@ -60,7 +60,7 @@ describe(&quot;Components&quot;, () =&gt; {
60 60 component.httpError(500, {});
61 61 expect(sweetAlert.swal).toHaveBeenCalledWith(jasmine.objectContaining({
62 62 text: "notification.http_error.500.message"
63   - }));
  63 + }), null);
64 64 done();
65 65 });
66 66  
... ... @@ -73,7 +73,22 @@ describe(&quot;Components&quot;, () =&gt; {
73 73 expect(sweetAlert.swal).toHaveBeenCalledWith(jasmine.objectContaining({
74 74 type: "success",
75 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 92 done();
78 93 });
79 94 });
... ...
src/app/shared/services/notification.service.ts
... ... @@ -36,15 +36,23 @@ export class NotificationService {
36 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 44 this.$log.debug("Notification message:", title, text, type, this.translatorService.currentLanguage());
41 45 this.SweetAlert.swal({
42 46 title: this.translatorService.translate(title),
43 47 text: this.translatorService.translate(text),
44 48 type: type,
45 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/languages/en.json
... ... @@ -35,7 +35,12 @@
35 35 "comment.pagination.more": "More",
36 36 "comment.post.success.title": "Good job!",
37 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 42 "comment.reply": "reply",
  43 + "comment.remove": "remove",
39 44 "article.actions.edit": "Edit",
40 45 "article.actions.delete": "Delete",
41 46 "article.actions.read_more": "Read More",
... ...
src/languages/pt.json
... ... @@ -35,7 +35,12 @@
35 35 "comment.pagination.more": "Mais",
36 36 "comment.post.success.title": "Bom trabalho!",
37 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 42 "comment.reply": "responder",
  43 + "comment.remove": "remover",
39 44 "article.actions.edit": "Editar",
40 45 "article.actions.delete": "Excluir",
41 46 "article.actions.read_more": "Ler mais",
... ...
src/lib/ng-noosfero-api/http/comment.service.spec.ts
... ... @@ -40,6 +40,17 @@ describe(&quot;Services&quot;, () =&gt; {
40 40 });
41 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 31 let articleElement = this.articleService.getElement(<number>article.id);
32 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/plugins/comment_paragraph/allow-comment/allow-comment.component.spec.ts
... ... @@ -57,7 +57,7 @@ describe(&quot;Components&quot;, () =&gt; {
57 57 });
58 58  
59 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 63 it('set display to true when click in show paragraph', () => {
... ...
src/plugins/comment_paragraph/allow-comment/allow-comment.html
1 1 <div class="paragraph" ng-class="{'active' : ctrl.display}">
2 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 4 <a href="#" popover-placement="right-top" popover-trigger="none"
5 5 uib-popover-template="'plugins/comment_paragraph/allow-comment/popover.html'"
6 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 15 width: 95%;
16 16 display: inline-block;
17 17 }
18   - .actions {
  18 + .paragraph-actions {
19 19 width: 3%;
20 20 display: inline-block;
21 21 vertical-align: top;
... ...