Merge Request #7

Merged
noosfero-themes/angular-theme!7
Created by Victor Costa

Frontend for comment paragraph plugin

Also:

  1. Add macro directive for articles
  2. Load components defined in plugins
  3. Add plugins in gulp build
  4. Create infra for plugins hotspots
  5. Merge plugin languages when building with gulp
  6. Improve visualization of comment's replies
Assignee: None
Milestone: 2016.05

Merged by Michel Felipe

Source branch has been removed
Commits (16)
3 participants
Showing 52 changed files   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
... ...
  • 4a20548511a65cfccc863520b70c3ee9?s=40&d=identicon
    Victor Costa @vfcosta

    Added 1 new commit:

    • 5dcbc016 - Limit comments identation and display replied comment in tooltip
    Choose File ...   File name...
    Cancel
  • 5bf9bf341e9d00ebd854cdaf1a4299b2?s=40&d=identicon
    Leandro Santos @leandronunes

    Milestone changed to 2016.05

    Choose File ...   File name...
    Cancel
  • Me
    Michel Felipe @mfdeveloper

    Reassigned to @mfdeveloper

    Choose File ...   File name...
    Cancel
  • Me
    Michel Felipe @mfdeveloper (Edited )

    Farei o merge pela urgência, mas dê uma olhada nos comentários @vfcosta. Em linhas gerais, o ideal é documentar as classes de plugin e de decorators de hostspot usando o @ngdoc. Além disso, não estou completamente certo se a abordagem de ter criado @Hotspot como um decorator e uma classe Plugins e seus plugins front-end herdando delas é realmente o melhor a ser feito. Tlvz padronizar (ambos serem classes pai, ou ambos serem decorators). È interessante uma reflexão acerca disso, uma vez q isso será genérico e "padrão" para tdos os plugins q venhamos a desenvolver no decorrer do tempo. Me deu a impressão de estarmos apenas tentando "adaptar" a abordagem de Plugins criada pelo noosfero com pouca mudança, sendo que, não sei se realmente é o melhor a fazer :(

    Além disso, achei os nomes dos arquivos muito grandes. A estrutura de pastas dentro desse plugin criado segue uma organização um pouco diferente dos componentes do core, o que pode ocasionar em confusão. Acho de grande importãncia documentar essa parte e o um exemplo de uso disto.

    Choose File ...   File name...
    Cancel
  • Me
    Michel Felipe @mfdeveloper

    Added 49 new commits:

    • 2abc9dd1 - Updated typescript version
    • 01d73906 - Upgrade Vagrantfile with shared folder
    • da7503d7 - Update gitignore with vagrant
    • e4637b83 - Merge branch 'devenv-update' into 'master'
    • ea217b65 - Fix bug in build task
    • 00882cc3 - changed to use @Output on post-comment-component to propagate event to the comme…
    • 45e13641 - Merge branch 'comments'
    • fc86b9f5 - Refactory into ComponentTestHelper and helpers.ts with improvements
    • 0e6cfa00 - Merge branch 'component-test-improvements' into 'master'
    • ed34cf65 - Support ckeditor in article edition
    • 28d86107 - Support storage of current object in generic restangular service
    • 2259690d - Create article and associate it to parent
    • a5984494 - Move basic editor to its own folder
    • ecac812c - Add cancel button in basic editor
    • ed2107bd - Translate basic editor component
    • 33ae65dd - Adapt basic editor to edit an existing article
    • 8f0e4db1 - Create component to encapsulate wysiwyg editor
    • 6b15acb7 - Add article edit button
    • ba389ecc - Edit visibility status of an article
    • 00eaab03 - Display basic error message when fail to save an article
    • 5f9ef2ae - Refactor tests of content viewer actions to use ComponentTestHelper
    • 1894afc0 - Merge branch 'create-article' into 'master'
    • 8616dd98 - Add missing type arguments from members and people blocks
    • dcc1120f - Fix styles watch from gulp
    • 8362948a - adding participa consulta theme
    • e59add5d - Created sidebar component with sass and a service to show/hide using Angular 2 EventEmitter sintax
    • 4c39bd75 - Added hamburguer sidebar toggle button to show only after login
    • 41cad538 - Refactory into unit tests to use EventEmitter mocks
    • f711ee9c - Refactory into ComponentTestHelper and sidebar unit tests enhancements
    • dd6a4819 - Added feature to sidebar pushing right content on show/hide
    • af427f22 - Created SidebarSectionsComponent with a dinamic menu/submenu
    • 7e4cecee - Added @ngdoc documentation and refactor the SidebarSection component
    • c0fd7cf6 - Merge branch 'sidebar-menu' into 'master'
    • 2cafe5da - Add macro directive for articles
    • 4b4877ed - Load components defined in plugins
    • 33544a8b - Add plugins in gulp build
    • 31b34a75 - Display paragraph comments besides the article
    • 1f246d3f - Load comments by paragraph
    • 56a44bd2 - Create infra for plugins hotspots
    • 5deddf45 - Set parahraph uuid when create comments in paragraphs
    • abdfbb35 - Activate/deactivate comment paragraph in articles
    • b77b920c - Emit event when toggle comment paragraph in articles
    • 8a1cb95c - Display comment counts beside paragraphs
    • 65c6a4ac - Merge plugin languages when building with gulp
    • 9bf4d461 - Improve layout of comment paragraph plugin
    • 7b6978a6 - Fix html path in karma conf
    • f98ac330 - Add tests for comment paragraph plugin
    • 4840baa9 - Add paragraph uuid in comment form
    • 800e2559 - Limit comments identation and display replied comment in tooltip
    Choose File ...   File name...
    Cancel
  • Me
    Michel Felipe @mfdeveloper

    Assignee removed

    Choose File ...   File name...
    Cancel
  • Me
    Michel Felipe @mfdeveloper
    Choose File ...   File name...
    Cancel