Commit 638280755f7b228312ab3daddf04b8e3e8f1aa05
Exists in
updated_signup_page
and in
1 other branch
Merge branch 'master' into tags-block
Showing
125 changed files
with
1743 additions
and
537 deletions
Show diff stats
Too many changes.
To preserve performance only 100 of 125 files displayed.
gulp/watch.js
... | ... | @@ -15,11 +15,13 @@ gulp.task('watch', ['inject'], function () { |
15 | 15 | |
16 | 16 | gulp.watch([path.join(conf.paths.src, '/*.html'), 'bower.json'], ['inject-reload']); |
17 | 17 | |
18 | - gulp.watch([ | |
19 | - path.join(conf.paths.src, '/app/**/*.css'), | |
20 | - path.join(conf.paths.src, '/app/**/*.scss'), | |
21 | - path.join(conf.paths.src, conf.paths.plugins, '/**/*.scss') | |
22 | - ], function(event) { | |
18 | + var stylePaths = [path.join(conf.paths.src, conf.paths.plugins, '/**/*.scss')]; | |
19 | + conf.paths.allSources.forEach(function(src) { | |
20 | + stylePaths.push(path.join(src, '/app/**/*.css')); | |
21 | + stylePaths.push(path.join(src, '/app/**/*.scss')); | |
22 | + }); | |
23 | + | |
24 | + gulp.watch(stylePaths, function(event) { | |
23 | 25 | if(isOnlyChange(event)) { |
24 | 26 | gulp.start('styles-reload'); |
25 | 27 | } else { | ... | ... |
karma.conf.js
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/cms/article-editor/article-editor.component.spec.ts
0 → 100644
... | ... | @@ -0,0 +1,28 @@ |
1 | +import {ArticleEditorComponent} from './article-editor.component'; | |
2 | +import {BasicEditorComponent} from "../basic-editor/basic-editor.component"; | |
3 | +import {ComponentTestHelper, createClass} from '../../../../spec/component-test-helper'; | |
4 | +import * as helpers from "../../../../spec/helpers"; | |
5 | + | |
6 | +const htmlTemplate: string = '<article-editor [article]="ctrl.article"></article-editor>'; | |
7 | + | |
8 | +describe("Components", () => { | |
9 | + describe("Article Editor Component", () => { | |
10 | + | |
11 | + let helper: ComponentTestHelper<ArticleEditorComponent>; | |
12 | + beforeEach(angular.mock.module("templates")); | |
13 | + | |
14 | + beforeEach((done) => { | |
15 | + let properties = { article: { type: "TextArticle" } }; | |
16 | + let cls = createClass({ | |
17 | + template: htmlTemplate, | |
18 | + directives: [ArticleEditorComponent, BasicEditorComponent], | |
19 | + properties: properties | |
20 | + }); | |
21 | + helper = new ComponentTestHelper<ArticleEditorComponent>(cls, done); | |
22 | + }); | |
23 | + | |
24 | + it("replace element with article basic editor when type is TextArticle", () => { | |
25 | + expect(helper.find("article-basic-editor").length).toEqual(1); | |
26 | + }); | |
27 | + }); | |
28 | +}); | ... | ... |
src/app/article/cms/article-editor/article-editor.component.ts
... | ... | @@ -16,10 +16,10 @@ export class ArticleEditorComponent { |
16 | 16 | private $compile: ng.ICompileService) { } |
17 | 17 | |
18 | 18 | ngOnInit() { |
19 | - let articleType = this.article.type.replace(/::/, ''); | |
19 | + let articleType = this.article && this.article.type ? this.article.type.replace(/::/, '') : "TextArticle"; | |
20 | 20 | let specificDirective = `${articleType.charAt(0).toLowerCase()}${articleType.substring(1)}Editor`; |
21 | 21 | let directiveName = "article-basic-editor"; |
22 | - if (this.$injector.has(specificDirective + 'Directive')) { | |
22 | + if (specificDirective !== "articleEditor" && this.$injector.has(specificDirective + 'Directive')) { | |
23 | 23 | directiveName = specificDirective.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); |
24 | 24 | } |
25 | 25 | this.$element.replaceWith(this.$compile('<' + directiveName + ' [article]="ctrl.article"></' + directiveName + '>')(this.$scope)); | ... | ... |
src/app/article/comment/comment.component.spec.ts
... | ... | @@ -7,16 +7,28 @@ const htmlTemplate: string = '<noosfero-comment [article]="ctrl.article" [commen |
7 | 7 | describe("Components", () => { |
8 | 8 | describe("Comment Component", () => { |
9 | 9 | |
10 | - beforeEach(angular.mock.module("templates")); | |
10 | + let properties: any; | |
11 | + let notificationService = helpers.mocks.notificationService; | |
12 | + let commentService = jasmine.createSpyObj("commentService", ["removeFromArticle"]); | |
11 | 13 | |
14 | + beforeEach(angular.mock.module("templates")); | |
15 | + beforeEach(() => { | |
16 | + properties = { | |
17 | + article: { id: 1, accept_comments: true }, | |
18 | + comment: { title: "title", body: "body" } | |
19 | + }; | |
20 | + }); | |
12 | 21 | |
13 | 22 | function createComponent() { |
14 | - let providers = helpers.provideFilters("translateFilter"); | |
23 | + let providers = [ | |
24 | + helpers.createProviderToValue('NotificationService', notificationService), | |
25 | + helpers.createProviderToValue("CommentService", commentService) | |
26 | + ].concat(helpers.provideFilters("translateFilter")); | |
15 | 27 | |
16 | 28 | @Component({ selector: 'test-container-component', directives: [CommentComponent], template: htmlTemplate, providers: providers }) |
17 | 29 | class ContainerComponent { |
18 | - article = { id: 1 }; | |
19 | - comment = { title: "title", body: "body" }; | |
30 | + article = properties['article']; | |
31 | + comment = properties['comment']; | |
20 | 32 | } |
21 | 33 | return helpers.createComponentFromClass(ContainerComponent); |
22 | 34 | } |
... | ... | @@ -52,5 +64,51 @@ describe("Components", () => { |
52 | 64 | done(); |
53 | 65 | }); |
54 | 66 | }); |
67 | + | |
68 | + it("display reply button", done => { | |
69 | + createComponent().then(fixture => { | |
70 | + expect(fixture.debugElement.queryAll(".comment .actions .reply").length).toEqual(1); | |
71 | + done(); | |
72 | + }); | |
73 | + }); | |
74 | + | |
75 | + it("not display reply button when accept_comments is false", done => { | |
76 | + properties['article']['accept_comments'] = false; | |
77 | + createComponent().then(fixture => { | |
78 | + expect(fixture.debugElement.queryAll(".comment .actions .reply").length).toEqual(0); | |
79 | + done(); | |
80 | + }); | |
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 | + }); | |
55 | 113 | }); |
56 | 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" 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/article/content-viewer/navbar-actions.html
1 | 1 | <ul class="nav navbar-nav"> |
2 | - <li ng-show="vm.profile"> | |
3 | - <a ng-show="vm.parentId" href="#" role="button" ui-sref="main.cms({profile: vm.profile.identifier, parent_id: vm.parentId})"> | |
4 | - <i class="fa fa-file fa-fw fa-lg"></i> {{"navbar.content_viewer_actions.new_post" | translate}} | |
5 | - </a> | |
6 | - </li> | |
7 | - <li ng-show="vm.profile"> | |
8 | - <a href="#" role="button" ui-sref="main.cms({profile: vm.profile.identifier, parent_id: vm.parentId, type: 'CommentParagraphPlugin::Discussion'})"> | |
9 | - <i class="fa fa-file fa-fw fa-lg"></i> {{"navbar.content_viewer_actions.new_post" | translate}} | |
10 | - </a> | |
11 | - </li> | |
2 | + <li class="dropdown profile-menu" uib-dropdown> | |
3 | + <a class="btn dropdown-toggle" data-toggle="dropdown" uib-dropdown-toggle> | |
4 | + {{"navbar.content_viewer_actions.new_item" | translate}} | |
5 | + <i class="fa fa-caret-down"></i> | |
6 | + </a> | |
7 | + <ul class="dropdown-menu" uib-dropdown-menu ng-show="vm.profile"> | |
8 | + <li ng-show="vm.parentId"> | |
9 | + <a href="#" ui-sref="main.cms({profile: vm.profile.identifier, parent_id: vm.parentId})"> | |
10 | + <i class="fa fa-file fa-fw fa-lg"></i> {{"navbar.content_viewer_actions.new_post" | translate}} | |
11 | + </a> | |
12 | + </li> | |
13 | + <li> | |
14 | + <a href="#" ui-sref="main.cms({profile: vm.profile.identifier, parent_id: vm.parentId, type: 'CommentParagraphPlugin::Discussion'})"> | |
15 | + <i class="fa fa-file fa-fw fa-lg"></i> {{"navbar.content_viewer_actions.new_discussion" | translate}} | |
16 | + </a> | |
17 | + </li> | |
18 | + </ul> | |
19 | + </li> | |
20 | + | |
12 | 21 | </ul> |
22 | + | ... | ... |
... | ... | @@ -0,0 +1,53 @@ |
1 | +import {TestComponentBuilder} from 'ng-forward/cjs/testing/test-component-builder'; | |
2 | +import {Provider} from 'ng-forward'; | |
3 | +import {ComponentTestHelper, createClass} from "./../../../spec/component-test-helper"; | |
4 | +import {providers} from 'ng-forward/cjs/testing/providers'; | |
5 | +import {ContentViewerActionsComponent} from '././content-viewer-actions.component'; | |
6 | +import * as helpers from "../../../spec/helpers"; | |
7 | + | |
8 | +const htmlTemplate: string = '<content-viewer-actions [article]="ctrl.article" [profile]="ctrl.profile"></content-viewer-actions>'; | |
9 | + | |
10 | +describe("Components", () => { | |
11 | + | |
12 | + describe("Content Viewer Actions Component", () => { | |
13 | + let serviceMock = { | |
14 | + getEnvironmentPeople: (filters: any): any => { | |
15 | + return Promise.resolve([{ identifier: "person1" }]); | |
16 | + } | |
17 | + }; | |
18 | + let providers = [ | |
19 | + new Provider('ArticleService', { useValue: helpers.mocks.articleService }), | |
20 | + new Provider('ProfileService', { useValue: helpers.mocks.profileService }) | |
21 | + ]; | |
22 | + | |
23 | + let helper: ComponentTestHelper<ContentViewerActionsComponent>; | |
24 | + | |
25 | + beforeEach(angular.mock.module("templates")); | |
26 | + | |
27 | + /** | |
28 | + * The beforeEach procedure will initialize the helper and parse | |
29 | + * the component according to the given providers. Unfortunetly, in | |
30 | + * this mode, the providers and properties given to the construtor | |
31 | + * can't be overriden. | |
32 | + */ | |
33 | + beforeEach((done) => { | |
34 | + // Create the component bed for the test. Optionally, this could be done | |
35 | + // in each test if one needs customization of these parameters per test | |
36 | + let cls = createClass({ | |
37 | + template: htmlTemplate, | |
38 | + directives: [ContentViewerActionsComponent], | |
39 | + providers: providers, | |
40 | + properties: {} | |
41 | + }); | |
42 | + helper = new ComponentTestHelper<ContentViewerActionsComponent>(cls, done); | |
43 | + }); | |
44 | + | |
45 | + it("render the actions new item menu", () => { | |
46 | + expect(helper.all("a[class|='btn dropdown-toggle']")[0]).not.toBeNull(); | |
47 | + }); | |
48 | + | |
49 | + it("render two menu item actions", () => { | |
50 | + expect(helper.all("ul")[1].find("li").length).toBe(2); | |
51 | + }); | |
52 | + }); | |
53 | +}); | ... | ... |
src/app/environment/environment.html
src/app/layout/blocks/display-content/display-content-block.component.spec.ts
0 → 100644
... | ... | @@ -0,0 +1,75 @@ |
1 | +import {TestComponentBuilder} from 'ng-forward/cjs/testing/test-component-builder'; | |
2 | +import {Provider, provide} from 'ng-forward'; | |
3 | +import {ComponentTestHelper, createClass} from './../../../../spec/component-test-helper'; | |
4 | +import {providers} from 'ng-forward/cjs/testing/providers'; | |
5 | +import {DisplayContentBlockComponent} from './display-content-block.component'; | |
6 | +import * as helpers from './../../../../spec/helpers'; | |
7 | + | |
8 | +const htmlTemplate: string = '<noosfero-display-content-block [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-display-content-block>'; | |
9 | + | |
10 | +describe("Components", () => { | |
11 | + | |
12 | + describe("Display Content Block Component", () => { | |
13 | + let state = jasmine.createSpyObj("state", ["go"]); | |
14 | + let providers = [ | |
15 | + provide('ArticleService', { | |
16 | + useValue: helpers.mocks.articleService | |
17 | + }), | |
18 | + provide('$state', { useValue: state }) | |
19 | + ].concat(helpers.provideFilters("translateFilter")); | |
20 | + | |
21 | + let sections: noosfero.Section[] = [ | |
22 | + { value: 'abstract', checked: 'abstract'}, | |
23 | + { value: 'title', checked: 'title' } | |
24 | + ]; | |
25 | + let settings: noosfero.Settings = { | |
26 | + limit: 6, | |
27 | + sections: sections | |
28 | + }; | |
29 | + | |
30 | + let helper: ComponentTestHelper<DisplayContentBlockComponent>; | |
31 | + | |
32 | + beforeEach(angular.mock.module("templates")); | |
33 | + | |
34 | + /** | |
35 | + * The beforeEach procedure will initialize the helper and parse | |
36 | + * the component according to the given providers. Unfortunetly, in | |
37 | + * this mode, the providers and properties given to the construtor | |
38 | + * can't be overriden. | |
39 | + */ | |
40 | + beforeEach((done) => { | |
41 | + // Create the component bed for the test. Optionally, this could be done | |
42 | + // in each test if one needs customization of these parameters per test | |
43 | + let cls = createClass({ | |
44 | + template: htmlTemplate, | |
45 | + directives: [DisplayContentBlockComponent], | |
46 | + providers: providers, | |
47 | + properties: { | |
48 | + block: { | |
49 | + settings: settings | |
50 | + } | |
51 | + } | |
52 | + }); | |
53 | + helper = new ComponentTestHelper<DisplayContentBlockComponent>(cls, done); | |
54 | + }); | |
55 | + | |
56 | + it("verify settings is injected", () => { | |
57 | + expect(helper.component.block).not.toBeNull; | |
58 | + expect(helper.component.block.settings).not.toBeNull; | |
59 | + expect(helper.component.block.settings.limit).toEqual(6); | |
60 | + expect(helper.component.block.settings.sections.length).toEqual(3); | |
61 | + }); | |
62 | + | |
63 | + it("verify abstract is displayed", () => { | |
64 | + expect(helper.all("div[ng-bind-html|='article.abstract']")[0]).not.toBeNull; | |
65 | + }); | |
66 | + | |
67 | + it("verify title is displayed", () => { | |
68 | + expect(helper.all("div > h5")[0]).not.toBeNull; | |
69 | + }); | |
70 | + | |
71 | + it("verify body is not displayed", () => { | |
72 | + expect(helper.all("div[ng-bind-html|='article.body']")[0]).toBeNull; | |
73 | + }); | |
74 | + }); | |
75 | +}); | ... | ... |
src/app/layout/blocks/display-content/display-content-block.component.ts
0 → 100644
... | ... | @@ -0,0 +1,54 @@ |
1 | +import {Input, Inject, Component} from "ng-forward"; | |
2 | +import {ArticleService} from "../../../../lib/ng-noosfero-api/http/article.service"; | |
3 | + | |
4 | +@Component({ | |
5 | + selector: "noosfero-display-content-block", | |
6 | + templateUrl: 'app/layout/blocks/display-content/display-content-block.html', | |
7 | +}) | |
8 | +@Inject(ArticleService, "$state") | |
9 | +export class DisplayContentBlockComponent { | |
10 | + | |
11 | + @Input() block: noosfero.Block; | |
12 | + @Input() owner: noosfero.Profile; | |
13 | + | |
14 | + profile: noosfero.Profile; | |
15 | + articles: noosfero.Article[]; | |
16 | + sections: noosfero.Section[]; | |
17 | + | |
18 | + documentsLoaded: boolean = false; | |
19 | + | |
20 | + constructor(private articleService: ArticleService, private $state: ng.ui.IStateService) {} | |
21 | + | |
22 | + ngOnInit() { | |
23 | + this.profile = this.owner; | |
24 | + let limit = ((this.block && this.block.settings) ? this.block.settings.limit : null) || 5; | |
25 | + this.articleService.getByProfile(this.profile, { content_type: 'TinyMceArticle', per_page: limit }) | |
26 | + .then((result: noosfero.RestResult<noosfero.Article[]>) => { | |
27 | + this.articles = <noosfero.Article[]>result.data; | |
28 | + this.sections = this.block.settings.sections; | |
29 | + // Add sections not defined by Noosfero API | |
30 | + this.addDefaultSections(); | |
31 | + this.documentsLoaded = true; | |
32 | + }); | |
33 | + } | |
34 | + | |
35 | + /** | |
36 | + * This configuration doesn't exists on Noosfero. Statically typing here. | |
37 | + */ | |
38 | + private addDefaultSections() { | |
39 | + let author: noosfero.Section = <noosfero.Section>{ value: 'author', checked: 'author' }; | |
40 | + this.sections.push(author); | |
41 | + } | |
42 | + | |
43 | + /** | |
44 | + * Returns whether a settings section should be displayed. | |
45 | + * | |
46 | + */ | |
47 | + private display(section_name: string): boolean { | |
48 | + let section: noosfero.Section = this.sections.find( function(section: noosfero.Section) { | |
49 | + return section.value === section_name; | |
50 | + }); | |
51 | + return section !== undefined && section.checked !== undefined; | |
52 | + } | |
53 | + | |
54 | +} | ... | ... |
src/app/layout/blocks/display-content/display-content-block.html
0 → 100644
... | ... | @@ -0,0 +1,46 @@ |
1 | +<div class="{{ctrl.type}}-block"> | |
2 | + <div ng-repeat="article in ctrl.articles" ui-sref="main.profile.page({profile: ctrl.profile.identifier, page: article.path})"" class="article"> | |
3 | + <!-- Article Title --> | |
4 | + <div class="page-header" ng-if="ctrl.display('title')"> | |
5 | + <h5 class="title media-heading" ng-bind="article.title"></h3> | |
6 | + </div> | |
7 | + | |
8 | + <div class="sub-header clearfix"> | |
9 | + <!-- Article Abstract and Read More Link --> | |
10 | + <div class="post-lead" ng-if="ctrl.display('abstract')"> | |
11 | + <div ng-bind-html="article.abstract"></div> | |
12 | + <a href="#" ui-sref="main.profile.page({profile: ctrl.profile.identifier, page: article.path})"> | |
13 | + <i class="fa fa-pencil-square-o fa-fw fa-lg"></i> {{"article.actions.read_more" | translate}} | |
14 | + </a> | |
15 | + </div> | |
16 | + <div class="page-info pull-right small text-muted" ng-if="ctrl.display('publish_date')"> | |
17 | + <!-- Article Published Date --> | |
18 | + <span class="time"> | |
19 | + <i class="fa fa-clock-o"></i> <span am-time-ago="article.created_at | dateFormat"></span> | |
20 | + </span> | |
21 | + <!-- Article Author --> | |
22 | + <span class="author" ng-if="ctrl.display('author')"> | |
23 | + <i class="fa fa-user"></i> | |
24 | + <a ui-sref="main.profile.home({profile: article.author.identifier})" ng-if="article.author"> | |
25 | + <span class="author-name" ng-bind="article.author.name"></span> | |
26 | + </a> | |
27 | + </span> | |
28 | + </div> | |
29 | + </div> | |
30 | + | |
31 | + <div class="post-lead"> | |
32 | + <!-- Article Image --> | |
33 | + <img ng-show="ctrl.display('image')" ng-src="{{article.image.url}}" class="img-responsive article-image"> | |
34 | + <!-- Article Body --> | |
35 | + <div ng-bind-html="article.body" ng-show="ctrl.display('body')"></div> | |
36 | + </div> | |
37 | + | |
38 | + <!-- Article Tags --> | |
39 | + <div ng-if="ctrl.display('tags')" class="post-lead"> | |
40 | + <div class="label" ng-repeat="tag in article.tag_list"> | |
41 | + <span class="badge" ng-bind="tag"></span> | |
42 | + </div> | |
43 | + </div> | |
44 | + | |
45 | + </div> | |
46 | +</div> | ... | ... |
src/app/layout/blocks/display-content/display-content-block.scss
0 → 100644
... | ... | @@ -0,0 +1,17 @@ |
1 | +.members-block { | |
2 | + .member { | |
3 | + img, i.profile-image { | |
4 | + width: 60px; | |
5 | + } | |
6 | + img { | |
7 | + display: inline-block; | |
8 | + vertical-align: top; | |
9 | + } | |
10 | + i.profile-image { | |
11 | + text-align: center; | |
12 | + background-color: #889DB1; | |
13 | + color: #F1F1F1; | |
14 | + font-size: 4.5em; | |
15 | + } | |
16 | + } | |
17 | +} | ... | ... |
src/app/layout/blocks/recent-documents/recent-documents-block.component.spec.ts
... | ... | @@ -11,9 +11,9 @@ describe("Components", () => { |
11 | 11 | describe("Recent Documents Block Component", () => { |
12 | 12 | |
13 | 13 | let settingsObj = {}; |
14 | - let mockedArticleService = { | |
15 | - getByProfile: (profile: noosfero.Profile, filters: any): any => { | |
16 | - return Promise.resolve({ data: [{ name: "article1" }], headers: (name: string) => { return name; } }); | |
14 | + let mockedBlockService = { | |
15 | + getApiContent: (block: noosfero.Block): any => { | |
16 | + return Promise.resolve({ articles: [{ name: "article1" }], headers: (name: string) => { return name; } }); | |
17 | 17 | } |
18 | 18 | }; |
19 | 19 | let profile = { name: 'profile-name' }; |
... | ... | @@ -25,8 +25,8 @@ describe("Components", () => { |
25 | 25 | function getProviders() { |
26 | 26 | return [ |
27 | 27 | new Provider('$state', { useValue: state }), |
28 | - new Provider('ArticleService', { | |
29 | - useValue: mockedArticleService | |
28 | + new Provider('BlockService', { | |
29 | + useValue: mockedBlockService | |
30 | 30 | }), |
31 | 31 | ].concat(provideFilters("truncateFilter", "stripTagsFilter")); |
32 | 32 | } |
... | ... | @@ -44,7 +44,7 @@ describe("Components", () => { |
44 | 44 | } |
45 | 45 | |
46 | 46 | |
47 | - it("get recent documents from the article service", done => { | |
47 | + it("get recent documents from the block service", done => { | |
48 | 48 | tcb.createAsync(getComponent()).then(fixture => { |
49 | 49 | let recentDocumentsBlock: RecentDocumentsBlockComponent = fixture.debugElement.componentViewChildren[0].componentInstance; |
50 | 50 | expect(recentDocumentsBlock.documents).toEqual([{ name: "article1" }]); |
... | ... | @@ -61,20 +61,5 @@ describe("Components", () => { |
61 | 61 | }); |
62 | 62 | }); |
63 | 63 | |
64 | - it("it uses default limit 5 if not defined on block", done => { | |
65 | - settingsObj = null; | |
66 | - mockedArticleService = jasmine.createSpyObj("mockedArticleService", ["getByProfile"]); | |
67 | - (<any>mockedArticleService).mocked = true; | |
68 | - let thenMocked = jasmine.createSpy("then"); | |
69 | - mockedArticleService.getByProfile = jasmine.createSpy("getByProfile").and.returnValue({then: thenMocked}); | |
70 | - let getByProfileFunct = mockedArticleService.getByProfile; | |
71 | - tcb.createAsync(getComponent()).then(fixture => { | |
72 | - let recentDocumentsBlock: RecentDocumentsBlockComponent = fixture.debugElement.componentViewChildren[0].componentInstance; | |
73 | - recentDocumentsBlock.openDocument({ path: "path", profile: { identifier: "identifier" } }); | |
74 | - expect(getByProfileFunct).toHaveBeenCalledWith(profile, { content_type: 'TinyMceArticle', per_page: 5 }); | |
75 | - done(); | |
76 | - }); | |
77 | - }); | |
78 | - | |
79 | 64 | }); |
80 | 65 | }); | ... | ... |
src/app/layout/blocks/recent-documents/recent-documents-block.component.ts
1 | 1 | import {Component, Inject, Input} from "ng-forward"; |
2 | -import {ArticleService} from "../../../../lib/ng-noosfero-api/http/article.service"; | |
2 | +import {BlockService} from "../../../../lib/ng-noosfero-api/http/block.service"; | |
3 | 3 | |
4 | 4 | @Component({ |
5 | 5 | selector: "noosfero-recent-documents-block", |
6 | 6 | templateUrl: 'app/layout/blocks/recent-documents/recent-documents-block.html' |
7 | 7 | }) |
8 | -@Inject(ArticleService, "$state") | |
8 | +@Inject(BlockService, "$state") | |
9 | 9 | export class RecentDocumentsBlockComponent { |
10 | 10 | |
11 | 11 | @Input() block: any; |
... | ... | @@ -13,23 +13,15 @@ export class RecentDocumentsBlockComponent { |
13 | 13 | |
14 | 14 | profile: any; |
15 | 15 | documents: any; |
16 | - | |
17 | 16 | documentsLoaded: boolean = false; |
18 | 17 | |
19 | - constructor(private articleService: ArticleService, private $state: any) { | |
20 | - } | |
18 | + constructor(private blockService: BlockService, private $state: any) { } | |
21 | 19 | |
22 | 20 | ngOnInit() { |
23 | 21 | this.profile = this.owner; |
24 | 22 | this.documents = []; |
25 | - | |
26 | - let limit = ((this.block && this.block.settings) ? this.block.settings.limit : null) || 5; | |
27 | - // FIXME get all text articles | |
28 | - // FIXME make the getByProfile a generic method where we tell the type passing a class TinyMceArticle | |
29 | - // and the promise should be of type TinyMceArticle[], per example | |
30 | - this.articleService.getByProfile(this.profile, { content_type: 'TinyMceArticle', per_page: limit }) | |
31 | - .then((result: noosfero.RestResult<noosfero.Article[]>) => { | |
32 | - this.documents = <noosfero.Article[]>result.data; | |
23 | + this.blockService.getApiContent(this.block).then((content: any) => { | |
24 | + this.documents = content.articles; | |
33 | 25 | this.documentsLoaded = true; |
34 | 26 | }); |
35 | 27 | } | ... | ... |
src/app/layout/boxes/box.html
1 | 1 | <div ng-class="{'col-md-2-5': box.position!=1, 'col-md-7': box.position==1}"> |
2 | - <div ng-repeat="block in box.blocks | orderBy: 'position'" class="panel panel-default block {{block.type | lowercase}}" > | |
2 | + <div ng-repeat="block in box.blocks | displayBlocks:ctrl.isHomepage:ctrl.currentUser | orderBy: 'position'" class="panel panel-default block {{block.type | lowercase}}" > | |
3 | 3 | <div class="panel-heading" ng-show="block.title"> |
4 | 4 | <h3 class="panel-title">{{block.title}}</h3> |
5 | 5 | </div> |
6 | - <div class="panel-body"> | |
6 | + <div class="panel-body {{block.type | lowercase}}" > | |
7 | 7 | <noosfero-block [block]="block" [owner]="ctrl.owner"></noosfero-block> |
8 | 8 | </div> |
9 | 9 | </div> | ... | ... |
src/app/layout/boxes/boxes.component.spec.ts
1 | 1 | import {Component} from 'ng-forward'; |
2 | - | |
3 | 2 | import {BoxesComponent} from './boxes.component'; |
4 | - | |
5 | -import { | |
6 | - createComponentFromClass, | |
7 | - quickCreateComponent, | |
8 | - provideEmptyObjects, | |
9 | - createProviderToValue, | |
10 | - provideFilters | |
11 | -} from "../../../spec/helpers"; | |
3 | +import * as helpers from "../../../spec/helpers"; | |
4 | +import {ComponentTestHelper, createClass} from '../../../spec/component-test-helper'; | |
12 | 5 | |
13 | 6 | // this htmlTemplate will be re-used between the container components in this spec file |
14 | 7 | const htmlTemplate: string = '<noosfero-boxes [boxes]="ctrl.boxes" [owner]="ctrl.profile"></noosfero-blog>'; |
... | ... | @@ -16,49 +9,79 @@ const htmlTemplate: string = '<noosfero-boxes [boxes]="ctrl.boxes" [owner]="ctrl |
16 | 9 | |
17 | 10 | describe("Boxes Component", () => { |
18 | 11 | |
12 | + let helper: ComponentTestHelper<BoxesComponent>; | |
19 | 13 | beforeEach(() => { |
20 | 14 | angular.mock.module("templates"); |
21 | 15 | }); |
22 | 16 | |
23 | - @Component({ | |
24 | - selector: 'test-container-component', | |
25 | - template: htmlTemplate, | |
26 | - directives: [BoxesComponent], | |
27 | - providers: [] | |
28 | - }) | |
29 | - class BoxesContainerComponent { | |
30 | - boxes: noosfero.Box[] = [ | |
17 | + let properties = { | |
18 | + boxes: [ | |
31 | 19 | { id: 1, position: 1 }, |
32 | 20 | { id: 2, position: 2 } |
33 | - ]; | |
34 | - | |
35 | - owner: noosfero.Profile = <noosfero.Profile> { | |
21 | + ], | |
22 | + owner: { | |
36 | 23 | id: 1, |
37 | 24 | identifier: 'profile-name', |
38 | 25 | type: 'Person' |
39 | - }; | |
40 | - } | |
26 | + } | |
27 | + }; | |
28 | + beforeEach((done) => { | |
29 | + let cls = createClass({ | |
30 | + template: htmlTemplate, | |
31 | + directives: [BoxesComponent], | |
32 | + properties: properties, | |
33 | + providers: [ | |
34 | + helpers.createProviderToValue('SessionService', helpers.mocks.sessionWithCurrentUser({})), | |
35 | + helpers.createProviderToValue('AuthService', helpers.mocks.authService), | |
36 | + helpers.createProviderToValue('$state', state), | |
37 | + helpers.createProviderToValue('TranslatorService', translatorService) | |
38 | + ] | |
39 | + }); | |
40 | + helper = new ComponentTestHelper<BoxesComponent>(cls, done); | |
41 | + }); | |
41 | 42 | |
42 | - it("renders boxes into a container", (done: Function) => { | |
43 | - createComponentFromClass(BoxesContainerComponent).then((fixture) => { | |
44 | - let boxesHtml = fixture.debugElement; | |
45 | - expect(boxesHtml.query('div.col-md-7').length).toEqual(1); | |
46 | - expect(boxesHtml.query('div.col-md-2-5').length).toEqual(1); | |
43 | + let translatorService = jasmine.createSpyObj("translatorService", ["currentLanguage"]); | |
44 | + let state = jasmine.createSpyObj("state", ["current"]); | |
45 | + state.current = { name: "" }; | |
47 | 46 | |
48 | - done(); | |
49 | - }); | |
47 | + it("renders boxes into a container", () => { | |
48 | + expect(helper.find('div.col-md-7').length).toEqual(1); | |
49 | + expect(helper.find('div.col-md-2-5').length).toEqual(1); | |
50 | + }); | |
51 | + | |
52 | + it("check the boxes order", () => { | |
53 | + expect(helper.component.boxesOrder(properties['boxes'][0])).toEqual(1); | |
54 | + expect(helper.component.boxesOrder(properties['boxes'][1])).toEqual(0); | |
50 | 55 | }); |
51 | 56 | |
52 | - it("check the boxes order", (done: Function) => { | |
53 | - createComponentFromClass(BoxesContainerComponent).then((fixture) => { | |
57 | + it("set isHomepage as false by default", () => { | |
58 | + expect(helper.component.isHomepage).toBeFalsy(); | |
59 | + }); | |
54 | 60 | |
55 | - let boxesComponent: BoxesComponent = fixture.debugElement.componentViewChildren[0].componentInstance; | |
56 | - let boxesContainer: BoxesContainerComponent = fixture.componentInstance; | |
61 | + it("set isHomepage as true when in profile home page", () => { | |
62 | + state.current = { name: "main.profile.home" }; | |
63 | + helper.component.ngOnInit(); | |
64 | + expect(helper.component.isHomepage).toBeTruthy(); | |
65 | + }); | |
57 | 66 | |
58 | - expect(boxesComponent.boxesOrder(boxesContainer.boxes[0])).toEqual(1); | |
59 | - expect(boxesComponent.boxesOrder(boxesContainer.boxes[1])).toEqual(0); | |
67 | + it("set isHomepage as true when in profile info page", () => { | |
68 | + state.current = { name: "main.profile.info" }; | |
69 | + helper.component.ngOnInit(); | |
70 | + expect(helper.component.isHomepage).toBeTruthy(); | |
71 | + }); | |
60 | 72 | |
61 | - done(); | |
62 | - }); | |
73 | + it("set isHomepage as true when in profile page", () => { | |
74 | + state.current = { name: "main.profile.page" }; | |
75 | + state.params = { page: "/page" }; | |
76 | + (<noosfero.Profile>helper.component.owner).homepage = '/page'; | |
77 | + helper.component.ngOnInit(); | |
78 | + expect(helper.component.isHomepage).toBeTruthy(); | |
79 | + }); | |
80 | + | |
81 | + it("set isHomepage as true when in environment home page", () => { | |
82 | + state.current = { name: "main.environment.home" }; | |
83 | + helper.component.owner = <noosfero.Environment>{}; | |
84 | + helper.component.ngOnInit(); | |
85 | + expect(helper.component.isHomepage).toBeTruthy(); | |
63 | 86 | }); |
64 | 87 | }); | ... | ... |
src/app/layout/boxes/boxes.component.ts
1 | 1 | import {Input, Inject, Component} from 'ng-forward'; |
2 | +import {SessionService, AuthService, AuthEvents} from "../../login"; | |
3 | +import {DisplayBlocks} from "./display-blocks.filter"; | |
2 | 4 | |
3 | 5 | @Component({ |
4 | 6 | selector: "noosfero-boxes", |
5 | - templateUrl: "app/layout/boxes/boxes.html" | |
7 | + templateUrl: "app/layout/boxes/boxes.html", | |
8 | + directives: [DisplayBlocks] | |
6 | 9 | }) |
10 | +@Inject("SessionService", 'AuthService', "$state", "$rootScope") | |
7 | 11 | export class BoxesComponent { |
8 | 12 | |
9 | 13 | @Input() boxes: noosfero.Box[]; |
10 | - @Input() owner: noosfero.Profile; | |
14 | + @Input() owner: noosfero.Profile | noosfero.Environment; | |
15 | + | |
16 | + currentUser: noosfero.User; | |
17 | + isHomepage = true; | |
18 | + | |
19 | + constructor(private session: SessionService, | |
20 | + private authService: AuthService, | |
21 | + private $state: ng.ui.IStateService, | |
22 | + private $rootScope: ng.IRootScopeService) { | |
23 | + | |
24 | + this.currentUser = this.session.currentUser(); | |
25 | + this.authService.subscribe(AuthEvents[AuthEvents.loginSuccess], () => { | |
26 | + this.currentUser = this.session.currentUser(); | |
27 | + this.verifyHomepage(); | |
28 | + }); | |
29 | + this.authService.subscribe(AuthEvents[AuthEvents.logoutSuccess], () => { | |
30 | + this.currentUser = this.session.currentUser(); | |
31 | + this.verifyHomepage(); | |
32 | + }); | |
33 | + this.$rootScope.$on("$stateChangeSuccess", (event: ng.IAngularEvent, toState: ng.ui.IState) => { | |
34 | + this.verifyHomepage(); | |
35 | + }); | |
36 | + } | |
37 | + | |
38 | + ngOnInit() { | |
39 | + this.verifyHomepage(); | |
40 | + } | |
11 | 41 | |
12 | 42 | boxesOrder(box: noosfero.Box) { |
13 | 43 | if (box.position === 2) return 0; |
14 | 44 | return box.position; |
15 | 45 | } |
46 | + | |
47 | + private verifyHomepage() { | |
48 | + if (this.owner && ["Profile", "Community", "Person"].indexOf((<any>this.owner)['type']) >= 0) { | |
49 | + let profile = <noosfero.Profile>this.owner; | |
50 | + this.isHomepage = this.$state.current.name === "main.profile.home"; | |
51 | + if (profile.homepage) { | |
52 | + this.isHomepage = this.isHomepage || | |
53 | + (this.$state.current.name === "main.profile.page" && profile.homepage === this.$state.params['page']); | |
54 | + } else { | |
55 | + this.isHomepage = this.isHomepage || this.$state.current.name === "main.profile.info"; | |
56 | + } | |
57 | + } else { | |
58 | + this.isHomepage = this.$state.current.name === "main.environment.home"; | |
59 | + } | |
60 | + } | |
16 | 61 | } | ... | ... |
... | ... | @@ -0,0 +1,100 @@ |
1 | +import {quickCreateComponent} from "../../../spec/helpers"; | |
2 | +import {DisplayBlocks} from './display-blocks.filter'; | |
3 | + | |
4 | +describe("Filters", () => { | |
5 | + describe("Display Blocks Filter", () => { | |
6 | + | |
7 | + let translatorService = jasmine.createSpyObj("translatorService", ["currentLanguage"]); | |
8 | + | |
9 | + it("not fail when blocks is null", done => { | |
10 | + let filter = new DisplayBlocks(translatorService); | |
11 | + expect(filter.transform(null, true, <noosfero.User>{})).toEqual([]); | |
12 | + done(); | |
13 | + }); | |
14 | + | |
15 | + it("return blocks when no setting is passed", done => { | |
16 | + let blocks = [{}]; | |
17 | + let filter = new DisplayBlocks(translatorService); | |
18 | + expect(filter.transform(<any>blocks, true, <noosfero.User>{})).toEqual(blocks); | |
19 | + done(); | |
20 | + }); | |
21 | + | |
22 | + it("return blocks when no display is passed", done => { | |
23 | + let blocks = [{ setting: {} }]; | |
24 | + let filter = new DisplayBlocks(translatorService); | |
25 | + expect(filter.transform(<any>blocks, true, <noosfero.User>{})).toEqual(blocks); | |
26 | + done(); | |
27 | + }); | |
28 | + | |
29 | + it("filter invisible blocks", done => { | |
30 | + let blocks = [{ settings: { display: "never" } }]; | |
31 | + let filter = new DisplayBlocks(translatorService); | |
32 | + expect(filter.transform(<any>blocks, true, <noosfero.User>{})).toEqual([]); | |
33 | + done(); | |
34 | + }); | |
35 | + | |
36 | + it("filter blocks with except_home_page in homepage", done => { | |
37 | + let blocks = [{ settings: { display: "except_home_page" } }]; | |
38 | + let filter = new DisplayBlocks(translatorService); | |
39 | + expect(filter.transform(<any>blocks, true, <noosfero.User>{})).toEqual([]); | |
40 | + done(); | |
41 | + }); | |
42 | + | |
43 | + it("filter blocks with home_page_only outside homepage", done => { | |
44 | + let blocks = [{ settings: { display: "home_page_only" } }]; | |
45 | + let filter = new DisplayBlocks(translatorService); | |
46 | + expect(filter.transform(<any>blocks, false, <noosfero.User>{})).toEqual([]); | |
47 | + done(); | |
48 | + }); | |
49 | + | |
50 | + it("show all blocks when display_user is all for logged user", done => { | |
51 | + let blocks = [{ settings: { display_user: "all" } }]; | |
52 | + let filter = new DisplayBlocks(translatorService); | |
53 | + expect(filter.transform(<any>blocks, true, <noosfero.User>{})).toEqual(blocks); | |
54 | + done(); | |
55 | + }); | |
56 | + | |
57 | + it("show all blocks when display_user is all for not logged user", done => { | |
58 | + let blocks = [{ settings: { display_user: "all" } }]; | |
59 | + let filter = new DisplayBlocks(translatorService); | |
60 | + expect(filter.transform(<any>blocks, true, null)).toEqual(blocks); | |
61 | + done(); | |
62 | + }); | |
63 | + | |
64 | + it("filter blocks when display_user is logged for not logged user", done => { | |
65 | + let blocks = [{ settings: { display_user: "logged" } }]; | |
66 | + let filter = new DisplayBlocks(translatorService); | |
67 | + expect(filter.transform(<any>blocks, true, null)).toEqual([]); | |
68 | + done(); | |
69 | + }); | |
70 | + | |
71 | + it("filter blocks when display_user is not_logged for logged user", done => { | |
72 | + let blocks = [{ settings: { display_user: "not_logged" } }]; | |
73 | + let filter = new DisplayBlocks(translatorService); | |
74 | + expect(filter.transform(<any>blocks, true, <noosfero.User>{})).toEqual([]); | |
75 | + done(); | |
76 | + }); | |
77 | + | |
78 | + it("filter blocks with different language", done => { | |
79 | + let blocks = [{ settings: { language: "en" } }]; | |
80 | + translatorService.currentLanguage = jasmine.createSpy("currentLanguage").and.returnValue("pt"); | |
81 | + let filter = new DisplayBlocks(translatorService); | |
82 | + expect(filter.transform(<any>blocks, true, <noosfero.User>{})).toEqual([]); | |
83 | + done(); | |
84 | + }); | |
85 | + | |
86 | + it("filter blocks when hide is true", done => { | |
87 | + let blocks = [{ hide: true }]; | |
88 | + let filter = new DisplayBlocks(translatorService); | |
89 | + expect(filter.transform(<any>blocks, true, null)).toEqual([]); | |
90 | + done(); | |
91 | + }); | |
92 | + | |
93 | + it("not filter blocks when hide is not true", done => { | |
94 | + let blocks = [{ id: 1, hide: false }, { id: 2 }]; | |
95 | + let filter = new DisplayBlocks(translatorService); | |
96 | + expect(filter.transform(<any>blocks, true, null)).toEqual(blocks); | |
97 | + done(); | |
98 | + }); | |
99 | + }); | |
100 | +}); | ... | ... |
... | ... | @@ -0,0 +1,39 @@ |
1 | +import {Pipe, Inject} from "ng-forward"; | |
2 | +import {TranslatorService} from "../../shared/services/translator.service"; | |
3 | + | |
4 | +@Pipe("displayBlocks") | |
5 | +@Inject(TranslatorService) | |
6 | +export class DisplayBlocks { | |
7 | + | |
8 | + constructor(private translatorService: TranslatorService) { } | |
9 | + | |
10 | + transform(blocks: noosfero.Block[], isHomepage: boolean, currentUser: noosfero.User) { | |
11 | + let selected: noosfero.Block[] = []; | |
12 | + blocks = blocks || []; | |
13 | + for (let block of blocks) { | |
14 | + if (this.visible(block, isHomepage) && this.displayToUser(block, currentUser) && | |
15 | + this.displayOnLanguage(block, this.translatorService.currentLanguage()) | |
16 | + && !block.hide) { | |
17 | + selected.push(block); | |
18 | + } | |
19 | + } | |
20 | + return selected; | |
21 | + } | |
22 | + | |
23 | + private visible(block: noosfero.Block, isHomepage: boolean) { | |
24 | + let display = block.settings ? (<any>block.settings)['display'] : null; | |
25 | + return !display || ((isHomepage ? display !== "except_home_page" : display !== "home_page_only") && display !== "never"); | |
26 | + } | |
27 | + | |
28 | + private displayToUser(block: noosfero.Block, currentUser: noosfero.User) { | |
29 | + let displayUser = block.settings ? (<any>block.settings)['display_user'] : null; | |
30 | + return !displayUser || displayUser === "all" || | |
31 | + (currentUser ? displayUser === "logged" : displayUser === "not_logged"); | |
32 | + } | |
33 | + | |
34 | + private displayOnLanguage(block: noosfero.Block, language: string) { | |
35 | + let displayLanguage = block.settings ? (<any>block.settings)['language'] : null; | |
36 | + return !displayLanguage || displayLanguage === "all" || | |
37 | + language === displayLanguage; | |
38 | + } | |
39 | +} | ... | ... |
src/app/layout/navbar/navbar.scss
src/app/layout/services/body-state-classes.service.ts
1 | 1 | import {Directive, Inject, Injectable} from "ng-forward"; |
2 | -import {AuthEvents} from "./../../login/auth-events"; | |
2 | +import {AuthEvents} from "../../login/auth-events"; | |
3 | 3 | import {AuthService} from "./../../login/auth.service"; |
4 | 4 | import {HtmlUtils} from "../html-utils"; |
5 | 5 | import {INgForwardJQuery} from 'ng-forward/cjs/util/jqlite-extensions'; | ... | ... |
src/app/main/main.component.ts
... | ... | @@ -10,15 +10,16 @@ import {BlockComponent} from "../layout/blocks/block.component"; |
10 | 10 | import {EnvironmentComponent} from "../environment/environment.component"; |
11 | 11 | import {EnvironmentHomeComponent} from "../environment/environment-home.component"; |
12 | 12 | import {PeopleBlockComponent} from "../layout/blocks/people/people-block.component"; |
13 | -import {LinkListBlockComponent} from "./../layout/blocks/link-list/link-list-block.component"; | |
13 | +import {DisplayContentBlockComponent} from "../layout/blocks/display-content/display-content-block.component"; | |
14 | +import {LinkListBlockComponent} from "../layout/blocks/link-list/link-list-block.component"; | |
14 | 15 | import {RecentDocumentsBlockComponent} from "../layout/blocks/recent-documents/recent-documents-block.component"; |
15 | 16 | import {ProfileImageBlockComponent} from "../layout/blocks/profile-image/profile-image-block.component"; |
16 | 17 | import {RawHTMLBlockComponent} from "../layout/blocks/raw-html/raw-html-block.component"; |
17 | 18 | import {StatisticsBlockComponent} from "../layout/blocks/statistics/statistics-block.component"; |
18 | 19 | import {TagsBlockComponent} from "../layout/blocks/tags/tags-block.component"; |
19 | 20 | |
20 | -import {MembersBlockComponent} from "./../layout/blocks/members/members-block.component"; | |
21 | -import {CommunitiesBlockComponent} from "./../layout/blocks/communities/communities-block.component"; | |
21 | +import {MembersBlockComponent} from "../layout/blocks/members/members-block.component"; | |
22 | +import {CommunitiesBlockComponent} from "../layout/blocks/communities/communities-block.component"; | |
22 | 23 | |
23 | 24 | import {LoginBlockComponent} from "../layout/blocks/login-block/login-block.component"; |
24 | 25 | |
... | ... | @@ -95,7 +96,7 @@ export class EnvironmentContent { |
95 | 96 | template: '<div ng-view></div>', |
96 | 97 | directives: [ |
97 | 98 | ArticleBlogComponent, ArticleViewComponent, BoxesComponent, BlockComponent, |
98 | - EnvironmentComponent, PeopleBlockComponent, | |
99 | + EnvironmentComponent, PeopleBlockComponent, DisplayContentBlockComponent, | |
99 | 100 | LinkListBlockComponent, CommunitiesBlockComponent, HtmlEditorComponent, |
100 | 101 | MainBlockComponent, RecentDocumentsBlockComponent, Navbar, SidebarComponent, ProfileImageBlockComponent, |
101 | 102 | MembersBlockComponent, NoosferoTemplate, DateFormat, RawHTMLBlockComponent, StatisticsBlockComponent, | ... | ... |
... | ... | @@ -0,0 +1,18 @@ |
1 | +<ul class="nav navbar-nav"> | |
2 | + <li class="dropdown profile-menu" uib-dropdown> | |
3 | + <a class="btn dropdown-toggle" data-toggle="dropdown" uib-dropdown-toggle> | |
4 | + {{"navbar.profile_actions.new_item" | translate}} | |
5 | + <i class="fa fa-caret-down"></i> | |
6 | + </a> | |
7 | + <ul class="dropdown-menu" uib-dropdown-menu ng-show="vm.profile"> | |
8 | + <!-- FIXED HERE BUT SHOULD BE A HOTSPOT TO INCLUDE LINKS FROM THE PLUGIN --> | |
9 | + <li> | |
10 | + <a href="#" ui-sref="main.cms({profile: vm.profile.identifier, parent_id: null, type: 'CommentParagraphPlugin::Discussion'})"> | |
11 | + <i class="fa fa-file fa-fw fa-lg"></i> {{"navbar.profile_actions.new_discussion" | translate}} | |
12 | + </a> | |
13 | + </li> | |
14 | + </ul> | |
15 | + </li> | |
16 | + | |
17 | +</ul> | |
18 | + | ... | ... |
... | ... | @@ -0,0 +1,21 @@ |
1 | +import {Component, Inject, provide} from "ng-forward"; | |
2 | +import {ProfileService} from "../../lib/ng-noosfero-api/http/profile.service"; | |
3 | + | |
4 | +@Component({ | |
5 | + selector: "profile-actions", | |
6 | + templateUrl: "app/article/content-viewer/navbar-actions.html", | |
7 | + providers: [ | |
8 | + provide('profileService', { useClass: ProfileService }) | |
9 | + ] | |
10 | +}) | |
11 | +@Inject(ProfileService) | |
12 | +export class ProfileActionsComponent { | |
13 | + profile: noosfero.Profile; | |
14 | + parentId: number; | |
15 | + | |
16 | + constructor(profileService: ProfileService) { | |
17 | + profileService.getCurrentProfile().then((profile: noosfero.Profile) => { | |
18 | + this.profile = profile; | |
19 | + }); | |
20 | + } | |
21 | +} | ... | ... |
src/app/profile/profile-home.component.ts
... | ... | @@ -18,8 +18,10 @@ export class ProfileHomeComponent { |
18 | 18 | return profileService.getHomePage(<number>this.profile.id, { fields: 'path' }); |
19 | 19 | }).then((response: restangular.IResponse) => { |
20 | 20 | if (response.data.article) { |
21 | + this.profile.homepage = response.data.article.path; | |
21 | 22 | $state.transitionTo('main.profile.page', { page: response.data.article.path, profile: this.profile.identifier }, { location: false }); |
22 | 23 | } else { |
24 | + this.profile.homepage = null; | |
23 | 25 | $state.transitionTo('main.profile.info', { profile: this.profile.identifier }, { location: false }); |
24 | 26 | } |
25 | 27 | }); | ... | ... |
src/app/profile/profile.component.ts
... | ... | @@ -9,7 +9,7 @@ import {ActivitiesComponent} from "./activities/activities.component"; |
9 | 9 | import {ProfileService} from "../../lib/ng-noosfero-api/http/profile.service"; |
10 | 10 | import {NotificationService} from "../shared/services/notification.service"; |
11 | 11 | import {MyProfileComponent} from "./myprofile.component"; |
12 | - | |
12 | +import {ProfileActionsComponent} from "./profile-actions.component"; | |
13 | 13 | |
14 | 14 | /** |
15 | 15 | * @ngdoc controller |
... | ... | @@ -37,13 +37,25 @@ import {MyProfileComponent} from "./myprofile.component"; |
37 | 37 | templateUrl: "app/profile/info/profile-info.html", |
38 | 38 | controller: ProfileInfoComponent, |
39 | 39 | controllerAs: "vm" |
40 | + }, | |
41 | + "actions@main": { | |
42 | + templateUrl: "app/profile/navbar-actions.html", | |
43 | + controller: ProfileActionsComponent, | |
44 | + controllerAs: "vm" | |
40 | 45 | } |
41 | 46 | } |
42 | 47 | }, |
43 | 48 | { |
44 | 49 | name: 'main.profile.settings', |
45 | 50 | url: "^/myprofile/:profile", |
46 | - component: MyProfileComponent | |
51 | + component: MyProfileComponent, | |
52 | + views: { | |
53 | + "actions@main": { | |
54 | + templateUrl: "app/profile/navbar-actions.html", | |
55 | + controller: ProfileActionsComponent, | |
56 | + controllerAs: "vm" | |
57 | + } | |
58 | + } | |
47 | 59 | }, |
48 | 60 | { |
49 | 61 | name: 'main.cms', | ... | ... |
src/app/profile/profile.html
1 | 1 | <div class="profile-container"> |
2 | 2 | <div class="profile-header" ng-bind-html="vm.profile.custom_header"></div> |
3 | 3 | <div class="row"> |
4 | - <noosfero-boxes [boxes]="vm.boxes" [owner]="vm.profile"></noosfero-boxes> | |
4 | + <noosfero-boxes ng-if="vm.boxes" [boxes]="vm.boxes" [owner]="vm.profile"></noosfero-boxes> | |
5 | 5 | </div> |
6 | 6 | <div class="profile-footer" ng-bind-html="vm.profile.custom_footer"></div> |
7 | 7 | </div> | ... | ... |
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
... | ... | @@ -24,7 +24,12 @@ |
24 | 24 | "auth.form.login": "Login / Email address", |
25 | 25 | "auth.form.password": "Password", |
26 | 26 | "auth.form.login_button": "Login", |
27 | + "navbar.content_viewer_actions.new_item": "New Item", | |
28 | + "navbar.profile_actions.new_item": "New Item", | |
27 | 29 | "navbar.content_viewer_actions.new_post": "New Post", |
30 | + "//TODO": "Create a way to load plugin translatios - Move plugins translations to the plugins translations files", | |
31 | + "navbar.content_viewer_actions.new_discussion": "New Discussion", | |
32 | + "navbar.profile_actions.new_discussion": "New Discussion", | |
28 | 33 | "notification.error.default.message": "Something went wrong!", |
29 | 34 | "notification.error.default.title": "Oops...", |
30 | 35 | "notification.profile.not_found": "Page not found", |
... | ... | @@ -35,8 +40,15 @@ |
35 | 40 | "comment.pagination.more": "More", |
36 | 41 | "comment.post.success.title": "Good job!", |
37 | 42 | "comment.post.success.message": "Comment saved!", |
43 | + "comment.remove.success.title": "Good job!", | |
44 | + "comment.remove.success.message": "Comment removed!", | |
45 | + "comment.remove.confirmation.title": "Are you sure?", | |
46 | + "comment.remove.confirmation.message": "You will not be able to recover this comment!", | |
38 | 47 | "comment.reply": "reply", |
48 | + "comment.remove": "remove", | |
39 | 49 | "article.actions.edit": "Edit", |
50 | + "article.actions.delete": "Delete", | |
51 | + "article.actions.read_more": "Read More", | |
40 | 52 | "article.basic_editor.title": "Title", |
41 | 53 | "article.basic_editor.body": "Body", |
42 | 54 | "article.basic_editor.save": "Save", | ... | ... |
src/languages/pt.json
... | ... | @@ -24,7 +24,9 @@ |
24 | 24 | "auth.form.login": "Login / Email", |
25 | 25 | "auth.form.password": "Senha", |
26 | 26 | "auth.form.login_button": "Login", |
27 | + "navbar.content_viewer_actions.new_item": "Novo Item", | |
27 | 28 | "navbar.content_viewer_actions.new_post": "Novo Artigo", |
29 | + "navbar.content_viewer_actions.new_discussion": "Nova Discussão", | |
28 | 30 | "notification.error.default.message": "Algo deu errado!", |
29 | 31 | "notification.error.default.title": "Oops...", |
30 | 32 | "notification.profile.not_found": "Página não encontrada", |
... | ... | @@ -35,8 +37,15 @@ |
35 | 37 | "comment.pagination.more": "Mais", |
36 | 38 | "comment.post.success.title": "Bom trabalho!", |
37 | 39 | "comment.post.success.message": "Comentário salvo com sucesso!", |
40 | + "comment.remove.success.title": "Bom trabalho!", | |
41 | + "comment.remove.success.message": "Comentário removido com sucesso!", | |
42 | + "comment.remove.confirmation.title": "Tem certeza?", | |
43 | + "comment.remove.confirmation.message": "Você não poderá recuperar o comentário removido!", | |
38 | 44 | "comment.reply": "responder", |
45 | + "comment.remove": "remover", | |
39 | 46 | "article.actions.edit": "Editar", |
47 | + "article.actions.delete": "Excluir", | |
48 | + "article.actions.read_more": "Ler mais", | |
40 | 49 | "article.basic_editor.title": "Título", |
41 | 50 | "article.basic_editor.body": "Corpo", |
42 | 51 | "article.basic_editor.save": "Salvar", | ... | ... |
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 | + | ... | ... |
... | ... | @@ -0,0 +1,36 @@ |
1 | +import {BlockService} from "./block.service"; | |
2 | + | |
3 | + | |
4 | +describe("Services", () => { | |
5 | + | |
6 | + describe("Block Service", () => { | |
7 | + | |
8 | + let $httpBackend: ng.IHttpBackendService; | |
9 | + let blockService: BlockService; | |
10 | + | |
11 | + beforeEach(angular.mock.module("noosferoApp", ($translateProvider: angular.translate.ITranslateProvider) => { | |
12 | + $translateProvider.translations('en', {}); | |
13 | + })); | |
14 | + | |
15 | + beforeEach(inject((_$httpBackend_: ng.IHttpBackendService, _BlockService_: BlockService) => { | |
16 | + $httpBackend = _$httpBackend_; | |
17 | + blockService = _BlockService_; | |
18 | + })); | |
19 | + | |
20 | + | |
21 | + describe("Succesfull requests", () => { | |
22 | + | |
23 | + it("should return api content of a block", (done) => { | |
24 | + let blockId = 1; | |
25 | + $httpBackend.expectGET(`/api/v1/blocks/${blockId}`).respond(200, { block: { api_content: [{ name: "article1" }] } }); | |
26 | + blockService.getApiContent(<noosfero.Block>{ id: blockId }).then((content: any) => { | |
27 | + expect(content).toEqual([{ name: "article1" }]); | |
28 | + done(); | |
29 | + }); | |
30 | + $httpBackend.flush(); | |
31 | + }); | |
32 | + }); | |
33 | + | |
34 | + | |
35 | + }); | |
36 | +}); | ... | ... |
... | ... | @@ -0,0 +1,40 @@ |
1 | +import { Injectable, Inject } from "ng-forward"; | |
2 | +import {RestangularService} from "./restangular_service"; | |
3 | +import {ProfileService} from "./profile.service"; | |
4 | + | |
5 | +@Injectable() | |
6 | +@Inject("Restangular", "$q", "$log") | |
7 | +export class BlockService extends RestangularService<noosfero.Block> { | |
8 | + | |
9 | + constructor(Restangular: restangular.IService, $q: ng.IQService, $log: ng.ILogService) { | |
10 | + super(Restangular, $q, $log); | |
11 | + } | |
12 | + | |
13 | + getResourcePath() { | |
14 | + return "blocks"; | |
15 | + } | |
16 | + | |
17 | + getDataKeys() { | |
18 | + return { | |
19 | + singular: 'block', | |
20 | + plural: 'blocks' | |
21 | + }; | |
22 | + } | |
23 | + | |
24 | + getApiContent(block: noosfero.Block) { | |
25 | + let apiContentPromise = this.$q.defer(); | |
26 | + if (block) { | |
27 | + if (block.api_content) { | |
28 | + apiContentPromise.resolve(block.api_content); | |
29 | + } else { | |
30 | + this.get(block.id) | |
31 | + .then((result: noosfero.RestResult<noosfero.Block>) => { | |
32 | + block = result.data; | |
33 | + apiContentPromise.resolve(block.api_content); | |
34 | + }); | |
35 | + } | |
36 | + } | |
37 | + return apiContentPromise.promise; | |
38 | + } | |
39 | + | |
40 | +} | ... | ... |
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
1 | 1 | |
2 | 2 | namespace noosfero { |
3 | 3 | export interface Article extends RestModel { |
4 | + abstract: string; | |
4 | 5 | path: string; |
5 | 6 | profile: Profile; |
6 | 7 | type: string; |
... | ... | @@ -12,5 +13,6 @@ namespace noosfero { |
12 | 13 | setting: any; |
13 | 14 | start_date: string; |
14 | 15 | end_date: string; |
16 | + accept_comments: boolean; | |
15 | 17 | } |
16 | 18 | } | ... | ... |
src/lib/ng-noosfero-api/interfaces/block.ts
src/lib/ng-noosfero-api/interfaces/profile.ts
... | ... | @@ -22,13 +22,13 @@ namespace noosfero { |
22 | 22 | * @returns {string} The unque identifier for the Profile |
23 | 23 | */ |
24 | 24 | identifier: string; |
25 | - | |
25 | + | |
26 | 26 | /** |
27 | 27 | * @ngdoc property |
28 | 28 | * @name created_at |
29 | 29 | * @propertyOf noofero.Profile |
30 | 30 | * @returns {string} The timestamp this object was created |
31 | - */ | |
31 | + */ | |
32 | 32 | created_at: string; |
33 | 33 | |
34 | 34 | /** |
... | ... | @@ -54,5 +54,13 @@ namespace noosfero { |
54 | 54 | * @returns {string} A key => value custom fields data of Profile (e.g.: "{'Address':'Street A, Number 102...'}") |
55 | 55 | */ |
56 | 56 | additional_data?: any; |
57 | + | |
58 | + /** | |
59 | + * @ngdoc property | |
60 | + * @name homepage | |
61 | + * @propertyOf noofero.Profile | |
62 | + * @returns {string} The Profile homepage | |
63 | + */ | |
64 | + homepage: string; | |
57 | 65 | } |
58 | 66 | } | ... | ... |
... | ... | @@ -0,0 +1,16 @@ |
1 | +namespace noosfero { | |
2 | + /** | |
3 | + * @ngdoc interface | |
4 | + * @name noosfero.Section | |
5 | + * @description | |
6 | + * Represents a block settings section. A Section has a value property, | |
7 | + * which represents the Section name, and an optinally checked property which | |
8 | + * has the same value as the value property indicating that this property is | |
9 | + * selected in the block configuration. | |
10 | + */ | |
11 | + export interface Section { | |
12 | + | |
13 | + value: string; | |
14 | + checked: string; | |
15 | + } | |
16 | +} | |
0 | 17 | \ No newline at end of file | ... | ... |
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/plugins/comment_paragraph/article/cms/discussion-editor/discussion-editor.component.spec.ts
0 → 100644
... | ... | @@ -0,0 +1,42 @@ |
1 | +import {DiscussionEditorComponent} from './discussion-editor.component'; | |
2 | +import {ComponentTestHelper, createClass} from './../../../../../spec/component-test-helper'; | |
3 | + | |
4 | +const htmlTemplate: string = '<comment-paragraph-plugin-discussion-editor [article]="ctrl.article"></comment-paragraph-plugin-discussion-editor>'; | |
5 | + | |
6 | +describe("Components", () => { | |
7 | + describe("Discussion Editor Component", () => { | |
8 | + | |
9 | + let helper: ComponentTestHelper<DiscussionEditorComponent>; | |
10 | + beforeEach(angular.mock.module("templates")); | |
11 | + | |
12 | + beforeEach((done) => { | |
13 | + let properties = { article: {} }; | |
14 | + let cls = createClass({ | |
15 | + template: htmlTemplate, | |
16 | + directives: [DiscussionEditorComponent], | |
17 | + properties: properties | |
18 | + }); | |
19 | + helper = new ComponentTestHelper<DiscussionEditorComponent>(cls, done); | |
20 | + }); | |
21 | + | |
22 | + it("set start_date as article start_date when it was defined", () => { | |
23 | + let article = {start_date: new Date()}; | |
24 | + helper.changeProperties({article: article}); | |
25 | + helper.component.ngOnInit(); | |
26 | + expect(helper.component.start_date.getTime()).toEqual(article.start_date.getTime()); | |
27 | + }); | |
28 | + | |
29 | + it("set start_date as current date when it was not defined", () => { | |
30 | + helper.changeProperties({article: {}}); | |
31 | + helper.component.ngOnInit(); | |
32 | + expect(helper.component.start_date.getTime()).toBeDefined(); | |
33 | + }); | |
34 | + | |
35 | + it("set end_date as article end_date when it was defined", () => { | |
36 | + let article = {end_date: new Date()}; | |
37 | + helper.changeProperties({article: article}); | |
38 | + helper.component.ngOnInit(); | |
39 | + expect(helper.component.end_date.getTime()).toEqual(article.end_date.getTime()); | |
40 | + }); | |
41 | + }); | |
42 | +}); | ... | ... |
src/plugins/comment_paragraph/block/discussion/discussion-block.component.spec.ts
0 → 100644
... | ... | @@ -0,0 +1,55 @@ |
1 | +import {TestComponentBuilder} from 'ng-forward/cjs/testing/test-component-builder'; | |
2 | +import {Provider, Input, provide, Component} from 'ng-forward'; | |
3 | +import {provideFilters} from '../../../../spec/helpers'; | |
4 | +import {DiscussionBlockComponent} from './discussion-block.component'; | |
5 | +import {ComponentTestHelper, createClass} from './../../../../spec/component-test-helper'; | |
6 | + | |
7 | +const htmlTemplate: string = '<noosfero-comment-paragraph-plugin-discussion-block [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-comment-paragraph-plugin-discussion-block>'; | |
8 | + | |
9 | +const tcb = new TestComponentBuilder(); | |
10 | + | |
11 | +describe("Components", () => { | |
12 | + describe("Discussion Block Component", () => { | |
13 | + | |
14 | + let helper: ComponentTestHelper<DiscussionBlockComponent>; | |
15 | + let settingsObj = {}; | |
16 | + let mockedBlockService = { | |
17 | + getApiContent: (content: any): any => { | |
18 | + return Promise.resolve({ articles: [{ name: "article1" }], headers: (name: string) => { return name; } }); | |
19 | + } | |
20 | + }; | |
21 | + let profile = { name: 'profile-name' }; | |
22 | + | |
23 | + let state = jasmine.createSpyObj("state", ["go"]); | |
24 | + | |
25 | + let providers = [ | |
26 | + new Provider('$state', { useValue: state }), | |
27 | + new Provider('BlockService', { | |
28 | + useValue: mockedBlockService | |
29 | + }), | |
30 | + ].concat(provideFilters("truncateFilter", "stripTagsFilter", "translateFilter", "amDateFormatFilter")); | |
31 | + | |
32 | + beforeEach(angular.mock.module("templates")); | |
33 | + | |
34 | + beforeEach((done) => { | |
35 | + let cls = createClass({ | |
36 | + template: htmlTemplate, | |
37 | + directives: [DiscussionBlockComponent], | |
38 | + providers: providers, | |
39 | + properties: { block: {} } | |
40 | + }); | |
41 | + helper = new ComponentTestHelper<DiscussionBlockComponent>(cls, done); | |
42 | + }); | |
43 | + | |
44 | + it("get discussions from the block service", () => { | |
45 | + expect(helper.component.documents).toEqual([{ name: "article1" }]); | |
46 | + expect(helper.component.block.hide).toEqual(false); | |
47 | + }); | |
48 | + | |
49 | + it("go to article page when open a document", () => { | |
50 | + let block = helper.component; | |
51 | + block.openDocument({ path: "path", profile: { identifier: "identifier" } }); | |
52 | + expect(state.go).toHaveBeenCalledWith("main.profile.page", { page: "path", profile: "identifier" }); | |
53 | + }); | |
54 | + }); | |
55 | +}); | ... | ... |
src/plugins/comment_paragraph/block/discussion/discussion-block.component.ts
1 | 1 | import {Component, Inject, Input} from "ng-forward"; |
2 | -import {ArticleService} from "../../../../lib/ng-noosfero-api/http/article.service"; | |
2 | +import {BlockService} from "../../../../lib/ng-noosfero-api/http/block.service"; | |
3 | 3 | |
4 | 4 | @Component({ |
5 | 5 | selector: "noosfero-comment-paragraph-plugin-discussion-block", |
6 | 6 | templateUrl: 'plugins/comment_paragraph/block/discussion/discussion-block.html' |
7 | 7 | }) |
8 | -@Inject(ArticleService, "$state") | |
8 | +@Inject(BlockService, "$state") | |
9 | 9 | export class DiscussionBlockComponent { |
10 | 10 | |
11 | 11 | @Input() block: any; |
12 | 12 | @Input() owner: any; |
13 | 13 | |
14 | - profile: any; | |
15 | - documents: any; | |
14 | + profile: noosfero.Profile; | |
15 | + documents: Array<noosfero.Article>; | |
16 | 16 | |
17 | - documentsLoaded: boolean = false; | |
18 | - | |
19 | - constructor(private articleService: ArticleService, private $state: any) { } | |
17 | + constructor(private blockService: BlockService, private $state: any) { } | |
20 | 18 | |
21 | 19 | ngOnInit() { |
22 | 20 | this.profile = this.owner; |
23 | - this.documents = []; | |
24 | - | |
25 | - let limit = ((this.block && this.block.settings) ? this.block.settings.limit : null) || 50; | |
26 | - let params: any = { content_type: 'CommentParagraphPlugin::Discussion', per_page: limit, order: 'start_date DESC' }; | |
27 | - let now = new Date().toISOString(); | |
28 | - switch (this.block.settings['discussion_status']) { | |
29 | - case 0: | |
30 | - params['from_start_date'] = now; | |
31 | - break; | |
32 | - case 1: | |
33 | - params['until_start_date'] = now; | |
34 | - params['from_end_date'] = now; | |
35 | - break; | |
36 | - case 2: | |
37 | - params['until_end_date'] = now; | |
38 | - break; | |
39 | - } | |
40 | - console.log(this.block.settings['discussion_status']); | |
41 | - this.articleService.getByProfile(this.profile, params) | |
42 | - .then((result: noosfero.RestResult<noosfero.Article[]>) => { | |
43 | - this.documents = <noosfero.Article[]>result.data; | |
44 | - this.documentsLoaded = true; | |
45 | - }); | |
21 | + this.blockService.getApiContent(this.block).then((content: any) => { | |
22 | + this.documents = content.articles; | |
23 | + this.block.hide = !this.documents || this.documents.length === 0; | |
24 | + }); | |
46 | 25 | } |
47 | 26 | |
48 | 27 | openDocument(article: any) { | ... | ... |
src/plugins/comment_paragraph/hotspot/article-content/article-content.component.spec.ts
0 → 100644
... | ... | @@ -0,0 +1,110 @@ |
1 | +import {CommentParagraphArticleContentHotspotComponent} from './article-content.component'; | |
2 | +import {ComponentTestHelper, createClass} from './../../../../spec/component-test-helper'; | |
3 | + | |
4 | +const htmlTemplate: string = '<comment-paragraph-article-content-hotspot [article]="ctrl.article"></comment-paragraph-article-content-hotspot>'; | |
5 | + | |
6 | +describe("Components", () => { | |
7 | + describe("Article Content Hotspot Component", () => { | |
8 | + | |
9 | + let helper: ComponentTestHelper<CommentParagraphArticleContentHotspotComponent>; | |
10 | + beforeEach(angular.mock.module("templates")); | |
11 | + | |
12 | + beforeEach((done) => { | |
13 | + let properties = { article: {} }; | |
14 | + let cls = createClass({ | |
15 | + template: htmlTemplate, | |
16 | + directives: [CommentParagraphArticleContentHotspotComponent], | |
17 | + properties: properties | |
18 | + }); | |
19 | + helper = new ComponentTestHelper<CommentParagraphArticleContentHotspotComponent>(cls, done); | |
20 | + }); | |
21 | + | |
22 | + it("return false in isDiscussion when no type was specified", () => { | |
23 | + expect(helper.component.isDiscussion()).toBeFalsy(); | |
24 | + }); | |
25 | + | |
26 | + it("return false in isDiscussion when other type was specified", () => { | |
27 | + helper.changeProperties({ article: { type: "TextArticle" } }); | |
28 | + expect(helper.component.isDiscussion()).toBeFalsy(); | |
29 | + }); | |
30 | + | |
31 | + it("return true in isDiscussion when discussion type was specified", () => { | |
32 | + helper.changeProperties({ article: { type: "CommentParagraphPlugin::Discussion" } }); | |
33 | + expect(helper.component.isDiscussion()).toBeTruthy(); | |
34 | + }); | |
35 | + | |
36 | + it("return true in notOpened when start date is after today", () => { | |
37 | + let date = new Date(); | |
38 | + date.setDate(date.getDate() + 1); | |
39 | + helper.changeProperties({ article: { start_date: date.toISOString() } }); | |
40 | + expect(helper.component.notOpened()).toBeTruthy(); | |
41 | + expect(helper.component.available()).toBeFalsy(); | |
42 | + expect(helper.component.closed()).toBeFalsy(); | |
43 | + }); | |
44 | + | |
45 | + it("return false in notOpened when start date is before today", () => { | |
46 | + let date = new Date(); | |
47 | + date.setDate(date.getDate() - 1); | |
48 | + helper.changeProperties({ article: { start_date: date.toISOString() } }); | |
49 | + expect(helper.component.notOpened()).toBeFalsy(); | |
50 | + }); | |
51 | + | |
52 | + it("return false in notOpened when start date is null", () => { | |
53 | + helper.changeProperties({ article: { start_date: null } }); | |
54 | + expect(helper.component.notOpened()).toBeFalsy(); | |
55 | + }); | |
56 | + | |
57 | + it("return true in closed when end date is before today", () => { | |
58 | + let date = new Date(); | |
59 | + date.setDate(date.getDate() - 1); | |
60 | + helper.changeProperties({ article: { end_date: date.toISOString() } }); | |
61 | + expect(helper.component.closed()).toBeTruthy(); | |
62 | + expect(helper.component.available()).toBeFalsy(); | |
63 | + expect(helper.component.notOpened()).toBeFalsy(); | |
64 | + }); | |
65 | + | |
66 | + it("return false in closed when start date is after today", () => { | |
67 | + let date = new Date(); | |
68 | + date.setDate(date.getDate() + 1); | |
69 | + helper.changeProperties({ article: { end_date: date.toISOString() } }); | |
70 | + expect(helper.component.closed()).toBeFalsy(); | |
71 | + }); | |
72 | + | |
73 | + it("return false in closed when end date is null", () => { | |
74 | + helper.changeProperties({ article: { start_date: null } }); | |
75 | + expect(helper.component.closed()).toBeFalsy(); | |
76 | + }); | |
77 | + | |
78 | + it("return true in available when start date is before today and end date is after", () => { | |
79 | + let date = new Date(); | |
80 | + date.setDate(date.getDate() - 1); | |
81 | + let startDate = date.toISOString(); | |
82 | + date.setDate(date.getDate() + 3); | |
83 | + let endDate = date.toISOString(); | |
84 | + helper.changeProperties({ article: { start_date: startDate, end_date: endDate } }); | |
85 | + expect(helper.component.available()).toBeTruthy(); | |
86 | + expect(helper.component.closed()).toBeFalsy(); | |
87 | + expect(helper.component.notOpened()).toBeFalsy(); | |
88 | + }); | |
89 | + | |
90 | + it("return true in available when start date is before today and end date is null", () => { | |
91 | + let date = new Date(); | |
92 | + date.setDate(date.getDate() - 1); | |
93 | + let startDate = date.toISOString(); | |
94 | + helper.changeProperties({ article: { start_date: startDate, end_date: null } }); | |
95 | + expect(helper.component.available()).toBeTruthy(); | |
96 | + expect(helper.component.closed()).toBeFalsy(); | |
97 | + expect(helper.component.notOpened()).toBeFalsy(); | |
98 | + }); | |
99 | + | |
100 | + it("return true in available when start date is null and end date is after today", () => { | |
101 | + let date = new Date(); | |
102 | + date.setDate(date.getDate() + 3); | |
103 | + let endDate = date.toISOString(); | |
104 | + helper.changeProperties({ article: { start_date: null, end_date: endDate } }); | |
105 | + expect(helper.component.available()).toBeTruthy(); | |
106 | + expect(helper.component.closed()).toBeFalsy(); | |
107 | + expect(helper.component.notOpened()).toBeFalsy(); | |
108 | + }); | |
109 | + }); | |
110 | +}); | ... | ... |
src/plugins/comment_paragraph/hotspot/article-content/article-content.component.ts
... | ... | @@ -13,4 +13,20 @@ export class CommentParagraphArticleContentHotspotComponent { |
13 | 13 | isDiscussion() { |
14 | 14 | return this.article.type === "CommentParagraphPlugin::Discussion"; |
15 | 15 | } |
16 | + | |
17 | + notOpened() { | |
18 | + let now = new Date(); | |
19 | + return !!this.article.start_date && new Date(this.article.start_date) > now; | |
20 | + } | |
21 | + | |
22 | + available() { | |
23 | + let now = new Date(); | |
24 | + return (!this.article.start_date || new Date(this.article.start_date) <= now) && | |
25 | + (!this.article.end_date || new Date(this.article.end_date) >= now); | |
26 | + } | |
27 | + | |
28 | + closed() { | |
29 | + let now = new Date(); | |
30 | + return !!this.article.end_date && new Date(this.article.end_date) < now; | |
31 | + } | |
16 | 32 | } | ... | ... |
src/plugins/comment_paragraph/hotspot/article-content/article-content.html
1 | 1 | <div class="discussion-header" ng-if="ctrl.isDiscussion()"> |
2 | - <span class="description">{{"comment-paragraph-plugin.discussion.header" | translate}}</span> | |
3 | - <span class="start-date date" ng-if="ctrl.article.start_date"> | |
4 | - <span class="description">{{"comment-paragraph-plugin.discussion.editor.start_date.label" | translate}}</span> | |
5 | - <span class="value">{{ctrl.article.start_date | amDateFormat:'DD/MM/YYYY'}}</span> | |
6 | - </span> | |
7 | - <span class="end-date date" ng-if="ctrl.article.end_date"> | |
8 | - <span class="description">{{"comment-paragraph-plugin.discussion.editor.end_date.label" | translate}}</span> | |
9 | - <span class="value">{{ctrl.article.end_date | amDateFormat:'DD/MM/YYYY'}}</span> | |
10 | - </span> | |
2 | + <div class="icon"> | |
3 | + <i class="fa fa-calendar fa-fw fa-lg"></i> | |
4 | + </div> | |
5 | + <div class="period"> | |
6 | + <div ng-if="ctrl.notOpened()" class="description not-opened"> | |
7 | + {{"comment-paragraph-plugin.discussion.notOpened.header" | translate:{date: (ctrl.article.start_date | dateFormat | amTimeAgo)} }} | |
8 | + </div> | |
9 | + <div ng-if="ctrl.available()" class="description available"> | |
10 | + <div ng-if="ctrl.article.end_date" class="with-end-date"> | |
11 | + {{"comment-paragraph-plugin.discussion.available.header" | translate:{date: (ctrl.article.end_date | dateFormat | amTimeAgo)} }} | |
12 | + </div> | |
13 | + <div ng-if="!ctrl.article.end_date" class="without-end-date"> | |
14 | + {{"comment-paragraph-plugin.discussion.available.without-end.header" | translate}} | |
15 | + </div> | |
16 | + </div> | |
17 | + <div ng-if="ctrl.closed()" class="description closed"> | |
18 | + {{"comment-paragraph-plugin.discussion.closed.header" | translate:{date: (ctrl.article.end_date | dateFormat | amTimeAgo)} }} | |
19 | + </div> | |
20 | + </div> | |
11 | 21 | </div> | ... | ... |
src/plugins/comment_paragraph/hotspot/article-content/article-content.scss
1 | 1 | .discussion-header { |
2 | 2 | @extend .pull-right; |
3 | 3 | margin-bottom: 20px; |
4 | - .date { | |
5 | - text-transform: lowercase; | |
6 | - font-size: 16px; | |
4 | + .period { | |
5 | + @extend .pull-right; | |
7 | 6 | .description { |
8 | 7 | font-weight: bold; |
9 | - color: #BFBFBF; | |
10 | - } | |
11 | - .value { | |
12 | - font-size: 15px; | |
13 | - color: #969696; | |
8 | + color: #A2A2A2; | |
14 | 9 | } |
15 | 10 | } |
16 | - .description { | |
17 | - font-weight: bold; | |
18 | - color: #BFBFBF; | |
11 | + .icon { | |
12 | + @extend .pull-right; | |
13 | + margin-left: 8px; | |
19 | 14 | } |
20 | 15 | } | ... | ... |
src/plugins/comment_paragraph/hotspot/comment-paragraph-article-button.component.spec.ts
... | ... | @@ -1,85 +0,0 @@ |
1 | -import {CommentParagraphArticleButtonHotspotComponent} from "./comment-paragraph-article-button.component"; | |
2 | -import {ComponentTestHelper, createClass} from '../../../spec/component-test-helper'; | |
3 | -import * as helpers from "../../../spec/helpers"; | |
4 | -import {Provider} from 'ng-forward'; | |
5 | -import {ComponentFixture} from 'ng-forward/cjs/testing/test-component-builder'; | |
6 | - | |
7 | -let htmlTemplate = '<comment-paragraph-article-button-hotspot [article]="ctrl.article"></comment-paragraph-article-button-hotspot>'; | |
8 | - | |
9 | -describe("Components", () => { | |
10 | - describe("Comment Paragraph Article Button Hotspot Component", () => { | |
11 | - | |
12 | - let serviceMock = jasmine.createSpyObj("CommentParagraphService", ["deactivateCommentParagraph", "activateCommentParagraph"]); | |
13 | - let eventServiceMock = jasmine.createSpyObj("CommentParagraphEventService", ["toggleCommentParagraph"]); | |
14 | - | |
15 | - let providers = [ | |
16 | - new Provider('CommentParagraphService', { useValue: serviceMock }), | |
17 | - new Provider('CommentParagraphEventService', { useValue: eventServiceMock }) | |
18 | - ].concat(helpers.provideFilters('translateFilter')); | |
19 | - let helper: ComponentTestHelper<CommentParagraphArticleButtonHotspotComponent>; | |
20 | - | |
21 | - beforeEach(angular.mock.module("templates")); | |
22 | - | |
23 | - beforeEach((done) => { | |
24 | - let cls = createClass({ | |
25 | - template: htmlTemplate, | |
26 | - directives: [CommentParagraphArticleButtonHotspotComponent], | |
27 | - providers: providers, | |
28 | - properties: { | |
29 | - article: {} | |
30 | - } | |
31 | - }); | |
32 | - helper = new ComponentTestHelper<CommentParagraphArticleButtonHotspotComponent>(cls, done); | |
33 | - }); | |
34 | - | |
35 | - it('emit event when deactivate comment paragraph in an article', () => { | |
36 | - serviceMock.deactivateCommentParagraph = jasmine.createSpy("deactivateCommentParagraph").and.returnValue( | |
37 | - { then: (fn: Function) => { fn({ data: {} }); } } | |
38 | - ); | |
39 | - eventServiceMock.toggleCommentParagraph = jasmine.createSpy("toggleCommentParagraph"); | |
40 | - helper.component.deactivateCommentParagraph(); | |
41 | - | |
42 | - expect(serviceMock.deactivateCommentParagraph).toHaveBeenCalled(); | |
43 | - expect(eventServiceMock.toggleCommentParagraph).toHaveBeenCalled(); | |
44 | - }); | |
45 | - | |
46 | - it('emit event when activate comment paragraph in an article', () => { | |
47 | - serviceMock.activateCommentParagraph = jasmine.createSpy("activateCommentParagraph").and.returnValue( | |
48 | - { then: (fn: Function) => { fn({ data: {} }); } } | |
49 | - ); | |
50 | - eventServiceMock.toggleCommentParagraph = jasmine.createSpy("toggleCommentParagraph"); | |
51 | - helper.component.activateCommentParagraph(); | |
52 | - | |
53 | - expect(serviceMock.activateCommentParagraph).toHaveBeenCalled(); | |
54 | - expect(eventServiceMock.toggleCommentParagraph).toHaveBeenCalled(); | |
55 | - }); | |
56 | - | |
57 | - it('return true when comment paragraph is active', () => { | |
58 | - helper.component.article = <noosfero.Article>{ setting: { comment_paragraph_plugin_activate: true } }; | |
59 | - helper.detectChanges(); | |
60 | - expect(helper.component.isActivated()).toBeTruthy(); | |
61 | - }); | |
62 | - | |
63 | - it('return false when comment paragraph is not active', () => { | |
64 | - expect(helper.component.isActivated()).toBeFalsy(); | |
65 | - }); | |
66 | - | |
67 | - it('return false when article has no setting attribute', () => { | |
68 | - helper.component.article = <noosfero.Article>{}; | |
69 | - helper.detectChanges(); | |
70 | - expect(helper.component.isActivated()).toBeFalsy(); | |
71 | - }); | |
72 | - | |
73 | - it('display activate button when comment paragraph is not active', () => { | |
74 | - expect(helper.all('.comment-paragraph-activate').length).toEqual(1); | |
75 | - expect(helper.all('.comment-paragraph-deactivate').length).toEqual(0); | |
76 | - }); | |
77 | - | |
78 | - it('display deactivate button when comment paragraph is active', () => { | |
79 | - helper.component.article = <noosfero.Article>{ setting: { comment_paragraph_plugin_activate: true } }; | |
80 | - helper.detectChanges(); | |
81 | - expect(helper.all('.comment-paragraph-deactivate').length).toEqual(1); | |
82 | - expect(helper.all('.comment-paragraph-activate').length).toEqual(0); | |
83 | - }); | |
84 | - }); | |
85 | -}); |
src/plugins/comment_paragraph/hotspot/comment-paragraph-article-button.component.ts
... | ... | @@ -1,38 +0,0 @@ |
1 | -import { Input, Inject, Component } from "ng-forward"; | |
2 | -import {Hotspot} from "../../../app/hotspot/hotspot.decorator"; | |
3 | -import {CommentParagraphService} from "../http/comment-paragraph.service"; | |
4 | -import {CommentParagraphEventService} from "../events/comment-paragraph-event.service"; | |
5 | - | |
6 | -@Component({ | |
7 | - selector: "comment-paragraph-article-button-hotspot", | |
8 | - templateUrl: "plugins/comment_paragraph/hotspot/comment-paragraph-article-button.html", | |
9 | -}) | |
10 | -@Inject("$scope", CommentParagraphService, CommentParagraphEventService) | |
11 | -@Hotspot("article_extra_toolbar_buttons") | |
12 | -export class CommentParagraphArticleButtonHotspotComponent { | |
13 | - | |
14 | - @Input() article: noosfero.Article; | |
15 | - | |
16 | - constructor(private $scope: ng.IScope, | |
17 | - private commentParagraphService: CommentParagraphService, | |
18 | - private commentParagraphEventService: CommentParagraphEventService) { } | |
19 | - | |
20 | - deactivateCommentParagraph() { | |
21 | - this.toggleCommentParagraph(this.commentParagraphService.deactivateCommentParagraph(this.article)); | |
22 | - } | |
23 | - | |
24 | - activateCommentParagraph() { | |
25 | - this.toggleCommentParagraph(this.commentParagraphService.activateCommentParagraph(this.article)); | |
26 | - } | |
27 | - | |
28 | - isActivated() { | |
29 | - return this.article && this.article.setting && this.article.setting.comment_paragraph_plugin_activate; | |
30 | - } | |
31 | - | |
32 | - private toggleCommentParagraph(promise: ng.IPromise<noosfero.RestResult<noosfero.Article>>) { | |
33 | - promise.then((result: noosfero.RestResult<noosfero.Article>) => { | |
34 | - this.article = result.data; | |
35 | - this.commentParagraphEventService.toggleCommentParagraph(this.article); | |
36 | - }); | |
37 | - } | |
38 | -} |
src/plugins/comment_paragraph/hotspot/comment-paragraph-article-button.html
... | ... | @@ -1,8 +0,0 @@ |
1 | -<a href='#' class="btn btn-default btn-xs comment-paragraph-activate" (click)="ctrl.activateCommentParagraph()" | |
2 | - ng-if="!ctrl.article.setting.comment_paragraph_plugin_activate"> | |
3 | - <i class="fa fa-fw fa-plus"></i> {{"comment-paragraph-plugin.title" | translate}} | |
4 | -</a> | |
5 | -<a href='#' class="btn btn-default btn-xs comment-paragraph-deactivate" (click)="ctrl.deactivateCommentParagraph()" | |
6 | - ng-if="ctrl.article.setting.comment_paragraph_plugin_activate"> | |
7 | - <i class="fa fa-fw fa-minus"></i> {{"comment-paragraph-plugin.title" | translate}} | |
8 | -</a> |
src/plugins/comment_paragraph/index.ts
1 | 1 | import {AllowCommentComponent} from "./allow-comment/allow-comment.component"; |
2 | -import {CommentParagraphArticleButtonHotspotComponent} from "./hotspot/comment-paragraph-article-button.component"; | |
3 | 2 | import {CommentParagraphFormHotspotComponent} from "./hotspot/comment-paragraph-form.component"; |
4 | 3 | import {DiscussionEditorComponent} from "./article/cms/discussion-editor/discussion-editor.component"; |
5 | 4 | import {CommentParagraphArticleContentHotspotComponent} from "./hotspot/article-content/article-content.component"; |
6 | 5 | import {DiscussionBlockComponent} from "./block/discussion/discussion-block.component"; |
7 | 6 | |
8 | 7 | export let mainComponents: any = [AllowCommentComponent, DiscussionEditorComponent, DiscussionBlockComponent]; |
9 | -export let hotspots: any = [CommentParagraphArticleButtonHotspotComponent, CommentParagraphFormHotspotComponent, CommentParagraphArticleContentHotspotComponent]; | |
8 | +export let hotspots: any = [CommentParagraphFormHotspotComponent, CommentParagraphArticleContentHotspotComponent]; | ... | ... |
src/plugins/comment_paragraph/languages/en.json
... | ... | @@ -2,5 +2,9 @@ |
2 | 2 | "comment-paragraph-plugin.title": "Paragraph Comments", |
3 | 3 | "comment-paragraph-plugin.discussion.editor.start_date.label": "From", |
4 | 4 | "comment-paragraph-plugin.discussion.editor.end_date.label": "To", |
5 | - "comment-paragraph-plugin.discussion.header": "Open for comments" | |
5 | + "comment-paragraph-plugin.discussion.header": "Open for comments", | |
6 | + "comment-paragraph-plugin.discussion.notOpened.header": "Discussion will start {{date}}", | |
7 | + "comment-paragraph-plugin.discussion.available.header": "Discussion will end {{date}}", | |
8 | + "comment-paragraph-plugin.discussion.available.without-end.header": "Discussion opened", | |
9 | + "comment-paragraph-plugin.discussion.closed.header": "Discussion finished {{date}}" | |
6 | 10 | } | ... | ... |
src/plugins/comment_paragraph/languages/pt.json
... | ... | @@ -2,5 +2,9 @@ |
2 | 2 | "comment-paragraph-plugin.title": "Comentários por Parágrafo", |
3 | 3 | "comment-paragraph-plugin.discussion.editor.start_date.label": "De", |
4 | 4 | "comment-paragraph-plugin.discussion.editor.end_date.label": "Até", |
5 | - "comment-paragraph-plugin.discussion.header": "Aberto para comentários" | |
5 | + "comment-paragraph-plugin.discussion.header": "Aberto para comentários", | |
6 | + "comment-paragraph-plugin.discussion.notOpened.header": "Discussão iniciará {{date}}", | |
7 | + "comment-paragraph-plugin.discussion.available.header": "Discussão terminará {{date}}", | |
8 | + "comment-paragraph-plugin.discussion.available.without-end.header": "Discussão aberta", | |
9 | + "comment-paragraph-plugin.discussion.closed.header": "Discussão finalizada {{date}}" | |
6 | 10 | } | ... | ... |
src/spec/component-test-helper.ts
... | ... | @@ -88,16 +88,23 @@ export class ComponentTestHelper<T extends any> { |
88 | 88 | this.debugElement = fixture.debugElement; |
89 | 89 | this.component = <T>this.debugElement.componentViewChildren[0].componentInstance; |
90 | 90 | let mockObj = new this.mockComponent(); |
91 | - Object.keys(mockObj).forEach((key: any) => { | |
92 | - (<any>this.component)[key] = <any>mockObj[key]; | |
93 | - }); | |
94 | - | |
91 | + if (this.component) { | |
92 | + Object.keys(mockObj).forEach((key: any) => { | |
93 | + (<any>this.component)[key] = <any>mockObj[key]; | |
94 | + }); | |
95 | + } | |
95 | 96 | }).then(() => { |
96 | 97 | // Force the resolution of components and sync |
97 | 98 | done(); |
98 | 99 | }); |
99 | 100 | } |
100 | 101 | |
102 | + changeProperties(properties: any) { | |
103 | + Object.keys(properties).forEach((key: any) => { | |
104 | + this.component[key] = properties[key]; | |
105 | + }); | |
106 | + } | |
107 | + | |
101 | 108 | /** |
102 | 109 | * @ngdoc method |
103 | 110 | * @name detectChanges | ... | ... |
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) => { | ... | ... |
... | ... | @@ -0,0 +1,70 @@ |
1 | +.block-head-with-icon { | |
2 | + padding: 15px 15px 15px 55px; | |
3 | +} | |
4 | + | |
5 | +.content-wrapper .block { | |
6 | + .panel-heading { | |
7 | + border: none; | |
8 | + border-radius: 3px 3px 0 0; | |
9 | + font-family: "Ubuntu Medium"; | |
10 | + font-size: 15px; | |
11 | + font-variant: normal; | |
12 | + font-weight: normal; | |
13 | + line-height: 30px; | |
14 | + padding: 15px 15px 15px 15px; | |
15 | + text-transform: capitalize; | |
16 | + background-size: 30px 30px; | |
17 | + background: #DAE1C4; | |
18 | + color: #4F9CAC; | |
19 | + } | |
20 | + | |
21 | + &.membersblock { | |
22 | + .panel-heading { | |
23 | + @extend .block-head-with-icon; | |
24 | + background: #DAE1C4 url("../assets/icons/participa-consulta/pessoas.png") no-repeat 15px center; | |
25 | + color: #4F9CAC; | |
26 | + } | |
27 | + } | |
28 | + | |
29 | + &.statisticsblock { | |
30 | + .panel-heading { | |
31 | + @extend .block-head-with-icon; | |
32 | + background: #69677C url("../assets/icons/participa-consulta/indicadores.png") no-repeat 15px center; | |
33 | + color: #DAE1C4; | |
34 | + } | |
35 | + } | |
36 | +} | |
37 | + | |
38 | +.col-md-7 { | |
39 | + .panel-body.linklistblock { | |
40 | + padding: 0px; | |
41 | + } | |
42 | + | |
43 | + .link-list-block { | |
44 | + font-family: "Ubuntu"; | |
45 | + font-size: 12px; | |
46 | + background: #69677C; | |
47 | + border-radius: 0 0 3px 3px; | |
48 | + padding: 4px 10px; | |
49 | + position: relative; | |
50 | + | |
51 | + div, a { | |
52 | + display: inline-block; | |
53 | + background: #69677C; | |
54 | + border-radius: 0; | |
55 | + color: #FFF; | |
56 | + font-family: "Ubuntu Medium"; | |
57 | + font-size: 16px; | |
58 | + margin: 0; | |
59 | + padding-right: 15px; | |
60 | + font-weight: bold; | |
61 | + } | |
62 | + | |
63 | + div:first-child { | |
64 | + padding: 15px 5px 15px 50px; | |
65 | + background: #69677C url("../assets/icons/participa-consulta/home.png") no-repeat 7px center; | |
66 | + color: #FFF; | |
67 | + background-size: 30px 30px; | |
68 | + } | |
69 | + } | |
70 | +} | ... | ... |
... | ... | @@ -0,0 +1,25 @@ |
1 | +.navbar { | |
2 | + min-height: 123px; | |
3 | + background-color: #f9c404; | |
4 | + background-image: -moz-radial-gradient(center, ellipse cover, #fcdd4e 1%, #f9c404 100%); | |
5 | + background-image: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(1%, #fcdd4e), color-stop(100%, #f9c404)); | |
6 | + background-image: -webkit-radial-gradient(center, ellipse cover, #fcdd4e 1%, #f9c404 100%); | |
7 | + background-image: -o-radial-gradient(center, ellipse cover, #fcdd4e 1%, #f9c404 100%); | |
8 | + background-image: -ms-radial-gradient(center, ellipse cover, #fcdd4e 1%, #f9c404 100%); | |
9 | + background-image: radial-gradient(ellipse at center, #fcdd4e 1%, #f9c404 100%); | |
10 | + | |
11 | + .container-fluid { | |
12 | + .navbar-brand { | |
13 | + .noosfero-logo { | |
14 | + display: none; | |
15 | + } | |
16 | + .noosfero-name { | |
17 | + color: #03316f; | |
18 | + font-size: 40px; | |
19 | + font-weight: 800; | |
20 | + line-height: 1em; | |
21 | + letter-spacing: -0.05em; | |
22 | + } | |
23 | + } | |
24 | + } | |
25 | +} | ... | ... |
themes/angular-participa-consulta/app/participa-consulta.scss
0 → 100644
... | ... | @@ -0,0 +1,35 @@ |
1 | +@font-face { | |
2 | + font-family: 'Ubuntu'; | |
3 | + font-weight: 300; | |
4 | + font-style: normal; | |
5 | + src: url('../assets/fonts/participa-consulta/Ubuntu-R.ttf'); | |
6 | +} | |
7 | + | |
8 | +@font-face { | |
9 | + font-family: 'Ubuntu Medium'; | |
10 | + font-weight: 300; | |
11 | + font-style: normal; | |
12 | + src: url('../assets/fonts/participa-consulta/Ubuntu-M.ttf'); | |
13 | +} | |
14 | + | |
15 | +@font-face { | |
16 | + font-family: 'Ubuntu'; | |
17 | + font-weight: 300; | |
18 | + font-style: italic; | |
19 | + src: url('../assets/fonts/participa-consulta/Ubuntu-RI.ttf'); | |
20 | +} | |
21 | + | |
22 | +.skin-whbl .notifications-list .item-footer { | |
23 | + background: #DAE1C4; | |
24 | + color: #4F9CAC; | |
25 | +} | |
26 | + | |
27 | +.profile-header, .profile-footer{ | |
28 | + text-align: center; | |
29 | +} | |
30 | + | |
31 | +.container-fluid .navbar-header .navbar-toggle{ | |
32 | + &:hover, &:focus { | |
33 | + background-color: #7E7E7E; | |
34 | + } | |
35 | +} | ... | ... |
themes/angular-participa-consulta/assets/fonts/participa-consulta/Ubuntu-B.ttf
0 → 100644
No preview for this file type
themes/angular-participa-consulta/assets/fonts/participa-consulta/Ubuntu-BI.ttf
0 → 100644
No preview for this file type
themes/angular-participa-consulta/assets/fonts/participa-consulta/Ubuntu-C.ttf
0 → 100644
No preview for this file type
themes/angular-participa-consulta/assets/fonts/participa-consulta/Ubuntu-L.ttf
0 → 100644
No preview for this file type
themes/angular-participa-consulta/assets/fonts/participa-consulta/Ubuntu-LI.ttf
0 → 100644
No preview for this file type
themes/angular-participa-consulta/assets/fonts/participa-consulta/Ubuntu-M.ttf
0 → 100644
No preview for this file type
themes/angular-participa-consulta/assets/fonts/participa-consulta/Ubuntu-MI.ttf
0 → 100644
No preview for this file type
themes/angular-participa-consulta/assets/fonts/participa-consulta/Ubuntu-R.ttf
0 → 100644
No preview for this file type
themes/angular-participa-consulta/assets/fonts/participa-consulta/Ubuntu-RI.ttf
0 → 100644
No preview for this file type
themes/angular-participa-consulta/assets/fonts/participa-consulta/UbuntuMono-B.ttf
0 → 100644
No preview for this file type
themes/angular-participa-consulta/assets/fonts/participa-consulta/UbuntuMono-BI.ttf
0 → 100644
No preview for this file type
themes/angular-participa-consulta/assets/fonts/participa-consulta/UbuntuMono-R.ttf
0 → 100644
No preview for this file type
themes/angular-participa-consulta/assets/fonts/participa-consulta/UbuntuMono-RI.ttf
0 → 100644
No preview for this file type
themes/angular-participa-consulta/assets/icons/participa-consulta/agenda-contagem.png
0 → 100644
921 Bytes
themes/angular-participa-consulta/assets/icons/participa-consulta/balao.png
0 → 100644
528 Bytes
themes/angular-participa-consulta/assets/icons/participa-consulta/balao_claro.png
0 → 100644
517 Bytes
themes/angular-participa-consulta/assets/icons/participa-consulta/closed.png
0 → 100644
577 Bytes
themes/angular-participa-consulta/assets/icons/participa-consulta/como-participar.png
0 → 100644
1.14 KB
themes/angular-participa-consulta/assets/icons/participa-consulta/home.png
0 → 100644
777 Bytes
themes/angular-participa-consulta/assets/icons/participa-consulta/icon-attend.png
0 → 100644
1.32 KB
themes/angular-participa-consulta/assets/icons/participa-consulta/indicadores.png
0 → 100644
1004 Bytes