Compare View
Commits (10)
-
Remove article See merge request !22
-
…n of the allowRemove method.
-
Add a button to remove comments from articles See merge request !21
-
Hide button to side comments when there is no comments to display and article doesn't accept comments anymore See merge request !24
-
TODO: Tests missing Signed-off-by: Fabio Teixeira <fabio1079@gmail.com>
Showing
35 changed files
Show diff stats
src/app/article/article-default-view-component.spec.ts
1 | 1 | import {Input, provide, Component} from 'ng-forward'; |
2 | 2 | import {ArticleViewComponent, ArticleDefaultViewComponent} from './article-default-view.component'; |
3 | +import {ComponentTestHelper, createClass} from './../../spec/component-test-helper'; | |
3 | 4 | |
4 | 5 | import * as helpers from "../../spec/helpers"; |
5 | 6 | |
... | ... | @@ -9,113 +10,82 @@ const htmlTemplate: string = '<noosfero-article [article]="ctrl.article" [profil |
9 | 10 | |
10 | 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 "./comment/comments.component"; |
4 | 4 | import {MacroDirective} from "./macro/macro.directive"; |
5 | 5 | import {ArticleToolbarHotspotComponent} from "../hotspot/article-toolbar-hotspot.component"; |
6 | 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 | 10 | * @ngdoc controller |
... | ... | @@ -16,11 +17,29 @@ import {ArticleContentHotspotComponent} from "../hotspot/article-content-hotspot |
16 | 17 | selector: 'noosfero-default-article', |
17 | 18 | templateUrl: 'app/article/article.html' |
18 | 19 | }) |
20 | +@Inject("$state", ArticleService) | |
19 | 21 | export class ArticleDefaultViewComponent { |
20 | 22 | |
21 | 23 | @Input() article: noosfero.Article; |
22 | 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 | 79 | private $injector: ng.auto.IInjectorService, |
61 | 80 | private $compile: ng.ICompileService) { |
62 | 81 | } |
82 | + | |
63 | 83 | } | ... | ... |
... | ... | @@ -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 | 7 | <a href="#" class="btn btn-default btn-xs" ui-sref="main.cmsEdit({profile: ctrl.profile.identifier, id: ctrl.article.id})"> |
8 | 8 | <i class="fa fa-pencil-square-o fa-fw fa-lg"></i> {{"article.actions.edit" | translate}} |
9 | 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 | 13 | <noosfero-hotspot-article-toolbar [article]="ctrl.article"></noosfero-hotspot-article-toolbar> |
11 | 14 | <div class="page-info pull-right small text-muted"> |
12 | 15 | <span class="time"> | ... | ... |
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("Components", () => { |
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/main/main.component.ts
... | ... | @@ -16,6 +16,7 @@ import {RecentDocumentsBlockComponent} from "../layout/blocks/recent-documents/r |
16 | 16 | import {ProfileImageBlockComponent} from "../layout/blocks/profile-image/profile-image-block.component"; |
17 | 17 | import {RawHTMLBlockComponent} from "../layout/blocks/raw-html/raw-html-block.component"; |
18 | 18 | import {StatisticsBlockComponent} from "../layout/blocks/statistics/statistics-block.component"; |
19 | +import {ProfileWallComponent} from "../profile/wall/profile-wall.component"; | |
19 | 20 | |
20 | 21 | import {MembersBlockComponent} from "../layout/blocks/members/members-block.component"; |
21 | 22 | import {CommunitiesBlockComponent} from "../layout/blocks/communities/communities-block.component"; |
... | ... | @@ -99,7 +100,7 @@ export class EnvironmentContent { |
99 | 100 | LinkListBlockComponent, CommunitiesBlockComponent, HtmlEditorComponent, |
100 | 101 | MainBlockComponent, RecentDocumentsBlockComponent, Navbar, SidebarComponent, ProfileImageBlockComponent, |
101 | 102 | MembersBlockComponent, NoosferoTemplate, DateFormat, RawHTMLBlockComponent, StatisticsBlockComponent, |
102 | - LoginBlockComponent | |
103 | + LoginBlockComponent, ProfileWallComponent | |
103 | 104 | ].concat(plugins.mainComponents).concat(plugins.hotspots), |
104 | 105 | |
105 | 106 | providers: [AuthService, SessionService, NotificationService, BodyStateClassesService] | ... | ... |
src/app/profile/info/profile-info.component.ts
... | ... | @@ -13,8 +13,6 @@ import {TranslateProfile} from "../../shared/pipes/translate-profile.filter"; |
13 | 13 | @Inject(ProfileService) |
14 | 14 | @Inject("amDateFormatFilter") |
15 | 15 | export class ProfileInfoComponent { |
16 | - | |
17 | - activities: any; | |
18 | 16 | profile: noosfero.Profile; |
19 | 17 | |
20 | 18 | constructor(private profileService: ProfileService, private amDateFormatFilter: any) { |
... | ... | @@ -24,9 +22,6 @@ export class ProfileInfoComponent { |
24 | 22 | init() { |
25 | 23 | this.profileService.getCurrentProfile().then((profile: noosfero.Profile) => { |
26 | 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 | 1 | <div class="profile-wall"> |
2 | - | |
3 | 2 | <div class="col-lg-3 col-md-4 col-sm-4"> |
4 | 3 | <div class="main-box clearfix"> |
5 | 4 | <header class="main-box-header clearfix"> |
... | ... | @@ -16,15 +15,6 @@ |
16 | 15 | </div> |
17 | 16 | |
18 | 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 | 19 | </div> |
30 | 20 | </div> | ... | ... |
... | ... | @@ -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 | +} | ... | ... |
... | ... | @@ -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 | 12 | \ No newline at end of file | ... | ... |
src/app/shared/models/interfaces.ts
src/app/shared/services/notification.service.spec.ts
... | ... | @@ -20,10 +20,10 @@ describe("Components", () => { |
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("Components", () => { |
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("Components", () => { |
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("Components", () => { |
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("Components", () => { |
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 | } | ... | ... |
... | ... | @@ -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 | 20 | \ No newline at end of file | ... | ... |
... | ... | @@ -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 | 37 | \ No newline at end of file | ... | ... |
src/languages/en.json
... | ... | @@ -35,8 +35,14 @@ |
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", |
45 | + "article.actions.delete": "Delete", | |
40 | 46 | "article.actions.read_more": "Read More", |
41 | 47 | "article.basic_editor.title": "Title", |
42 | 48 | "article.basic_editor.body": "Body", | ... | ... |
src/languages/pt.json
... | ... | @@ -35,8 +35,14 @@ |
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", |
45 | + "article.actions.delete": "Excluir", | |
40 | 46 | "article.actions.read_more": "Ler mais", |
41 | 47 | "article.basic_editor.title": "Título", |
42 | 48 | "article.basic_editor.body": "Corpo", | ... | ... |
src/lib/ng-noosfero-api/http/article.service.spec.ts
... | ... | @@ -20,6 +20,15 @@ describe("Services", () => { |
20 | 20 | |
21 | 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 | 32 | it("should return article children", (done) => { |
24 | 33 | let articleId = 1; |
25 | 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 | 2 | import {RestangularService} from "./restangular_service"; |
3 | 3 | import {ProfileService} from "./profile.service"; |
4 | +import {NoosferoRootScope} from "./../../../app/shared/models/interfaces"; | |
4 | 5 | |
5 | 6 | @Injectable() |
6 | 7 | @Inject("Restangular", "$q", "$log", ProfileService) |
7 | - | |
8 | 8 | export class ArticleService extends RestangularService<noosfero.Article> { |
9 | 9 | |
10 | + private articleRemoved: EventEmitter<noosfero.Article> = new EventEmitter<noosfero.Article>(); | |
11 | + | |
10 | 12 | constructor(Restangular: restangular.IService, $q: ng.IQService, $log: ng.ILogService, protected profileService: ProfileService) { |
11 | 13 | super(Restangular, $q, $log); |
12 | 14 | } |
... | ... | @@ -22,6 +24,31 @@ export class ArticleService extends RestangularService<noosfero.Article> { |
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 | 52 | updateArticle(article: noosfero.Article) { |
26 | 53 | let headers = { |
27 | 54 | 'Content-Type': 'application/json' |
... | ... | @@ -103,3 +130,4 @@ export class ArticleService extends RestangularService<noosfero.Article> { |
103 | 130 | |
104 | 131 | |
105 | 132 | } |
133 | + | ... | ... |
src/lib/ng-noosfero-api/http/comment.service.spec.ts
... | ... | @@ -40,6 +40,17 @@ describe("Services", () => { |
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<noosfero.Comment> { |
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/lib/ng-noosfero-api/interfaces/article.ts
src/plugins/comment_paragraph/allow-comment/allow-comment.component.spec.ts
... | ... | @@ -57,7 +57,7 @@ describe("Components", () => { |
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', () => { |
... | ... | @@ -74,5 +74,17 @@ describe("Components", () => { |
74 | 74 | functionToggleCommentParagraph({ id: 2 }); |
75 | 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 | 32 | } |
33 | 33 | |
34 | 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 | 40 | showParagraphComments() { | ... | ... |
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
src/spec/mocks.ts
... | ... | @@ -76,6 +76,32 @@ export var mocks: any = { |
76 | 76 | isAuthenticated: () => { } |
77 | 77 | }, |
78 | 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 | 105 | getByProfile: (profileId: number, params?: any) => { |
80 | 106 | return { |
81 | 107 | then: (func?: Function) => { | ... | ... |