Compare View

switch
from
...
to
 
Commits (203)
Showing 304 changed files   Show diff stats

Too many changes.

To preserve performance only 100 of 304 files displayed.

bower.json
... ... @@ -34,12 +34,21 @@
34 34 "angular-dynamic-locale": "^0.1.30",
35 35 "angular-i18n": "^1.5.0",
36 36 "angular-load": "^0.4.1",
37   - "angular-translate-interpolation-messageformat": "^2.10.0"
  37 + "angular-translate-interpolation-messageformat": "^2.10.0",
  38 + "angular-bind-html-compile": "^1.2.1",
  39 + "angular-click-outside": "^2.7.1",
  40 + "ng-ckeditor": "^0.2.1"
38 41 },
39 42 "devDependencies": {
40 43 "angular-mocks": "~1.5.0"
41 44 },
42 45 "overrides": {
  46 + "ng-ckeditor": {
  47 + "main": [
  48 + "ng-ckeditor.js",
  49 + "libs/ckeditor/ckeditor.js"
  50 + ]
  51 + },
43 52 "bootstrap-sass": {
44 53 "main": [
45 54 "assets/stylesheets/_bootstrap.scss",
... ...
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,32 @@ 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: 'noosfero.templates.' + partialPath,
  34 + standalone: true,
  35 + root: partialPath
  36 + }))
  37 + .pipe(gulp.dest(conf.paths.tmp + '/partials/')));
22 38 });
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/'));
  39 + return merged;
