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.

@@ -34,12 +34,21 @@ @@ -34,12 +34,21 @@
34 "angular-dynamic-locale": "^0.1.30", 34 "angular-dynamic-locale": "^0.1.30",
35 "angular-i18n": "^1.5.0", 35 "angular-i18n": "^1.5.0",
36 "angular-load": "^0.4.1", 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 "devDependencies": { 42 "devDependencies": {
40 "angular-mocks": "~1.5.0" 43 "angular-mocks": "~1.5.0"
41 }, 44 },
42 "overrides": { 45 "overrides": {
  46 + "ng-ckeditor": {
  47 + "main": [
  48 + "ng-ckeditor.js",
  49 + "libs/ckeditor/ckeditor.js"
  50 + ]
  51 + },
43 "bootstrap-sass": { 52 "bootstrap-sass": {
44 "main": [ 53 "main": [
45 "assets/stylesheets/_bootstrap.scss", 54 "assets/stylesheets/_bootstrap.scss",
@@ -6,6 +6,7 @@ var rename = require('gulp-rename'); @@ -6,6 +6,7 @@ var rename = require('gulp-rename');
6 var insert = require('gulp-insert'); 6 var insert = require('gulp-insert');
7 var merge = require('merge-stream'); 7 var merge = require('merge-stream');
8 var conf = require('./conf'); 8 var conf = require('./conf');
  9 +var languages = require('./languages');
9 10
10 var themeName = conf.paths.theme.replace('-', ' '); 11 var themeName = conf.paths.theme.replace('-', ' ');
11 themeName = themeName.charAt(0).toUpperCase() + themeName.slice(1); 12 themeName = themeName.charAt(0).toUpperCase() + themeName.slice(1);
@@ -16,25 +17,32 @@ var $ = require('gulp-load-plugins')({ @@ -16,25 +17,32 @@ var $ = require('gulp-load-plugins')({
16 }); 17 });
17 18
18 gulp.task('partials', function () { 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 gulp.task('html', ['inject', 'partials'], function () { 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 var partialsInjectOptions = { 46 var partialsInjectOptions = {
39 starttag: '<!-- inject:partials -->', 47 starttag: '<!-- inject:partials -->',
40 ignorePath: path.join(conf.paths.tmp, '/partials'), 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,6 +81,7 @@ gulp.task(&#39;html&#39;, [&#39;inject&#39;, &#39;partials&#39;], function () {
73 .pipe($.useref()) 81 .pipe($.useref())
74 .pipe($.revReplace({prefix: noosferoThemePrefix})) 82 .pipe($.revReplace({prefix: noosferoThemePrefix}))
75 .pipe(htmlFilter) 83 .pipe(htmlFilter)
  84 + .pipe($.replace('/bower_components/ng-ckeditor/libs/ckeditor/', noosferoThemePrefix + 'ng-ckeditor/libs/ckeditor/'))
76 .pipe($.minifyHtml({ 85 .pipe($.minifyHtml({
77 empty: true, 86 empty: true,
78 spare: true, 87 spare: true,
@@ -93,6 +102,11 @@ gulp.task(&#39;fonts&#39;, function () { @@ -93,6 +102,11 @@ gulp.task(&#39;fonts&#39;, function () {
93 .pipe(gulp.dest(path.join(conf.paths.dist, '/fonts/'))); 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 gulp.task('locale', function () { 110 gulp.task('locale', function () {
97 return gulp.src([ 111 return gulp.src([
98 path.join("bower_components/angular-i18n", '*.js'), 112 path.join("bower_components/angular-i18n", '*.js'),
@@ -124,6 +138,10 @@ gulp.task(&#39;clean-docs&#39;, [], function() { @@ -124,6 +138,10 @@ gulp.task(&#39;clean-docs&#39;, [], function() {
124 return $.del([path.join(conf.paths.docs, '/')]); 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 gulp.task('noosfero', ['html'], function () { 145 gulp.task('noosfero', ['html'], function () {
128 var layouts = gulp.src('layouts/**/*') 146 var layouts = gulp.src('layouts/**/*')
129 .pipe(gulp.dest(path.join(conf.paths.dist, "layouts"))); 147 .pipe(gulp.dest(path.join(conf.paths.dist, "layouts")));
@@ -136,4 +154,4 @@ gulp.task(&#39;noosfero&#39;, [&#39;html&#39;], function () { @@ -136,4 +154,4 @@ gulp.task(&#39;noosfero&#39;, [&#39;html&#39;], function () {
136 return merge(layouts, theme, index); 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']);
@@ -15,11 +15,13 @@ var path = require(&#39;path&#39;); @@ -15,11 +15,13 @@ var path = require(&#39;path&#39;);
15 */ 15 */
16 exports.paths = { 16 exports.paths = {
17 src: 'src', 17 src: 'src',
  18 + plugins: 'plugins',
18 dist: 'dist', 19 dist: 'dist',
19 tmp: '.tmp', 20 tmp: '.tmp',
20 e2e: 'e2e', 21 e2e: 'e2e',
21 docs: 'docs', 22 docs: 'docs',
22 - themes: 'themes' 23 + themes: 'themes',
  24 + languages: 'languages'
23 }; 25 };
24 exports.configTheme = function(theme) { 26 exports.configTheme = function(theme) {
25 exports.paths.theme = theme || "angular-default"; 27 exports.paths.theme = theme || "angular-default";
gulp/languages.js 0 → 100644
@@ -0,0 +1,29 @@ @@ -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,7 +45,7 @@ browserSync.use(browserSyncSpa({
45 selector: '[ng-app]'// Only needed for angular apps 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 var srcPaths = [path.join(conf.paths.tmp, '/serve')]; 49 var srcPaths = [path.join(conf.paths.tmp, '/serve')];
50 conf.paths.allSources.reverse().forEach(function(src) { 50 conf.paths.allSources.reverse().forEach(function(src) {
51 srcPaths.push(src); 51 srcPaths.push(src);
gulp/styles.js
@@ -31,6 +31,7 @@ var buildStyles = function() { @@ -31,6 +31,7 @@ var buildStyles = function() {
31 ]; 31 ];
32 conf.paths.allSources.forEach(function(src) { 32 conf.paths.allSources.forEach(function(src) {
33 srcPaths.push(path.join(src, '/app/**/*.scss')); 33 srcPaths.push(path.join(src, '/app/**/*.scss'));
  34 + srcPaths.push(path.join(src, conf.paths.plugins, '/**/*.scss'));
34 }); 35 });
35 var injectFiles = gulp.src(srcPaths, { read: false }); 36 var injectFiles = gulp.src(srcPaths, { read: false });
36 37
@@ -3,6 +3,7 @@ @@ -3,6 +3,7 @@
3 var path = require('path'); 3 var path = require('path');
4 var gulp = require('gulp'); 4 var gulp = require('gulp');
5 var conf = require('./conf'); 5 var conf = require('./conf');
  6 +var languages = require('./languages');
6 7
7 var browserSync = require('browser-sync'); 8 var browserSync = require('browser-sync');
8 9
@@ -14,10 +15,13 @@ gulp.task(&#39;watch&#39;, [&#39;inject&#39;], function () { @@ -14,10 +15,13 @@ gulp.task(&#39;watch&#39;, [&#39;inject&#39;], function () {
14 15
15 gulp.watch([path.join(conf.paths.src, '/*.html'), 'bower.json'], ['inject-reload']); 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 if(isOnlyChange(event)) { 25 if(isOnlyChange(event)) {
22 gulp.start('styles-reload'); 26 gulp.start('styles-reload');
23 } else { 27 } else {
@@ -33,9 +37,14 @@ gulp.task(&#39;watch&#39;, [&#39;inject&#39;], function () { @@ -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 var watchPaths = []; 44 var watchPaths = [];
37 conf.paths.allSources.forEach(function(src) { 45 conf.paths.allSources.forEach(function(src) {
38 watchPaths.push(path.join(src, '/app/**/*.html')); 46 watchPaths.push(path.join(src, '/app/**/*.html'));
  47 + watchPaths.push(path.join(src, conf.paths.plugins, '/**/*.html'));
39 }); 48 });
40 gulp.watch(watchPaths, function(event) { 49 gulp.watch(watchPaths, function(event) {
41 browserSync.reload(event.path); 50 browserSync.reload(event.path);
@@ -49,7 +49,7 @@ var _ = require(&#39;lodash&#39;); @@ -49,7 +49,7 @@ var _ = require(&#39;lodash&#39;);
49 var wiredep = require('wiredep'); 49 var wiredep = require('wiredep');
50 50
51 var pathSrcHtml = [ 51 var pathSrcHtml = [
52 - path.join('./src/app/**/*.html') 52 + path.join('./src/**/*.html')
53 ]; 53 ];
54 54
55 var glob = require("glob"); 55 var glob = require("glob");
@@ -126,6 +126,10 @@ module.exports = function (config) { @@ -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 if (coverage) { 133 if (coverage) {
130 134
131 /*configuration.webpack = { 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 <%= render file: Rails.root.join("app/views/layouts/application-ng.html.erb"), use_full_path: false %> 2 <%= render file: Rails.root.join("app/views/layouts/application-ng.html.erb"), use_full_path: false %>
3 <% else %> 3 <% else %>
4 <%= from_theme_include(current_theme, "index") %> 4 <%= from_theme_include(current_theme, "index") %>
@@ -49,6 +49,7 @@ @@ -49,6 +49,7 @@
49 "gulp-insert": "^0.5.0", 49 "gulp-insert": "^0.5.0",
50 "gulp-inject": "~3.0.0", 50 "gulp-inject": "~3.0.0",
51 "gulp-load-plugins": "~0.10.0", 51 "gulp-load-plugins": "~0.10.0",
  52 + "gulp-merge-json": "^0.4.0",
52 "gulp-minify-css": "~1.2.1", 53 "gulp-minify-css": "~1.2.1",
53 "gulp-minify-html": "~1.0.4", 54 "gulp-minify-html": "~1.0.4",
54 "gulp-ng-annotate": "~1.1.0", 55 "gulp-ng-annotate": "~1.1.0",
src/app/article/article-default-view-component.spec.ts
1 import {Input, provide, Component} from 'ng-forward'; 1 import {Input, provide, Component} from 'ng-forward';
2 import {ArticleViewComponent, ArticleDefaultViewComponent} from './article-default-view.component'; 2 import {ArticleViewComponent, ArticleDefaultViewComponent} from './article-default-view.component';
  3 +import {ComponentTestHelper, createClass} from './../../spec/component-test-helper';
3 4
4 import * as helpers from "../../spec/helpers"; 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,111 +10,114 @@ const htmlTemplate: string = &#39;&lt;noosfero-article [article]=&quot;ctrl.article&quot; [profil
9 10
10 describe("Components", () => { 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 import { bundle, Input, Inject, Component, Directive } from 'ng-forward'; 1 import { bundle, Input, Inject, Component, Directive } from 'ng-forward';
2 import {ArticleBlogComponent} from "./types/blog/blog.component"; 2 import {ArticleBlogComponent} from "./types/blog/blog.component";
3 import {CommentsComponent} from "./comment/comments.component"; 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 * @ngdoc controller 12 * @ngdoc controller
@@ -11,13 +17,37 @@ import {CommentsComponent} from &quot;./comment/comments.component&quot;; @@ -11,13 +17,37 @@ import {CommentsComponent} from &quot;./comment/comments.component&quot;;
11 */ 17 */
12 @Component({ 18 @Component({
13 selector: 'noosfero-default-article', 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 export class ArticleDefaultViewComponent { 24 export class ArticleDefaultViewComponent {
17 25
18 @Input() article: noosfero.Article; 26 @Input() article: noosfero.Article;
19 @Input() profile: noosfero.Profile; 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,7 +60,9 @@ export class ArticleDefaultViewComponent {
30 @Component({ 60 @Component({
31 selector: 'noosfero-article', 61 selector: 'noosfero-article',
32 template: 'not-used', 62 template: 'not-used',
33 - directives: [ArticleDefaultViewComponent, ArticleBlogComponent, CommentsComponent] 63 + directives: [ArticleDefaultViewComponent, ArticleBlogComponent,
  64 + CommentsComponent, MacroDirective, ArticleToolbarHotspotComponent,
  65 + ArticleContentHotspotComponent]
34 }) 66 })
35 @Inject("$element", "$scope", "$injector", "$compile") 67 @Inject("$element", "$scope", "$injector", "$compile")
36 export class ArticleViewComponent { 68 export class ArticleViewComponent {
@@ -40,7 +72,8 @@ export class ArticleViewComponent { @@ -40,7 +72,8 @@ export class ArticleViewComponent {
40 directiveName: string; 72 directiveName: string;
41 73
42 ngOnInit() { 74 ngOnInit() {
43 - let specificDirective = 'noosfero' + this.article.type; 75 + let articleType = this.article.type.replace(/::/, '');
  76 + let specificDirective = 'noosfero' + articleType;
44 this.directiveName = "noosfero-default-article"; 77 this.directiveName = "noosfero-default-article";
45 if (this.$injector.has(specificDirective + 'Directive')) { 78 if (this.$injector.has(specificDirective + 'Directive')) {
46 this.directiveName = specificDirective.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); 79 this.directiveName = specificDirective.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
@@ -53,6 +86,6 @@ export class ArticleViewComponent { @@ -53,6 +86,6 @@ export class ArticleViewComponent {
53 private $scope: ng.IScope, 86 private $scope: ng.IScope,
54 private $injector: ng.auto.IInjectorService, 87 private $injector: ng.auto.IInjectorService,
55 private $compile: ng.ICompileService) { 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 @@ @@ -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,6 +4,15 @@
4 </div> 4 </div>
5 5
6 <div class="sub-header clearfix"> 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 <div class="page-info pull-right small text-muted"> 16 <div class="page-info pull-right small text-muted">
8 <span class="time"> 17 <span class="time">
9 <i class="fa fa-clock-o"></i> <span am-time-ago="ctrl.article.created_at | dateFormat"></span> 18 <i class="fa fa-clock-o"></i> <span am-time-ago="ctrl.article.created_at | dateFormat"></span>
@@ -16,9 +25,9 @@ @@ -16,9 +25,9 @@
16 </span> 25 </span>
17 </div> 26 </div>
18 </div> 27 </div>
19 - 28 + <noosfero-hotspot-article-content [article]="ctrl.article"></noosfero-hotspot-article-content>
20 <div class="page-body"> 29 <div class="page-body">
21 - <div ng-bind-html="ctrl.article.body"></div> 30 + <div bind-html-compile="ctrl.article.body"></div>
22 </div> 31 </div>
23 32
24 <noosfero-comments [article]="ctrl.article"></noosfero-comments> 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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 import {Provider, provide, Component} from 'ng-forward'; 1 import {Provider, provide, Component} from 'ng-forward';
2 import * as helpers from "../../../spec/helpers"; 2 import * as helpers from "../../../spec/helpers";
3 -  
4 import {CommentComponent} from './comment.component'; 3 import {CommentComponent} from './comment.component';
5 4
6 const htmlTemplate: string = '<noosfero-comment [article]="ctrl.article" [comment]="ctrl.comment"></noosfero-comment>'; 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,37 +7,108 @@ const htmlTemplate: string = &#39;&lt;noosfero-comment [article]=&quot;ctrl.article&quot; [commen
8 describe("Components", () => { 7 describe("Components", () => {
9 describe("Comment Component", () => { 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 beforeEach(angular.mock.module("templates")); 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 it("render a comment", done => { 36 it("render a comment", done => {
20 - helpers.createComponentFromClass(ContainerComponent).then(fixture => { 37 + createComponent().then(fixture => {
21 expect(fixture.debugElement.queryAll(".comment").length).toEqual(1); 38 expect(fixture.debugElement.queryAll(".comment").length).toEqual(1);
22 done(); 39 done();
23 }); 40 });
24 }); 41 });
25 42
26 it("not render a post comment tag in the beginning", done => { 43 it("not render a post comment tag in the beginning", done => {
27 - helpers.createComponentFromClass(ContainerComponent).then(fixture => { 44 + createComponent().then(fixture => {
28 expect(fixture.debugElement.queryAll("noosfero-post-comment").length).toEqual(0); 45 expect(fixture.debugElement.queryAll("noosfero-post-comment").length).toEqual(0);
29 done(); 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 let component: CommentComponent = fixture.debugElement.componentViewChildren[0].componentInstance; 52 let component: CommentComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
36 component.reply(); 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 done(); 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 @Component({ 6 @Component({
4 selector: 'noosfero-comment', 7 selector: 'noosfero-comment',
  8 + outputs: ['commentRemoved'],
5 templateUrl: 'app/article/comment/comment.html' 9 templateUrl: 'app/article/comment/comment.html'
6 }) 10 })
  11 +@Inject(CommentService, NotificationService)
7 export class CommentComponent { 12 export class CommentComponent {
8 13
9 - @Input() comment: noosfero.Comment; 14 + @Input() comment: noosfero.CommentViewModel;
10 @Input() article: noosfero.Article; 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 reply() { 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,17 +9,24 @@
9 <a class="pull-left" ui-sref="main.profile.home({profile: ctrl.comment.author.identifier})"> 9 <a class="pull-left" ui-sref="main.profile.home({profile: ctrl.comment.author.identifier})">
10 <h4 class="media-heading">{{ctrl.comment.author.name}}</h4> 10 <h4 class="media-heading">{{ctrl.comment.author.name}}</h4>
11 </a> 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 <span class="date" am-time-ago="ctrl.comment.created_at | dateFormat"></span> 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 </div> 17 </div>
19 <div class="title">{{ctrl.comment.title}}</div> 18 <div class="title">{{ctrl.comment.title}}</div>
20 <div class="body">{{ctrl.comment.body}}</div> 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 </div> 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 </div> 32 </div>
src/app/article/comment/comment.scss
@@ -5,16 +5,43 @@ @@ -5,16 +5,43 @@
5 @extend .text-muted; 5 @extend .text-muted;
6 @extend .small; 6 @extend .small;
7 margin-left: 8px; 7 margin-left: 8px;
  8 + font-size: 12px;
8 } 9 }
9 .title { 10 .title {
10 font-weight: bold; 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 .media-left { 29 .media-left {
13 min-width: 40px; 30 min-width: 40px;
14 } 31 }
15 .media-body { 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 noosfero-profile-image { 46 noosfero-profile-image {
20 img { 47 img {
@@ -28,5 +55,24 @@ @@ -28,5 +55,24 @@
28 font-size: 1.7em; 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,38 +17,91 @@ describe(&quot;Components&quot;, () =&gt; {
17 commentService.getByArticle = jasmine.createSpy("getByArticle") 17 commentService.getByArticle = jasmine.createSpy("getByArticle")
18 .and.returnValue(helpers.mocks.promiseResultTemplate({ data: comments })); 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 it("render comments associated to an article", done => { 37 it("render comments associated to an article", done => {
31 - helpers.createComponentFromClass(ContainerComponent).then(fixture => { 38 + createComponent().then(fixture => {
32 expect(fixture.debugElement.queryAll("noosfero-comment").length).toEqual(2); 39 expect(fixture.debugElement.queryAll("noosfero-comment").length).toEqual(2);
33 done(); 40 done();
34 }); 41 });
35 }); 42 });
36 43
37 it("render a post comment tag", done => { 44 it("render a post comment tag", done => {
38 - helpers.createComponentFromClass(ContainerComponent).then(fixture => { 45 + createComponent().then(fixture => {
39 expect(fixture.debugElement.queryAll("noosfero-post-comment").length).toEqual(1); 46 expect(fixture.debugElement.queryAll("noosfero-post-comment").length).toEqual(1);
40 done(); 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 expect(fixture.debugElement.queryAll("noosfero-comment").length).toEqual(3); 56 expect(fixture.debugElement.queryAll("noosfero-comment").length).toEqual(3);
49 done(); 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,23 +6,81 @@ import { CommentComponent } from &quot;./comment.component&quot;;
6 @Component({ 6 @Component({
7 selector: 'noosfero-comments', 7 selector: 'noosfero-comments',
8 templateUrl: 'app/article/comment/comments.html', 8 templateUrl: 'app/article/comment/comments.html',
9 - directives: [PostCommentComponent, CommentComponent] 9 + directives: [PostCommentComponent, CommentComponent],
  10 + outputs: ['commentAdded']
10 }) 11 })
11 -@Inject(CommentService, "$rootScope") 12 +@Inject(CommentService, "$scope")
12 export class CommentsComponent { 13 export class CommentsComponent {
13 14
14 - comments: noosfero.Comment[] = []; 15 + comments: noosfero.CommentViewModel[] = [];
  16 + @Input() showForm = true;
15 @Input() article: noosfero.Article; 17 @Input() article: noosfero.Article;
  18 + @Input() parent: noosfero.CommentViewModel;
  19 + protected page = 1;
  20 + protected perPage = 5;
  21 + protected total = 0;
16 22
17 - constructor(private commentService: CommentService, private $rootScope: ng.IScope) {  
18 - $rootScope.$on(PostCommentComponent.EVENT_COMMENT_RECEIVED, (event: ng.IAngularEvent, comment: noosfero.Comment) => {  
19 - this.comments.push(comment); 23 + newComment = <noosfero.Comment>{};
  24 +
  25 + constructor(protected commentService: CommentService, private $scope: ng.IScope) { }
  26 +
  27 + ngOnInit() {
  28 + if (this.parent) {
  29 + this.comments = this.parent.replies;
  30 + } else {
  31 + this.loadNextPage();
  32 + }
  33 + }
  34 +
  35 + commentAdded(comment: noosfero.CommentViewModel): void {
  36 + comment.__show_reply = false;
  37 + if (comment.reply_of) {
  38 + this.comments.forEach((commentOnList) => {
  39 + if (commentOnList.id === comment.reply_of.id) {
  40 + if (commentOnList.replies) {
  41 + commentOnList.replies.push(comment);
  42 + } else {
  43 + commentOnList.replies = [comment];
  44 + }
  45 + }
  46 + });
  47 + }
  48 + this.comments.push(comment);
  49 + this.resetShowReply();
  50 + this.$scope.$apply();
  51 + }
  52 +
  53 + commentRemoved(comment: noosfero.Comment): void {
  54 + let index = this.comments.indexOf(comment, 0);
  55 + if (index >= 0) {
  56 + this.comments.splice(index, 1);
  57 + }
  58 + }
  59 +
  60 + private resetShowReply() {
  61 + this.comments.forEach((comment: noosfero.CommentViewModel) => {
  62 + comment.__show_reply = false;
20 }); 63 });
  64 + if (this.parent) {
  65 + this.parent.__show_reply = false;
  66 + }
  67 +
21 } 68 }
22 69
23 - ngOnInit() {  
24 - this.commentService.getByArticle(this.article).then((result: noosfero.RestResult<noosfero.Comment[]>) => {  
25 - this.comments = result.data; 70 + loadComments() {
  71 + return this.commentService.getByArticle(this.article, { page: this.page, per_page: this.perPage });
  72 + }
  73 +
  74 + loadNextPage() {
  75 + this.loadComments().then((result: noosfero.RestResult<noosfero.Comment[]>) => {
  76 + this.comments = this.comments.concat(result.data);
  77 + this.total = result.headers ? result.headers("total") : this.comments.length;
  78 + this.page++;
26 }); 79 });
27 } 80 }
  81 +
  82 + displayMore() {
  83 + let pages = Math.ceil(this.total / this.perPage);
  84 + return !this.parent && pages >= this.page;
  85 + }
28 } 86 }
src/app/article/comment/comments.html
1 <div class="comments"> 1 <div class="comments">
2 - <noosfero-post-comment [article]="ctrl.article"></noosfero-post-comment> 2 + <noosfero-post-comment (comment-saved)="ctrl.commentAdded($event.detail)" ng-if="ctrl.showForm" [article]="ctrl.article" [parent]="ctrl.parent" [comment]="ctrl.newComment"></noosfero-post-comment>
3 3
4 <div class="comments-list"> 4 <div class="comments-list">
5 - <noosfero-comment ng-repeat="comment in ctrl.comments" [comment]="comment" [article]="ctrl.article"></noosfero-comment> 5 + <noosfero-comment (comment-removed)="ctrl.commentRemoved($event.detail)" ng-repeat="comment in ctrl.comments | orderBy: 'created_at':true" [comment]="comment" [article]="ctrl.article"></noosfero-comment>
6 </div> 6 </div>
  7 + <button type="button" ng-if="ctrl.displayMore()" class="more-comments btn btn-default btn-block" ng-click="ctrl.loadNextPage()">{{"comment.pagination.more" | translate}}</button>
7 </div> 8 </div>
src/app/article/comment/comments.scss 0 → 100644
@@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
  1 +.comments {
  2 + border-top: 2px solid #F3F3F3;
  3 + .comments {
  4 + border-top: 0;
  5 + }
  6 +}
src/app/article/comment/post-comment/post-comment.component.spec.ts
1 import {Provider, provide, Component} from 'ng-forward'; 1 import {Provider, provide, Component} from 'ng-forward';
2 import * as helpers from "../../../../spec/helpers"; 2 import * as helpers from "../../../../spec/helpers";
3 -  
4 import {PostCommentComponent} from './post-comment.component'; 3 import {PostCommentComponent} from './post-comment.component';
5 4
6 const htmlTemplate: string = '<noosfero-post-comment [article]="ctrl.article" [reply-of]="ctrl.comment"></noosfero-post-comment>'; 5 const htmlTemplate: string = '<noosfero-post-comment [article]="ctrl.article" [reply-of]="ctrl.comment"></noosfero-post-comment>';
@@ -8,17 +7,21 @@ const htmlTemplate: string = &#39;&lt;noosfero-post-comment [article]=&quot;ctrl.article&quot; [r @@ -8,17 +7,21 @@ const htmlTemplate: string = &#39;&lt;noosfero-post-comment [article]=&quot;ctrl.article&quot; [r
8 describe("Components", () => { 7 describe("Components", () => {
9 describe("Post Comment Component", () => { 8 describe("Post Comment Component", () => {
10 9
  10 + let properties = { article: { id: 1, accept_comments: true } };
  11 +
11 beforeEach(angular.mock.module("templates")); 12 beforeEach(angular.mock.module("templates"));
12 13
13 let commentService = jasmine.createSpyObj("commentService", ["createInArticle"]); 14 let commentService = jasmine.createSpyObj("commentService", ["createInArticle"]);
  15 + let user = {};
14 let providers = [ 16 let providers = [
15 new Provider('CommentService', { useValue: commentService }), 17 new Provider('CommentService', { useValue: commentService }),
16 - new Provider('NotificationService', { useValue: helpers.mocks.notificationService }) 18 + new Provider('NotificationService', { useValue: helpers.mocks.notificationService }),
  19 + new Provider('SessionService', { useValue: helpers.mocks.sessionWithCurrentUser(user) })
17 ].concat(helpers.provideFilters("translateFilter")); 20 ].concat(helpers.provideFilters("translateFilter"));
18 21
19 @Component({ selector: 'test-container-component', directives: [PostCommentComponent], template: htmlTemplate, providers: providers }) 22 @Component({ selector: 'test-container-component', directives: [PostCommentComponent], template: htmlTemplate, providers: providers })
20 class ContainerComponent { 23 class ContainerComponent {
21 - article = { id: 1 }; 24 + article = properties['article'];
22 comment = { id: 2 }; 25 comment = { id: 2 };
23 } 26 }
24 27
@@ -29,13 +32,21 @@ describe(&quot;Components&quot;, () =&gt; { @@ -29,13 +32,21 @@ describe(&quot;Components&quot;, () =&gt; {
29 }); 32 });
30 }); 33 });
31 34
  35 + it("not render the post comment form when article doesn't accept comments", done => {
  36 + properties['article'].accept_comments = false;
  37 + helpers.createComponentFromClass(ContainerComponent).then(fixture => {
  38 + expect(fixture.debugElement.queryAll("form").length).toEqual(0);
  39 + done();
  40 + });
  41 + });
  42 +
32 it("emit an event when create comment", done => { 43 it("emit an event when create comment", done => {
33 helpers.createComponentFromClass(ContainerComponent).then(fixture => { 44 helpers.createComponentFromClass(ContainerComponent).then(fixture => {
34 let component: PostCommentComponent = fixture.debugElement.componentViewChildren[0].componentInstance; 45 let component: PostCommentComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
  46 + component.commentSaved.next = jasmine.createSpy("next");
35 commentService.createInArticle = jasmine.createSpy("createInArticle").and.returnValue(helpers.mocks.promiseResultTemplate({ data: {} })); 47 commentService.createInArticle = jasmine.createSpy("createInArticle").and.returnValue(helpers.mocks.promiseResultTemplate({ data: {} }));
36 - component["$rootScope"].$emit = jasmine.createSpy("$emit");  
37 component.save(); 48 component.save();
38 - expect(component["$rootScope"].$emit).toHaveBeenCalledWith(PostCommentComponent.EVENT_COMMENT_RECEIVED, jasmine.any(Object)); 49 + expect(component.commentSaved.next).toHaveBeenCalled();
39 done(); 50 done();
40 }); 51 });
41 }); 52 });
@@ -55,9 +66,9 @@ describe(&quot;Components&quot;, () =&gt; { @@ -55,9 +66,9 @@ describe(&quot;Components&quot;, () =&gt; {
55 helpers.createComponentFromClass(ContainerComponent).then(fixture => { 66 helpers.createComponentFromClass(ContainerComponent).then(fixture => {
56 let component: PostCommentComponent = fixture.debugElement.componentViewChildren[0].componentInstance; 67 let component: PostCommentComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
57 component.comment = <any>{ reply_of_id: null }; 68 component.comment = <any>{ reply_of_id: null };
58 - component.replyOf = <any>{ id: 10 }; 69 + component.parent = <any>{ id: 10 };
59 component.save(); 70 component.save();
60 - expect(component.comment.reply_of_id).toEqual(component.replyOf.id); 71 + expect(component.comment.reply_of_id).toEqual(component.parent.id);
61 done(); 72 done();
62 }); 73 });
63 }); 74 });
src/app/article/comment/post-comment/post-comment.component.ts
1 -import { Inject, Input, Component } from 'ng-forward'; 1 +import { Inject, Input, Output, EventEmitter, Component } from 'ng-forward';
2 import { CommentService } from "../../../../lib/ng-noosfero-api/http/comment.service"; 2 import { CommentService } from "../../../../lib/ng-noosfero-api/http/comment.service";
3 import { NotificationService } from "../../../shared/services/notification.service"; 3 import { NotificationService } from "../../../shared/services/notification.service";
  4 +import { SessionService } from "../../../login";
  5 +import { CommentFormHotspotComponent } from "../../../hotspot/comment-form-hotspot.component";
4 6
5 @Component({ 7 @Component({
6 selector: 'noosfero-post-comment', 8 selector: 'noosfero-post-comment',
7 - templateUrl: 'app/article/comment/post-comment/post-comment.html' 9 + templateUrl: 'app/article/comment/post-comment/post-comment.html',
  10 + outputs: ['commentSaved'],
  11 + directives: [CommentFormHotspotComponent]
8 }) 12 })
9 -@Inject(CommentService, NotificationService, "$rootScope") 13 +@Inject(CommentService, NotificationService, SessionService)
10 export class PostCommentComponent { 14 export class PostCommentComponent {
11 15
12 public static EVENT_COMMENT_RECEIVED = "comment.received"; 16 public static EVENT_COMMENT_RECEIVED = "comment.received";
13 17
14 @Input() article: noosfero.Article; 18 @Input() article: noosfero.Article;
15 - @Input() replyOf: noosfero.Comment; 19 + @Input() parent: noosfero.Comment;
  20 + @Output() commentSaved: EventEmitter<Comment> = new EventEmitter<Comment>();
  21 + @Input() comment = <noosfero.Comment>{};
  22 + private currentUser: noosfero.User;
16 23
17 - comment: noosfero.Comment;  
18 -  
19 - constructor(private commentService: CommentService, private notificationService: NotificationService, private $rootScope: ng.IScope) { } 24 + constructor(private commentService: CommentService,
  25 + private notificationService: NotificationService,
  26 + private session: SessionService) {
  27 + this.currentUser = this.session.currentUser();
  28 + }
20 29
21 save() { 30 save() {
22 - if (this.replyOf && this.comment) {  
23 - this.comment.reply_of_id = this.replyOf.id; 31 + if (this.parent && this.comment) {
  32 + this.comment.reply_of_id = this.parent.id;
24 } 33 }
25 this.commentService.createInArticle(this.article, this.comment).then((result: noosfero.RestResult<noosfero.Comment>) => { 34 this.commentService.createInArticle(this.article, this.comment).then((result: noosfero.RestResult<noosfero.Comment>) => {
26 - this.$rootScope.$emit(PostCommentComponent.EVENT_COMMENT_RECEIVED, result.data);  
27 - this.notificationService.success({ title: "Good job!", message: "Comment saved!" }); 35 + this.commentSaved.next(result.data);
  36 + this.comment.body = "";
  37 + this.notificationService.success({ title: "comment.post.success.title", message: "comment.post.success.message" });
28 }); 38 });
29 } 39 }
30 } 40 }
src/app/article/comment/post-comment/post-comment.html
1 -<form> 1 +<form class="clearfix post-comment" ng-if="ctrl.article.accept_comments">
2 <div class="form-group"> 2 <div class="form-group">
3 - <textarea class="form-control custom-control" rows="3" ng-model="ctrl.comment.body"></textarea> 3 + <div class="comment media">
  4 + <div class="media-left">
  5 + <a ui-sref="main.profile.home({profile: ctrl.currentUser.person.identifier})">
  6 + <noosfero-profile-image [profile]="ctrl.currentUser.person"></noosfero-profile-image>
  7 + </a>
  8 + </div>
  9 + <div class="media-body">
  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>
  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>
  13 + </div>
  14 + </div>
4 </div> 15 </div>
5 - <button type="submit" class="btn btn-default" ng-click="ctrl.save()">{{"comment.post" | translate}}</button>  
6 </form> 16 </form>
src/app/article/comment/post-comment/post-comment.scss 0 → 100644
@@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
  1 +.comments {
  2 + .post-comment {
  3 + .media {
  4 + padding-top: 10px;
  5 + .media-left {
  6 + padding: 10px 0;
  7 + }
  8 + button {
  9 + margin-top: 10px;
  10 + &.ng-hide-add {
  11 + animation: 0.5s lightSpeedOut ease;
  12 + }
  13 + &.ng-hide-remove {
  14 + animation: 0.5s lightSpeedIn ease;
  15 + }
  16 + }
  17 + }
  18 + }
  19 +}
src/app/article/content-viewer/content-viewer-actions.component.spec.ts
1 -import {providers} from 'ng-forward/cjs/testing/providers';  
2 -  
3 import {Input, Component, provide} from 'ng-forward'; 1 import {Input, Component, provide} from 'ng-forward';
4 2
5 import * as helpers from "../../../spec/helpers"; 3 import * as helpers from "../../../spec/helpers";
6 -  
7 -import {ComponentFixture} from 'ng-forward/cjs/testing/test-component-builder'; 4 +import {ComponentTestHelper, createClass} from '../../../spec/component-test-helper';
8 import {ContentViewerActionsComponent} from './content-viewer-actions.component'; 5 import {ContentViewerActionsComponent} from './content-viewer-actions.component';
9 6
10 // this htmlTemplate will be re-used between the container components in this spec file 7 // this htmlTemplate will be re-used between the container components in this spec file
@@ -12,56 +9,57 @@ const htmlTemplate: string = &#39;&lt;content-viewer-actions [article]=&quot;ctrl.article&quot; [ @@ -12,56 +9,57 @@ const htmlTemplate: string = &#39;&lt;content-viewer-actions [article]=&quot;ctrl.article&quot; [
12 9
13 describe('Content Viewer Actions Component', () => { 10 describe('Content Viewer Actions Component', () => {
14 11
15 - beforeEach(() => { 12 + let helper: ComponentTestHelper<ContentViewerActionsComponent>;
16 13
17 - angular.mock.module("templates"); 14 + beforeEach(angular.mock.module("templates"));
18 15
19 - providers((provide: any) => {  
20 - return <any>[  
21 - provide('ProfileService', {  
22 - useValue: helpers.mocks.profileService  
23 - })  
24 - ];  
25 - });  
26 - }); 16 + let providers = [
  17 + provide('ProfileService', {
  18 + useValue: helpers.mocks.profileService
  19 + }),
  20 + provide('ArticleService', {
  21 + useValue: helpers.mocks.articleService
  22 + })
  23 + ].concat(helpers.provideFilters("translateFilter"));
27 24
28 - let buildComponent = (): Promise<ComponentFixture> => {  
29 - return helpers.quickCreateComponent({  
30 - providers: [  
31 - helpers.provideEmptyObjects('Restangular'),  
32 - helpers.provideFilters('translateFilter')  
33 - ], 25 + beforeEach((done) => {
  26 + let cls = createClass({
  27 + template: htmlTemplate,
34 directives: [ContentViewerActionsComponent], 28 directives: [ContentViewerActionsComponent],
35 - template: htmlTemplate 29 + providers: providers
36 }); 30 });
37 - }; 31 + helper = new ComponentTestHelper<ContentViewerActionsComponent>(cls, done);
  32 + });
38 33
39 - it('renders content viewer actions directive', (done: Function) => {  
40 - buildComponent().then((fixture: ComponentFixture) => {  
41 - expect(fixture.debugElement.query('content-viewer-actions').length).toEqual(1); 34 + it('renders content viewer actions directive', () => {
  35 + expect(helper.all("content-viewer-actions").length).toEqual(1);
  36 + });
42 37
43 - done();  
44 - }); 38 + it('return article parent as container when it is not a folder', () => {
  39 + let article = <noosfero.Article>({ id: 1, type: 'TextArticle', parent: { id: 2 } });
  40 + expect(helper.component.getArticleContainer(article)).toEqual(2);
  41 + });
  42 +
  43 + it('return article as container when it is a folder', () => {
  44 + let article = <noosfero.Article>({ id: 1, type: 'Folder' });
  45 + expect(helper.component.getArticleContainer(article)).toEqual(1);
45 }); 46 });
46 47
47 - it('check if profile was loaded', (done: Function) => { 48 + it('return article as container when it is a blog', () => {
  49 + let article = <noosfero.Article>({ id: 1, type: 'Blog' });
  50 + expect(helper.component.getArticleContainer(article)).toEqual(1);
  51 + });
  52 +
  53 + it('check if profile was loaded', () => {
48 let profile: any = { 54 let profile: any = {
49 id: 1, 55 id: 1,
50 identifier: 'the-profile-test', 56 identifier: 'the-profile-test',
51 type: 'Person' 57 type: 'Person'
52 }; 58 };
53 -  
54 helpers.mocks.profileService.getCurrentProfile = () => { 59 helpers.mocks.profileService.getCurrentProfile = () => {
55 return helpers.mocks.promiseResultTemplate(profile); 60 return helpers.mocks.promiseResultTemplate(profile);
56 }; 61 };
57 -  
58 - buildComponent().then((fixture: ComponentFixture) => {  
59 - let contentViewerComp: ContentViewerActionsComponent = fixture.debugElement.componentViewChildren[0].componentInstance;  
60 -  
61 - expect(contentViewerComp.profile).toEqual(jasmine.objectContaining(profile));  
62 -  
63 - done();  
64 - }); 62 + let component = new ContentViewerActionsComponent(<any>helpers.mocks.profileService, <any>helpers.mocks.articleService);
  63 + expect(component.profile).toEqual(jasmine.objectContaining(profile));
65 }); 64 });
66 -  
67 }); 65 });
src/app/article/content-viewer/content-viewer-actions.component.ts
1 import {Component, Inject, provide} from "ng-forward"; 1 import {Component, Inject, provide} from "ng-forward";
2 import {ProfileService} from "../../../lib/ng-noosfero-api/http/profile.service"; 2 import {ProfileService} from "../../../lib/ng-noosfero-api/http/profile.service";
  3 +import {ArticleService} from "../../../lib/ng-noosfero-api/http/article.service";
3 4
4 @Component({ 5 @Component({
5 selector: "content-viewer-actions", 6 selector: "content-viewer-actions",
6 templateUrl: "app/article/content-viewer/navbar-actions.html", 7 templateUrl: "app/article/content-viewer/navbar-actions.html",
7 - providers: [provide('profileService', { useClass: ProfileService })] 8 + providers: [
  9 + provide('profileService', { useClass: ProfileService }),
  10 + provide('articleService', { useClass: ArticleService })
  11 + ]
8 }) 12 })
9 -@Inject(ProfileService) 13 +@Inject(ProfileService, ArticleService)
10 export class ContentViewerActionsComponent { 14 export class ContentViewerActionsComponent {
11 15
12 article: noosfero.Article; 16 article: noosfero.Article;
13 profile: noosfero.Profile; 17 profile: noosfero.Profile;
  18 + parentId: number;
14 19
15 - constructor(profileService: ProfileService) { 20 + constructor(profileService: ProfileService, articleService: ArticleService) {
16 profileService.getCurrentProfile().then((profile: noosfero.Profile) => { 21 profileService.getCurrentProfile().then((profile: noosfero.Profile) => {
17 this.profile = profile; 22 this.profile = profile;
  23 + return articleService.getCurrent();
  24 + }).then((article: noosfero.Article) => {
  25 + this.article = article;
  26 + this.parentId = this.getArticleContainer(article);
18 }); 27 });
19 } 28 }
  29 +
  30 + getArticleContainer(article: noosfero.Article) {
  31 + // FIXME get folder types from api
  32 + if (article.type === "Blog" || article.type === "Folder") {
  33 + return article.id;
  34 + } else if (article.parent) {
  35 + return article.parent.id;
  36 + }
  37 + }
20 } 38 }
src/app/article/content-viewer/content-viewer.component.ts
@@ -28,11 +28,12 @@ export class ContentViewerComponent { @@ -28,11 +28,12 @@ export class ContentViewerComponent {
28 } 28 }
29 29
30 activate() { 30 activate() {
31 - this.profileService.getCurrentProfile().then((profile: noosfero.Profile) => { 31 + this.profileService.getCurrentProfile().then((profile: noosfero.Profile) => {
32 this.profile = profile; 32 this.profile = profile;
33 return this.articleService.getArticleByProfileAndPath(this.profile, this.$stateParams["page"]); 33 return this.articleService.getArticleByProfileAndPath(this.profile, this.$stateParams["page"]);
34 }).then((result: noosfero.RestResult<any>) => { 34 }).then((result: noosfero.RestResult<any>) => {
35 this.article = <noosfero.Article>result.data; 35 this.article = <noosfero.Article>result.data;
  36 + this.articleService.setCurrent(this.article);
36 }); 37 });
37 } 38 }
38 } 39 }
src/app/article/content-viewer/navbar-actions.html
1 -<ul class="nav navbar-nav">  
2 - <li ng-show="vm.profile">  
3 - <a href="#" role="button" ui-sref="main.profile.cms({profile: vm.profile.identifier})">  
4 - <i class="fa fa-file fa-fw fa-lg"></i> {{"navbar.content_viewer_actions.new_post" | translate}}  
5 - </a>  
6 - </li> 1 +<ul class="nav navbar-nav" permission="vm.profile.permissions" permission-action="allow_edit">
  2 + <li class="dropdown profile-menu" uib-dropdown>
  3 + <a class="btn dropdown-toggle" data-toggle="dropdown" uib-dropdown-toggle>
  4 + {{"navbar.content_viewer_actions.new_item" | translate}}
  5 + <i class="fa fa-caret-down"></i>
  6 + </a>
  7 + <ul class="dropdown-menu" uib-dropdown-menu ng-show="vm.profile">
  8 + <li>
  9 + <a href="#" ui-sref="main.cms({profile: vm.profile.identifier, parent_id: vm.parentId})">
  10 + <i class="fa fa-file fa-fw fa-lg"></i> {{"navbar.content_viewer_actions.new_post" | translate}}
  11 + </a>
  12 + </li>
  13 + <li>
  14 + <a href="#" ui-sref="main.cms({profile: vm.profile.identifier, parent_id: vm.parentId, type: 'CommentParagraphPlugin::Discussion'})">
  15 + <i class="fa fa-file fa-fw fa-lg"></i> {{"navbar.content_viewer_actions.new_discussion" | translate}}
  16 + </a>
  17 + </li>
  18 + </ul>
  19 + </li>
7 </ul> 20 </ul>
src/app/article/content-viewer/navbar-actions.spec.ts 0 → 100644
@@ -0,0 +1,53 @@ @@ -0,0 +1,53 @@
  1 +import {TestComponentBuilder} from 'ng-forward/cjs/testing/test-component-builder';
  2 +import {Provider} from 'ng-forward';
  3 +import {ComponentTestHelper, createClass} from "./../../../spec/component-test-helper";
  4 +import {providers} from 'ng-forward/cjs/testing/providers';
  5 +import {ContentViewerActionsComponent} from '././content-viewer-actions.component';
  6 +import * as helpers from "../../../spec/helpers";
  7 +
  8 +const htmlTemplate: string = '<content-viewer-actions [article]="ctrl.article" [profile]="ctrl.profile"></content-viewer-actions>';
  9 +
  10 +describe("Components", () => {
  11 +
  12 + describe("Content Viewer Actions Component", () => {
  13 + let serviceMock = {
  14 + getEnvironmentPeople: (filters: any): any => {
  15 + return Promise.resolve([{ identifier: "person1" }]);
  16 + }
  17 + };
  18 + let providers = [
  19 + new Provider('ArticleService', { useValue: helpers.mocks.articleService }),
  20 + new Provider('ProfileService', { useValue: helpers.mocks.profileService })
  21 + ];
  22 +
  23 + let helper: ComponentTestHelper<ContentViewerActionsComponent>;
  24 +
  25 + beforeEach(angular.mock.module("templates"));
  26 +
  27 + /**
  28 + * The beforeEach procedure will initialize the helper and parse
  29 + * the component according to the given providers. Unfortunetly, in
  30 + * this mode, the providers and properties given to the construtor
  31 + * can't be overriden.
  32 + */
  33 + beforeEach((done) => {
  34 + // Create the component bed for the test. Optionally, this could be done
  35 + // in each test if one needs customization of these parameters per test
  36 + let cls = createClass({
  37 + template: htmlTemplate,
  38 + directives: [ContentViewerActionsComponent],
  39 + providers: providers,
  40 + properties: {}
  41 + });
  42 + helper = new ComponentTestHelper<ContentViewerActionsComponent>(cls, done);
  43 + });
  44 +
  45 + it("render the actions new item menu", () => {
  46 + expect(helper.all("a[class|='btn dropdown-toggle']")[0]).not.toBeNull();
  47 + });
  48 +
  49 + it("render two menu item actions", () => {
  50 + expect(helper.all("ul")[1].find("li").length).toBe(2);
  51 + });
  52 + });
  53 +});
src/app/article/index.ts
1 /* Module Index Entry - generated using the script npm run generate-index */ 1 /* Module Index Entry - generated using the script npm run generate-index */
2 export * from "./article-default-view.component"; 2 export * from "./article-default-view.component";
3 -export * from "./basic-editor.component";  
src/app/article/macro/macro.directive.spec.ts 0 → 100644
@@ -0,0 +1,25 @@ @@ -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 @@ @@ -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/article/types/blog/blog.component.spec.ts
1 -import {  
2 -providers  
3 -} from 'ng-forward/cjs/testing/providers';  
4 -  
5 -import {  
6 -Input,  
7 -Component  
8 -} from 'ng-forward';  
9 -import {  
10 -ArticleBlogComponent  
11 -} from './blog.component';  
12 -  
13 -import {  
14 -createComponentFromClass,  
15 -quickCreateComponent,  
16 -provideEmptyObjects,  
17 -createProviderToValue,  
18 -provideFilters  
19 -} from "../../../../spec/helpers.ts"; 1 +import {providers} from 'ng-forward/cjs/testing/providers';
  2 +
  3 +import {Input, provide, Component} from 'ng-forward';
  4 +import {ArticleBlogComponent} from './blog.component';
  5 +
  6 +import {createComponentFromClass, quickCreateComponent, provideEmptyObjects, createProviderToValue, provideFilters} from "../../../../spec/helpers.ts";
  7 +
  8 +import {ComponentTestHelper, createClass} from './../../../../spec/component-test-helper';
20 9
21 // this htmlTemplate will be re-used between the container components in this spec file 10 // this htmlTemplate will be re-used between the container components in this spec file
22 const htmlTemplate: string = '<noosfero-blog [article]="ctrl.article" [profile]="ctrl.profile"></noosfero-blog>'; 11 const htmlTemplate: string = '<noosfero-blog [article]="ctrl.article" [profile]="ctrl.profile"></noosfero-blog>';
@@ -42,38 +31,30 @@ describe(&quot;Blog Component&quot;, () =&gt; { @@ -42,38 +31,30 @@ describe(&quot;Blog Component&quot;, () =&gt; {
42 } 31 }
43 } 32 }
44 33
  34 + let article1 = <noosfero.Article>{
  35 + id: 1,
  36 + title: 'The article test'
  37 + };
  38 +
  39 + let article2 = <noosfero.Article>{
  40 + id: 1,
  41 + title: 'The article test'
  42 + };
  43 +
  44 + let articles = [ article1, article2 ];
  45 +
45 let articleService = { 46 let articleService = {
46 getChildren: (article_id: number, filters: {}) => { 47 getChildren: (article_id: number, filters: {}) => {
47 return promiseResultTemplate(null); 48 return promiseResultTemplate(null);
48 - } 49 + },
  50 + subscribeToArticleRemoved: (fn: Function) => {}
49 }; 51 };
50 52
51 - @Component({  
52 - selector: 'test-container-component',  
53 - template: htmlTemplate,  
54 - directives: [ArticleBlogComponent],  
55 - providers: [  
56 - provideEmptyObjects('Restangular'),  
57 - createProviderToValue('ArticleService', articleService),  
58 - provideFilters('truncateFilter')  
59 - ]  
60 - })  
61 - class BlogContainerComponent {  
62 - article = {  
63 - type: 'anyArticleType'  
64 - };  
65 - profile = {  
66 - name: 'profile-name'  
67 - };  
68 - } 53 + let helper: ComponentTestHelper<ArticleBlogComponent>;
69 54
70 - beforeEach(() => { 55 + beforeEach(angular.mock.module("templates"));
71 56
72 - // the karma preprocessor html2js transform the templates html into js files which put  
73 - // the templates to the templateCache into the module templates  
74 - // we need to load the module templates here as the template for the  
75 - // component Noosfero ArtileView will be load on our tests  
76 - angular.mock.module("templates"); 57 + beforeEach((done) => {
77 58
78 providers((provide: any) => { 59 providers((provide: any) => {
79 return <any>[ 60 return <any>[
@@ -82,48 +63,26 @@ describe(&quot;Blog Component&quot;, () =&gt; { @@ -82,48 +63,26 @@ describe(&quot;Blog Component&quot;, () =&gt; {
82 }) 63 })
83 ]; 64 ];
84 }); 65 });
85 - });  
86 -  
87 - it("renders the blog content", (done: Function) => {  
88 -  
89 - createComponentFromClass(BlogContainerComponent).then((fixture) => {  
90 -  
91 - expect(fixture.debugElement.query('div.blog').length).toEqual(1);  
92 -  
93 - done(); 66 + let providersHelper = [
  67 + provide('ArticleService', { useValue: articleService })
  68 + ];
  69 + let cls = createClass({
  70 + template: htmlTemplate,
  71 + directives: [ArticleBlogComponent],
  72 + providers: providersHelper,
  73 + properties: {
  74 + posts: articles
  75 + }
94 }); 76 });
  77 + helper = new ComponentTestHelper<ArticleBlogComponent>(cls, done);
95 }); 78 });
96 79
97 - it("verify the blog data", (done: Function) => {  
98 -  
99 - let articles = [{  
100 - id: 1,  
101 - title: 'The article test'  
102 - }];  
103 -  
104 - let result = { data: articles, headers: (name: string) => { return 1; } };  
105 -  
106 - // defining a mock result to articleService.getChildren method  
107 - articleService.getChildren = (article_id: number, filters: {}) => {  
108 - return promiseResultTemplate(result);  
109 - };  
110 -  
111 - createComponentFromClass(BlogContainerComponent).then((fixture) => {  
112 -  
113 - // gets the children component of BlogContainerComponent  
114 - let articleBlog: BlogContainerComponent = fixture.debugElement.componentViewChildren[0].componentInstance;  
115 -  
116 - // check if the component property are the provided by the mocked articleService  
117 - let post = {  
118 - id: 1,  
119 - title: 'The article test'  
120 - };  
121 - expect((<any>articleBlog)["posts"][0]).toEqual(jasmine.objectContaining(post));  
122 - expect((<any>articleBlog)["totalPosts"]).toEqual(1);  
123 -  
124 - done();  
125 - }); 80 + it("renders the blog content", () => {
  81 + expect(helper.debugElement.query('div.blog').length).toEqual(1);
  82 + });
126 83
  84 + it("verify the blog data", () => {
  85 + expect(helper.component["posts"][0]).toEqual(jasmine.objectContaining(article1));
127 }); 86 });
128 87
129 }); 88 });
130 \ No newline at end of file 89 \ No newline at end of file
src/app/article/types/blog/blog.component.ts
@@ -31,7 +31,7 @@ export class ArticleBlogComponent { @@ -31,7 +31,7 @@ export class ArticleBlogComponent {
31 31
32 loadPage() { 32 loadPage() {
33 let filters = { 33 let filters = {
34 - content_type: "TinyMceArticle", 34 + content_type: "TextArticle",
35 per_page: this.perPage, 35 per_page: this.perPage,
36 page: this.currentPage 36 page: this.currentPage
37 }; 37 };
src/app/article/types/blog/blog.html
@@ -17,8 +17,8 @@ @@ -17,8 +17,8 @@
17 </div> 17 </div>
18 </div> 18 </div>
19 19
20 - <pagination ng-model="ctrl.currentPage" total-items="ctrl.totalPosts" class="pagination-sm center-block" 20 + <uib-pagination ng-model="ctrl.currentPage" total-items="ctrl.totalPosts" class="pagination-sm center-block"
21 boundary-links="true" items-per-page="ctrl.perPage" ng-change="ctrl.loadPage()" 21 boundary-links="true" items-per-page="ctrl.perPage" ng-change="ctrl.loadPage()"
22 first-text="«" last-text="»" previous-text="‹" next-text="›"> 22 first-text="«" last-text="»" previous-text="‹" next-text="›">
23 - </pagination> 23 + </uib-pagination>
24 </div> 24 </div>
src/app/environment/environment-home.component.ts
@@ -22,7 +22,7 @@ export class EnvironmentHomeComponent { @@ -22,7 +22,7 @@ export class EnvironmentHomeComponent {
22 environment: noosfero.Environment; 22 environment: noosfero.Environment;
23 23
24 constructor(private environmentService: EnvironmentService, private $sce: ng.ISCEService) { 24 constructor(private environmentService: EnvironmentService, private $sce: ng.ISCEService) {
25 - environmentService.getByIdentifier("default").then((result: noosfero.Environment) => { 25 + environmentService.get().then((result: noosfero.Environment) => {
26 this.environment = result; 26 this.environment = result;
27 }); 27 });
28 } 28 }
src/app/environment/environment.component.spec.ts
@@ -9,6 +9,7 @@ describe(&quot;Components&quot;, () =&gt; { @@ -9,6 +9,7 @@ describe(&quot;Components&quot;, () =&gt; {
9 let environmentServiceMock: any; 9 let environmentServiceMock: any;
10 let notificationMock: any; 10 let notificationMock: any;
11 let $state: any; 11 let $state: any;
  12 + let defaultEnvironment = <any> {id: 1, name: 'Noosfero' };
12 13
13 beforeEach(inject((_$rootScope_: ng.IRootScopeService, _$q_: ng.IQService) => { 14 beforeEach(inject((_$rootScope_: ng.IRootScopeService, _$q_: ng.IQService) => {
14 $rootScope = _$rootScope_; 15 $rootScope = _$rootScope_;
@@ -17,44 +18,40 @@ describe(&quot;Components&quot;, () =&gt; { @@ -17,44 +18,40 @@ describe(&quot;Components&quot;, () =&gt; {
17 18
18 beforeEach(() => { 19 beforeEach(() => {
19 $state = jasmine.createSpyObj("$state", ["transitionTo"]); 20 $state = jasmine.createSpyObj("$state", ["transitionTo"]);
20 - environmentServiceMock = jasmine.createSpyObj("environmentServiceMock", ["getByIdentifier", "getBoxes"]); 21 + environmentServiceMock = jasmine.createSpyObj("environmentServiceMock", ["get", "getBoxes"]);
21 notificationMock = jasmine.createSpyObj("notificationMock", ["error"]); 22 notificationMock = jasmine.createSpyObj("notificationMock", ["error"]);
22 23
23 - let environmentResponse = $q.defer();  
24 - environmentResponse.resolve({ id: 1 });  
25 let getBoxesResponse = $q.defer(); 24 let getBoxesResponse = $q.defer();
26 getBoxesResponse.resolve({ data: { boxes: [{ id: 2 }] } }); 25 getBoxesResponse.resolve({ data: { boxes: [{ id: 2 }] } });
27 26
28 - environmentServiceMock.getByIdentifier = jasmine.createSpy('getByIdentifier').and.returnValue(environmentResponse.promise);  
29 environmentServiceMock.getBoxes = jasmine.createSpy("getBoxes").and.returnValue(getBoxesResponse.promise); 27 environmentServiceMock.getBoxes = jasmine.createSpy("getBoxes").and.returnValue(getBoxesResponse.promise);
30 }); 28 });
31 29
32 it("get the default environment", done => { 30 it("get the default environment", done => {
33 - let component: EnvironmentComponent = new EnvironmentComponent(environmentServiceMock, $state, notificationMock); 31 + let component: EnvironmentComponent = new EnvironmentComponent(environmentServiceMock, $state, notificationMock, defaultEnvironment);
34 $rootScope.$apply(); 32 $rootScope.$apply();
35 - expect(component.environment).toEqual({ id: 1 }); 33 + expect(component.environment).toEqual({ id: 1, name: 'Noosfero' });
36 done(); 34 done();
37 }); 35 });
38 36
39 it("get the environment boxes", done => { 37 it("get the environment boxes", done => {
40 - let component: EnvironmentComponent = new EnvironmentComponent(environmentServiceMock, $state, notificationMock); 38 + let component: EnvironmentComponent = new EnvironmentComponent(environmentServiceMock, $state, notificationMock, defaultEnvironment);
41 $rootScope.$apply(); 39 $rootScope.$apply();
42 expect(environmentServiceMock.getBoxes).toHaveBeenCalled(); 40 expect(environmentServiceMock.getBoxes).toHaveBeenCalled();
43 expect(component.boxes).toEqual({ data: { boxes: [{ id: 2 }] } }); 41 expect(component.boxes).toEqual({ data: { boxes: [{ id: 2 }] } });
44 done(); 42 done();
45 }); 43 });
46 44
47 - it("display notification error when the environment wasn't found", done => { 45 + it("display notification error when does not find boxes to the environment", done => {
48 let environmentResponse = $q.defer(); 46 let environmentResponse = $q.defer();
49 environmentResponse.reject(); 47 environmentResponse.reject();
50 48
51 - environmentServiceMock.getByIdentifier = jasmine.createSpy('getByIdentifier').and.returnValue(environmentResponse.promise); 49 + environmentServiceMock.getBoxes = jasmine.createSpy('getBoxes').and.returnValue(environmentResponse.promise);
52 50
53 - let component: EnvironmentComponent = new EnvironmentComponent(environmentServiceMock, $state, notificationMock); 51 + let component: EnvironmentComponent = new EnvironmentComponent(environmentServiceMock, $state, notificationMock, defaultEnvironment);
54 $rootScope.$apply(); 52 $rootScope.$apply();
55 53
56 expect(notificationMock.error).toHaveBeenCalled(); 54 expect(notificationMock.error).toHaveBeenCalled();
57 - expect(component.environment).toBeUndefined();  
58 done(); 55 done();
59 }); 56 });
60 57
src/app/environment/environment.component.ts
@@ -31,21 +31,23 @@ import {EnvironmentHomeComponent} from &quot;./environment-home.component&quot;; @@ -31,21 +31,23 @@ import {EnvironmentHomeComponent} from &quot;./environment-home.component&quot;;
31 } 31 }
32 } 32 }
33 ]) 33 ])
34 -@Inject(EnvironmentService, "$state") 34 +@Inject(EnvironmentService, "$state", "currentEnvironment")
35 export class EnvironmentComponent { 35 export class EnvironmentComponent {
36 36
37 boxes: noosfero.Box[]; 37 boxes: noosfero.Box[];
38 environment: noosfero.Environment; 38 environment: noosfero.Environment;
39 39
40 - constructor(environmentService: EnvironmentService, $state: ng.ui.IStateService, notificationService: NotificationService) {  
41 - let boxesPromisse = environmentService.getByIdentifier("default").then((environment: noosfero.Environment) => {  
42 - this.environment = environment;  
43 - return environmentService.getBoxes(this.environment.id);  
44 - }).then((boxes: noosfero.Box[]) => {  
45 - this.boxes = boxes;  
46 - }).catch(() => {  
47 - $state.transitionTo('main');  
48 - notificationService.error({ message: "notification.environment.not_found" });  
49 - }); 40 + constructor(private environmentService: EnvironmentService, private $state: ng.ui.IStateService, private notificationService: NotificationService, currentEnvironment: noosfero.Environment) {
  41 + this.environment = currentEnvironment;
  42 +
  43 + this.environmentService.getBoxes(this.environment.id)
  44 + .then((boxes: noosfero.Box[]) => {
  45 + this.boxes = boxes;
  46 + }).catch(() => {
  47 + this.$state.transitionTo('main');
  48 + this.notificationService.error({ message: "notification.environment.not_found" });
  49 + });
  50 +
50 } 51 }
  52 +
51 } 53 }
src/app/environment/environment.html
1 <div class="environment-container"> 1 <div class="environment-container">
2 <div class="row"> 2 <div class="row">
3 - <noosfero-boxes [boxes]="vm.boxes" [owner]="vm.environment"></noosfero-boxes> 3 + <noosfero-boxes ng-if="vm.boxes" [boxes]="vm.boxes" [owner]="vm.environment"></noosfero-boxes>
4 </div> 4 </div>
5 </div> 5 </div>
src/app/hotspot/article-content-hotspot.component.ts 0 → 100644
@@ -0,0 +1,25 @@ @@ -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-content",
  8 + template: "<span></span>"
  9 +})
  10 +@Inject("$element", "$scope", "$compile")
  11 +export class ArticleContentHotspotComponent 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_content");
  20 + }
  21 +
  22 + addHotspot(directiveName: string) {
  23 + this.$element.append(this.$compile('<' + directiveName + ' [article]="ctrl.article"></' + directiveName + '>')(this.$scope));
  24 + }
  25 +}
src/app/hotspot/article-toolbar-hotspot.component.ts 0 → 100644
@@ -0,0 +1,25 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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.scss
@@ -28,12 +28,16 @@ $primary-color-dark: #0288d1; @@ -28,12 +28,16 @@ $primary-color-dark: #0288d1;
28 $default-bg-hover-color: #f8f8f8; 28 $default-bg-hover-color: #f8f8f8;
29 $red-color: #e84e40; 29 $red-color: #e84e40;
30 $red-color-dark: #dd191d; 30 $red-color-dark: #dd191d;
  31 +$green-color: #8bc34a;
  32 +$green-color-dark: #689f38;
31 33
32 //GRID - media queries breakpoints 34 //GRID - media queries breakpoints
33 $break-xxs-min: 420px; 35 $break-xxs-min: 420px;
34 $break-xs-min: 768px; 36 $break-xs-min: 768px;
  37 +$break-sm-min: 992px;
35 38
36 $break-xxs-max: ($break-xxs-min - 1); 39 $break-xxs-max: ($break-xxs-min - 1);
  40 +$break-sm-max: ($break-sm-min - 1);
37 $break-xs-max: ($break-xs-min - 1); 41 $break-xs-max: ($break-xs-min - 1);
38 42
39 43
@@ -76,4 +80,5 @@ h1, h2, h3, h4, h5 { @@ -76,4 +80,5 @@ h1, h2, h3, h4, h5 {
76 @import "layout/scss/mixins"; 80 @import "layout/scss/mixins";
77 @import "layout/scss/bootstrap-overrides"; 81 @import "layout/scss/bootstrap-overrides";
78 @import "layout/scss/layout"; 82 @import "layout/scss/layout";
  83 +@import "layout/scss/sidebar";
79 @import "layout/scss/tables"; 84 @import "layout/scss/tables";
src/app/index.ts
1 -import {NoosferoApp} from "./index.module"; 1 +import {bootstrap} from "ng-forward";
2 import {noosferoModuleConfig} from "./index.config"; 2 import {noosferoModuleConfig} from "./index.config";
3 import {noosferoAngularRunBlock} from "./index.run"; 3 import {noosferoAngularRunBlock} from "./index.run";
4 -  
5 import {MainComponent} from "./main/main.component"; 4 import {MainComponent} from "./main/main.component";
6 -import {bootstrap, bundle} from "ng-forward";  
7 -  
8 -import {AUTH_EVENTS} from "./login/auth-events";  
9 -import {AuthController} from "./login/auth.controller";  
10 -  
11 -import {Navbar} from "./layout/navbar/navbar"; 5 +import {AuthEvents} from "./login/auth-events";
12 6
13 declare var moment: any; 7 declare var moment: any;
14 8
15 -let noosferoApp: any = bundle("noosferoApp", MainComponent, ["ngAnimate", "ngCookies", "ngStorage", "ngTouch",  
16 - "ngSanitize", "ngMessages", "ngAria", "restangular",  
17 - "ui.router", "ui.bootstrap", "toastr",  
18 - "angularMoment", "angular.filter", "akoenig.deckgrid",  
19 - "angular-timeline", "duScroll", "oitozero.ngSweetAlert",  
20 - "pascalprecht.translate", "tmh.dynamicLocale", "angularLoad"]).publish();  
21 -  
22 -NoosferoApp.angularModule = noosferoApp;  
23 -  
24 -  
25 -NoosferoApp.addConstants("moment", moment);  
26 -NoosferoApp.addConstants("AUTH_EVENTS", AUTH_EVENTS);  
27 -  
28 -NoosferoApp.addConfig(noosferoModuleConfig);  
29 -NoosferoApp.run(noosferoAngularRunBlock); 9 +//FIXME see a better way to declare template modules for dev mode
  10 +try {
  11 + angular.module('noosfero.templates.app');
  12 +} catch (error) {
  13 + angular.module('noosfero.templates.app', []);
  14 +}
  15 +try {
  16 + angular.module('noosfero.templates.plugins');
  17 +} catch (error) {
  18 + angular.module('noosfero.templates.plugins', []);
  19 +}
  20 +angular.module('noosfero.init', ['noosfero.templates.app', 'noosfero.templates.plugins']).
  21 + config(noosferoModuleConfig).
  22 + run(noosferoAngularRunBlock).
  23 + constant("moment", moment).
  24 + constant("AuthEvents", AuthEvents);
  25 +bootstrap(MainComponent);
src/app/layout/blocks/block.component.ts
@@ -11,7 +11,7 @@ export class BlockComponent { @@ -11,7 +11,7 @@ export class BlockComponent {
11 @Input() owner: any; 11 @Input() owner: any;
12 12
13 ngOnInit() { 13 ngOnInit() {
14 - let blockName = (this.block && this.block.type) ? this.block.type.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() : "default-block"; 14 + let blockName = (this.block && this.block.type) ? this.block.type.replace(/::/, '').replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() : "default-block";
15 this.$element.replaceWith(this.$compile('<noosfero-' + blockName + ' [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-' + blockName + '>')(this.$scope)); 15 this.$element.replaceWith(this.$compile('<noosfero-' + blockName + ' [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-' + blockName + '>')(this.$scope));
16 } 16 }
17 17
src/app/layout/blocks/communities/communities-block.component.spec.ts 0 → 100644
@@ -0,0 +1,51 @@ @@ -0,0 +1,51 @@
  1 +import {TestComponentBuilder} from 'ng-forward/cjs/testing/test-component-builder';
  2 +import {Provider, Input, provide, Component} from 'ng-forward';
  3 +
  4 +import {CommunitiesBlockComponent} from './communities-block.component';
  5 +
  6 +const htmlTemplate: string = '<noosfero-communities-block [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-communities-block>';
  7 +
  8 +const tcb = new TestComponentBuilder();
  9 +
  10 +describe("Components", () => {
  11 + describe("Communities Block Component", () => {
  12 +
  13 + beforeEach(angular.mock.module("templates"));
  14 +
  15 + let state = jasmine.createSpyObj("state", ["go"]);
  16 + let providers = [
  17 + new Provider('truncateFilter', { useValue: () => { } }),
  18 + new Provider('stripTagsFilter', { useValue: () => { } }),
  19 + new Provider('$state', { useValue: state }),
  20 + new Provider('CommunityService', {
  21 + useValue: {
  22 + getByOwner: (owner: any, params: any): any => {
  23 + return Promise.resolve({ data: [{ identifier: "community1" }] });
  24 + }
  25 + }
  26 + }),
  27 + ];
  28 + @Component({ selector: 'test-container-component', template: htmlTemplate, directives: [CommunitiesBlockComponent], providers: providers })
  29 + class BlockContainerComponent {
  30 + block = { type: 'Block', settings: {} };
  31 + owner = { name: 'profile-name' };
  32 + }
  33 +
  34 + it("get communities", done => {
  35 + tcb.createAsync(BlockContainerComponent).then(fixture => {
  36 + let block: CommunitiesBlockComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
  37 + expect(block.profiles).toEqual([{ identifier: "community1" }]);
  38 + done();
  39 + });
  40 + });
  41 +
  42 + it("render the profile image for each community", done => {
  43 + tcb.createAsync(BlockContainerComponent).then(fixture => {
  44 + fixture.debugElement.getLocal("$rootScope").$apply();
  45 + expect(fixture.debugElement.queryAll("noosfero-profile-image").length).toEqual(1);
  46 + done();
  47 + });
  48 + });
  49 +
  50 + });
  51 +});
src/app/layout/blocks/communities/communities-block.component.ts 0 → 100644
@@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
  1 +import {Input, Inject, Component} from "ng-forward";
  2 +import {CommunityService} from "../../../../lib/ng-noosfero-api/http/community.service";
  3 +
  4 +@Component({
  5 + selector: "noosfero-communities-block",
  6 + templateUrl: 'app/layout/blocks/communities/communities-block.html',
  7 +})
  8 +@Inject(CommunityService)
  9 +export class CommunitiesBlockComponent {
  10 +
  11 + @Input() block: noosfero.Block;
  12 + @Input() owner: noosfero.Profile;
  13 +
  14 + profiles: any = [];
  15 +
  16 + constructor(private communityService: CommunityService) { }
  17 +
  18 + ngOnInit() {
  19 + let limit: number = ((this.block && this.block.settings) ? this.block.settings.limit : null) || 5;
  20 + this.communityService.getByOwner(this.owner, { limit: limit }).then((result: noosfero.RestResult<noosfero.Community[]>) => {
  21 + this.profiles = result.data;
  22 + });
  23 + }
  24 +}
src/app/layout/blocks/communities/communities-block.html 0 → 100644
@@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
  1 +<div class="communities-block">
  2 + <a ng-repeat="profile in ctrl.profiles" ui-sref="main.profile.home({profile: profile.identifier})" class="profile">
  3 + <noosfero-profile-image [profile]="profile"></noosfero-profile-image>
  4 + </a>
  5 +</div>
src/app/layout/blocks/communities/communities-block.scss 0 → 100644
@@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
  1 +.communities-block {
  2 + .profile {
  3 + margin: 10px;
  4 + img, i.profile-image {
  5 + width: 60px;
  6 + }
  7 + img {
  8 + display: inline-block;
  9 + vertical-align: top;
  10 + }
  11 + i.profile-image {
  12 + text-align: center;
  13 + font-size: 4.5em;
  14 + }
  15 + }
  16 +}
src/app/layout/blocks/communities/index.ts 0 → 100644
@@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
  1 +/* Module Index Entry - generated using the script npm run generate-index */
  2 +export * from "./communities-block.component";
src/app/layout/blocks/display-content/display-content-block.component.spec.ts 0 → 100644
@@ -0,0 +1,75 @@ @@ -0,0 +1,75 @@
  1 +import {TestComponentBuilder} from 'ng-forward/cjs/testing/test-component-builder';
  2 +import {Provider, provide} from 'ng-forward';
  3 +import {ComponentTestHelper, createClass} from './../../../../spec/component-test-helper';
  4 +import {providers} from 'ng-forward/cjs/testing/providers';
  5 +import {DisplayContentBlockComponent} from './display-content-block.component';
  6 +import * as helpers from './../../../../spec/helpers';
  7 +
  8 +const htmlTemplate: string = '<noosfero-display-content-block [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-display-content-block>';
  9 +
  10 +describe("Components", () => {
  11 +
  12 + describe("Display Content Block Component", () => {
  13 + let state = jasmine.createSpyObj("state", ["go"]);
  14 + let providers = [
  15 + provide('ArticleService', {
  16 + useValue: helpers.mocks.articleService
  17 + }),
  18 + provide('$state', { useValue: state })
  19 + ].concat(helpers.provideFilters("translateFilter"));
  20 +
  21 + let sections: noosfero.Section[] = [
  22 + { value: 'abstract', checked: 'abstract'},
  23 + { value: 'title', checked: 'title' }
  24 + ];
  25 + let settings: noosfero.Settings = {
  26 + limit: 6,
  27 + sections: sections
  28 + };
  29 +
  30 + let helper: ComponentTestHelper<DisplayContentBlockComponent>;
  31 +
  32 + beforeEach(angular.mock.module("templates"));
  33 +
  34 + /**
  35 + * The beforeEach procedure will initialize the helper and parse
  36 + * the component according to the given providers. Unfortunetly, in
  37 + * this mode, the providers and properties given to the construtor
  38 + * can't be overriden.
  39 + */
  40 + beforeEach((done) => {
  41 + // Create the component bed for the test. Optionally, this could be done
  42 + // in each test if one needs customization of these parameters per test
  43 + let cls = createClass({
  44 + template: htmlTemplate,
  45 + directives: [DisplayContentBlockComponent],
  46 + providers: providers,
  47 + properties: {
  48 + block: {
  49 + settings: settings
  50 + }
  51 + }
  52 + });
  53 + helper = new ComponentTestHelper<DisplayContentBlockComponent>(cls, done);
  54 + });
  55 +
  56 + it("verify settings is injected", () => {
  57 + expect(helper.component.block).not.toBeNull;
  58 + expect(helper.component.block.settings).not.toBeNull;
  59 + expect(helper.component.block.settings.limit).toEqual(6);
  60 + expect(helper.component.block.settings.sections.length).toEqual(3);
  61 + });
  62 +
  63 + it("verify abstract is displayed", () => {
  64 + expect(helper.all("div[ng-bind-html|='article.abstract']")[0]).not.toBeNull;
  65 + });
  66 +
  67 + it("verify title is displayed", () => {
  68 + expect(helper.all("div > h5")[0]).not.toBeNull;
  69 + });
  70 +
  71 + it("verify body is not displayed", () => {
  72 + expect(helper.all("div[ng-bind-html|='article.body']")[0]).toBeNull;
  73 + });
  74 + });
  75 +});
src/app/layout/blocks/display-content/display-content-block.component.ts 0 → 100644
@@ -0,0 +1,54 @@ @@ -0,0 +1,54 @@
  1 +import {Input, Inject, Component} from "ng-forward";
  2 +import {ArticleService} from "../../../../lib/ng-noosfero-api/http/article.service";
  3 +
  4 +@Component({
  5 + selector: "noosfero-display-content-block",
  6 + templateUrl: 'app/layout/blocks/display-content/display-content-block.html',
  7 +})
  8 +@Inject(ArticleService, "$state")
  9 +export class DisplayContentBlockComponent {
  10 +
  11 + @Input() block: noosfero.Block;
  12 + @Input() owner: noosfero.Profile;
  13 +
  14 + profile: noosfero.Profile;
  15 + articles: noosfero.Article[];
  16 + sections: noosfero.Section[];
  17 +
  18 + documentsLoaded: boolean = false;
  19 +
  20 + constructor(private articleService: ArticleService, private $state: ng.ui.IStateService) {}
  21 +
  22 + ngOnInit() {
  23 + this.profile = this.owner;
  24 + let limit = ((this.block && this.block.settings) ? this.block.settings.limit : null) || 5;
  25 + this.articleService.getByProfile(this.profile, { content_type: 'TinyMceArticle', per_page: limit })
  26 + .then((result: noosfero.RestResult<noosfero.Article[]>) => {
  27 + this.articles = <noosfero.Article[]>result.data;
  28 + this.sections = this.block.settings.sections;
  29 + // Add sections not defined by Noosfero API
  30 + this.addDefaultSections();
  31 + this.documentsLoaded = true;
  32 + });
  33 + }
  34 +
  35 + /**
  36 + * This configuration doesn't exists on Noosfero. Statically typing here.
  37 + */
  38 + private addDefaultSections() {
  39 + let author: noosfero.Section = <noosfero.Section>{ value: 'author', checked: 'author' };
  40 + this.sections.push(author);
  41 + }
  42 +
  43 + /**
  44 + * Returns whether a settings section should be displayed.
  45 + *
  46 + */
  47 + private display(section_name: string): boolean {
  48 + let section: noosfero.Section = this.sections.find( function(section: noosfero.Section) {
  49 + return section.value === section_name;
  50 + });
  51 + return section !== undefined && section.checked !== undefined;
  52 + }
  53 +
  54 +}
src/app/layout/blocks/display-content/display-content-block.html 0 → 100644
@@ -0,0 +1,46 @@ @@ -0,0 +1,46 @@
  1 +<div class="{{ctrl.type}}-block">
  2 + <div ng-repeat="article in ctrl.articles" ui-sref="main.profile.page({profile: ctrl.profile.identifier, page: article.path})"" class="article">
  3 + <!-- Article Title -->
  4 + <div class="page-header" ng-if="ctrl.display('title')">
  5 + <h5 class="title media-heading" ng-bind="article.title"></h3>
  6 + </div>
  7 +
  8 + <div class="sub-header clearfix">
  9 + <!-- Article Abstract and Read More Link -->
  10 + <div class="post-lead" ng-if="ctrl.display('abstract')">
  11 + <div ng-bind-html="article.abstract"></div>
  12 + <a href="#" ui-sref="main.profile.page({profile: ctrl.profile.identifier, page: article.path})">
  13 + <i class="fa fa-pencil-square-o fa-fw fa-lg"></i> {{"article.actions.read_more" | translate}}
  14 + </a>
  15 + </div>
  16 + <div class="page-info pull-right small text-muted" ng-if="ctrl.display('publish_date')">
  17 + <!-- Article Published Date -->
  18 + <span class="time">
  19 + <i class="fa fa-clock-o"></i> <span am-time-ago="article.created_at | dateFormat"></span>
  20 + </span>
  21 + <!-- Article Author -->
  22 + <span class="author" ng-if="ctrl.display('author')">
  23 + <i class="fa fa-user"></i>
  24 + <a ui-sref="main.profile.home({profile: article.author.identifier})" ng-if="article.author">
  25 + <span class="author-name" ng-bind="article.author.name"></span>
  26 + </a>
  27 + </span>
  28 + </div>
  29 + </div>
  30 +
  31 + <div class="post-lead">
  32 + <!-- Article Image -->
  33 + <img ng-show="ctrl.display('image')" ng-src="{{article.image.url}}" class="img-responsive article-image">
  34 + <!-- Article Body -->
  35 + <div ng-bind-html="article.body" ng-show="ctrl.display('body')"></div>
  36 + </div>
  37 +
  38 + <!-- Article Tags -->
  39 + <div ng-if="ctrl.display('tags')" class="post-lead">
  40 + <div class="label" ng-repeat="tag in article.tag_list">
  41 + <span class="badge" ng-bind="tag"></span>
  42 + </div>
  43 + </div>
  44 +
  45 + </div>
  46 +</div>
src/app/layout/blocks/display-content/display-content-block.scss 0 → 100644
@@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
  1 +.members-block {
  2 + .member {
  3 + img, i.profile-image {
  4 + width: 60px;
  5 + }
  6 + img {
  7 + display: inline-block;
  8 + vertical-align: top;
  9 + }
  10 + i.profile-image {
  11 + text-align: center;
  12 + background-color: #889DB1;
  13 + color: #F1F1F1;
  14 + font-size: 4.5em;
  15 + }
  16 + }
  17 +}
src/app/layout/blocks/display-content/index.ts 0 → 100644
@@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
  1 +/* Module Index Entry - generated using the script npm run generate-index */
  2 +export * from "./display-content-block.component";
src/app/layout/blocks/link-list/index.ts
1 /* Module Index Entry - generated using the script npm run generate-index */ 1 /* Module Index Entry - generated using the script npm run generate-index */
2 -export * from "./link-list.component"; 2 +export * from "./link-list-block.component";
src/app/layout/blocks/link-list/link-list-block.component.spec.ts 0 → 100644
@@ -0,0 +1,65 @@ @@ -0,0 +1,65 @@
  1 +import {TestComponentBuilder} from 'ng-forward/cjs/testing/test-component-builder';
  2 +import {Pipe, Input, provide, Component} from 'ng-forward';
  3 +import {provideFilters} from '../../../../spec/helpers';
  4 +
  5 +import {LinkListBlockComponent} from './link-list-block.component';
  6 +
  7 +const tcb = new TestComponentBuilder();
  8 +
  9 +const htmlTemplate: string = '<noosfero-link-list-block [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-link-list-block>';
  10 +
  11 +
  12 +describe("Components", () => {
  13 +
  14 + describe("Link List Block Component", () => {
  15 +
  16 + beforeEach(angular.mock.module("templates"));
  17 +
  18 + it("receives the block and the owner as inputs", done => {
  19 +
  20 + // Creating a container component (BlockContainerComponent) to include
  21 + // the component under test (Block)
  22 + @Component({ selector: 'test-container-component', template: htmlTemplate, directives: [LinkListBlockComponent] })
  23 + class BlockContainerComponent {
  24 + block = { type: 'Block' };
  25 + owner = { name: 'profile-name' };
  26 + constructor() {
  27 + }
  28 + }
  29 +
  30 + // uses the TestComponentBuilder instance to initialize the component
  31 + // .overrideView(LinkListBlock, { template: 'asdasdasd', pipes: [NoosferoTemplate] })
  32 + tcb.createAsync(BlockContainerComponent).then(fixture => {
  33 + // and here we can inspect and run the test assertions
  34 + let myComponent: LinkListBlockComponent = fixture.componentInstance;
  35 +
  36 + // assure the block object inside the Block matches
  37 + // the provided through the parent component
  38 + expect(myComponent.block.type).toEqual("Block");
  39 + expect(myComponent.owner.name).toEqual("profile-name");
  40 + done();
  41 + });
  42 + });
  43 +
  44 +
  45 + it("display links stored in block settings", done => {
  46 +
  47 + @Component({
  48 + selector: 'test-container-component',
  49 + template: htmlTemplate,
  50 + directives: [LinkListBlockComponent],
  51 + providers: provideFilters("noosferoTemplateFilter")
  52 + })
  53 + class CustomBlockType {
  54 + block: any = { settings: { links: [{ name: 'link1', address: 'address1' }, { name: 'link2', address: 'address2' }] } };
  55 + owner: any = { name: 'profile-name' };
  56 + }
  57 + tcb.createAsync(CustomBlockType).then(fixture => {
  58 + expect(fixture.debugElement.queryAll(".link-list-block a").length).toEqual(2);
  59 + done();
  60 + });
  61 + });
  62 +
  63 + });
  64 +
  65 +});
src/app/layout/blocks/link-list/link-list-block.component.ts 0 → 100644
@@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
  1 +import {Component, Input} from "ng-forward";
  2 +
  3 +@Component({
  4 + selector: "noosfero-link-list-block",
  5 + templateUrl: "app/layout/blocks/link-list/link-list-block.html"
  6 +})
  7 +export class LinkListBlockComponent {
  8 +
  9 + @Input() block: any;
  10 + @Input() owner: any;
  11 +
  12 + links: any;
  13 +
  14 + ngOnInit() {
  15 + if (this.block && this.block.settings) {
  16 + this.links = this.block.settings.links;
  17 + }
  18 + }
  19 +
  20 +}
src/app/layout/blocks/link-list/link-list-block.html 0 → 100644
@@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
  1 +<div class="link-list-block">
  2 + <div ng-repeat="link in ctrl.links">
  3 + <a ng-href="{{link.address | noosferoTemplate:{profile: ctrl.owner.identifier} }}">
  4 + <i class="fa fa-fw icon-{{link.icon}}"></i> <span>{{link.name}}</span>
  5 + </a>
  6 + </div>
  7 +</div>
src/app/layout/blocks/link-list/link-list-block.scss 0 → 100644
@@ -0,0 +1,34 @@ @@ -0,0 +1,34 @@
  1 +.icon-event {
  2 + @extend .fa-calendar;
  3 +}
  4 +.icon-photos {
  5 + @extend .fa-photo;
  6 +}
  7 +.icon-edit {
  8 + @extend .fa-edit;
  9 +}
  10 +.icon-ok {
  11 + @extend .fa-check;
  12 +}
  13 +.icon-send {
  14 + @extend .fa-send-o;
  15 +}
  16 +.icon-menu-people {
  17 + @extend .fa-user;
  18 +}
  19 +.icon-forum {
  20 + @extend .fa-users;
  21 +}
  22 +.icon-new {
  23 + @extend .fa-file-o;
  24 +}
  25 +.icon-save {
  26 + @extend .fa-save;
  27 +}
  28 +
  29 +.link-list-block {
  30 + a i {
  31 + line-height: 25px;
  32 + color: #949494;
  33 + }
  34 +}
src/app/layout/blocks/login-block/index.ts 0 → 100644
src/app/layout/blocks/login-block/login-block.component.spec.ts 0 → 100644
@@ -0,0 +1,110 @@ @@ -0,0 +1,110 @@
  1 +import {TestComponentBuilder} from 'ng-forward/cjs/testing/test-component-builder';
  2 +import {Injectable, Provider, provide} from "ng-forward";
  3 +import {ComponentTestHelper, createClass} from './../../../../spec/component-test-helper';
  4 +import {ComponentFixture} from 'ng-forward/cjs/testing/test-component-builder';
  5 +import {providers} from 'ng-forward/cjs/testing/providers';
  6 +import {LoginBlockComponent} from './login-block.component';
  7 +import * as helpers from "./../../../../spec/helpers";
  8 +import {SessionService, AuthService, AuthController, AuthEvents} from "./../../../login";
  9 +
  10 +const htmlTemplate: string = '<noosfero-login-block></noosfero-login-block>';
  11 +
  12 +describe("Components", () => {
  13 +
  14 + describe("Login Block Component", () => {
  15 + let helper: ComponentTestHelper<LoginBlockComponent>;
  16 + let person: any = null;
  17 +
  18 + /**
  19 + * Mock objects
  20 + */
  21 + let authService: any = helpers.mocks.authService;
  22 + let user = <noosfero.User>{ person: person };
  23 + let sessionService: any = <any>helpers.mocks.sessionWithCurrentUser(user);
  24 + let state = jasmine.createSpyObj("$state", ["go"]);
  25 + let scope = helpers.mocks.scopeWithEvents;
  26 +
  27 + let providers = [
  28 + new Provider('SessionService', { useValue: sessionService }),
  29 + new Provider('$state', { useValue: state }),
  30 + new Provider('AuthService', { useValue: authService }),
  31 + new Provider('$scope', { useValue: scope })
  32 + ];
  33 +
  34 + beforeEach( angular.mock.module("templates") );
  35 +
  36 + beforeEach( (done: Function) => {
  37 + let cls = createClass({
  38 + template: htmlTemplate,
  39 + directives: [LoginBlockComponent],
  40 + providers: providers,
  41 + properties: {}
  42 + });
  43 + helper = new ComponentTestHelper<LoginBlockComponent>(cls, done);
  44 + });
  45 +
  46 + it("expect person to be null with no logged in user", () => {
  47 + expect(helper.component.currentUser).toBeNull;
  48 + });
  49 +
  50 + it("expect person to be defined when user login", () => {
  51 + // Executes the login method on the component
  52 + doComponentLogin();
  53 + expect(helper.component.currentUser.person).toBe(person);
  54 + });
  55 +
  56 + it("expect person to be null when user logout", () => {
  57 + // First do a login
  58 + doComponentLogin();
  59 + // The logout the user
  60 + doComponentLogout();
  61 + // Check if the current user was cleared
  62 + expect(helper.component.currentUser).toBeNull;
  63 + });
  64 +
  65 + /**
  66 + * Execute the logout method on the target component
  67 + */
  68 + function doComponentLogout() {
  69 + // Create a mock for the AuthService logout method
  70 + spyOn(authService, "logout");
  71 + helper.component.logout();
  72 + expect(authService.logout).toHaveBeenCalled();
  73 + // After the component logout method execution, fire the
  74 + // AuthService event
  75 + simulateLogoutEvent();
  76 + }
  77 +
  78 + /**
  79 + * Execute the login method on the target component
  80 + */
  81 + function doComponentLogin() {
  82 + // Create a mock for the AuthService login method
  83 + spyOn(authService, "login");
  84 + helper.component.login();
  85 + expect(authService.login).toHaveBeenCalled();
  86 + // After the component login method execution, fire the
  87 + // AuthService event
  88 + simulateLoginEvent();
  89 + }
  90 +
  91 + /**
  92 + * Simulate the AuthService loginSuccess event
  93 + */
  94 + function simulateLoginEvent() {
  95 + let successEvent: string = AuthEvents[AuthEvents.loginSuccess];
  96 +
  97 + (<any>helper.component.authService)[successEvent].next(user);
  98 + }
  99 +
  100 + /**
  101 + * Simulate the AuthService logoutSuccess event
  102 + */
  103 + function simulateLogoutEvent() {
  104 + let successEvent: string = AuthEvents[AuthEvents.logoutSuccess];
  105 +
  106 + (<any>helper.component.authService)[successEvent].next(user);
  107 + }
  108 + });
  109 +
  110 +});
0 \ No newline at end of file 111 \ No newline at end of file
src/app/layout/blocks/login-block/login-block.component.ts 0 → 100644
@@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
  1 +import {Input, Inject, Component} from "ng-forward";
  2 +import {SessionService, AuthService, AuthEvents} from "./../../../login";
  3 +
  4 +/**
  5 + * @ngdoc controller
  6 + * @name layout.blocks.LoginBlockComponent
  7 + * @description
  8 + * The Noosfero block responible for presenting a login form and user status
  9 + */
  10 +@Component({
  11 + selector: "noosfero-login-block",
  12 + templateUrl: 'app/layout/blocks/login-block/login-block.html',
  13 +})
  14 +@Inject("SessionService", "$state", 'AuthService', "$scope")
  15 +export class LoginBlockComponent {
  16 +
  17 + /**
  18 + * @ngdoc property
  19 + * @name currentUser
  20 + * @propertyOf layout.blocks.LoginBlockComponent
  21 + * @description
  22 + * The current loged in user
  23 + */
  24 + currentUser: noosfero.User;
  25 +
  26 + /**
  27 + * @ngdoc property
  28 + * @name credentials
  29 + * @propertyOf layout.blocks.LoginBlockComponent
  30 + * @description
  31 + * The credentials of the currentUser
  32 + */
  33 + credentials: noosfero.Credentials;
  34 +
  35 + constructor(
  36 + private session: SessionService,
  37 + private $state: ng.ui.IStateService,
  38 + public authService: AuthService,
  39 + private $scope: ng.IScope) {
  40 + this.currentUser = this.session.currentUser();
  41 +
  42 + this.authService.subscribe(AuthEvents[AuthEvents.loginSuccess], () => {
  43 + this.currentUser = this.session.currentUser();
  44 + });
  45 +
  46 + this.authService.subscribe(AuthEvents[AuthEvents.logoutSuccess], () => {
  47 + this.currentUser = this.session.currentUser();
  48 + });
  49 +
  50 + }
  51 +
  52 + /**
  53 + * @ngdoc method
  54 + * @name login
  55 + * @methodOf layout.blocks.LoginBlockComponent
  56 + * @description
  57 + * Logs in the user using its credentials
  58 + */
  59 + login() {
  60 + this.authService.login(this.credentials);
  61 + }
  62 +
  63 + /**
  64 + * @ngdoc method
  65 + * @name logout
  66 + * @methodOf layout.blocks.LoginBlockComponent
  67 + * @description
  68 + * Logout the user
  69 + */
  70 + logout() {
  71 + this.authService.logout();
  72 + };
  73 +}
src/app/layout/blocks/login-block/login-block.html 0 → 100644
@@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
  1 +<div class="logged-user-info" ng-show="ctrl.currentUser">
  2 + <h4>Logged in as {{ctrl.currentUser.person.identifier}}</h4>
  3 + <ul>
  4 + <li>User since
  5 + <span class="time">
  6 + <span am-time-ago="ctrl.currentUser.person.created_at | dateFormat"></span>
  7 + </span>
  8 + </li>
  9 + <li><a ui-sref="main.profile.info({profile: ctrl.currentUser.person.identifier})">Profile Homepage</a></li>
  10 + </ul>
  11 + <div class="user-actions">
  12 + <a href="#" ng-click="ctrl.logout()"><i class="fa fa-fw fa-power-off"></i> {{"navbar.logout" | translate}}</a>
  13 + </div>
  14 +</div>
  15 +<div class="logged-user-info" ng-show="!ctrl.currentUser">
  16 + <form>
  17 + <div class="form-group">
  18 + <label for="email">{{"auth.form.login" | translate}}</label>
  19 + <input type="text" class="form-control" id="email" placeholder="{{'auth.form.login' | translate}}" ng-model="ctrl.credentials.username">
  20 + </div>
  21 + <div class="form-group">
  22 + <label for="passwd">{{"auth.form.password" | translate}}</label>
  23 + <input type="password" class="form-control" id="passwd" placeholder="{{'auth.form.password' | translate}}" ng-model="ctrl.credentials.password">
  24 + </div>
  25 + <button type="submit" class="btn btn-default" ng-click="ctrl.login()">{{"auth.form.login_button" | translate}}</button>
  26 + </form>
  27 +</div>
src/app/layout/blocks/main/index.ts 0 → 100644
@@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
  1 +/* Module Index Entry - generated using the script npm run generate-index */
  2 +export * from "./main-block.component";
src/app/layout/blocks/main/main-block.component.spec.ts 0 → 100644
@@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
  1 +import {TestComponentBuilder} from 'ng-forward/cjs/testing/test-component-builder';
  2 +import {Input, provide, Component, StateConfig} from 'ng-forward';
  3 +import {MainBlockComponent} from './main-block.component';
  4 +
  5 +
  6 +const tcb = new TestComponentBuilder();
  7 +
  8 +const htmlTemplate: string = '<noosfero-main-block [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-main-block>';
  9 +
  10 +describe("Components", () => {
  11 + describe("Main Block Component", () => {
  12 +
  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 Block will be load on our tests
  17 + beforeEach(angular.mock.module("templates"));
  18 +
  19 + it("check if the main block has a tag with ui-view attribute", done => {
  20 +
  21 + // Creating a container component (BlockContainerComponent) to include
  22 + // the component under test (Block)
  23 + @Component({ selector: 'test-container-component', template: htmlTemplate, directives: [MainBlockComponent] })
  24 + class BlockContainerComponent {
  25 + }
  26 +
  27 + // uses the TestComponentBuilder instance to initialize the component
  28 + tcb.createAsync(BlockContainerComponent).then(fixture => {
  29 + // and here we can inspect and run the test assertions
  30 + // let myComponent: MainBlockComponent = fixture.componentInstance;
  31 +
  32 + // assure the block object inside the Block matches
  33 + // the provided through the parent component
  34 + expect(fixture.debugElement.queryAll('[ui-view="mainBlockContent"]').length).toEqual(1);
  35 + done();
  36 + });
  37 + });
  38 + });
  39 +});
src/app/layout/blocks/main/main-block.component.ts 0 → 100644
@@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
  1 +import {Component, Input} from 'ng-forward';
  2 +import {BlockComponent} from '../block.component';
  3 +
  4 +@Component({
  5 + selector: 'noosfero-main-block',
  6 + templateUrl: 'app/layout/blocks/main/main-block.html'
  7 +})
  8 +export class MainBlockComponent {
  9 +
  10 +}