Merge Request #30

Closed
noosfero-themes/angular-theme!30
Created by Michel Felipe

Comment Paragraph - Comment count update

Added EventEmitter structure using Typescript mixin pattern to allow multiple events by different component objects with plugin workflow created by @vfcosta

This screenshot bellow shows with the value updated when the user add a new comment:

count-comment

Assignee: Ábner Oliveira
Milestone: None

Closed by Ábner Oliveira

Changes were not merged into target branch

Commits (3)
3 participants
src/app/article/comment/comments.component.spec.ts
... ... @@ -11,7 +11,7 @@ describe("Components", () => {
11 11  
12 12 beforeEach(angular.mock.module("templates"));
13 13  
14   - let commentService = jasmine.createSpyObj("commentService", ["getByArticle"]);
  14 + let commentService = helpers.mocks.commentService;
15 15  
16 16 let comments = [{ id: 2 }, { id: 3 }];
17 17 commentService.getByArticle = jasmine.createSpy("getByArticle")
... ... @@ -19,7 +19,7 @@ describe("Components", () => {
19 19  
20 20 let properties = { article: { id: 1 }, parent: <any>null };
21 21 function createComponent() {
22   - // postCommentEventService = jasmine.createSpyObj("postCommentEventService", ["subscribe"]);
  22 +
23 23 let providers = [
24 24 helpers.createProviderToValue('CommentService', commentService),
25 25 helpers.createProviderToValue('NotificationService', helpers.mocks.notificationService),
... ... @@ -52,7 +52,9 @@ describe(&quot;Components&quot;, () =&gt; {
52 52 it("update comments list when receive an reply", done => {
53 53 properties.parent = { id: 3 };
54 54 createComponent().then(fixture => {
55   - (<CommentsComponent>fixture.debugElement.componentViewChildren[0].componentInstance).commentAdded(<noosfero.Comment>{ id: 1, reply_of: { id: 3 } });
  55 + let component = (<CommentsComponent>fixture.debugElement.componentViewChildren[0].componentInstance);
  56 + component.commentAdded(<noosfero.Comment>{ id: 1, reply_of: { id: 3 } });
  57 +
56 58 fixture.detectChanges();
57 59 expect(fixture.debugElement.queryAll("noosfero-comment").length).toEqual(3);
58 60 done();
... ...
src/app/article/comment/comments.component.ts
... ... @@ -6,10 +6,9 @@ import { CommentComponent } from &quot;./comment.component&quot;;
6 6 @Component({
7 7 selector: 'noosfero-comments',
8 8 templateUrl: 'app/article/comment/comments.html',
9   - directives: [PostCommentComponent, CommentComponent],
10   - outputs: ['commentAdded']
  9 + directives: [PostCommentComponent, CommentComponent]
11 10 })
12   -@Inject(CommentService, "$element")
  11 +@Inject(CommentService, "$scope")
13 12 export class CommentsComponent {
14 13  
15 14 comments: noosfero.CommentViewModel[] = [];
... ... @@ -22,7 +21,7 @@ export class CommentsComponent {
22 21  
23 22 newComment = <noosfero.Comment>{};
24 23  
25   - constructor(protected commentService: CommentService, private $scope: ng.IScope) { }
  24 + constructor(public commentService: CommentService, private $scope: ng.IScope) { }
26 25  
27 26 ngOnInit() {
28 27 if (this.parent) {
... ... @@ -30,20 +29,27 @@ export class CommentsComponent {
30 29 } else {
31 30 this.loadNextPage();
32 31 }
  32 +
  33 + this.commentService.addEvent<noosfero.Comment>({
  34 + event: 'onSave',
  35 + component: 'CommentsComponent',
  36 + callback: (comment: noosfero.Comment) => {
  37 +
  38 + if (this.commentService.called !== <string>comment.id.toString()) {
  39 +
  40 + this.commentAdded(comment);
  41 + this.commentService.called = comment.id.toString();
  42 + }
  43 + }
  44 + });
  45 +
33 46 }
34 47  
35   - commentAdded(comment: noosfero.Comment): void {
  48 + commentAdded(comment: noosfero.CommentViewModel): void {
  49 + comment.__show_reply = false;
36 50 this.comments.push(comment);
37 51 this.resetShowReply();
38   - }
39   -
40   - private resetShowReply() {
41   - this.comments.forEach((comment: noosfero.CommentViewModel) => {
42   - comment.__show_reply = false;
43   - });
44   - if (this.parent) {
45   - this.parent.__show_reply = false;
46   - }
  52 + this.$scope.$apply();
47 53 }
48 54  
49 55 loadComments() {
... ... @@ -62,4 +68,13 @@ export class CommentsComponent {
62 68 let pages = Math.ceil(this.total / this.perPage);
63 69 return !this.parent && pages >= this.page;
64 70 }
  71 +
  72 + private resetShowReply() {
  73 + this.comments.forEach((comment: noosfero.CommentViewModel) => {
  74 + comment.__show_reply = false;
  75 + });
  76 + if (this.parent) {
  77 + this.parent.__show_reply = false;
  78 + }
  79 + }
65 80 }
... ...
src/app/article/comment/comments.html
1 1 <div class="comments">
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   -
  2 + <noosfero-post-comment ng-if="ctrl.showForm" [article]="ctrl.article" [parent]="ctrl.parent" [comment]="ctrl.newComment"></noosfero-post-comment>
  3 +
4 4 <div class="comments-list">
5 5 <noosfero-comment ng-repeat="comment in ctrl.comments | orderBy: 'created_at':true" [comment]="comment" [article]="ctrl.article"></noosfero-comment>
6 6 </div>
... ...
src/app/article/comment/post-comment/post-comment.component.spec.ts
... ... @@ -11,7 +11,8 @@ describe(&quot;Components&quot;, () =&gt; {
11 11  
12 12 beforeEach(angular.mock.module("templates"));
13 13  
14   - let commentService = jasmine.createSpyObj("commentService", ["createInArticle"]);
  14 + let commentService = helpers.mocks.commentService;
  15 + commentService.emitEvent = jasmine.createSpy("emitEvent");
15 16 let user = {};
16 17 let providers = [
17 18 new Provider('CommentService', { useValue: commentService }),
... ... @@ -43,10 +44,9 @@ describe(&quot;Components&quot;, () =&gt; {
43 44 it("emit an event when create comment", done => {
44 45 helpers.createComponentFromClass(ContainerComponent).then(fixture => {
45 46 let component: PostCommentComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
46   - component.commentSaved.next = jasmine.createSpy("next");
47 47 commentService.createInArticle = jasmine.createSpy("createInArticle").and.returnValue(helpers.mocks.promiseResultTemplate({ data: {} }));
48 48 component.save();
49   - expect(component.commentSaved.next).toHaveBeenCalled();
  49 + expect(component.commentService.emitEvent).toHaveBeenCalled();
50 50 done();
51 51 });
52 52 });
... ...
src/app/article/comment/post-comment/post-comment.component.ts
  1 +import {ngClass} from 'ng-forward/cjs/testing/test-component-builder';
1 2 import { Inject, Input, Output, EventEmitter, Component } from 'ng-forward';
2 3 import { CommentService } from "../../../../lib/ng-noosfero-api/http/comment.service";
3 4 import { NotificationService } from "../../../shared/services/notification.service";
... ... @@ -17,11 +18,12 @@ export class PostCommentComponent {
17 18  
18 19 @Input() article: noosfero.Article;
19 20 @Input() parent: noosfero.Comment;
20   - @Output() commentSaved: EventEmitter<Comment> = new EventEmitter<Comment>();
21 21 @Input() comment = <noosfero.Comment>{};
  22 + @Input() identifier: any;
22 23 private currentUser: noosfero.User;
23 24  
24   - constructor(private commentService: CommentService,
  25 + constructor(
  26 + public commentService: CommentService,
25 27 private notificationService: NotificationService,
26 28 private session: SessionService) {
27 29 this.currentUser = this.session.currentUser();
... ... @@ -32,7 +34,22 @@ export class PostCommentComponent {
32 34 this.comment.reply_of_id = this.parent.id;
33 35 }
34 36 this.commentService.createInArticle(this.article, this.comment).then((result: noosfero.RestResult<noosfero.Comment>) => {
35   - this.commentSaved.next(result.data);
  37 +
  38 + if (this.identifier) {
  39 + this.commentService.emitEvent({
  40 + id: this.identifier,
  41 + event: 'onSave',
  42 + param: result.data
  43 + });
  44 + } else {
  45 + this.commentService.emitEvent({
  46 + id: result.data.id,
  47 + component: 'CommentsComponent',
  48 + event: 'onSave',
  49 + param: result.data
  50 + });
  51 + }
  52 +
36 53 this.comment.body = "";
37 54 this.notificationService.success({ title: "comment.post.success.title", message: "comment.post.success.message" });
38 55 });
... ...
src/lib/ng-noosfero-api/decorators/mixins.ts 0 → 100644
... ... @@ -0,0 +1,9 @@
  1 +export function Mixins(...mixins: Function[]) {
  2 + return function(target: Function) {
  3 + mixins.forEach(mixin => {
  4 + Object.getOwnPropertyNames(mixin.prototype).forEach(name => {
  5 + target.prototype[name] = mixin.prototype[name];
  6 + });
  7 + });
  8 + };
  9 +}
... ...
src/lib/ng-noosfero-api/http/comment.service.ts
1   -import { Injectable, Inject } from "ng-forward";
  1 +import { Injectable, Inject, EventEmitter } from "ng-forward";
2 2 import {RestangularService} from "./restangular_service";
3 3 import {ArticleService} from "./article.service";
  4 +import {Mixins, EventService, IEvent} from '../mixins/event-service.mixin';
  5 +
4 6  
5 7 @Injectable()
  8 +@Mixins(EventService)
6 9 @Inject("Restangular", "$q", "$log", ArticleService)
7   -export class CommentService extends RestangularService<noosfero.Comment> {
  10 +export class CommentService extends RestangularService<noosfero.Comment> implements EventService {
  11 +
  12 + events: IEvent[] = [];
  13 +
  14 + called: string;
8 15  
9 16 constructor(Restangular: restangular.IService, $q: ng.IQService, $log: ng.ILogService, protected articleService: ArticleService) {
10 17 super(Restangular, $q, $log);
... ... @@ -31,4 +38,9 @@ export class CommentService extends RestangularService&lt;noosfero.Comment&gt; {
31 38 let articleElement = this.articleService.getElement(<number>article.id);
32 39 return this.create(comment, articleElement, null, { 'Content-Type': 'application/json' }, false);
33 40 }
  41 +
  42 + // Mixin "EventService" declarations
  43 + addEvent: <T>(data: { event: string, callback: Function, id?: any, component?: string, scope?: any }) => void;
  44 + getEvents: (data: { event: string, id?: any, component?: string }) => void;
  45 + emitEvent: <T>(data: { event: string, param: T, id?: any, component?: string }) => void;
34 46 }
... ...
src/lib/ng-noosfero-api/mixins/event-service.mixin.ts 0 → 100644
... ... @@ -0,0 +1,104 @@
  1 +import { EventEmitter } from "ng-forward";
  2 +export {Mixins} from "../decorators/mixins";
  3 +
  4 +export interface IEvent {
  5 + id: any;
  6 + component: string;
  7 + emitters: {
  8 + onSave?: EventEmitter<noosfero.Comment>
  9 + };
  10 +}
  11 +
  12 +/**
  13 + * @ngdoc object
  14 + * @name mixins.EventService
  15 + * @see Typescript official documentation https://www.typescriptlang.org/docs/mixins.html
  16 + * @description
  17 + *
  18 + * A mixin class to be implemented by another class (e.g. Angular Services)
  19 + * to emit's and subscribe events
  20 + *
  21 + * <pre>
  22 + * @Mixins(EventService)
  23 + * export class MyService implements EventService {
  24 + *
  25 + * }
  26 + * </pre>
  27 + */
  28 +export class EventService {
  29 +
  30 + events: IEvent[] = [];
  31 +
  32 + called: string;
  33 +
  34 + addEvent<T>(data: { event: string, callback: Function, id?: any, component?: string, scope?: any }) {
  35 +
  36 + let hasEvent = false;
  37 +
  38 + if (this.events.length > 0) {
  39 +
  40 + this.events.forEach((eventData: IEvent, index: number) => {
  41 + if (data.id === eventData.id) {
  42 +
  43 + if (!(<any>this.events)[index].emitters[data.event]) {
  44 + (<any>this.events)[index].emitters[data.event] = new EventEmitter();
  45 + } else {
  46 + (<EventEmitter<T>>(<any>this.events)[index].emitters[data.event]).subscribe(data.callback.bind(data.scope || this));
  47 + }
  48 +
  49 + return (hasEvent = true);
  50 + }
  51 + });
  52 + }
  53 +
  54 + if (!hasEvent) {
  55 +
  56 + let newEvent: IEvent = {
  57 + id: data.id,
  58 + component: data.component,
  59 + emitters: <any>{}
  60 + };
  61 +
  62 + (<any>newEvent.emitters)[data.event] = <EventEmitter<T>>new EventEmitter();
  63 + (<any>newEvent.emitters)[data.event].subscribe(data.callback);
  64 +
  65 + this.events.push(newEvent);
  66 + }
  67 + }
  68 +
  69 + getEvents(data: { event: string, id?: any, component?: string }): any {
  70 + let result: any = {};
  71 +
  72 + for (let i = 0; i < this.events.length; i++) {
  73 + if (data.id === (<Array<IEvent>>this.events)[i].id && !data.component) {
  74 +
  75 + result = (<any>this.events)[i].emitters[data.event];
  76 + break;
  77 + } else if (data.component === (<any>this.events)[i].component) {
  78 + result[data.component] = (<any>this.events)[i].emitters[data.event];
  79 +
  80 + break;
  81 + } else {
  82 + result[(<any>this.events)[i].component] = (<any>this.events)[i].emitters[data.event];
  83 + }
  84 + }
  85 +
  86 + return result;
  87 + }
  88 +
  89 + emitEvent<T>(data: { event: string, param: T, id?: any, component?: string }) {
  90 +
  91 + let eventOrEmitters = this.getEvents(data);
  92 +
  93 + if (eventOrEmitters instanceof EventEmitter) {
  94 +
  95 + eventOrEmitters.next(data.param);
  96 + } else {
  97 +
  98 + Object.keys(eventOrEmitters).forEach((emitter: string) => {
  99 + (<EventEmitter<T>>eventOrEmitters[emitter]).next(data.param);
  100 + });
  101 +
  102 + }
  103 + }
  104 +}
... ...
src/plugins/comment_paragraph/allow-comment/allow-comment.component.spec.ts
... ... @@ -24,7 +24,8 @@ describe(&quot;Components&quot;, () =&gt; {
24 24  
25 25 let providers = [
26 26 new Provider('CommentParagraphService', { useValue: serviceMock }),
27   - new Provider('CommentParagraphEventService', { useValue: eventServiceMock })
  27 + new Provider('CommentParagraphEventService', { useValue: eventServiceMock }),
  28 + new Provider('CommentService', { useValue: helpers.mocks.commentService })
28 29 ];
29 30 let helper: ComponentTestHelper<AllowCommentComponent>;
30 31  
... ...
src/plugins/comment_paragraph/allow-comment/allow-comment.component.ts
1   -import {Component, Input, Inject} from "ng-forward";
  1 +import {Component, Input, Inject, bundleStore} from "ng-forward";
2 2 import {SideCommentsComponent} from "../side-comments/side-comments.component";
3 3 import {CommentParagraphEventService} from "../events/comment-paragraph-event.service";
4 4 import {CommentParagraphService} from "../http/comment-paragraph.service";
  5 +import {CommentService} from "../../../lib/ng-noosfero-api/http/comment.service";
  6 +import {PostCommentComponent} from '../../../app/article/comment/post-comment/post-comment.component';
5 7  
6 8 @Component({
7 9 selector: "comment-paragraph-plugin-allow-comment",
8 10 templateUrl: "plugins/comment_paragraph/allow-comment/allow-comment.html",
9 11 directives: [SideCommentsComponent]
10 12 })
11   -@Inject("$scope", CommentParagraphEventService, CommentParagraphService)
  13 +@Inject("$scope", CommentParagraphEventService, CommentParagraphService, CommentService)
12 14 export class AllowCommentComponent {
13 15  
14 16 @Input() content: string;
15 17 @Input() paragraphUuid: string;
16 18 @Input() article: noosfero.Article;
17   - commentsCount: number;
  19 + commentsCount: number = 0;
18 20 display = false;
19 21  
20   - constructor(private $scope: ng.IScope,
  22 + constructor(
  23 + private $scope: ng.IScope,
21 24 private commentParagraphEventService: CommentParagraphEventService,
22   - private commentParagraphService: CommentParagraphService) { }
  25 + private commentParagraphService: CommentParagraphService,
  26 + private commentService: CommentService
  27 + ) { }
23 28  
24 29 ngOnInit() {
25 30 this.commentParagraphEventService.subscribeToggleCommentParagraph((article: noosfero.Article) => {
... ... @@ -29,6 +34,20 @@ export class AllowCommentComponent {
29 34 this.commentParagraphService.commentParagraphCount(this.article, this.paragraphUuid).then((count: number) => {
30 35 this.commentsCount = count;
31 36 });
  37 +
  38 + this.commentService.addEvent<noosfero.Comment>({
  39 + id: this.paragraphUuid,
  40 + event: 'onSave',
  41 + component: 'SideCommentsComponent',
  42 + scope: this,
  43 + callback: () => {
  44 + if (!this.commentsCount) {
  45 + this.commentsCount = 0;
  46 + }
  47 + this.commentsCount++;
  48 + this.$scope.$apply();
  49 + }
  50 + });
32 51 }
33 52  
34 53 isActivated() {
... ...
src/plugins/comment_paragraph/side-comments/side-comments.component.spec.ts
... ... @@ -12,14 +12,9 @@ describe(&quot;Components&quot;, () =&gt; {
12 12 let serviceMock = jasmine.createSpyObj("CommentParagraphService", ["getByArticle"]);
13 13 serviceMock.getByArticle = jasmine.createSpy("getByArticle").and.returnValue(Promise.resolve({ data: {} }));
14 14  
15   - let commentServiceMock = {};
16   - let postCommentEventService = jasmine.createSpyObj("postCommentEventService", ["emit", "subscribe"]);
17   - postCommentEventService.subscribe = jasmine.createSpy("subscribe");
18   -
19 15 let providers = [
20 16 new Provider('CommentParagraphService', { useValue: serviceMock }),
21   - new Provider('CommentService', { useValue: commentServiceMock }),
22   - new Provider('PostCommentEventService', { useValue: postCommentEventService })
  17 + new Provider('CommentService', { useValue: helpers.mocks.commentService }),
23 18 ];
24 19 let helper: ComponentTestHelper<SideCommentsComponent>;
25 20  
... ...
src/plugins/comment_paragraph/side-comments/side-comments.component.ts
... ... @@ -5,7 +5,7 @@ import {CommentParagraphService} from &quot;../http/comment-paragraph.service&quot;;
5 5  
6 6 @Component({
7 7 selector: "comment-paragraph-side-comments",
8   - templateUrl: 'app/article/comment/comments.html',
  8 + templateUrl: 'plugins/comment_paragraph/side-comments/side-comments.html',
9 9 })
10 10 @Inject(CommentService, "$scope", CommentParagraphService)
11 11 export class SideCommentsComponent extends CommentsComponent {
... ...
src/plugins/comment_paragraph/side-comments/side-comments.html 0 → 100644
... ... @@ -0,0 +1,8 @@
  1 +<div class="comments">
  2 + <noosfero-post-comment ng-if="ctrl.showForm" [article]="ctrl.article" [parent]="ctrl.parent" [comment]="ctrl.newComment" [identifier]="ctrl.paragraphUuid"></noosfero-post-comment>
  3 +
  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>
  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>
  8 +</div>
... ...
src/spec/mocks.ts
... ... @@ -122,9 +122,13 @@ export var mocks: any = {
122 122 instant: () => { }
123 123 },
124 124 commentService: {
  125 + events: <any[]>[],
  126 + called: <string>'',
125 127 getByArticle: (article: noosfero.Article) => {
126 128 return Promise.resolve({ data: {} });
127   - }
  129 + },
  130 + addEvent: () => { },
  131 + emitEvent: () => { }
128 132 },
129 133 sessionWithCurrentUser: (user: any) => {
130 134 return {
... ...