34 40 });
35 41  
36 42 gulp.task('html', ['inject', 'partials'], function () {
37   - var partialsInjectFile = gulp.src(path.join(conf.paths.tmp, '/partials/templateCacheHtml.js'), { read: false });
  43 + var partialsInjectFile = gulp.src([
  44 + path.join(conf.paths.tmp, '/partials/templateCacheHtml-app.js'),
  45 + path.join(conf.paths.tmp, '/partials/templateCacheHtml-plugins.js')], { read: false });
38 46 var partialsInjectOptions = {
39 47 starttag: '<!-- inject:partials -->',
40 48 ignorePath: path.join(conf.paths.tmp, '/partials'),
... ... @@ -73,6 +81,7 @@ gulp.task(&#39;html&#39;, [&#39;inject&#39;, &#39;partials&#39;], function () {
73 81 .pipe($.useref())
74 82 .pipe($.revReplace({prefix: noosferoThemePrefix}))
75 83 .pipe(htmlFilter)
  84 + .pipe($.replace('/bower_components/ng-ckeditor/libs/ckeditor/', noosferoThemePrefix + 'ng-ckeditor/libs/ckeditor/'))
76 85 .pipe($.minifyHtml({
77 86 empty: true,
78 87 spare: true,
... ... @@ -93,6 +102,11 @@ gulp.task(&#39;fonts&#39;, function () {
93 102 .pipe(gulp.dest(path.join(conf.paths.dist, '/fonts/')));
94 103 });
95 104  
  105 +gulp.task('ckeditor', function () {
  106 + conf.wiredep.exclude.push(/bower_components\/ng-ckeditor\/libs\/ckeditor/); // exclude ckeditor from build to improve performance
  107 + return gulp.src(['bower_components/ng-ckeditor/**/*']).pipe(gulp.dest(path.join(conf.paths.dist, '/ng-ckeditor')));
  108 +});
  109 +
96 110 gulp.task('locale', function () {
97 111 return gulp.src([
98 112 path.join("bower_components/angular-i18n", '*.js'),
... ... @@ -124,6 +138,10 @@ gulp.task(&#39;clean-docs&#39;, [], function() {
124 138 return $.del([path.join(conf.paths.docs, '/')]);
125 139 });
126 140  
  141 +gulp.task('plugin-languages', ['locale'], function() {
  142 + return languages.pluginLanguages(conf.paths.dist);
  143 +});
  144 +
127 145 gulp.task('noosfero', ['html'], function () {
128 146 var layouts = gulp.src('layouts/**/*')
129 147 .pipe(gulp.dest(path.join(conf.paths.dist, "layouts")));
... ... @@ -136,4 +154,4 @@ gulp.task(&#39;noosfero&#39;, [&#39;html&#39;], function () {
136 154 return merge(layouts, theme, index);
137 155 });
138 156  
139   -gulp.task('build', ['html', 'fonts', 'other', 'locale', 'noosfero']);
  157 +gulp.task('build', ['ckeditor', '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,10 +15,13 @@ 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   - gulp.watch([
18   - path.join(conf.paths.src, '/app/**/*.css'),
19   - path.join(conf.paths.src, '/app/**/*.scss')
20   - ], function(event) {
  18 + var stylePaths = [path.join(conf.paths.src, conf.paths.plugins, '/**/*.scss')];
  19 + conf.paths.allSources.forEach(function(src) {
  20 + stylePaths.push(path.join(src, '/app/**/*.css'));
  21 + stylePaths.push(path.join(src, '/app/**/*.scss'));
  22 + });
  23 +
  24 + gulp.watch(stylePaths, function(event) {
21 25 if(isOnlyChange(event)) {
22 26 gulp.start('styles-reload');
23 27 } else {
... ... @@ -33,9 +37,14 @@ gulp.task(&#39;watch&#39;, [&#39;inject&#39;], function () {
33 37 }
34 38 });
35 39  
  40 + gulp.watch(path.join(conf.paths.src, '**', conf.paths.languages, '*.json'), function(event) {
  41 + languages.pluginLanguage(event.path, path.join(conf.paths.tmp, '/serve'));
  42 + });
  43 +
36 44 var watchPaths = [];
37 45 conf.paths.allSources.forEach(function(src) {
38 46 watchPaths.push(path.join(src, '/app/**/*.html'));
  47 + watchPaths.push(path.join(src, conf.paths.plugins, '/**/*.html'));
39 48 });
40 49 gulp.watch(watchPaths, function(event) {
41 50 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");
... ... @@ -126,6 +126,10 @@ module.exports = function (config) {
126 126 };
127 127  
128 128  
  129 + if(config.grep) {
  130 + configuration.client = { args: ['--grep', config.grep] };
  131 + }
  132 +
129 133 if (coverage) {
130 134  
131 135 /*configuration.webpack = {
... ...
layouts/angular-layout.html.erb
1   -<% if params[:angular_theme_old] || request.fullpath.start_with?('/myprofile') %>
  1 +<% if params[:angular_theme_old] %>
2 2 <%= render file: Rails.root.join("app/views/layouts/application-ng.html.erb"), use_full_path: false %>
3 3 <% else %>
4 4 <%= from_theme_include(current_theme, "index") %>
... ...
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.spec.ts
1 1 import {Input, provide, Component} from 'ng-forward';
2 2 import {ArticleViewComponent, ArticleDefaultViewComponent} from './article-default-view.component';
  3 +import {ComponentTestHelper, createClass} from './../../spec/component-test-helper';
3 4  
4 5 import * as helpers from "../../spec/helpers";
5 6  
... ... @@ -9,111 +10,114 @@ const htmlTemplate: string = &#39;&lt;noosfero-article [article]=&quot;ctrl.article&quot; [profil
9 10  
10 11 describe("Components", () => {
11 12  
12   - describe("ArticleView Component", () => {
13   -
14   - // the karma preprocessor html2js transform the templates html into js files which put
15   - // the templates to the templateCache into the module templates
16   - // we need to load the module templates here as the template for the
17   - // component Noosfero ArtileView will be load on our tests
18   - beforeEach(angular.mock.module("templates"));
19   -
20   - it("renders the default component when no specific component is found", (done: Function) => {
21   - // Creating a container component (ArticleContainerComponent) to include
22   - // the component under test (ArticleView)
23   - @Component({
24   - selector: 'test-container-component',
25   - template: htmlTemplate,
26   - directives: [ArticleViewComponent],
27   - providers: [
28   - helpers.createProviderToValue('CommentService', helpers.mocks.commentService),
29   - helpers.provideFilters("translateFilter"),
30   - helpers.createProviderToValue('NotificationService', helpers.mocks.notificationService)
31   - ]
32   - })
33   - class ArticleContainerComponent {
34   - article = { type: 'anyArticleType' };
35   - profile = { name: 'profile-name' };
  13 + // the karma preprocessor html2js transform the templates html into js files which put
  14 + // the templates to the templateCache into the module templates
  15 + // we need to load the module templates here as the template for the
  16 + // component Noosfero ArtileView will be load on our tests
  17 + beforeEach(angular.mock.module("templates"));
  18 +
  19 + describe("Article Default View Component", () => {
  20 + let helper: ComponentTestHelper<ArticleDefaultViewComponent>;
  21 + const defaultViewTemplate: string = '<noosfero-default-article [article]="ctrl.article" [profile]="ctrl.profile"></noosfero-default-article>';
  22 + let notificationService = helpers.mocks.notificationService;
  23 + let articleService: any = helpers.mocks.articleService;
  24 + let article = <noosfero.Article>{
  25 + id: 1,
  26 + profile: {
  27 + identifier: "1"
36 28 }
  29 + };
  30 + let state = <ng.ui.IStateService>jasmine.createSpyObj("state", ["go", "transitionTo"]);
  31 + let providers = [
  32 + provide('$state', { useValue: state }),
  33 + provide('ArticleService', { useValue: articleService }),
  34 + helpers.createProviderToValue('NotificationService', notificationService),
  35 + ].concat(helpers.provideFilters("translateFilter"));
  36 +
  37 + /**
  38 + * The beforeEach procedure will initialize the helper and parse
  39 + * the component according to the given providers. Unfortunetly, in
  40 + * this mode, the providers and properties given to the construtor
  41 + * can't be overriden.
  42 + */
  43 + beforeEach((done) => {
  44 + // Create the component bed for the test. Optionally, this could be done
  45 + // in each test if one needs customization of these parameters per test
  46 + let cls = createClass({
  47 + template: defaultViewTemplate,
  48 + directives: [ArticleDefaultViewComponent],
  49 + providers: providers,
  50 + properties: {
  51 + article: article
  52 + }
  53 + });
  54 + helper = new ComponentTestHelper<ArticleDefaultViewComponent>(cls, done);
  55 + });
37 56  
38   - helpers.createComponentFromClass(ArticleContainerComponent).then((fixture) => {
39   - // and here we can inspect and run the test assertions
40   -
41   - // gets the children component of ArticleContainerComponent
42   - let articleView: ArticleViewComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
43   -
44   - // and checks if the article View rendered was the Default Article View
45   - expect(articleView.constructor.prototype).toEqual(ArticleDefaultViewComponent.prototype);
  57 + function getArticle() {
  58 + return this.article;
  59 + }
46 60  
47   - // done needs to be called (it isn't really needed, as we can read in
48   - // here (https://github.com/ngUpgraders/ng-forward/blob/master/API.md#createasync)
49   - // because createAsync in ng-forward is not really async, but as the intention
50   - // here is write tests in angular 2 ways, this is recommended
51   - done();
52   - });
  61 + it("it should delete article when delete is activated", () => {
  62 + expect(helper.component.article).toEqual(article);
  63 + // Spy the state service
  64 + doDeleteArticle();
  65 + expect(state.transitionTo).toHaveBeenCalled();
  66 + });
53 67  
  68 + it("hide button to delete article when user doesn't have permission", () => {
  69 + expect(helper.find(".article-toolbar .delete-article").attr('style')).toEqual("display: none; ");
54 70 });
55 71  
56   - it("receives the article and profile as inputs", (done: Function) => {
57   -
58   - // Creating a container component (ArticleContainerComponent) to include
59   - // the component under test (ArticleView)
60   - @Component({
61   - selector: 'test-container-component',
62   - template: htmlTemplate,
63   - directives: [ArticleViewComponent],
64   - providers: [
65   - helpers.createProviderToValue('CommentService', helpers.mocks.commentService),
66   - helpers.provideFilters("translateFilter"),
67   - helpers.createProviderToValue('NotificationService', helpers.mocks.notificationService)
68   - ]
69   - })
70   - class ArticleContainerComponent {
71   - article = { type: 'anyArticleType' };
72   - profile = { name: 'profile-name' };
73   - }
  72 + it("hide button to edit article when user doesn't have permission", () => {
  73 + expect(helper.find(".article-toolbar .edit-article").attr('style')).toEqual("display: none; ");
  74 + });
74 75  
75   - // uses the TestComponentBuilder instance to initialize the component
76   - helpers.createComponentFromClass(ArticleContainerComponent).then((fixture) => {
77   - // and here we can inspect and run the test assertions
78   - let articleView: ArticleViewComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
79   -
80   - // assure the article object inside the ArticleView matches
81   - // the provided through the parent component
82   - expect(articleView.article.type).toEqual("anyArticleType");
83   - expect(articleView.profile.name).toEqual("profile-name");
84   -
85   - // done needs to be called (it isn't really needed, as we can read in
86   - // here (https://github.com/ngUpgraders/ng-forward/blob/master/API.md#createasync)
87   - // because createAsync in ng-forward is not really async, but as the intention
88   - // here is write tests in angular 2 ways, this is recommended
89   - done();
90   - });
  76 + it("show button to edit article when user has permission", () => {
  77 + (<any>helper.component['article'])['permissions'] = ['allow_edit'];
  78 + helper.detectChanges();
  79 + expect(helper.find(".article-toolbar .edit-article").attr('style')).toEqual('');
91 80 });
92 81  
  82 + it("show button to delete article when user has permission", () => {
  83 + (<any>helper.component['article'])['permissions'] = ['allow_delete'];
  84 + helper.detectChanges();
  85 + expect(helper.find(".article-toolbar .delete-article").attr('style')).toEqual('');
  86 + });
93 87  
94   - it("renders a article view which matches to the article type", done => {
95   - // NoosferoTinyMceArticle component created to check if it will be used
96   - // when a article with type 'TinyMceArticle' is provided to the noosfero-article (ArticleView)
97   - // *** Important *** - the selector is what ng-forward uses to define the name of the directive provider
98   - @Component({ selector: 'noosfero-tiny-mce-article', template: "<h1>TinyMceArticle</h1>" })
99   - class TinyMceArticleView {
100   - @Input() article: any;
101   - @Input() profile: any;
102   - }
  88 + /**
  89 + * Execute the delete method on the target component
  90 + */
  91 + function doDeleteArticle() {
  92 + // Create a mock for the notification service confirmation
  93 + spyOn(helper.component.notificationService, 'confirmation').and.callFake(function(params: Function) {
103 94  
104   - // Creating a container component (ArticleContainerComponent) to include our NoosferoTinyMceArticle
105   - @Component({ selector: 'test-container-component', template: htmlTemplate, directives: [ArticleViewComponent, TinyMceArticleView] })
106   - class CustomArticleType {
107   - article = { type: 'TinyMceArticle' };
108   - profile = { name: 'profile-name' };
109   - }
110   - helpers.createComponentFromClass(CustomArticleType).then(fixture => {
111   - let myComponent: CustomArticleType = fixture.componentInstance;
112   - expect(myComponent.article.type).toEqual("TinyMceArticle");
113   - expect(fixture.debugElement.componentViewChildren[0].text()).toEqual("TinyMceArticle");
114   - done();
115 95 });
116   - });
  96 + // Create a mock for the ArticleService removeArticle method
  97 + spyOn(helper.component.articleService, 'remove').and.callFake(function(param: noosfero.Article) {
117 98  
  99 + return {
  100 + catch: () => { }
  101 + };
  102 + });
  103 + helper.component.delete();
  104 + expect(notificationService.confirmation).toHaveBeenCalled();
  105 + helper.component.doDelete();
  106 + expect(articleService.remove).toHaveBeenCalled();
  107 +
  108 + // After the component delete method execution, fire the
  109 + // ArticleEvent.removed event
  110 + simulateRemovedEvent();
  111 + }
  112 +
  113 + /**
  114 + * Simulate the Notification Service confirmation and ArticleService
  115 + * notifyArticleRemovedListeners event
  116 + */
  117 + function simulateRemovedEvent() {
  118 + helper.component.notificationService["confirmation"]({ title: "Title", message: "Message" }, () => { });
  119 + helper.component.articleService["modelRemovedEventEmitter"].next(article);
  120 + }
118 121 });
  122 +
119 123 });
... ...
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";
  6 +import {ArticleContentHotspotComponent} from "../hotspot/article-content-hotspot.component";
  7 +import {ArticleService} from "./../../lib/ng-noosfero-api/http/article.service";
  8 +import { NotificationService } from "./../shared/services/notification.service";
  9 +import {PermissionDirective} from '../shared/components/permission/permission.directive';
4 10  
5 11 /**
6 12 * @ngdoc controller
... ... @@ -11,13 +17,37 @@ import {CommentsComponent} from &quot;./comment/comments.component&quot;;
11 17 */
12 18 @Component({
13 19 selector: 'noosfero-default-article',
14   - templateUrl: 'app/article/article.html'
  20 + templateUrl: 'app/article/article.html',
  21 + directives: [PermissionDirective]
15 22 })
  23 +@Inject("$state", ArticleService, NotificationService)
16 24 export class ArticleDefaultViewComponent {
17 25  
18 26 @Input() article: noosfero.Article;
19 27 @Input() profile: noosfero.Profile;
20 28  
  29 + constructor(private $state: ng.ui.IStateService, public articleService: ArticleService, public notificationService: NotificationService) {
  30 + // Subscribe to the Article Removed Event
  31 + this.articleService.subscribeToModelRemoved((article: noosfero.Article) => {
  32 + if (this.article.parent) {
  33 + this.$state.transitionTo('main.profile.page', { page: this.article.parent.path, profile: this.article.profile.identifier });
  34 + } else {
  35 + this.$state.transitionTo('main.profile.info', { profile: this.article.profile.identifier });
  36 + }
  37 + this.notificationService.success({ title: "article.remove.success.title", message: "article.remove.success.message" });
  38 + });
  39 + }
  40 +
  41 + delete() {
  42 + this.notificationService.confirmation({ title: "article.remove.confirmation.title", message: "article.remove.confirmation.message" }, () => {
  43 + this.doDelete();
  44 + });
  45 +
  46 + }
  47 +
  48 + doDelete() {
  49 + this.articleService.remove(this.article);
  50 + }
21 51 }
22 52  
23 53 /**
... ... @@ -30,7 +60,9 @@ export class ArticleDefaultViewComponent {
30 60 @Component({
31 61 selector: 'noosfero-article',
32 62 template: 'not-used',
33   - directives: [ArticleDefaultViewComponent, ArticleBlogComponent, CommentsComponent]
  63 + directives: [ArticleDefaultViewComponent, ArticleBlogComponent,
  64 + CommentsComponent, MacroDirective, ArticleToolbarHotspotComponent,
  65 + ArticleContentHotspotComponent]
34 66 })
35 67 @Inject("$element", "$scope", "$injector", "$compile")
36 68 export class ArticleViewComponent {
... ... @@ -40,7 +72,8 @@ export class ArticleViewComponent {
40 72 directiveName: string;
41 73  
42 74 ngOnInit() {
43   - let specificDirective = 'noosfero' + this.article.type;
  75 + let articleType = this.article.type.replace(/::/, '');
  76 + let specificDirective = 'noosfero' + articleType;
44 77 this.directiveName = "noosfero-default-article";
45 78 if (this.$injector.has(specificDirective + 'Directive')) {
46 79 this.directiveName = specificDirective.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
... ... @@ -53,6 +86,6 @@ export class ArticleViewComponent {
53 86 private $scope: ng.IScope,
54 87 private $injector: ng.auto.IInjectorService,
55 88 private $compile: ng.ICompileService) {
56   -
57 89 }
  90 +
58 91 }
... ...
src/app/article/article-view-component.spec.ts 0 → 100644
... ... @@ -0,0 +1,124 @@
  1 +import {Input, provide, Component} from 'ng-forward';
  2 +import {ArticleViewComponent, ArticleDefaultViewComponent} from './article-default-view.component';
  3 +
  4 +import * as helpers from "../../spec/helpers";
  5 +
  6 +// this htmlTemplate will be re-used between the container components in this spec file
  7 +const htmlTemplate: string = '<noosfero-article [article]="ctrl.article" [profile]="ctrl.profile"></noosfero-article>';
  8 +
  9 +
  10 +describe("Components", () => {
  11 +
  12 + // the karma preprocessor html2js transform the templates html into js files which put
  13 + // the templates to the templateCache into the module templates
  14 + // we need to load the module templates here as the template for the
  15 + // component Noosfero ArtileView will be load on our tests
  16 + beforeEach(angular.mock.module("templates"));
  17 +
  18 + describe("ArticleView Component", () => {
  19 + let state = <ng.ui.IStateService>jasmine.createSpyObj("state", ["go", "transitionTo"]);
  20 + it("renders the default component when no specific component is found", (done: Function) => {
  21 + // Creating a container component (ArticleContainerComponent) to include
  22 + // the component under test (ArticleView)
  23 + @Component({
  24 + selector: 'test-container-component',
  25 + template: htmlTemplate,
  26 + directives: [ArticleViewComponent],
  27 + providers: [
  28 + helpers.createProviderToValue('CommentService', helpers.mocks.commentService),
  29 + helpers.provideFilters("translateFilter"),
  30 + helpers.createProviderToValue('NotificationService', helpers.mocks.notificationService),
  31 + helpers.createProviderToValue('SessionService', helpers.mocks.sessionWithCurrentUser({})),
  32 + helpers.createProviderToValue('ArticleService', helpers.mocks.articleService),
  33 + helpers.createProviderToValue('$state', state)
  34 + ]
  35 + })
  36 + class ArticleContainerComponent {
  37 + article = { type: 'anyArticleType' };
  38 + profile = { name: 'profile-name' };
  39 + }
  40 +
  41 + helpers.createComponentFromClass(ArticleContainerComponent).then((fixture) => {
  42 + // and here we can inspect and run the test assertions
  43 +
  44 + // gets the children component of ArticleContainerComponent
  45 + let articleView: ArticleViewComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
  46 +
  47 + // and checks if the article View rendered was the Default Article View
  48 + expect(articleView.constructor.prototype).toEqual(ArticleDefaultViewComponent.prototype);
  49 +
  50 + // done needs to be called (it isn't really needed, as we can read in
  51 + // here (https://github.com/ngUpgraders/ng-forward/blob/master/API.md#createasync)
  52 + // because createAsync in ng-forward is not really async, but as the intention
  53 + // here is write tests in angular 2 ways, this is recommended
  54 + done();
  55 + });
  56 + });
  57 +
  58 + it("receives the article and profile as inputs", (done: Function) => {
  59 +
  60 + // Creating a container component (ArticleContainerComponent) to include
  61 + // the component under test (ArticleView)
  62 + @Component({
  63 + selector: 'test-container-component',
  64 + template: htmlTemplate,
  65 + directives: [ArticleViewComponent],
  66 + providers: [
  67 + helpers.createProviderToValue('CommentService', helpers.mocks.commentService),
  68 + helpers.provideFilters("translateFilter"),
  69 + helpers.createProviderToValue('NotificationService', helpers.mocks.notificationService),
  70 + helpers.createProviderToValue('SessionService', helpers.mocks.sessionWithCurrentUser({})),
  71 + helpers.createProviderToValue('ArticleService', helpers.mocks.articleService),
  72 + helpers.createProviderToValue('$state', state)
  73 + ]
  74 + })
  75 + class ArticleContainerComponent {
  76 + article = { type: 'anyArticleType' };
  77 + profile = { name: 'profile-name' };
  78 + }
  79 +
  80 + // uses the TestComponentBuilder instance to initialize the component
  81 + helpers.createComponentFromClass(ArticleContainerComponent).then((fixture) => {
  82 + // and here we can inspect and run the test assertions
  83 + let articleView: ArticleViewComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
  84 +
  85 + // assure the article object inside the ArticleView matches
  86 + // the provided through the parent component
  87 + expect(articleView.article.type).toEqual("anyArticleType");
  88 + expect(articleView.profile.name).toEqual("profile-name");
  89 +
  90 + // done needs to be called (it isn't really needed, as we can read in
  91 + // here (https://github.com/ngUpgraders/ng-forward/blob/master/API.md#createasync)
  92 + // because createAsync in ng-forward is not really async, but as the intention
  93 + // here is write tests in angular 2 ways, this is recommended
  94 + done();
  95 + });
  96 + });
  97 +
  98 +
  99 + it("renders a article view which matches to the article type", done => {
  100 + // NoosferoTinyMceArticle component created to check if it will be used
  101 + // when a article with type 'TinyMceArticle' is provided to the noosfero-article (ArticleView)
  102 + // *** Important *** - the selector is what ng-forward uses to define the name of the directive provider
  103 + @Component({ selector: 'noosfero-tiny-mce-article', template: "<h1>TinyMceArticle</h1>" })
  104 + class TinyMceArticleView {
  105 + @Input() article: any;
  106 + @Input() profile: any;
  107 + }
  108 +
  109 + // Creating a container component (ArticleContainerComponent) to include our NoosferoTinyMceArticle
  110 + @Component({ selector: 'test-container-component', template: htmlTemplate, directives: [ArticleViewComponent, TinyMceArticleView] })
  111 + class CustomArticleType {
  112 + article = { type: 'TinyMceArticle' };
  113 + profile = { name: 'profile-name' };
  114 + }
  115 + helpers.createComponentFromClass(CustomArticleType).then(fixture => {
  116 + let myComponent: CustomArticleType = fixture.componentInstance;
  117 + expect(myComponent.article.type).toEqual("TinyMceArticle");
  118 + expect(fixture.debugElement.componentViewChildren[0].text()).toEqual("TinyMceArticle");
  119 + done();
  120 + });
  121 + });
  122 +
  123 + });
  124 +});
... ...
src/app/article/article.html
... ... @@ -4,6 +4,15 @@
4 4 </div>
5 5  
6 6 <div class="sub-header clearfix">
  7 + <div class="article-toolbar">
  8 + <a href="#" permission="ctrl.article.permissions" permission-action="allow_edit" class="btn btn-default btn-xs edit-article" ui-sref="main.cmsEdit({profile: ctrl.profile.identifier, id: ctrl.article.id})">
  9 + <i class="fa fa-pencil-square-o fa-fw fa-lg"></i> {{"article.actions.edit" | translate}}
  10 + </a>
  11 + <a href="#" permission="ctrl.article.permissions" permission-action="allow_delete" class="btn btn-default btn-xs delete-article" ng-click="ctrl.delete()">
  12 + <i class="fa fa-trash-o fa-fw fa-lg" ng-click="ctrl.delete()"></i> {{"article.actions.delete" | translate}}
  13 + </a>
  14 + <noosfero-hotspot-article-toolbar [article]="ctrl.article"></noosfero-hotspot-article-toolbar>
  15 + </div>
7 16 <div class="page-info pull-right small text-muted">
8 17 <span class="time">
9 18 <i class="fa fa-clock-o"></i> <span am-time-ago="ctrl.article.created_at | dateFormat"></span>
... ... @@ -16,9 +25,9 @@
16 25 </span>
17 26 </div>
18 27 </div>
19   -
  28 + <noosfero-hotspot-article-content [article]="ctrl.article"></noosfero-hotspot-article-content>
20 29 <div class="page-body">
21   - <div ng-bind-html="ctrl.article.body"></div>
  30 + <div bind-html-compile="ctrl.article.body"></div>
22 31 </div>
23 32  
24 33 <noosfero-comments [article]="ctrl.article"></noosfero-comments>
... ...
src/app/article/cms/article-editor/article-editor.component.spec.ts 0 → 100644
... ... @@ -0,0 +1,28 @@
  1 +import {ArticleEditorComponent} from './article-editor.component';
  2 +import {BasicEditorComponent} from "../basic-editor/basic-editor.component";
  3 +import {ComponentTestHelper, createClass} from '../../../../spec/component-test-helper';
  4 +import * as helpers from "../../../../spec/helpers";
  5 +
  6 +const htmlTemplate: string = '<article-editor [article]="ctrl.article"></article-editor>';
  7 +
  8 +describe("Components", () => {
  9 + describe("Article Editor Component", () => {
  10 +
  11 + let helper: ComponentTestHelper<ArticleEditorComponent>;
  12 + beforeEach(angular.mock.module("templates"));
  13 +
  14 + beforeEach((done) => {
  15 + let properties = { article: { type: "TextArticle" } };
  16 + let cls = createClass({
  17 + template: htmlTemplate,
  18 + directives: [ArticleEditorComponent, BasicEditorComponent],
  19 + properties: properties
  20 + });
  21 + helper = new ComponentTestHelper<ArticleEditorComponent>(cls, done);
  22 + });
  23 +
  24 + it("replace element with article basic editor when type is TextArticle", () => {
  25 + expect(helper.find("article-basic-editor").length).toEqual(1);
  26 + });
  27 + });
  28 +});
... ...
src/app/article/cms/article-editor/article-editor.component.ts 0 → 100644
... ... @@ -0,0 +1,27 @@
  1 +import {Component, Input, Inject} from 'ng-forward';
  2 +
  3 +@Component({
  4 + selector: 'article-editor',
  5 + template: "not-used"
  6 +})
  7 +@Inject("$element", "$scope", "$injector", "$compile")
  8 +export class ArticleEditorComponent {
  9 +
  10 + @Input() article: noosfero.Article;
  11 +
  12 + constructor(
  13 + private $element: any,
  14 + private $scope: ng.IScope,
  15 + private $injector: ng.auto.IInjectorService,
  16 + private $compile: ng.ICompileService) { }
  17 +
  18 + ngOnInit() {
  19 + let articleType = this.article && this.article.type ? this.article.type.replace(/::/, '') : "TextArticle";
  20 + let specificDirective = `${articleType.charAt(0).toLowerCase()}${articleType.substring(1)}Editor`;
  21 + let directiveName = "article-basic-editor";
  22 + if (specificDirective !== "articleEditor" && this.$injector.has(specificDirective + 'Directive')) {
  23 + directiveName = specificDirective.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
  24 + }
  25 + this.$element.replaceWith(this.$compile('<' + directiveName + ' [article]="ctrl.article"></' + directiveName + '>')(this.$scope));
  26 + }
  27 +}
... ...
src/app/article/cms/basic-editor/basic-editor.component.ts 0 → 100644
... ... @@ -0,0 +1,10 @@
  1 +import {Component, Input} from 'ng-forward';
  2 +
  3 +@Component({
  4 + selector: 'article-basic-editor',
  5 + templateUrl: "app/article/cms/basic-editor/basic-editor.html"
  6 +})
  7 +export class BasicEditorComponent {
  8 +
  9 + @Input() article: noosfero.Article;
  10 +}
... ...
src/app/article/cms/basic-editor/basic-editor.html 0 → 100644
... ... @@ -0,0 +1,10 @@
  1 +<form>
  2 + <div class="form-group">
  3 + <label for="titleInput">{{"article.basic_editor.title" | translate}}</label>
  4 + <input type="text" class="form-control" id="titleInput" placeholder="{{'article.basic_editor.title' | translate}}" ng-model="ctrl.article.name">
  5 + </div>
  6 + <div class="form-group">
  7 + <label for="bodyInput">{{"article.basic_editor.body" | translate}}</label>
  8 + <html-editor [(value)]="ctrl.article.body"></html-editor>
  9 + </div>
  10 +</form>
... ...
src/app/article/cms/basic-editor/basic-editor.scss 0 → 100644
src/app/article/cms/basic-options/basic-options.component.ts 0 → 100644
... ... @@ -0,0 +1,11 @@
  1 +import {Component, Input} from 'ng-forward';
  2 +
  3 +@Component({
  4 + selector: 'article-basic-options',
  5 + templateUrl: "app/article/cms/basic-options/basic-options.html"
  6 +})
  7 +export class BasicOptionsComponent {
  8 +
  9 + @Input() article: noosfero.Article;
  10 +
  11 +}
... ...
src/app/article/cms/basic-options/basic-options.html 0 → 100644
... ... @@ -0,0 +1,15 @@
  1 +<div class="side-options">
  2 + <div class="visibility panel panel-default">
  3 + <div class="panel-heading">{{"article.basic_editor.visibility" | translate}}</div>
  4 + <div class="panel-body">
  5 + <div>
  6 + <input type="radio" ng-model="ctrl.article.published" ng-value="true">
  7 + <i class="fa fa-unlock fa-fw"></i> {{"article.basic_editor.visibility.public" | translate}}
  8 + </div>
  9 + <div>
  10 + <input type="radio" ng-model="ctrl.article.published" ng-value="false">
  11 + <i class="fa fa-lock fa-fw"></i> {{"article.basic_editor.visibility.private" | translate}}
  12 + </div>
  13 + </div>
  14 + </div>
  15 +</div>
... ...
src/app/article/cms/basic-options/basic-options.scss 0 → 100644
... ... @@ -0,0 +1,15 @@
  1 +.side-options {
  2 + margin-top: 25px;
  3 +
  4 + .visibility {
  5 + .panel-heading {
  6 + background-color: transparent;
  7 + font-weight: bold;
  8 + }
  9 + .panel-body {
  10 + i {
  11 + color: #A5A5A5;
  12 + }
  13 + }
  14 + }
  15 +}
... ...
src/app/article/cms/cms.component.spec.ts 0 → 100644
... ... @@ -0,0 +1,85 @@
  1 +import {quickCreateComponent} from "../../../spec/helpers";
  2 +import {CmsComponent} from "./cms.component";
  3 +
  4 +
  5 +describe("Article Cms", () => {
  6 +
  7 + let $rootScope: ng.IRootScopeService;
  8 + let $q: ng.IQService;
  9 + let articleServiceMock: any;
  10 + let profileServiceMock: any;
  11 + let $state: any;
  12 + let $stateParams: any;
  13 + let $window: any;
  14 + let profile = { id: 1 };
  15 + let notification: any;
  16 +
  17 +
  18 + beforeEach(inject((_$rootScope_: ng.IRootScopeService, _$q_: ng.IQService) => {
  19 + $rootScope = _$rootScope_;
  20 + $q = _$q_;
  21 + }));
  22 +
  23 + beforeEach(() => {
  24 + $window = jasmine.createSpyObj("$window", ["back"]);
  25 + $state = jasmine.createSpyObj("$state", ["go"]);
  26 + notification = jasmine.createSpyObj("notification", ["success"]);
  27 + profileServiceMock = jasmine.createSpyObj("profileServiceMock", ["setCurrentProfileByIdentifier"]);
  28 + articleServiceMock = jasmine.createSpyObj("articleServiceMock", ["createInParent", "updateArticle", "get"]);
  29 +
  30 + $stateParams = { profile: "profile" };
  31 +
  32 + let setCurrentProfileByIdentifierResponse = $q.defer();
  33 + setCurrentProfileByIdentifierResponse.resolve(profile);
  34 +
  35 + let articleCreate = $q.defer();
  36 + articleCreate.resolve({ data: { path: "path", profile: { identifier: "profile" } } });
  37 +
  38 + let articleGet = $q.defer();
  39 + articleGet.resolve({ data: { path: "parent-path", profile: { identifier: "profile" } } });
  40 +
  41 + profileServiceMock.setCurrentProfileByIdentifier = jasmine.createSpy("setCurrentProfileByIdentifier").and.returnValue(setCurrentProfileByIdentifierResponse.promise);
  42 + articleServiceMock.createInParent = jasmine.createSpy("createInParent").and.returnValue(articleCreate.promise);
  43 + articleServiceMock.updateArticle = jasmine.createSpy("updateArticle").and.returnValue(articleCreate.promise);
  44 + articleServiceMock.get = jasmine.createSpy("get").and.returnValue(articleGet.promise);
  45 + });
  46 +
  47 + it("create an article in the current profile when save", done => {
  48 + $stateParams['parent_id'] = 1;
  49 + let component: CmsComponent = new CmsComponent(articleServiceMock, profileServiceMock, $state, notification, $stateParams, $window);
  50 + component.save();
  51 + $rootScope.$apply();
  52 + expect(profileServiceMock.setCurrentProfileByIdentifier).toHaveBeenCalled();
  53 + expect(articleServiceMock.createInParent).toHaveBeenCalledWith(1, component.article);
  54 + done();
  55 + });
  56 +
  57 + it("got to the new article page and display an alert when saving sucessfully", done => {
  58 + $stateParams['parent_id'] = 1;
  59 + let component: CmsComponent = new CmsComponent(articleServiceMock, profileServiceMock, $state, notification, $stateParams, $window);
  60 + component.save();
  61 + $rootScope.$apply();
  62 + expect($state.go).toHaveBeenCalledWith("main.profile.page", { page: "path", profile: "profile" });
  63 + expect(notification.success).toHaveBeenCalled();
  64 + done();
  65 + });
  66 +
  67 + it("go back when cancel article edition", done => {
  68 + let component: CmsComponent = new CmsComponent(articleServiceMock, profileServiceMock, $state, notification, $stateParams, $window);
  69 + $window.history = { back: jasmine.createSpy('back') };
  70 + component.cancel();
  71 + expect($window.history.back).toHaveBeenCalled();
  72 + done();
  73 + });
  74 +
  75 + it("edit existing article when save", done => {
  76 + $stateParams['parent_id'] = null;
  77 + $stateParams['id'] = 2;
  78 + let component: CmsComponent = new CmsComponent(articleServiceMock, profileServiceMock, $state, notification, $stateParams, $window);
  79 + component.save();
  80 + $rootScope.$apply();
  81 + expect(articleServiceMock.updateArticle).toHaveBeenCalledWith(component.article);
  82 + done();
  83 + });
  84 +
  85 +});
... ...
src/app/article/cms/cms.component.ts 0 → 100644
... ... @@ -0,0 +1,77 @@
  1 +import {StateConfig, Component, Inject, provide} from 'ng-forward';
  2 +import {ArticleService} from "../../../lib/ng-noosfero-api/http/article.service";
  3 +import {ProfileService} from "../../../lib/ng-noosfero-api/http/profile.service";
  4 +import {NotificationService} from "../../shared/services/notification.service.ts";
  5 +import {BasicOptionsComponent} from './basic-options/basic-options.component';
  6 +import {BasicEditorComponent} from './basic-editor/basic-editor.component';
  7 +import {ArticleEditorComponent} from './article-editor/article-editor.component';
  8 +
  9 +@Component({
  10 + selector: 'article-cms',
  11 + templateUrl: "app/article/cms/cms.html",
  12 + providers: [
  13 + provide('articleService', { useClass: ArticleService }),
  14 + provide('profileService', { useClass: ProfileService }),
  15 + provide('notification', { useClass: NotificationService })
  16 + ],
  17 + directives: [ArticleEditorComponent, BasicOptionsComponent, BasicEditorComponent]
  18 +})
  19 +@Inject(ArticleService, ProfileService, "$state", NotificationService, "$stateParams", "$window")
  20 +export class CmsComponent {
  21 +
  22 + article: noosfero.Article;
  23 + parent: noosfero.Article = <noosfero.Article>{};
  24 +
  25 + id: number;
  26 + parentId: number;
  27 + profileIdentifier: string;
  28 +
  29 + constructor(private articleService: ArticleService,
  30 + private profileService: ProfileService,
  31 + private $state: ng.ui.IStateService,
  32 + private notification: NotificationService,
  33 + private $stateParams: ng.ui.IStateParamsService,
  34 + private $window: ng.IWindowService) {
  35 +
  36 + this.parentId = this.$stateParams['parent_id'];
  37 + this.profileIdentifier = this.$stateParams["profile"];
  38 + this.id = this.$stateParams['id'];
  39 +
  40 + if (this.parentId) {
  41 + this.articleService.get(this.parentId).then((result: noosfero.RestResult<noosfero.Article>) => {
  42 + this.parent = result.data;
  43 + });
  44 + }
  45 + if (this.id) {
  46 + this.articleService.get(this.id).then((result: noosfero.RestResult<noosfero.Article>) => {
  47 + this.article = result.data;
  48 + this.article.name = this.article.title; // FIXME
  49 + });
  50 + } else {
  51 + this.article = <noosfero.Article>{ type: this.$stateParams['type'] || "TextArticle", published: true };
  52 + }
  53 + }
  54 +
  55 + save() {
  56 + this.profileService.setCurrentProfileByIdentifier(this.profileIdentifier).then((profile: noosfero.Profile) => {
  57 + if (this.id) {
  58 + return this.articleService.updateArticle(this.article);
  59 + } else if (this.parentId) {
  60 + return this.articleService.createInParent(this.parentId, this.article);
  61 + } else {
  62 + return this.articleService.createInProfile(profile, this.article);
  63 + }
  64 + }).then((response: noosfero.RestResult<noosfero.Article>) => {
  65 + let article = (<noosfero.Article>response.data);
  66 + this.$state.go('main.profile.page', { page: article.path, profile: article.profile.identifier });
  67 + this.notification.success({ title: "article.basic_editor.success.title", message: "article.basic_editor.success.message" });
  68 + }).catch(() => {
  69 + this.notification.error({ message: "article.basic_editor.save.failed" });
  70 + });
  71 + }
  72 +
  73 + cancel() {
  74 + this.$window.history.back();
  75 + }
  76 +
  77 +}
... ...
src/app/article/cms/cms.html 0 → 100644
... ... @@ -0,0 +1,18 @@
  1 +<div class="cms">
  2 + <div class="row">
  3 + <div class="col-md-1"></div>
  4 + <div class="col-md-8">
  5 + <article-editor ng-if="vm.article" [article]="vm.article"></article-editor>
  6 + </div>
  7 + <div class="col-md-3">
  8 + <article-basic-options ng-if="vm.article" [article]="vm.article"></article-basic-options>
  9 + </div>
  10 + </div>
  11 + <div class="row">
  12 + <div class="col-md-1"></div>
  13 + <div class="col-md-8">
  14 + <button type="submit" class="btn btn-default" ng-click="vm.save()">{{"article.basic_editor.save" | translate}}</button>
  15 + <button type="button" class="btn btn-danger" ng-click="vm.cancel()">{{"article.basic_editor.cancel" | translate}}</button>
  16 + </div>
  17 + </div>
  18 +</div>
... ...
src/app/article/cms/cms.scss 0 → 100644
... ... @@ -0,0 +1,4 @@
  1 +.cms {
  2 + @extend .container-fluid;
  3 + padding: 0 1%;
  4 +}
... ...
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.spec.ts
1 1 import {Provider, provide, Component} from 'ng-forward';
2 2 import * as helpers from "../../../spec/helpers";
3   -
4 3 import {CommentComponent} from './comment.component';
5 4  
6 5 const htmlTemplate: string = '<noosfero-comment [article]="ctrl.article" [comment]="ctrl.comment"></noosfero-comment>';
... ... @@ -8,37 +7,108 @@ const htmlTemplate: string = &#39;&lt;noosfero-comment [article]=&quot;ctrl.article&quot; [commen
8 7 describe("Components", () => {
9 8 describe("Comment Component", () => {
10 9  
  10 + let properties: any;
  11 + let notificationService = helpers.mocks.notificationService;
  12 + let commentService = jasmine.createSpyObj("commentService", ["removeFromArticle"]);
  13 +
11 14 beforeEach(angular.mock.module("templates"));
  15 + beforeEach(() => {
  16 + properties = {
  17 + article: { id: 1, accept_comments: true },
  18 + comment: { title: "title", body: "body" }
  19 + };
  20 + });
  21 +
  22 + function createComponent() {
  23 + let providers = [
  24 + helpers.createProviderToValue('NotificationService', notificationService),
  25 + helpers.createProviderToValue("CommentService", commentService)
  26 + ].concat(helpers.provideFilters("translateFilter"));
12 27  
13   - @Component({ selector: 'test-container-component', directives: [CommentComponent], template: htmlTemplate, providers: helpers.provideFilters("translateFilter") })
14   - class ContainerComponent {
15   - article = { id: 1 };
16   - comment = { title: "title", body: "body" };
  28 + @Component({ selector: 'test-container-component', directives: [CommentComponent], template: htmlTemplate, providers: providers })
  29 + class ContainerComponent {
  30 + article = properties['article'];
  31 + comment = properties['comment'];
  32 + }
  33 + return helpers.createComponentFromClass(ContainerComponent);
17 34 }
18 35  
19 36 it("render a comment", done => {
20   - helpers.createComponentFromClass(ContainerComponent).then(fixture => {
  37 + createComponent().then(fixture => {
21 38 expect(fixture.debugElement.queryAll(".comment").length).toEqual(1);
22 39 done();
23 40 });
24 41 });
25 42  
26 43 it("not render a post comment tag in the beginning", done => {
27   - helpers.createComponentFromClass(ContainerComponent).then(fixture => {
  44 + createComponent().then(fixture => {
28 45 expect(fixture.debugElement.queryAll("noosfero-post-comment").length).toEqual(0);
29 46 done();
30 47 });
31 48 });
32 49  
33   - it("render a post comment tag when click in reply", done => {
34   - helpers.createComponentFromClass(ContainerComponent).then(fixture => {
  50 + it("set show reply to true when click reply", done => {
  51 + createComponent().then(fixture => {
35 52 let component: CommentComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
36 53 component.reply();
37   - fixture.debugElement.getLocal("$rootScope").$apply();
38   - expect(fixture.debugElement.queryAll("noosfero-post-comment").length).toEqual(1);
  54 + expect(component.showReply()).toBeTruthy("Reply was expected to be true");
  55 + done();
  56 + });
  57 + });
  58 +
  59 + it("show reply relies on current comment __showReply attribute", done => {
  60 + createComponent().then(fixture => {
  61 + let component = fixture.debugElement.componentViewChildren[0];
  62 + component.componentInstance.comment.__showReply = false;
  63 + expect(component.componentInstance.showReply()).toEqual(false);
39 64 done();
40 65 });
41 66 });
42 67  
  68 + it("display reply button", done => {
  69 + createComponent().then(fixture => {
  70 + expect(fixture.debugElement.queryAll(".comment .actions .reply").length).toEqual(1);
  71 + done();
  72 + });
  73 + });
  74 +
  75 + it("not display reply button when accept_comments is false", done => {
  76 + properties['article']['accept_comments'] = false;
  77 + createComponent().then(fixture => {
  78 + expect(fixture.debugElement.queryAll(".comment .actions .reply").length).toEqual(0);
  79 + done();
  80 + });
  81 + });
  82 +
  83 + it("does not show the Remove button if user is not allowed to remove", done => {
  84 + createComponent().then(fixture => {
  85 + let component: CommentComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
  86 + component.allowRemove = () => false;
  87 + fixture.detectChanges();
  88 + expect(fixture.debugElement.queryAll("a.action.remove").length).toEqual(0);
  89 + done();
  90 + });
  91 + });
  92 +
  93 + it("shows the Remove button if user is allowed to remove", done => {
  94 + createComponent().then(fixture => {
  95 + let component: CommentComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
  96 + component.allowRemove = () => true;
  97 + fixture.detectChanges();
  98 + expect(fixture.debugElement.queryAll("a.action.remove").length).toEqual(1);
  99 + done();
  100 + });
  101 + });
  102 +
  103 + it("call comment service to remove comment", done => {
  104 + notificationService.confirmation = (params: any, func: Function) => { func(); };
  105 + commentService.removeFromArticle = jasmine.createSpy("removeFromArticle").and.returnValue(Promise.resolve());
  106 + createComponent().then(fixture => {
  107 + let component = fixture.debugElement.componentViewChildren[0].componentInstance;
  108 + component.remove();
  109 + expect(commentService.removeFromArticle).toHaveBeenCalled();
  110 + done();
  111 + });
  112 + });
43 113 });
44 114 });
... ...
src/app/article/comment/comment.component.ts
1   -import { Input, Component } from 'ng-forward';
  1 +import { Inject, Input, Component, Output, EventEmitter } from 'ng-forward';
  2 +import { PostCommentComponent } from "./post-comment/post-comment.component";
  3 +import { CommentService } from "../../../lib/ng-noosfero-api/http/comment.service";
  4 +import { NotificationService } from "../../shared/services/notification.service";
2 5  
3 6 @Component({
4 7 selector: 'noosfero-comment',
  8 + outputs: ['commentRemoved'],
5 9 templateUrl: 'app/article/comment/comment.html'
6 10 })
  11 +@Inject(CommentService, NotificationService)
7 12 export class CommentComponent {
8 13  
9   - @Input() comment: noosfero.Comment;
  14 + @Input() comment: noosfero.CommentViewModel;
10 15 @Input() article: noosfero.Article;
  16 + @Input() displayActions = true;
  17 + @Input() displayReplies = true;
  18 + @Output() commentRemoved: EventEmitter<Comment> = new EventEmitter<Comment>();
11 19  
12   - showReply: boolean = false;
  20 + showReply() {
  21 + return this.comment && this.comment.__show_reply === true;
  22 + }
  23 +
  24 + constructor(private commentService: CommentService,
  25 + private notificationService: NotificationService) { }
13 26  
14 27 reply() {
15   - this.showReply = true;
  28 + this.comment.__show_reply = !this.comment.__show_reply;
  29 + }
  30 +
  31 + allowRemove() {
  32 + return true;
  33 + }
  34 +
  35 + remove() {
  36 + this.notificationService.confirmation({ title: "comment.remove.confirmation.title", message: "comment.remove.confirmation.message" }, () => {
  37 + this.commentService.removeFromArticle(this.article, this.comment).then((result: noosfero.RestResult<noosfero.Comment>) => {
  38 + this.commentRemoved.next(this.comment);
  39 + this.notificationService.success({ title: "comment.remove.success.title", message: "comment.remove.success.message" });
  40 + });
  41 + });
16 42 }
17 43 }
... ...
src/app/article/comment/comment.html
... ... @@ -9,17 +9,24 @@
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   - <a href="#" (click)="ctrl.reply()">
14   - <span class="pull-right small text-muted">
15   - {{"comment.reply" | translate}}
16   - </span>
17   - </a>
18 17 </div>
19 18 <div class="title">{{ctrl.comment.title}}</div>
20 19 <div class="body">{{ctrl.comment.body}}</div>
  20 + <div class="actions" ng-if="ctrl.displayActions">
  21 + <a href="#" (click)="ctrl.reply()" class="action small text-muted reply" ng-if="ctrl.article.accept_comments">
  22 + <span class="bullet-separator">•</span>
  23 + {{"comment.reply" | translate}}
  24 + </a>
  25 + <a href="#" (click)="ctrl.remove()" class="action small text-muted remove" ng-if="ctrl.allowRemove()">
  26 + <span class="bullet-separator">•</span>
  27 + {{"comment.remove" | translate}}
  28 + </a>
  29 + </div>
21 30 </div>
22   -
23   - <noosfero-post-comment ng-if="ctrl.showReply" [article]="ctrl.article" [reply-of]="ctrl.comment"></noosfero-post-comment>
24   -
  31 + <noosfero-comments [show-form]="ctrl.showReply()" [article]="ctrl.article" [parent]="ctrl.comment" ng-if="ctrl.displayReplies"></noosfero-comments>
25 32 </div>
... ...
src/app/article/comment/comment.scss
... ... @@ -5,16 +5,43 @@
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;
11 12 }
  13 + .actions {
  14 + .action {
  15 + text-decoration: none;
  16 + &:first-child {
  17 + .bullet-separator {
  18 + display: none;
  19 + }
  20 + }
  21 + .bullet-separator {
  22 + font-size: 8px;
  23 + vertical-align: middle;
  24 + margin: 3px;
  25 + color: #B6C2CA;
  26 + }
  27 + }
  28 + }
12 29 .media-left {
13 30 min-width: 40px;
14 31 }
15 32 .media-body {
16   - background-color: #F9F9F9;
17   - padding: 10px;
  33 + padding: 0 10px 10px 0;
  34 + .reply-of {
  35 + font-size: 12px;
  36 + color: #B5B5B5;
  37 + margin-left: 5px;
  38 + i {
  39 + font-size: 10px;
  40 + }
  41 + }
  42 + h4 {
  43 + font-size: 16px;
  44 + }
18 45 }
19 46 noosfero-profile-image {
20 47 img {
... ... @@ -28,5 +55,24 @@
28 55 font-size: 1.7em;
29 56 }
30 57 }
  58 + // Limit identation of replies
  59 + .comments {
  60 + margin-left: 30px;
  61 + .comments .comments {
  62 + margin-left: 0px;
  63 + .comment {
  64 + margin-left: 0px;
  65 + }
  66 + }
  67 + }
  68 + .tooltip-inner {
  69 + max-width: 350px;
  70 + text-align: left;
  71 + .reply-tooltip {
  72 + .comment {
  73 + margin: 5px;
  74 + }
  75 + }
  76 + }
31 77 }
32 78 }
... ...
src/app/article/comment/comments.component.spec.ts
... ... @@ -17,38 +17,91 @@ describe(&quot;Components&quot;, () =&gt; {
17 17 commentService.getByArticle = jasmine.createSpy("getByArticle")
18 18 .and.returnValue(helpers.mocks.promiseResultTemplate({ data: comments }));
19 19  
20   - let providers = [
21   - new Provider('CommentService', { useValue: commentService }),
22   - new Provider('NotificationService', { useValue: helpers.mocks.notificationService })
23   - ].concat(helpers.provideFilters("translateFilter"));
24   -
25   - @Component({ selector: 'test-container-component', directives: [CommentsComponent], template: htmlTemplate, providers: providers })
26   - class ContainerComponent {
27   - article = { id: 1 };
  20 + let properties = { article: { id: 1 }, parent: <any>null };
  21 + function createComponent() {
  22 + let providers = [
  23 + helpers.createProviderToValue('CommentService', commentService),
  24 + helpers.createProviderToValue('NotificationService', helpers.mocks.notificationService),
  25 + helpers.createProviderToValue('SessionService', helpers.mocks.sessionWithCurrentUser({}))
  26 + ].concat(helpers.provideFilters("translateFilter"));
  27 +
  28 + return helpers.quickCreateComponent({
  29 + providers: providers,
  30 + directives: [CommentsComponent],
  31 + template: htmlTemplate,
  32 + properties: properties
  33 + });
28 34 }
29 35  
  36 +
30 37 it("render comments associated to an article", done => {
31   - helpers.createComponentFromClass(ContainerComponent).then(fixture => {
  38 + createComponent().then(fixture => {
32 39 expect(fixture.debugElement.queryAll("noosfero-comment").length).toEqual(2);
33 40 done();
34 41 });
35 42 });
36 43  
37 44 it("render a post comment tag", done => {
38   - helpers.createComponentFromClass(ContainerComponent).then(fixture => {
  45 + createComponent().then(fixture => {
39 46 expect(fixture.debugElement.queryAll("noosfero-post-comment").length).toEqual(1);
40 47 done();
41 48 });
42 49 });
43 50  
44   - it("update comments list when receive an event", done => {
45   - helpers.createComponentFromClass(ContainerComponent).then(fixture => {
46   - fixture.debugElement.getLocal("$rootScope").$emit(PostCommentComponent.EVENT_COMMENT_RECEIVED, { id: 1 });
47   - fixture.debugElement.getLocal("$rootScope").$apply();
  51 + it("update comments list when receive an reply", done => {
  52 + properties.parent = { id: 3 };
  53 + createComponent().then(fixture => {
  54 + (<CommentsComponent>fixture.debugElement.componentViewChildren[0].componentInstance).commentAdded(<noosfero.Comment>{ id: 1, reply_of: { id: 3 } });
  55 + fixture.detectChanges();
48 56 expect(fixture.debugElement.queryAll("noosfero-comment").length).toEqual(3);
49 57 done();
50 58 });
51 59 });
52 60  
  61 + it("load comments for next page", done => {
  62 + createComponent().then(fixture => {
  63 + let headers = jasmine.createSpy("headers").and.returnValue(3);
  64 + commentService.getByArticle = jasmine.createSpy("getByArticle")
  65 + .and.returnValue(helpers.mocks.promiseResultTemplate({ data: { id: 4 }, headers: headers }));
  66 + let component: CommentsComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
  67 + component.loadNextPage();
  68 + expect(component['page']).toEqual(3);
  69 + expect(component.comments.length).toEqual(3);
  70 + expect(component['total']).toEqual(3);
  71 + done();
  72 + });
  73 + });
  74 +
  75 + it("not display more when there is no more comments to load", done => {
  76 + createComponent().then(fixture => {
  77 + let component: CommentsComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
  78 + component['total'] = 0;
  79 + component.parent = null;
  80 + expect(component.displayMore()).toBeFalsy();
  81 + done();
  82 + });
  83 + });
  84 +
  85 + it("remove comment when calling commentRemoved", done => {
  86 + createComponent().then(fixture => {
  87 + let component: CommentsComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
  88 + let comment = { id: 1 };
  89 + component.comments = <any>[comment];
  90 + component.commentRemoved(<any>comment);
  91 + expect(component.comments).toEqual([]);
  92 + done();
  93 + });
  94 + });
  95 +
  96 + it("do nothing when call commentRemoved with a comment that doesn't belongs to the comments list", done => {
  97 + createComponent().then(fixture => {
  98 + let component: CommentsComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
  99 + let comment = { id: 1 };
  100 + component.comments = <any>[comment];
  101 + component.commentRemoved(<any>{ id: 2 });
  102 + expect(component.comments).toEqual([comment]);
  103 + done();
  104 + });
  105 + });
53 106 });
54 107 });
... ...
src/app/article/comment/comments.component.ts
... ... @@ -6,23 +6,81 @@ import { CommentComponent } from &quot;./comment.component&quot;;
6 6 @Component({
7 7 selector: 'noosfero-comments',
8 8 templateUrl: 'app/article/comment/comments.html',
9   - directives: [PostCommentComponent, CommentComponent]