Commit 12f1379fee7191aa0a0289d4cbfadfa8592e9b77

Authored by Michel Felipe
2 parents c0fd7cf6 800e2559

Merge branch 'comment-paragraph' into 'master'

Frontend for comment paragraph plugin

See merge request !7
Showing 52 changed files with 1055 additions and 78 deletions   Show diff stats
bower.json
... ... @@ -35,6 +35,8 @@
35 35 "angular-i18n": "^1.5.0",
36 36 "angular-load": "^0.4.1",
37 37 "angular-translate-interpolation-messageformat": "^2.10.0",
  38 + "angular-bind-html-compile": "^1.2.1",
  39 + "angular-click-outside": "^2.7.1",
38 40 "ng-ckeditor": "^0.2.1",
39 41 "ckeditor": "^4.5.8"
40 42 },
... ...
gulp/build.js
... ... @@ -6,6 +6,7 @@ var rename = require('gulp-rename');
6 6 var insert = require('gulp-insert');
7 7 var merge = require('merge-stream');
8 8 var conf = require('./conf');
  9 +var languages = require('./languages');
9 10  
10 11 var themeName = conf.paths.theme.replace('-', ' ');
11 12 themeName = themeName.charAt(0).toUpperCase() + themeName.slice(1);
... ... @@ -16,25 +17,31 @@ var $ = require('gulp-load-plugins')({
16 17 });
17 18  
18 19 gulp.task('partials', function () {
19   - var srcPaths = [path.join(conf.paths.tmp, '/serve/app/**/*.html')];
20   - conf.paths.allSources.forEach(function(src) {
21   - srcPaths.push(path.join(src, '/app/**/*.html'));
  20 + var merged = merge();
  21 + ['app', conf.paths.plugins].forEach(function(partialPath) {
  22 + var srcPaths = [path.join(conf.paths.tmp, '/serve/app/**/*.html')];
  23 + conf.paths.allSources.forEach(function(src) {
  24 + srcPaths.push(path.join(src, partialPath, '/**/*.html'));
  25 + });
  26 + merged.add(gulp.src(srcPaths)
  27 + .pipe($.minifyHtml({
  28 + empty: true,
  29 + spare: true,
  30 + quotes: true
  31 + }))
  32 + .pipe($.angularTemplatecache('templateCacheHtml-'+partialPath+'.js', {
  33 + module: 'noosferoApp',
  34 + root: partialPath
  35 + }))
  36 + .pipe(gulp.dest(conf.paths.tmp + '/partials/')));
22 37 });
23   - return gulp.src(srcPaths)
24   - .pipe($.minifyHtml({
25   - empty: true,
26   - spare: true,
27   - quotes: true
28   - }))
29   - .pipe($.angularTemplatecache('templateCacheHtml.js', {
30   - module: 'noosferoApp',
31   - root: 'app'
32   - }))
33   - .pipe(gulp.dest(conf.paths.tmp + '/partials/'));
  38 + return merged;
34 39 });
35 40  
36 41 gulp.task('html', ['inject', 'partials'], function () {
37   - var partialsInjectFile = gulp.src(path.join(conf.paths.tmp, '/partials/templateCacheHtml.js'), { read: false });
  42 + var partialsInjectFile = gulp.src([
  43 + path.join(conf.paths.tmp, '/partials/templateCacheHtml-app.js'),
  44 + path.join(conf.paths.tmp, '/partials/templateCacheHtml-plugins.js')], { read: false });
38 45 var partialsInjectOptions = {
39 46 starttag: '<!-- inject:partials -->',
40 47 ignorePath: path.join(conf.paths.tmp, '/partials'),
... ... @@ -124,6 +131,10 @@ gulp.task(&#39;clean-docs&#39;, [], function() {
124 131 return $.del([path.join(conf.paths.docs, '/')]);
125 132 });
126 133  
  134 +gulp.task('plugin-languages', ['locale'], function() {
  135 + return languages.pluginLanguages(conf.paths.dist);
  136 +});
  137 +
127 138 gulp.task('noosfero', ['html'], function () {
128 139 var layouts = gulp.src('layouts/**/*')
129 140 .pipe(gulp.dest(path.join(conf.paths.dist, "layouts")));
... ... @@ -136,4 +147,4 @@ gulp.task(&#39;noosfero&#39;, [&#39;html&#39;], function () {
136 147 return merge(layouts, theme, index);
137 148 });
138 149  
139   -gulp.task('build', ['html', 'fonts', 'other', 'locale', 'noosfero']);
  150 +gulp.task('build', ['html', 'fonts', 'other', 'locale', 'plugin-languages', 'noosfero']);
... ...
gulp/conf.js
... ... @@ -15,11 +15,13 @@ var path = require(&#39;path&#39;);
15 15 */
16 16 exports.paths = {
17 17 src: 'src',
  18 + plugins: 'plugins',
18 19 dist: 'dist',
19 20 tmp: '.tmp',
20 21 e2e: 'e2e',
21 22 docs: 'docs',
22   - themes: 'themes'
  23 + themes: 'themes',
  24 + languages: 'languages'
23 25 };
24 26 exports.configTheme = function(theme) {
25 27 exports.paths.theme = theme || "angular-default";
... ...
gulp/languages.js 0 → 100644
... ... @@ -0,0 +1,29 @@
  1 +'use strict';
  2 +
  3 +var path = require('path');
  4 +var gulp = require('gulp');
  5 +var merge = require('merge-stream');
  6 +var conf = require('./conf');
  7 +var mergeJson = require('gulp-merge-json');
  8 +var glob = require("glob");
  9 +
  10 +exports.pluginLanguages = function(dest) {
  11 + var merged = merge();
  12 + glob(path.join(conf.paths.src, conf.paths.languages, "*.json"), function (er, files) {
  13 + files.forEach(function(file) {
  14 + merged.add(exports.pluginLanguage(file, dest));
  15 + });
  16 + });
  17 + return merged;
  18 +}
  19 +
  20 +exports.pluginLanguage = function(file, dest) {
  21 + var language = file.split('/').pop().replace('\.json','');
  22 + return gulp.src(path.join(conf.paths.src, '**', conf.paths.languages, language+'.json'))
  23 + .pipe(mergeJson(path.join(conf.paths.languages, language+'.json')))
  24 + .pipe(gulp.dest(dest))
  25 +}
  26 +
  27 +gulp.task('serve-languages', function() {
  28 + return exports.pluginLanguages(path.join(conf.paths.tmp, '/serve'));
  29 +});
... ...
gulp/server.js
... ... @@ -45,7 +45,7 @@ browserSync.use(browserSyncSpa({
45 45 selector: '[ng-app]'// Only needed for angular apps
46 46 }));
47 47  
48   -gulp.task('serve', ['watch'], function () {
  48 +gulp.task('serve', ['serve-languages', 'watch'], function () {
49 49 var srcPaths = [path.join(conf.paths.tmp, '/serve')];
50 50 conf.paths.allSources.reverse().forEach(function(src) {
51 51 srcPaths.push(src);
... ...
gulp/styles.js
... ... @@ -31,6 +31,7 @@ var buildStyles = function() {
31 31 ];
32 32 conf.paths.allSources.forEach(function(src) {
33 33 srcPaths.push(path.join(src, '/app/**/*.scss'));
  34 + srcPaths.push(path.join(src, conf.paths.plugins, '/**/*.scss'));
34 35 });
35 36 var injectFiles = gulp.src(srcPaths, { read: false });
36 37  
... ...
gulp/watch.js
... ... @@ -3,6 +3,7 @@
3 3 var path = require('path');
4 4 var gulp = require('gulp');
5 5 var conf = require('./conf');
  6 +var languages = require('./languages');
6 7  
7 8 var browserSync = require('browser-sync');
8 9  
... ... @@ -14,13 +15,11 @@ gulp.task(&#39;watch&#39;, [&#39;inject&#39;], function () {
14 15  
15 16 gulp.watch([path.join(conf.paths.src, '/*.html'), 'bower.json'], ['inject-reload']);
16 17  
17   - var stylePaths = [];
18   - conf.paths.allSources.forEach(function(src) {
19   - stylePaths.push(path.join(src, '/app/**/*.css'));
20   - stylePaths.push(path.join(src, '/app/**/*.scss'));
21   - });
22   -
23   - gulp.watch(stylePaths, function(event) {
  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) {
24 23 if(isOnlyChange(event)) {
25 24 gulp.start('styles-reload');
26 25 } else {
... ... @@ -36,9 +35,14 @@ gulp.task(&#39;watch&#39;, [&#39;inject&#39;], function () {
36 35 }
37 36 });
38 37  
  38 + gulp.watch(path.join(conf.paths.src, '**', conf.paths.languages, '*.json'), function(event) {
  39 + languages.pluginLanguage(event.path, path.join(conf.paths.tmp, '/serve'));
  40 + });
  41 +
39 42 var watchPaths = [];
40 43 conf.paths.allSources.forEach(function(src) {
41 44 watchPaths.push(path.join(src, '/app/**/*.html'));
  45 + watchPaths.push(path.join(src, conf.paths.plugins, '/**/*.html'));
42 46 });
43 47 gulp.watch(watchPaths, function(event) {
44 48 browserSync.reload(event.path);
... ...
karma.conf.js
... ... @@ -49,7 +49,7 @@ var _ = require(&#39;lodash&#39;);
49 49 var wiredep = require('wiredep');
50 50  
51 51 var pathSrcHtml = [
52   - path.join('./src/app/**/*.html')
  52 + path.join('./src/**/*.html')
53 53 ];
54 54  
55 55 var glob = require("glob");
... ...
package.json
... ... @@ -49,6 +49,7 @@
49 49 "gulp-insert": "^0.5.0",
50 50 "gulp-inject": "~3.0.0",
51 51 "gulp-load-plugins": "~0.10.0",
  52 + "gulp-merge-json": "^0.4.0",
52 53 "gulp-minify-css": "~1.2.1",
53 54 "gulp-minify-html": "~1.0.4",
54 55 "gulp-ng-annotate": "~1.1.0",
... ...
src/app/article/article-default-view.component.ts
1 1 import { bundle, Input, Inject, Component, Directive } from 'ng-forward';
2 2 import {ArticleBlogComponent} from "./types/blog/blog.component";
3 3 import {CommentsComponent} from "./comment/comments.component";
  4 +import {MacroDirective} from "./macro/macro.directive";
  5 +import {ArticleToolbarHotspotComponent} from "../hotspot/article-toolbar-hotspot.component";
4 6  
5 7 /**
6 8 * @ngdoc controller
... ... @@ -30,7 +32,8 @@ export class ArticleDefaultViewComponent {
30 32 @Component({
31 33 selector: 'noosfero-article',
32 34 template: 'not-used',
33   - directives: [ArticleDefaultViewComponent, ArticleBlogComponent, CommentsComponent]
  35 + directives: [ArticleDefaultViewComponent, ArticleBlogComponent,
  36 + CommentsComponent, MacroDirective, ArticleToolbarHotspotComponent]
34 37 })
35 38 @Inject("$element", "$scope", "$injector", "$compile")
36 39 export class ArticleViewComponent {
... ... @@ -53,6 +56,5 @@ export class ArticleViewComponent {
53 56 private $scope: ng.IScope,
54 57 private $injector: ng.auto.IInjectorService,
55 58 private $compile: ng.ICompileService) {
56   -
57 59 }
58 60 }
... ...
src/app/article/article.html
... ... @@ -4,9 +4,7 @@
4 4 </div>
5 5  
6 6 <div class="sub-header clearfix">
7   - <a href="#" class="btn btn-default btn-xs" ui-sref="main.cmsEdit({profile: ctrl.profile.identifier, id: ctrl.article.id})">
8   - <i class="fa fa-pencil-square-o fa-fw fa-lg"></i> {{"article.actions.edit" | translate}}
9   - </a>
  7 + <noosfero-hotspot-article-toolbar [article]="ctrl.article"></noosfero-hotspot-article-toolbar>
10 8 <div class="page-info pull-right small text-muted">
11 9 <span class="time">
12 10 <i class="fa fa-clock-o"></i> <span am-time-ago="ctrl.article.created_at | dateFormat"></span>
... ... @@ -21,7 +19,7 @@
21 19 </div>
22 20  
23 21 <div class="page-body">
24   - <div ng-bind-html="ctrl.article.body"></div>
  22 + <div bind-html-compile="ctrl.article.body"></div>
25 23 </div>
26 24  
27 25 <noosfero-comments [article]="ctrl.article"></noosfero-comments>
... ...
src/app/article/comment/comment-reply-tooltip.html 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +<div class="reply-tooltip">
  2 + <noosfero-comment [comment]="ctrl.comment.reply_of" [article]="ctrl.article" [display-actions]="false" [display-replies]="false"></noosfero-comment>
  3 +</div>
... ...
src/app/article/comment/comment.component.ts
... ... @@ -9,6 +9,8 @@ export class CommentComponent {
9 9  
10 10 @Input() comment: noosfero.CommentViewModel;
11 11 @Input() article: noosfero.Article;
  12 + @Input() displayActions = true;
  13 + @Input() displayReplies = true;
12 14  
13 15 showReply() {
14 16 return this.comment && this.comment.__show_reply === true;
... ...
src/app/article/comment/comment.html
... ... @@ -9,13 +9,19 @@
9 9 <a class="pull-left" ui-sref="main.profile.home({profile: ctrl.comment.author.identifier})">
10 10 <h4 class="media-heading">{{ctrl.comment.author.name}}</h4>
11 11 </a>
  12 + <span class="reply-of" ng-if="ctrl.comment.reply_of" uib-tooltip-template="'app/article/comment/comment-reply-tooltip.html'">
  13 + <i class="fa fa-fw fa-mail-forward"></i>
  14 + <span class="author">{{ctrl.comment.reply_of.author.name}}</span>
  15 + </span>
12 16 <span class="date" am-time-ago="ctrl.comment.created_at | dateFormat"></span>
13 17 </div>
14 18 <div class="title">{{ctrl.comment.title}}</div>
15 19 <div class="body">{{ctrl.comment.body}}</div>
16   - <a href="#" (click)="ctrl.reply()" class="small text-muted">
17   - {{"comment.reply" | translate}}
18   - </a>
  20 + <div class="actions" ng-if="ctrl.displayActions">
  21 + <a href="#" (click)="ctrl.reply()" class="small text-muted">
  22 + {{"comment.reply" | translate}}
  23 + </a>
  24 + </div>
19 25 </div>
20   - <noosfero-comments [show-form]="ctrl.showReply()" [article]="ctrl.article" [parent]="ctrl.comment"></noosfero-comments>
  26 + <noosfero-comments [show-form]="ctrl.showReply" [article]="ctrl.article" [parent]="ctrl.comment" ng-if="ctrl.displayReplies"></noosfero-comments>
21 27 </div>
... ...
src/app/article/comment/comment.scss
... ... @@ -5,6 +5,7 @@
5 5 @extend .text-muted;
6 6 @extend .small;
7 7 margin-left: 8px;
  8 + font-size: 12px;
8 9 }
9 10 .title {
10 11 font-weight: bold;
... ... @@ -13,7 +14,18 @@
13 14 min-width: 40px;
14 15 }
15 16 .media-body {
16   - padding: 0 10px 10px 10px;
  17 + padding: 0 10px 10px 0;
  18 + .reply-of {
  19 + font-size: 12px;
  20 + color: #B5B5B5;
  21 + margin-left: 5px;
  22 + i {
  23 + font-size: 10px;
  24 + }
  25 + }
  26 + h4 {
  27 + font-size: 16px;
  28 + }
17 29 }
18 30 noosfero-profile-image {
19 31 img {
... ... @@ -27,8 +39,24 @@
27 39 font-size: 1.7em;
28 40 }
29 41 }
  42 + // Limit identation of replies
30 43 .comments {
31 44 margin-left: 30px;
  45 + .comments .comments {
  46 + margin-left: 0px;
  47 + .comment {
  48 + margin-left: 0px;
  49 + }
  50 + }
  51 + }
  52 + .tooltip-inner {
  53 + max-width: 350px;
  54 + text-align: left;
  55 + .reply-tooltip {
  56 + .comment {
  57 + margin: 5px;
  58 + }
  59 + }
32 60 }
33 61 }
34 62 }
... ...
src/app/article/comment/comments.component.ts
1   -import { Inject, Input, Output, Component, provide, EventEmitter } from 'ng-forward';
2   -import {INgForwardJQuery} from "ng-forward/cjs/util/jqlite-extensions";
3   -
4   -
  1 +import { Inject, Input, Component, provide } from 'ng-forward';
5 2 import { PostCommentComponent } from "./post-comment/post-comment.component";
6 3 import { CommentService } from "../../../lib/ng-noosfero-api/http/comment.service";
7 4 import { CommentComponent } from "./comment.component";
  5 +import { PostCommentEventService } from "./post-comment/post-comment-event.service";
8 6  
9 7 @Component({
10 8 selector: 'noosfero-comments',
11 9 templateUrl: 'app/article/comment/comments.html',
12   - directives: [PostCommentComponent, CommentComponent],
13   - outputs: ['commentAdded']
  10 + directives: [PostCommentComponent, CommentComponent]
14 11 })
15   -@Inject(CommentService, "$element")
  12 +@Inject(CommentService, PostCommentEventService, "$scope")
16 13 export class CommentsComponent {
17 14  
18   - comments: noosfero.CommentViewModel[] = [];
  15 + comments: noosfero.Comment[] = [];
19 16 @Input() showForm = true;
20 17 @Input() article: noosfero.Article;
21   - @Input() parent: noosfero.CommentViewModel;
22   -
  18 + @Input() parent: noosfero.Comment;
23 19 protected page = 1;
24 20 protected perPage = 5;
25 21 protected total = 0;
26 22  
27   - constructor(protected commentService: CommentService) { }
  23 + newComment = <noosfero.Comment>{};
  24 +
  25 + constructor(protected commentService: CommentService, private postCommentEventService: PostCommentEventService, private $scope: ng.IScope) { }
28 26  
29 27 ngOnInit() {
30 28 if (this.parent) {
... ... @@ -32,20 +30,13 @@ export class CommentsComponent {
32 30 } else {
33 31 this.loadNextPage();
34 32 }
35   - }
36   -
37   - commentAdded(comment: noosfero.Comment): void {
38   - this.comments.push(comment);
39   - this.resetShowReply();
40   - }
41   -
42   - private resetShowReply() {
43   - this.comments.forEach((comment: noosfero.CommentViewModel) => {
44   - comment.__show_reply = false;
  33 + this.postCommentEventService.subscribe((comment: noosfero.Comment) => {
  34 + if ((!this.parent && !comment.reply_of) || (comment.reply_of && this.parent && comment.reply_of.id === this.parent.id)) {
  35 + if (!this.comments) this.comments = [];
  36 + this.comments.push(comment);
  37 + this.$scope.$apply();
  38 + }
45 39 });
46   - if (this.parent) {
47   - this.parent.__show_reply = false;
48   - }
49 40 }
50 41  
51 42 loadComments() {
... ...
src/app/article/comment/comments.html
1 1 <div class="comments">
2   - <noosfero-post-comment (comment-saved)="ctrl.commentAdded($event.detail)" ng-if="ctrl.showForm" [article]="ctrl.article" [parent]="ctrl.parent"></noosfero-post-comment>
  2 + <noosfero-post-comment 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 5 <noosfero-comment ng-repeat="comment in ctrl.comments | orderBy: 'created_at':true" [comment]="comment" [article]="ctrl.article"></noosfero-comment>
... ...
src/app/article/comment/post-comment/post-comment.component.ts
1   -import { Inject, Input, Output, EventEmitter, Component } from 'ng-forward';
  1 +import { Inject, Input, Component } from 'ng-forward';
2 2 import { CommentService } from "../../../../lib/ng-noosfero-api/http/comment.service";
3 3 import { NotificationService } from "../../../shared/services/notification.service";
4 4 import { SessionService } from "../../../login";
  5 +import { PostCommentEventService } from "./post-comment-event.service";
  6 +import { CommentFormHotspotComponent } from "../../../hotspot/comment-form-hotspot.component";
5 7  
6 8 @Component({
7 9 selector: 'noosfero-post-comment',
8 10 templateUrl: 'app/article/comment/post-comment/post-comment.html',
9   - outputs: ['commentSaved']
  11 + directives: [CommentFormHotspotComponent]
10 12 })
11   -@Inject(CommentService, NotificationService, SessionService)
  13 +@Inject(CommentService, NotificationService, SessionService, PostCommentEventService)
12 14 export class PostCommentComponent {
13 15  
  16 + public static EVENT_COMMENT_RECEIVED = "comment.received";
  17 +
14 18 @Input() article: noosfero.Article;
15 19 @Input() parent: noosfero.Comment;
16   - @Output() commentSaved: EventEmitter<Comment> = new EventEmitter<Comment>();
17   -
18   - comment = <noosfero.Comment>{};
  20 + @Input() comment = <noosfero.Comment>{};
19 21 private currentUser: noosfero.User;
20 22  
21 23 constructor(private commentService: CommentService,
22 24 private notificationService: NotificationService,
23   - private session: SessionService) {
  25 + private session: SessionService,
  26 + private postCommentEventService: PostCommentEventService) {
24 27 this.currentUser = this.session.currentUser();
25 28 }
26 29  
... ... @@ -29,7 +32,7 @@ export class PostCommentComponent {
29 32 this.comment.reply_of_id = this.parent.id;
30 33 }
31 34 this.commentService.createInArticle(this.article, this.comment).then((result: noosfero.RestResult<noosfero.Comment>) => {
32   - this.commentSaved.next(result.data);
  35 + this.postCommentEventService.emit(result.data);
33 36 this.comment.body = "";
34 37 this.notificationService.success({ title: "comment.post.success.title", message: "comment.post.success.message" });
35 38 });
... ...
src/app/article/comment/post-comment/post-comment.html
... ... @@ -8,6 +8,7 @@
8 8 </div>
9 9 <div class="media-body">
10 10 <textarea class="form-control custom-control" rows="1" ng-model="ctrl.comment.body" placeholder="{{'comment.post.placeholder' | translate}}"></textarea>
  11 + <noosfero-hotspot-comment-form [comment]="ctrl.comment" [parent]="ctrl.parent"></noosfero-hotspot-comment-form>
11 12 <button ng-show="ctrl.comment.body" type="submit" class="btn btn-default pull-right ng-hide" ng-click="ctrl.save()">{{"comment.post" | translate}}</button>
12 13 </div>
13 14 </div>
... ...
src/app/article/macro/macro.directive.spec.ts 0 → 100644
... ... @@ -0,0 +1,25 @@
  1 +import {Input, provide, Component} from 'ng-forward';
  2 +import {MacroDirective} from "./macro.directive";
  3 +
  4 +import * as helpers from "../../../spec/helpers";
  5 +
  6 +const htmlTemplate: string = '<div data-macro="macro_component" data-macro-custom="custom"></div>';
  7 +
  8 +describe("Directives", () => {
  9 +
  10 + describe("Macro directive", () => {
  11 + it("renders a macro component using the name passed in data-macro", (done: Function) => {
  12 + helpers.quickCreateComponent({ template: htmlTemplate, directives: [MacroDirective] }).then((fixture) => {
  13 + expect(fixture.debugElement.queryAll('macro-component').length).toEqual(1);
  14 + done();
  15 + });
  16 + });
  17 +
  18 + it("extract custom attributes from macro", (done: Function) => {
  19 + helpers.quickCreateComponent({ template: htmlTemplate, directives: [MacroDirective] }).then((fixture) => {
  20 + expect(fixture.debugElement.query('macro-component').attr("custom")).toEqual("custom");
  21 + done();
  22 + });
  23 + });
  24 + });
  25 +});
... ...
src/app/article/macro/macro.directive.ts 0 → 100644
... ... @@ -0,0 +1,34 @@
  1 +import {Directive, Inject} from "ng-forward";
  2 +
  3 +@Directive({
  4 + selector: '[macro]',
  5 + providers: []
  6 +})
  7 +@Inject('$element', '$scope', '$compile')
  8 +export class MacroDirective {
  9 +
  10 + private macroPrefix = "data-macro";
  11 +
  12 + constructor(private $element: any, private $scope: ng.IScope, private $compile: ng.ICompileService) {
  13 + let macro = $element[0].attributes[this.macroPrefix].value;
  14 + let componentName = this.normalizeName(macro);
  15 + let content = $element.html().replace(/"/g, '&quot;');
  16 + let customAttributes = this.extractCustomAttributes($element[0].attributes);
  17 + $element.replaceWith($compile(`<${componentName} [article]="ctrl.article" content="${content}" ${customAttributes}></${componentName}>`)($scope));
  18 + }
  19 +
  20 + extractCustomAttributes(attributes: any) {
  21 + let customAttributes = "";
  22 + for (let attr of attributes) {
  23 + if (attr.name.startsWith(this.macroPrefix + '-')) {
  24 + let name = this.normalizeName(attr.name.replace(this.macroPrefix + '-', ''));
  25 + customAttributes += ` ${name}='${attr.value}'`;
  26 + }
  27 + }
  28 + return customAttributes;
  29 + }
  30 +
  31 + normalizeName(name: string) {
  32 + return name.replace(/[_\/]/g, '-').toLowerCase();
  33 + }
  34 +}
... ...
src/app/hotspot/article-toolbar-hotspot.component.ts 0 → 100644
... ... @@ -0,0 +1,25 @@
  1 +import {Component, Input, Inject} from "ng-forward";
  2 +import * as plugins from "../../plugins";
  3 +import {dasherize} from "ng-forward/cjs/util/helpers";
  4 +import {PluginHotspot} from "./plugin-hotspot";
  5 +
  6 +@Component({
  7 + selector: "noosfero-hotspot-article-toolbar",
  8 + template: "<span></span>"
  9 +})
  10 +@Inject("$element", "$scope", "$compile")
  11 +export class ArticleToolbarHotspotComponent extends PluginHotspot {
  12 +
  13 + @Input() article: noosfero.Article;
  14 +
  15 + constructor(
  16 + private $element: any,
  17 + private $scope: ng.IScope,
  18 + private $compile: ng.ICompileService) {
  19 + super("article_extra_toolbar_buttons");
  20 + }
  21 +
  22 + addHotspot(directiveName: string) {
  23 + this.$element.append(this.$compile('<' + directiveName + ' [article]="ctrl.article"></' + directiveName + '>')(this.$scope));
  24 + }
  25 +}
... ...
src/app/hotspot/comment-form-hotspot.component.ts 0 → 100644
... ... @@ -0,0 +1,26 @@
  1 +import {Component, Input, Inject} from "ng-forward";
  2 +import * as plugins from "../../plugins";
  3 +import {dasherize} from "ng-forward/cjs/util/helpers";
  4 +import {PluginHotspot} from "./plugin-hotspot";
  5 +
  6 +@Component({
  7 + selector: "noosfero-hotspot-comment-form",
  8 + template: "<span></span>"
  9 +})
  10 +@Inject("$element", "$scope", "$compile")
  11 +export class CommentFormHotspotComponent extends PluginHotspot {
  12 +
  13 + @Input() comment: noosfero.Comment;
  14 + @Input() parent: noosfero.Comment;
  15 +
  16 + constructor(
  17 + private $element: any,
  18 + private $scope: ng.IScope,
  19 + private $compile: ng.ICompileService) {
  20 + super("comment_form_extra_contents");
  21 + }
  22 +
  23 + addHotspot(directiveName: string) {
  24 + this.$element.append(this.$compile('<' + directiveName + ' [comment]="ctrl.comment" [parent]="ctrl.parent"></' + directiveName + '>')(this.$scope));
  25 + }
  26 +}
... ...
src/app/hotspot/hotspot.decorator.ts 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +export function Hotspot(hotspotName: string) {
  2 + return (target: any) => {
  3 + target['hotspot'] = hotspotName;
  4 + };
  5 +}
... ...
src/app/hotspot/plugin-hotspot.ts 0 → 100644
... ... @@ -0,0 +1,19 @@
  1 +import {Component, Input, Inject} from "ng-forward";
  2 +import * as plugins from "../../plugins";
  3 +import {dasherize} from "ng-forward/cjs/util/helpers";
  4 +
  5 +export abstract class PluginHotspot {
  6 +
  7 + constructor(protected hotspot: string) { }
  8 +
  9 + ngOnInit() {
  10 + for (let component of plugins.hotspots) {
  11 + if (component.hotspot === this.hotspot) {
  12 + let directiveName = dasherize(component.name.replace('Component', ''));
  13 + this.addHotspot(directiveName);
  14 + }
  15 + }
  16 + }
  17 +
  18 + abstract addHotspot(directiveName: string): any;
  19 +}
... ...
src/app/index.ts
... ... @@ -15,9 +15,10 @@ declare var moment: any;
15 15 let noosferoApp: any = bundle("noosferoApp", MainComponent, ["ngAnimate", "ngCookies", "ngStorage", "ngTouch",
16 16 "ngSanitize", "ngMessages", "ngAria", "restangular",
17 17 "ui.router", "ui.bootstrap", "toastr", "ngCkeditor",
18   - "angularMoment", "angular.filter", "akoenig.deckgrid",
  18 + "angular-bind-html-compile","angularMoment", "angular.filter", "akoenig.deckgrid",
19 19 "angular-timeline", "duScroll", "oitozero.ngSweetAlert",
20   - "pascalprecht.translate", "tmh.dynamicLocale", "angularLoad"]).publish();
  20 + "pascalprecht.translate", "tmh.dynamicLocale", "angularLoad",
  21 + "angular-click-outside"]).publish();
21 22  
22 23 NoosferoApp.angularModule = noosferoApp;
23 24  
... ...
src/app/main/main.component.ts
  1 +import * as plugins from "../../plugins";
1 2 import {bundle, Component, StateConfig, Inject} from "ng-forward";
2 3 import {ArticleBlogComponent} from "./../article/types/blog/blog.component";
3 4  
... ... @@ -93,7 +94,7 @@ export class EnvironmentContent {
93 94 LinkListBlockComponent, CommunitiesBlockComponent, HtmlEditorComponent,
94 95 MainBlockComponent, RecentDocumentsBlockComponent, Navbar, SidebarComponent, ProfileImageBlockComponent,
95 96 MembersBlockComponent, NoosferoTemplate, DateFormat, RawHTMLBlockComponent
96   - ],
  97 + ].concat(plugins.mainComponents).concat(plugins.hotspots),
97 98 providers: [AuthService, SessionService, NotificationService, BodyStateClassesService]
98 99 })
99 100 @StateConfig([
... ...
src/lib/ng-noosfero-api/http/restangular_service.spec.ts
... ... @@ -207,4 +207,27 @@ describe(&quot;Restangular Service - base Class&quot;, () =&gt; {
207 207 $httpBackend.flush();
208 208 });
209 209  
210   -});
211 210 \ No newline at end of file
  211 + it("post('customPath', rootObject) calls POST /rootObjects/1/customPath", (done) => {
  212 + let rootObj: RootObjectModel = rootObjectRestService.getElement(1);
  213 + $httpBackend.expectPOST("/api/v1/rootObjects/1/customPath").respond(201, { object: { attr: 1, rootId: 1 } });
  214 + objectRestService.post('customPath', rootObj).then((result: noosfero.RestResult<ObjectModel>) => {
  215 + expect(result.data).toBeDefined();
  216 + expect((<any>result.data).attr).toEqual(1);
  217 + expect((<any>result.data).rootId).toEqual(1);
  218 + done();
  219 + });
  220 + $httpBackend.flush();
  221 + });
  222 +
  223 + it("post('customPath') calls POST /objects/customPath", (done) => {
  224 + $httpBackend.expectPOST("/api/v1/objects/customPath").respond(201, { object: { attr: 1, rootId: 1 } });
  225 + objectRestService.post('customPath').then((result: noosfero.RestResult<ObjectModel>) => {
  226 + expect(result.data).toBeDefined();
  227 + expect((<any>result.data).attr).toEqual(1);
  228 + expect((<any>result.data).rootId).toEqual(1);
  229 + done();
  230 + });
  231 + $httpBackend.flush();
  232 + });
  233 +
  234 +});
... ...
src/lib/ng-noosfero-api/http/restangular_service.ts
... ... @@ -261,6 +261,22 @@ export abstract class RestangularService&lt;T extends noosfero.RestModel&gt; {
261 261 return deferred.promise;
262 262 }
263 263  
  264 +
  265 + public post(path: string, rootElement?: restangular.IElement, data?: any, headers?: any): ng.IPromise<noosfero.RestResult<T>> {
  266 + let deferred = this.$q.defer<noosfero.RestResult<T>>();
  267 + let restRequest: ng.IPromise<any>;
  268 +
  269 + if (rootElement) {
  270 + restRequest = rootElement.customPOST(data, path, headers);
  271 + } else {
  272 + restRequest = this.baseResource.customPOST(data, path, headers);
  273 + }
  274 + restRequest
  275 + .then(this.getHandleSuccessFunction(deferred))
  276 + .catch(this.getHandleErrorFunction(deferred));
  277 + return deferred.promise;
  278 + }
  279 +
264 280 /**
265 281 * Returns a Restangular IElement representing the
266 282 */
... ...
src/lib/ng-noosfero-api/interfaces/article.ts
1 1  
2 2 namespace noosfero {
3   - export interface Article extends RestModel {
  3 + export interface Article extends RestModel {
4 4 path: string;
5 5 profile: Profile;
6 6 type: string;
7   - parent: Article;
  7 + parent: Article;
8 8 body: string;
9 9 title: string;
10 10 name: string;
11 11 published: boolean;
  12 + setting: any;
12 13 }
13 14 -}
  15 +}
14 16 \ No newline at end of file
... ...
src/plugins/comment_paragraph/allow-comment/allow-comment.component.spec.ts 0 → 100644
... ... @@ -0,0 +1,78 @@
  1 +import {AllowCommentComponent} from "./allow-comment.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-plugin-allow-comment [content]="ctrl.content" [paragraph-uuid]="ctrl.paragraphUuid" [article]="ctrl.article"></comment-paragraph-plugin-allow-comment>';
  8 +
  9 +describe("Components", () => {
  10 + describe("Allow Comment Component", () => {
  11 +
  12 + let serviceMock = {
  13 + commentParagraphCount: () => {
  14 + return Promise.resolve(5);
  15 + }
  16 + };
  17 + let functionToggleCommentParagraph: Function;
  18 + let eventServiceMock = {
  19 + // toggleCommentParagraph
  20 + subscribeToggleCommentParagraph: (fn: Function) => {
  21 + functionToggleCommentParagraph = fn;
  22 + }
  23 + };
  24 +
  25 + let providers = [
  26 + new Provider('CommentParagraphService', { useValue: serviceMock }),
  27 + new Provider('CommentParagraphEventService', { useValue: eventServiceMock })
  28 + ];
  29 + let helper: ComponentTestHelper;
  30 +
  31 + beforeEach(angular.mock.module("templates"));
  32 +
  33 + beforeEach((done) => {
  34 + let cls = createClass({
  35 + template: htmlTemplate,
  36 + directives: [AllowCommentComponent],
  37 + providers: providers,
  38 + properties: {
  39 + content: "",
  40 + paragraphUuid: "uuid",
  41 + article: {
  42 + setting: {
  43 + comment_paragraph_plugin_activate: true
  44 + }
  45 + }
  46 + }
  47 + });
  48 + helper = new ComponentTestHelper(cls, done);
  49 + });
  50 +
  51 + it('update comments count', () => {
  52 + expect(helper.component.commentsCount).toEqual(5);
  53 + });
  54 +
  55 + it('display paragraph content', () => {
  56 + expect(helper.all(".paragraph .paragraph-content").length).toEqual(1);
  57 + });
  58 +
  59 + it('display button to side comments', () => {
  60 + expect(helper.all(".paragraph .actions a").length).toEqual(1);
  61 + });
  62 +
  63 + it('set display to true when click in show paragraph', () => {
  64 + helper.component.showParagraphComments();
  65 + expect(helper.component.display).toBeTruthy();
  66 + });
  67 +
  68 + it('set display to false when click in hide paragraph', () => {
  69 + helper.component.hideParagraphComments();
  70 + expect(helper.component.display).toBeFalsy();
  71 + });
  72 +
  73 + it('update article when receive a toogle paragraph event', () => {
  74 + functionToggleCommentParagraph({ id: 2 });
  75 + expect(helper.component.article.id).toEqual(2);
  76 + });
  77 + });
  78 +});
... ...
src/plugins/comment_paragraph/allow-comment/allow-comment.component.ts 0 → 100644
... ... @@ -0,0 +1,45 @@
  1 +import {Component, Input, Inject} from "ng-forward";
  2 +import {SideCommentsComponent} from "../side-comments/side-comments.component";
  3 +import {CommentParagraphEventService} from "../events/comment-paragraph-event.service";
  4 +import {CommentParagraphService} from "../http/comment-paragraph.service";
  5 +
  6 +@Component({
  7 + selector: "comment-paragraph-plugin-allow-comment",
  8 + templateUrl: "plugins/comment_paragraph/allow-comment/allow-comment.html",
  9 + directives: [SideCommentsComponent]
  10 +})
  11 +@Inject("$scope", CommentParagraphEventService, CommentParagraphService)
  12 +export class AllowCommentComponent {
  13 +
  14 + @Input() content: string;
  15 + @Input() paragraphUuid: string;
  16 + @Input() article: noosfero.Article;
  17 + commentsCount: number;
  18 + display = false;
  19 +
  20 + constructor(private $scope: ng.IScope,
  21 + private commentParagraphEventService: CommentParagraphEventService,
  22 + private commentParagraphService: CommentParagraphService) { }
  23 +
  24 + ngOnInit() {
  25 + this.commentParagraphEventService.subscribeToggleCommentParagraph((article: noosfero.Article) => {
  26 + this.article = article;
  27 + this.$scope.$apply();
  28 + });
  29 + this.commentParagraphService.commentParagraphCount(this.article, this.paragraphUuid).then((count: number) => {
  30 + this.commentsCount = count;
  31 + });
  32 + }
  33 +
  34 + isActivated() {
  35 + return this.article && this.article.setting && this.article.setting.comment_paragraph_plugin_activate;
  36 + }
  37 +
  38 + showParagraphComments() {
  39 + this.display = true;
  40 + }
  41 +
  42 + hideParagraphComments() {
  43 + this.display = false;
  44 + }
  45 +}
... ...
src/plugins/comment_paragraph/allow-comment/allow-comment.html 0 → 100644
... ... @@ -0,0 +1,12 @@
  1 +<div class="paragraph" ng-class="{'active' : ctrl.display}">
  2 + <div class="paragraph-content" ng-bind-html="ctrl.content" ng-class="{'active' : ctrl.display}"></div>
  3 + <div ng-if="ctrl.isActivated()" class="actions">
  4 + <a href="#" popover-placement="right-top" popover-trigger="none"
  5 + uib-popover-template="'plugins/comment_paragraph/allow-comment/popover.html'"
  6 + (click)="ctrl.showParagraphComments()" popover-is-open="ctrl.display">
  7 + <div class="arrow_box" ng-class="{'active' : ctrl.display}">
  8 + <span class="count">{{ctrl.commentsCount > 0 ? ctrl.commentsCount : '+'}}</span>
  9 + </div>
  10 + </a>
  11 + </div>
  12 +</div>
... ...
src/plugins/comment_paragraph/allow-comment/allow-comment.scss 0 → 100644
... ... @@ -0,0 +1,62 @@
  1 +$balloon-selected-color: #50BF68;
  2 +$balloon-color: #c4c4c4;
  3 +
  4 +comment-paragraph-plugin-allow-comment {
  5 + .paragraph {
  6 + width: 100%;
  7 + &.active {
  8 + width: 80%;
  9 + }
  10 + .popover {
  11 + &.right > .arrow {
  12 + }
  13 + }
  14 + .paragraph-content {
  15 + width: 95%;
  16 + display: inline-block;
  17 + }
  18 + .actions {
  19 + width: 3%;
  20 + display: inline-block;
  21 + vertical-align: top;
  22 + margin-left: 10px;
  23 + .popover {
  24 + width: 100%;
  25 + max-width: 330px;
  26 + }
  27 + .count {
  28 + font-size: 14px;
  29 + font-weight: bold;
  30 + color: white;
  31 + text-align: center;
  32 + width: 100%;
  33 + display: inline-block;
  34 + }
  35 + .arrow_box {
  36 + position: relative;
  37 + background: $balloon-color;
  38 + margin-top: 5px;
  39 + width: 25px;
  40 + border-radius: 2px;
  41 + &:after {
  42 + top: 100%;
  43 + left: 50%;
  44 + border: solid transparent;
  45 + content: " ";
  46 + position: absolute;
  47 + pointer-events: none;
  48 + border-color: rgba(196, 196, 196, 0);
  49 + border-top-color: $balloon-color;
  50 + border-width: 6px;
  51 + margin-left: -6px;
  52 + }
  53 + &:hover, &.active {
  54 + background: $balloon-selected-color;
  55 + &:after {
  56 + border-top-color: $balloon-selected-color;
  57 + }
  58 + }
  59 + }
  60 + }
  61 + }
  62 +}
... ...
src/plugins/comment_paragraph/allow-comment/popover.html 0 → 100644
... ... @@ -0,0 +1 @@
  1 +<comment-paragraph-side-comments id="side-comments-{{ctrl.paragraphUuid}}" click-outside="ctrl.hideParagraphComments()" [article]="ctrl.article" [paragraph-uuid]="ctrl.paragraphUuid"></comment-paragraph-side-comments>
... ...
src/plugins/comment_paragraph/events/comment-paragraph-event.service.spec.ts 0 → 100644
... ... @@ -0,0 +1,28 @@
  1 +import {CommentParagraphEventService} from "./comment-paragraph-event.service";
  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 +describe("Services", () => {
  8 + describe("Comment Paragraph Event Service", () => {
  9 + let eventService: CommentParagraphEventService;
  10 +
  11 + beforeEach(() => {
  12 + eventService = new CommentParagraphEventService();
  13 + eventService['toggleCommentParagraphEmitter'] = jasmine.createSpyObj("toggleCommentParagraphEmitter", ["next", "subscribe"]);
  14 + });
  15 +
  16 + it('subscribe to toggle comment paragraph event', () => {
  17 + eventService['toggleCommentParagraphEmitter'].subscribe = jasmine.createSpy("subscribe");
  18 + eventService.subscribeToggleCommentParagraph(() => { });
  19 + expect(eventService['toggleCommentParagraphEmitter'].subscribe).toHaveBeenCalled();
  20 + });
  21 +
  22 + it('emit event when toggle comment paragraph', () => {
  23 + eventService['toggleCommentParagraphEmitter'].subscribe = jasmine.createSpy("next");
  24 + eventService.toggleCommentParagraph(<noosfero.Article>{});
  25 + expect(eventService['toggleCommentParagraphEmitter'].next).toHaveBeenCalled();
  26 + });
  27 + });
  28 +});
... ...
src/plugins/comment_paragraph/events/comment-paragraph-event.service.ts 0 → 100644
... ... @@ -0,0 +1,19 @@
  1 +import {Injectable, EventEmitter} from "ng-forward";
  2 +
  3 +@Injectable()
  4 +export class CommentParagraphEventService {
  5 +
  6 + private toggleCommentParagraphEmitter: EventEmitter<noosfero.Article>;
  7 +
  8 + constructor() {
  9 + this.toggleCommentParagraphEmitter = new EventEmitter();
  10 + }
  11 +
  12 + toggleCommentParagraph(article: noosfero.Article) {
  13 + this.toggleCommentParagraphEmitter.next(article);
  14 + }
  15 +
  16 + subscribeToggleCommentParagraph(fn: (article: noosfero.Article) => void) {
  17 + this.toggleCommentParagraphEmitter.subscribe(fn);
  18 + }
  19 +}
... ...
src/plugins/comment_paragraph/hotspot/comment-paragraph-article-button.component.spec.ts 0 → 100644
... ... @@ -0,0 +1,85 @@
  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;
  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(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 = { 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 = {};
  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 = { 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 0 → 100644
... ... @@ -0,0 +1,38 @@
  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 0 → 100644
... ... @@ -0,0 +1,8 @@
  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/hotspot/comment-paragraph-form.component.ts 0 → 100644
... ... @@ -0,0 +1,26 @@
  1 +import { Inject, Input, Component } from "ng-forward";
  2 +import {Hotspot} from "../../../app/hotspot/hotspot.decorator";
  3 +
  4 +@Component({
  5 + selector: "comment-paragraph-form-hotspot",
  6 + template: "<span></span>",
  7 +})
  8 +@Hotspot("comment_form_extra_contents")
  9 +@Inject("$scope")
  10 +export class CommentParagraphFormHotspotComponent {
  11 +
  12 + @Input() comment: noosfero.Comment;
  13 + @Input() parent: noosfero.Comment;
  14 +
  15 + constructor(private $scope: ng.IScope) { }
  16 +
  17 + ngOnInit() {
  18 + this.$scope.$watch(() => {
  19 + return this.parent;
  20 + }, () => {
  21 + if (this.parent && (<any>this.parent).paragraph_uuid) {
  22 + (<any>this.comment).paragraph_uuid = (<any>this.parent).paragraph_uuid;
  23 + }
  24 + });
  25 + }
  26 +}
... ...
src/plugins/comment_paragraph/hotspot/comment-paragraph-from.component.spec.ts 0 → 100644
... ... @@ -0,0 +1,33 @@
  1 +import {CommentParagraphFormHotspotComponent} from "./comment-paragraph-form.component";
  2 +import {ComponentTestHelper, createClass} from '../../../spec/component-test-helper';
  3 +import * as helpers from "../../../spec/helpers";
  4 +import {ComponentFixture} from 'ng-forward/cjs/testing/test-component-builder';
  5 +
  6 +let htmlTemplate = '<comment-paragraph-form-hotspot [comment]="ctrl.comment" [parent]="ctrl.parent"></comment-paragraph-form-hotspot>';
  7 +
  8 +describe("Components", () => {
  9 + describe("Comment Paragraph Form Hotspot Component", () => {
  10 +
  11 + let helper: ComponentTestHelper;
  12 +
  13 + beforeEach(angular.mock.module("templates"));
  14 +
  15 + beforeEach((done) => {
  16 + let cls = createClass({
  17 + template: htmlTemplate,
  18 + directives: [CommentParagraphFormHotspotComponent],
  19 + providers: [],
  20 + properties: {
  21 + comment: {}
  22 + }
  23 + });
  24 + helper = new ComponentTestHelper(cls, done);
  25 + });
  26 +
  27 + it('set paragraph uuid when parent has it setted', () => {
  28 + helper.component.parent = { paragraph_uuid: 'uuid' };
  29 + helper.detectChanges();
  30 + expect((<any>helper.component.comment).paragraph_uuid).toEqual('uuid');
  31 + });
  32 + });
  33 +});
... ...
src/plugins/comment_paragraph/http/comment-paragraph.service.spec.ts 0 → 100644
... ... @@ -0,0 +1,90 @@
  1 +import {CommentParagraphService} from "./comment-paragraph.service";
  2 +
  3 +
  4 +describe("Services", () => {
  5 +
  6 + describe("Comment Paragraph Service", () => {
  7 +
  8 + let $httpBackend: ng.IHttpBackendService;
  9 + let commentParagraphService: CommentParagraphService;
  10 +
  11 + beforeEach(angular.mock.module("noosferoApp", ($translateProvider: angular.translate.ITranslateProvider) => {
  12 + $translateProvider.translations('en', {});
  13 + }));
  14 +
  15 + beforeEach(inject((_$httpBackend_: ng.IHttpBackendService, _CommentParagraphService_: CommentParagraphService) => {
  16 + $httpBackend = _$httpBackend_;
  17 + commentParagraphService = _CommentParagraphService_;
  18 + }));
  19 +
  20 +
  21 + describe("Succesfull requests", () => {
  22 +
  23 + it("should return paragraph comments by article", (done) => {
  24 + let articleId = 1;
  25 + $httpBackend.expectGET(`/api/v1/articles/${articleId}/comment_paragraph_plugin/comments?without_reply=true`).respond(200, { comments: [{ body: "comment1" }] });
  26 + commentParagraphService.getByArticle(<noosfero.Article>{ id: articleId }).then((result: noosfero.RestResult<noosfero.Comment[]>) => {
  27 + expect(result.data).toEqual([{ body: "comment1" }]);
  28 + done();
  29 + });
  30 + $httpBackend.flush();
  31 + });
  32 +
  33 + it("should create a paragraph comment", (done) => {
  34 + let articleId = 1;
  35 + $httpBackend.expectPOST(`/api/v1/articles/${articleId}/comment_paragraph_plugin/comments`).respond(200, { comment: { body: "comment1" } });
  36 + commentParagraphService.createInArticle(<noosfero.Article>{ id: articleId }, <noosfero.Comment>{ body: "comment1" }).then((result: noosfero.RestResult<noosfero.Comment>) => {
  37 + expect(result.data).toEqual({ body: "comment1" });
  38 + done();
  39 + });
  40 + $httpBackend.flush();
  41 + });
  42 +
  43 + it("activate paragraph comments for an article", (done) => {
  44 + let articleId = 1;
  45 + $httpBackend.expectPOST(`/api/v1/articles/${articleId}/comment_paragraph_plugin/activate`).respond(200, { article: { title: "article1" } });
  46 + commentParagraphService.activateCommentParagraph(<noosfero.Article>{ id: articleId }).then((result: noosfero.RestResult<noosfero.Article>) => {
  47 + expect(result.data).toEqual({ title: "article1" });
  48 + done();
  49 + });
  50 + $httpBackend.flush();
  51 + });
  52 +
  53 + it("deactivate paragraph comments for an article", (done) => {
  54 + let articleId = 1;
  55 + $httpBackend.expectPOST(`/api/v1/articles/${articleId}/comment_paragraph_plugin/deactivate`).respond(200, { article: { title: "article1" } });
  56 + commentParagraphService.deactivateCommentParagraph(<noosfero.Article>{ id: articleId }).then((result: noosfero.RestResult<noosfero.Article>) => {
  57 + expect(result.data).toEqual({ title: "article1" });
  58 + done();
  59 + });
  60 + $httpBackend.flush();
  61 + });
  62 +
  63 + it("return counts for paragraph comments", (done) => {
  64 + let articleId = 1;
  65 + $httpBackend.expectGET(`/api/v1/articles/${articleId}/comment_paragraph_plugin/comments/count`).respond(200, { '1': 5, '2': 6 });
  66 + commentParagraphService.commentParagraphCount(<noosfero.Article>{ id: articleId }, '1').then((count: number) => {
  67 + expect(count).toEqual(5);
  68 + done();
  69 + });
  70 + $httpBackend.flush();
  71 + });
  72 +
  73 + it("reset promise when comment paragraph counts fails", (done) => {
  74 + let articleId = 1;
  75 + commentParagraphService['articleService'] = jasmine.createSpyObj("articleService", ["getElement"]);
  76 + commentParagraphService['articleService'].getElement = jasmine.createSpy("getElement").and.returnValue(
  77 + {
  78 + customGET: (path: string) => {
  79 + return Promise.reject({});
  80 + }
  81 + }
  82 + );
  83 + commentParagraphService.commentParagraphCount(<noosfero.Article>{ id: articleId }, '1').catch(() => {
  84 + expect(commentParagraphService['commentParagraphCountsPromise']).toBeNull();
  85 + done();
  86 + });
  87 + });
  88 + });
  89 + });
  90 +});
... ...
src/plugins/comment_paragraph/http/comment-paragraph.service.ts 0 → 100644
... ... @@ -0,0 +1,64 @@
  1 +import { Injectable, Inject } from "ng-forward";
  2 +import {RestangularService} from "../../../lib/ng-noosfero-api/http/restangular_service";
  3 +import {ArticleService} from "../../../lib/ng-noosfero-api/http/article.service";
  4 +
  5 +@Injectable()
  6 +@Inject("Restangular", "$q", "$log", ArticleService)
  7 +export class CommentParagraphService extends RestangularService<noosfero.Comment> {
  8 +
  9 + private commentParagraphCountsPromise: ng.IPromise<any>;
  10 +
  11 + constructor(Restangular: restangular.IService, $q: ng.IQService, $log: ng.ILogService, protected articleService: ArticleService) {
  12 + super(Restangular, $q, $log);
  13 + }
  14 +
  15 + getResourcePath() {
  16 + return "comment_paragraph_plugin/comments";
  17 + }
  18 +
  19 + getDataKeys() {
  20 + return {
  21 + singular: 'comment',
  22 + plural: 'comments'
  23 + };
  24 + }
  25 +
  26 + getByArticle(article: noosfero.Article, params: any = {}): ng.IPromise<noosfero.RestResult<noosfero.Comment[]>> {
  27 + params['without_reply'] = true;
  28 + let articleElement = this.articleService.getElement(<number>article.id);
  29 + return this.list(articleElement, params);
  30 + }
  31 +
  32 + createInArticle(article: noosfero.Article, comment: noosfero.Comment): ng.IPromise<noosfero.RestResult<noosfero.Comment>> {
  33 + let articleElement = this.articleService.getElement(<number>article.id);
  34 + return this.create(comment, articleElement, null, { 'Content-Type': 'application/json' }, false);
  35 + }
  36 +
  37 + activateCommentParagraph(article: noosfero.Article) {
  38 + let articleElement = this.articleService.getElement(<number>article.id);
  39 + return this.articleService.post("comment_paragraph_plugin/activate", articleElement);
  40 + }
  41 +
  42 + deactivateCommentParagraph(article: noosfero.Article) {
  43 + let articleElement = this.articleService.getElement(<number>article.id);
  44 + return this.articleService.post("comment_paragraph_plugin/deactivate", articleElement);
  45 + }
  46 +
  47 + commentParagraphCount(article: noosfero.Article, paragraphUuid: string) {
  48 + return this.commentParagraphCounts(article).then((counts: any) => {
  49 + return counts[paragraphUuid];
  50 + });
  51 + }
  52 +
  53 + private commentParagraphCounts(article: noosfero.Article) {
  54 + if (!this.commentParagraphCountsPromise) {
  55 + let articleElement = this.articleService.getElement(<number>article.id);
  56 + this.commentParagraphCountsPromise = articleElement.customGET("comment_paragraph_plugin/comments/count").then((response: restangular.IResponse) => {
  57 + return response.data;
  58 + }).catch(() => {
  59 + this.commentParagraphCountsPromise = null;
  60 + });
  61 + }
  62 + return this.commentParagraphCountsPromise;
  63 + }
  64 +}
... ...
src/plugins/comment_paragraph/index.ts 0 → 100644
... ... @@ -0,0 +1,6 @@
  1 +import {AllowCommentComponent} from "./allow-comment/allow-comment.component";
  2 +import {CommentParagraphArticleButtonHotspotComponent} from "./hotspot/comment-paragraph-article-button.component";
  3 +import {CommentParagraphFormHotspotComponent} from "./hotspot/comment-paragraph-form.component";
  4 +
  5 +export let mainComponents: any = [AllowCommentComponent];
  6 +export let hotspots: any = [CommentParagraphArticleButtonHotspotComponent, CommentParagraphFormHotspotComponent];
... ...
src/plugins/comment_paragraph/languages/en.json 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +{
  2 + "comment-paragraph-plugin.title": "Paragraph Comments"
  3 +}
... ...
src/plugins/comment_paragraph/languages/pt.json 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +{
  2 + "comment-paragraph-plugin.title": "Comentários por Parágrafo"
  3 +}
... ...
src/plugins/comment_paragraph/side-comments/side-comments.component.spec.ts 0 → 100644
... ... @@ -0,0 +1,50 @@
  1 +import {SideCommentsComponent} from "./side-comments.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-side-comments [article]="ctrl.article" [paragraph-uuid]="ctrl.paragraphUuid"></comment-paragraph-side-comments>';
  8 +
  9 +describe("Components", () => {
  10 + describe("Side Comments Component", () => {
  11 +
  12 + let serviceMock = jasmine.createSpyObj("CommentParagraphService", ["getByArticle"]);
  13 + serviceMock.getByArticle = jasmine.createSpy("getByArticle").and.returnValue(Promise.resolve({ data: {} }));
  14 +
  15 + let commentServiceMock = {};
  16 + let postCommentEventService = jasmine.createSpyObj("postCommentEventService", ["emit", "subscribe"]);
  17 + postCommentEventService.subscribe = jasmine.createSpy("subscribe");
  18 +
  19 + let providers = [
  20 + new Provider('CommentParagraphService', { useValue: serviceMock }),
  21 + new Provider('CommentService', { useValue: commentServiceMock }),
  22 + new Provider('PostCommentEventService', { useValue: postCommentEventService })
  23 + ];
  24 + let helper: ComponentTestHelper;
  25 +
  26 + beforeEach(angular.mock.module("templates"));
  27 +
  28 + beforeEach((done) => {
  29 + let cls = createClass({
  30 + template: htmlTemplate,
  31 + directives: [SideCommentsComponent],
  32 + providers: providers,
  33 + properties: {
  34 + paragraphUuid: "uuid",
  35 + article: {}
  36 + }
  37 + });
  38 + helper = new ComponentTestHelper(cls, done);
  39 + });
  40 +
  41 + it('call service to load paragraph comments', () => {
  42 + helper.component.loadComments();
  43 + expect(serviceMock.getByArticle).toHaveBeenCalled();
  44 + });
  45 +
  46 + it('set paragraph uuid in new comment object', () => {
  47 + expect(helper.component.newComment['paragraph_uuid']).toEqual('uuid');
  48 + });
  49 + });
  50 +});
... ...
src/plugins/comment_paragraph/side-comments/side-comments.component.ts 0 → 100644
... ... @@ -0,0 +1,32 @@
  1 +import {Component, Inject, Input, Output} from "ng-forward";
  2 +import {CommentsComponent} from "../../../app/article/comment/comments.component";
  3 +import {CommentService} from "../../../lib/ng-noosfero-api/http/comment.service";
  4 +import {CommentParagraphService} from "../http/comment-paragraph.service";
  5 +import { PostCommentEventService } from "../../../app/article/comment/post-comment/post-comment-event.service";
  6 +
  7 +@Component({
  8 + selector: "comment-paragraph-side-comments",
  9 + templateUrl: 'app/article/comment/comments.html',
  10 +})
  11 +@Inject(CommentService, PostCommentEventService, "$scope", CommentParagraphService)
  12 +export class SideCommentsComponent extends CommentsComponent {
  13 +
  14 + @Input() article: noosfero.Article;
  15 + @Input() paragraphUuid: string;
  16 +
  17 + constructor(commentService: CommentService,
  18 + postCommentEventService: PostCommentEventService,
  19 + $scope: ng.IScope,
  20 + private commentParagraphService: CommentParagraphService) {
  21 + super(commentService, postCommentEventService, $scope);
  22 + }
  23 +
  24 + ngOnInit() {
  25 + super.ngOnInit();
  26 + (<any>this.newComment).paragraph_uuid = this.paragraphUuid;
  27 + }
  28 +
  29 + loadComments() {
  30 + return this.commentParagraphService.getByArticle(this.article, { page: this.page, per_page: this.perPage, paragraph_uuid: this.paragraphUuid });
  31 + }
  32 +}
... ...
src/plugins/comment_paragraph/side-comments/side-comments.scss 0 → 100644
... ... @@ -0,0 +1,13 @@
  1 +comment-paragraph-side-comments {
  2 + .comments {
  3 + .comment {
  4 + margin: 0;
  5 + &.media {
  6 + border-top: 0;
  7 + }
  8 + .comments {
  9 + margin-left: 0;
  10 + }
  11 + }
  12 + }
  13 +}
... ...
src/plugins/index.ts 0 → 100644
... ... @@ -0,0 +1,7 @@
  1 +import * as commentParagraph from "./comment_paragraph";
  2 +
  3 +export let mainComponents: any = [];
  4 +mainComponents = mainComponents.concat(commentParagraph.mainComponents);
  5 +
  6 +export let hotspots: any = [];
  7 +hotspots = hotspots.concat(commentParagraph.hotspots);
... ...
src/spec/component-test-helper.ts
... ... @@ -68,6 +68,8 @@ export class ComponentTestHelper&lt;T extends any&gt; {
68 68 this.init(done);
69 69 }
70 70  
  71 + fixture: any;
  72 +
71 73 /**
72 74 * @ngdoc method
73 75 * @name init
... ... @@ -79,7 +81,8 @@ export class ComponentTestHelper&lt;T extends any&gt; {
79 81 let promisse = this.tcb.createAsync(this.mockComponent) as Promise<ComponentFixture>;
80 82 return promisse.then((fixture: any) => {
81 83 // Fire all angular events and parsing
82   - fixture.detectChanges();
  84 + this.fixture = fixture;
  85 + this.detectChanges();
83 86 // The main debug element
84 87 this.debugElement = fixture.debugElement;
85 88 this.component = <T>this.debugElement.componentViewChildren[0].componentInstance;
... ... @@ -96,6 +99,17 @@ export class ComponentTestHelper&lt;T extends any&gt; {
96 99  
97 100 /**
98 101 * @ngdoc method
  102 + * @name detectChanges
  103 + * @methodOf spec.ComponentTestHelper
  104 + * @description
  105 + * Detect changes in component
  106 + */
  107 + detectChanges() {
  108 + this.fixture.detectChanges();
  109 + }
  110 +
  111 + /**
  112 + * @ngdoc method
99 113 * @name all
100 114 * @methodOf spec.ComponentTestHelper
101 115 * @description
... ...