Commit 49c1ee2611f73c70c3f6873b49c2fa9d369d8af2

Authored by Carlos Purificação
2 parents b420f699 ab10b960

Merging with master

Merge remote-tracking branch 'origin/master' into login-block
Showing 204 changed files with 4245 additions and 1182 deletions   Show diff stats
@@ -34,10 +34,14 @@ @@ -34,10 +34,14 @@
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"
38 }, 40 },
39 "devDependencies": { 41 "devDependencies": {
40 - "angular-mocks": "~1.5.0" 42 + "angular-mocks": "~1.5.0",
  43 + "ng-ckeditor": "^0.2.1",
  44 + "ckeditor": "^4.5.8"
41 }, 45 },
42 "overrides": { 46 "overrides": {
43 "bootstrap-sass": { 47 "bootstrap-sass": {
@@ -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,31 @@ var $ = require('gulp-load-plugins')({ @@ -16,25 +17,31 @@ 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: 'noosferoApp',
  34 + root: partialPath
  35 + }))
  36 + .pipe(gulp.dest(conf.paths.tmp + '/partials/')));
22 }); 37 });
23 - return gulp.src(srcPaths)  
24 - .pipe($.minifyHtml({  
25 - empty: true,  
26 - spare: true,  
27 - quotes: true  
28 - }))  
29 - .pipe($.angularTemplatecache('templateCacheHtml.js', {  
30 - module: 'noosferoApp',  
31 - root: 'app'  
32 - }))  
33 - .pipe(gulp.dest(conf.paths.tmp + '/partials/')); 38 + return merged;
34 }); 39 });
35 40
36 gulp.task('html', ['inject', 'partials'], function () { 41 gulp.task('html', ['inject', 'partials'], function () {
37 - var partialsInjectFile = gulp.src(path.join(conf.paths.tmp, '/partials/templateCacheHtml.js'), { read: false }); 42 + var partialsInjectFile = gulp.src([
  43 + path.join(conf.paths.tmp, '/partials/templateCacheHtml-app.js'),
  44 + path.join(conf.paths.tmp, '/partials/templateCacheHtml-plugins.js')], { read: false });
38 var partialsInjectOptions = { 45 var partialsInjectOptions = {
39 starttag: '<!-- inject:partials -->', 46 starttag: '<!-- inject:partials -->',
40 ignorePath: path.join(conf.paths.tmp, '/partials'), 47 ignorePath: path.join(conf.paths.tmp, '/partials'),
@@ -73,6 +80,8 @@ gulp.task(&#39;html&#39;, [&#39;inject&#39;, &#39;partials&#39;], function () { @@ -73,6 +80,8 @@ gulp.task(&#39;html&#39;, [&#39;inject&#39;, &#39;partials&#39;], function () {
73 .pipe($.useref()) 80 .pipe($.useref())
74 .pipe($.revReplace({prefix: noosferoThemePrefix})) 81 .pipe($.revReplace({prefix: noosferoThemePrefix}))
75 .pipe(htmlFilter) 82 .pipe(htmlFilter)
  83 + .pipe($.replace('/bower_components/ng-ckeditor/libs/ckeditor/', noosferoThemePrefix + 'ng-ckeditor/libs/ckeditor/'))
  84 + .pipe($.replace('/bower_components/ng-ckeditor/ng-ckeditor.min.js', noosferoThemePrefix + 'ng-ckeditor/ng-ckeditor.min.js'))
76 .pipe($.minifyHtml({ 85 .pipe($.minifyHtml({
77 empty: true, 86 empty: true,
78 spare: true, 87 spare: true,
@@ -93,6 +102,10 @@ gulp.task(&#39;fonts&#39;, function () { @@ -93,6 +102,10 @@ 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 + return gulp.src(['bower_components/ng-ckeditor/**/*']).pipe(gulp.dest(path.join(conf.paths.dist, '/ng-ckeditor')));
  107 +});
  108 +
96 gulp.task('locale', function () { 109 gulp.task('locale', function () {
97 return gulp.src([ 110 return gulp.src([
98 path.join("bower_components/angular-i18n", '*.js'), 111 path.join("bower_components/angular-i18n", '*.js'),
@@ -124,6 +137,10 @@ gulp.task(&#39;clean-docs&#39;, [], function() { @@ -124,6 +137,10 @@ gulp.task(&#39;clean-docs&#39;, [], function() {
124 return $.del([path.join(conf.paths.docs, '/')]); 137 return $.del([path.join(conf.paths.docs, '/')]);
125 }); 138 });
126 139
  140 +gulp.task('plugin-languages', ['locale'], function() {
  141 + return languages.pluginLanguages(conf.paths.dist);
  142 +});
  143 +
127 gulp.task('noosfero', ['html'], function () { 144 gulp.task('noosfero', ['html'], function () {
128 var layouts = gulp.src('layouts/**/*') 145 var layouts = gulp.src('layouts/**/*')
129 .pipe(gulp.dest(path.join(conf.paths.dist, "layouts"))); 146 .pipe(gulp.dest(path.join(conf.paths.dist, "layouts")));
@@ -136,4 +153,4 @@ gulp.task(&#39;noosfero&#39;, [&#39;html&#39;], function () { @@ -136,4 +153,4 @@ gulp.task(&#39;noosfero&#39;, [&#39;html&#39;], function () {
136 return merge(layouts, theme, index); 153 return merge(layouts, theme, index);
137 }); 154 });
138 155
139 -gulp.task('build', ['html', 'fonts', 'other', 'locale', 'noosfero']); 156 +gulp.task('build', ['html', 'fonts', 'other', 'locale', 'ckeditor', '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";
@@ -34,7 +36,7 @@ exports.configTheme(argv.theme); @@ -34,7 +36,7 @@ exports.configTheme(argv.theme);
34 * to inject css preprocessor deps and js files in karma 36 * to inject css preprocessor deps and js files in karma
35 */ 37 */
36 exports.wiredep = { 38 exports.wiredep = {
37 - exclude: [/jquery/, /\/bootstrap\.js$/, /\/bootstrap-sass\/.*\.js/, /\/bootstrap\.css/], 39 + exclude: [/jquery/, /\/bootstrap\.js$/, /\/bootstrap-sass\/.*\.js/, /\/bootstrap\.css/, /ckeditor/],
38 directory: 'bower_components' 40 directory: 'bower_components'
39 }; 41 };
40 42
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
@@ -16,7 +17,8 @@ gulp.task(&#39;watch&#39;, [&#39;inject&#39;], function () { @@ -16,7 +17,8 @@ gulp.task(&#39;watch&#39;, [&#39;inject&#39;], function () {
16 17
17 gulp.watch([ 18 gulp.watch([
18 path.join(conf.paths.src, '/app/**/*.css'), 19 path.join(conf.paths.src, '/app/**/*.css'),
19 - path.join(conf.paths.src, '/app/**/*.scss') 20 + path.join(conf.paths.src, '/app/**/*.scss'),
  21 + path.join(conf.paths.src, conf.paths.plugins, '/**/*.scss')
20 ], function(event) { 22 ], function(event) {
21 if(isOnlyChange(event)) { 23 if(isOnlyChange(event)) {
22 gulp.start('styles-reload'); 24 gulp.start('styles-reload');
@@ -33,9 +35,14 @@ gulp.task(&#39;watch&#39;, [&#39;inject&#39;], function () { @@ -33,9 +35,14 @@ gulp.task(&#39;watch&#39;, [&#39;inject&#39;], function () {
33 } 35 }
34 }); 36 });
35 37
  38 + gulp.watch(path.join(conf.paths.src, '**', conf.paths.languages, '*.json'), function(event) {
  39 + languages.pluginLanguage(event.path, path.join(conf.paths.tmp, '/serve'));
  40 + });
  41 +
36 var watchPaths = []; 42 var watchPaths = [];
37 conf.paths.allSources.forEach(function(src) { 43 conf.paths.allSources.forEach(function(src) {
38 watchPaths.push(path.join(src, '/app/**/*.html')); 44 watchPaths.push(path.join(src, '/app/**/*.html'));
  45 + watchPaths.push(path.join(src, conf.paths.plugins, '/**/*.html'));
39 }); 46 });
40 gulp.watch(watchPaths, function(event) { 47 gulp.watch(watchPaths, function(event) {
41 browserSync.reload(event.path); 48 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");
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.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";
4 7
5 /** 8 /**
6 * @ngdoc controller 9 * @ngdoc controller
@@ -30,7 +33,9 @@ export class ArticleDefaultViewComponent { @@ -30,7 +33,9 @@ export class ArticleDefaultViewComponent {
30 @Component({ 33 @Component({
31 selector: 'noosfero-article', 34 selector: 'noosfero-article',
32 template: 'not-used', 35 template: 'not-used',
33 - directives: [ArticleDefaultViewComponent, ArticleBlogComponent, CommentsComponent] 36 + directives: [ArticleDefaultViewComponent, ArticleBlogComponent,
  37 + CommentsComponent, MacroDirective, ArticleToolbarHotspotComponent,
  38 + ArticleContentHotspotComponent]
34 }) 39 })
35 @Inject("$element", "$scope", "$injector", "$compile") 40 @Inject("$element", "$scope", "$injector", "$compile")
36 export class ArticleViewComponent { 41 export class ArticleViewComponent {
@@ -40,7 +45,8 @@ export class ArticleViewComponent { @@ -40,7 +45,8 @@ export class ArticleViewComponent {
40 directiveName: string; 45 directiveName: string;
41 46
42 ngOnInit() { 47 ngOnInit() {
43 - let specificDirective = 'noosfero' + this.article.type; 48 + let articleType = this.article.type.replace(/::/, '');
  49 + let specificDirective = 'noosfero' + articleType;
44 this.directiveName = "noosfero-default-article"; 50 this.directiveName = "noosfero-default-article";
45 if (this.$injector.has(specificDirective + 'Directive')) { 51 if (this.$injector.has(specificDirective + 'Directive')) {
46 this.directiveName = specificDirective.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); 52 this.directiveName = specificDirective.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
@@ -53,6 +59,5 @@ export class ArticleViewComponent { @@ -53,6 +59,5 @@ export class ArticleViewComponent {
53 private $scope: ng.IScope, 59 private $scope: ng.IScope,
54 private $injector: ng.auto.IInjectorService, 60 private $injector: ng.auto.IInjectorService,
55 private $compile: ng.ICompileService) { 61 private $compile: ng.ICompileService) {
56 -  
57 } 62 }
58 } 63 }
src/app/article/article.html
@@ -4,6 +4,10 @@ @@ -4,6 +4,10 @@
4 </div> 4 </div>
5 5
6 <div class="sub-header clearfix"> 6 <div class="sub-header clearfix">
  7 + <a href="#" class="btn btn-default btn-xs" ui-sref="main.cmsEdit({profile: ctrl.profile.identifier, id: ctrl.article.id})">
  8 + <i class="fa fa-pencil-square-o fa-fw fa-lg"></i> {{"article.actions.edit" | translate}}
  9 + </a>
  10 + <noosfero-hotspot-article-toolbar [article]="ctrl.article"></noosfero-hotspot-article-toolbar>
7 <div class="page-info pull-right small text-muted"> 11 <div class="page-info pull-right small text-muted">
8 <span class="time"> 12 <span class="time">
9 <i class="fa fa-clock-o"></i> <span am-time-ago="ctrl.article.created_at | dateFormat"></span> 13 <i class="fa fa-clock-o"></i> <span am-time-ago="ctrl.article.created_at | dateFormat"></span>
@@ -16,9 +20,9 @@ @@ -16,9 +20,9 @@
16 </span> 20 </span>
17 </div> 21 </div>
18 </div> 22 </div>
19 - 23 + <noosfero-hotspot-article-content [article]="ctrl.article"></noosfero-hotspot-article-content>
20 <div class="page-body"> 24 <div class="page-body">
21 - <div ng-bind-html="ctrl.article.body"></div> 25 + <div bind-html-compile="ctrl.article.body"></div>
22 </div> 26 </div>
23 27
24 <noosfero-comments [article]="ctrl.article"></noosfero-comments> 28 <noosfero-comments [article]="ctrl.article"></noosfero-comments>
src/app/article/basic-editor.component.spec.ts
@@ -1,55 +0,0 @@ @@ -1,55 +0,0 @@
1 -import {quickCreateComponent} from "../../spec/helpers";  
2 -import {BasicEditorComponent} from "./basic-editor.component";  
3 -  
4 -  
5 -describe("Article BasicEditor", () => {  
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 profile = { id: 1 };  
13 - let notification: any;  
14 -  
15 -  
16 - beforeEach(inject((_$rootScope_: ng.IRootScopeService, _$q_: ng.IQService) => {  
17 - $rootScope = _$rootScope_;  
18 - $q = _$q_;  
19 - }));  
20 -  
21 - beforeEach(() => {  
22 - $state = jasmine.createSpyObj("$state", ["transitionTo"]);  
23 - notification = jasmine.createSpyObj("notification", ["success"]);  
24 - profileServiceMock = jasmine.createSpyObj("profileServiceMock", ["getCurrentProfile"]);  
25 - articleServiceMock = jasmine.createSpyObj("articleServiceMock", ["createInProfile"]);  
26 -  
27 - let getCurrentProfileResponse = $q.defer();  
28 - getCurrentProfileResponse.resolve(profile);  
29 -  
30 - let articleCreate = $q.defer();  
31 - articleCreate.resolve({ data: { path: "path", profile: { identifier: "profile" } } });  
32 -  
33 - profileServiceMock.getCurrentProfile = jasmine.createSpy("getCurrentProfile").and.returnValue(getCurrentProfileResponse.promise);  
34 - articleServiceMock.createInProfile = jasmine.createSpy("createInProfile").and.returnValue(articleCreate.promise);  
35 - });  
36 -  
37 - it("create an article in the current profile when save", done => {  
38 - let component: BasicEditorComponent = new BasicEditorComponent(articleServiceMock, profileServiceMock, $state, notification);  
39 - component.save();  
40 - $rootScope.$apply();  
41 - expect(profileServiceMock.getCurrentProfile).toHaveBeenCalled();  
42 - expect(articleServiceMock.createInProfile).toHaveBeenCalledWith(profile, component.article);  
43 - done();  
44 - });  
45 -  
46 - it("got to the new article page and display an alert when saving sucessfully", done => {  
47 - let component: BasicEditorComponent = new BasicEditorComponent(articleServiceMock, profileServiceMock, $state, notification);  
48 - component.save();  
49 - $rootScope.$apply();  
50 - expect($state.transitionTo).toHaveBeenCalledWith("main.profile.page", { page: "path", profile: "profile" });  
51 - expect(notification.success).toHaveBeenCalled();  
52 - done();  
53 - });  
54 -  
55 -});  
src/app/article/basic-editor.component.ts
@@ -1,35 +0,0 @@ @@ -1,35 +0,0 @@
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 -  
6 -@Component({  
7 - selector: 'article-basic-editor',  
8 - templateUrl: "app/article/basic-editor.html",  
9 - providers: [  
10 - provide('articleService', { useClass: ArticleService }),  
11 - provide('profileService', { useClass: ProfileService }),  
12 - provide('notification', { useClass: NotificationService })  
13 - ]  
14 -})  
15 -@Inject(ArticleService, ProfileService, "$state", NotificationService)  
16 -export class BasicEditorComponent {  
17 -  
18 - article: noosfero.Article = <noosfero.Article>{};  
19 -  
20 - constructor(private articleService: ArticleService,  
21 - private profileService: ProfileService,  
22 - private $state: ng.ui.IStateService,  
23 - private notification: NotificationService) { }  
24 -  
25 - save() {  
26 - this.profileService.getCurrentProfile().then((profile: noosfero.Profile) => {  
27 - return this.articleService.createInProfile(profile, this.article);  
28 - }).then((response: noosfero.RestResult<noosfero.Article>) => {  
29 - let article = (<noosfero.Article>response.data);  
30 - this.$state.transitionTo('main.profile.page', { page: article.path, profile: article.profile.identifier });  
31 - this.notification.success({ title: "Good job!", message: "Article saved!" });  
32 - });  
33 - }  
34 -  
35 -}  
src/app/article/basic-editor.html
@@ -1,11 +0,0 @@ @@ -1,11 +0,0 @@
1 -<form>  
2 - <div class="form-group">  
3 - <label for="titleInput">Title</label>  
4 - <input type="text" class="form-control" id="titleInput" placeholder="title" ng-model="vm.article.name">  
5 - </div>  
6 - <div class="form-group">  
7 - <label for="bodyInput">Text</label>  
8 - <textarea class="form-control" id="bodyInput" rows="10" ng-model="vm.article.body"></textarea>  
9 - </div>  
10 - <button type="submit" class="btn btn-default" ng-click="vm.save()">Save</button>  
11 -</form>  
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.type.replace(/::/, '');
  20 + let specificDirective = `${articleType.charAt(0).toLowerCase()}${articleType.substring(1)}Editor`;
  21 + let directiveName = "article-basic-editor";
  22 + if (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,84 @@ @@ -0,0 +1,84 @@
  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 + let component: CmsComponent = new CmsComponent(articleServiceMock, profileServiceMock, $state, notification, $stateParams, $window);
  59 + component.save();
  60 + $rootScope.$apply();
  61 + expect($state.go).toHaveBeenCalledWith("main.profile.page", { page: "path", profile: "profile" });
  62 + expect(notification.success).toHaveBeenCalled();
  63 + done();
  64 + });
  65 +
  66 + it("go back when cancel article edition", done => {
  67 + let component: CmsComponent = new CmsComponent(articleServiceMock, profileServiceMock, $state, notification, $stateParams, $window);
  68 + $window.history = { back: jasmine.createSpy('back') };
  69 + component.cancel();
  70 + expect($window.history.back).toHaveBeenCalled();
  71 + done();
  72 + });
  73 +
  74 + it("edit existing article when save", done => {
  75 + $stateParams['parent_id'] = null;
  76 + $stateParams['id'] = 2;
  77 + let component: CmsComponent = new CmsComponent(articleServiceMock, profileServiceMock, $state, notification, $stateParams, $window);
  78 + component.save();
  79 + $rootScope.$apply();
  80 + expect(articleServiceMock.updateArticle).toHaveBeenCalledWith(component.article);
  81 + done();
  82 + });
  83 +
  84 +});
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.ts
@@ -9,6 +9,8 @@ export class CommentComponent { @@ -9,6 +9,8 @@ export class CommentComponent {
9 9
10 @Input() comment: noosfero.CommentViewModel; 10 @Input() comment: noosfero.CommentViewModel;
11 @Input() article: noosfero.Article; 11 @Input() article: noosfero.Article;
  12 + @Input() displayActions = true;
  13 + @Input() displayReplies = true;
12 14
13 showReply() { 15 showReply() {
14 return this.comment && this.comment.__show_reply === true; 16 return this.comment && this.comment.__show_reply === true;
src/app/article/comment/comment.html
@@ -9,13 +9,19 @@ @@ -9,13 +9,19 @@
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 </div> 17 </div>
14 <div class="title">{{ctrl.comment.title}}</div> 18 <div class="title">{{ctrl.comment.title}}</div>
15 <div class="body">{{ctrl.comment.body}}</div> 19 <div class="body">{{ctrl.comment.body}}</div>
16 - <a href="#" (click)="ctrl.reply()" class="small text-muted">  
17 - {{"comment.reply" | translate}}  
18 - </a> 20 + <div class="actions" ng-if="ctrl.displayActions">
  21 + <a href="#" (click)="ctrl.reply()" class="small text-muted" ng-if="ctrl.article.accept_comments">
  22 + {{"comment.reply" | translate}}
  23 + </a>
  24 + </div>
19 </div> 25 </div>
20 - <noosfero-comments [show-form]="ctrl.showReply()" [article]="ctrl.article" [parent]="ctrl.comment"></noosfero-comments> 26 + <noosfero-comments [show-form]="ctrl.showReply()" [article]="ctrl.article" [parent]="ctrl.comment" ng-if="ctrl.displayReplies"></noosfero-comments>
21 </div> 27 </div>
src/app/article/comment/comment.scss
@@ -5,6 +5,7 @@ @@ -5,6 +5,7 @@
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;
@@ -13,7 +14,18 @@ @@ -13,7 +14,18 @@
13 min-width: 40px; 14 min-width: 40px;
14 } 15 }
15 .media-body { 16 .media-body {
16 - padding: 0 10px 10px 10px; 17 + padding: 0 10px 10px 0;
  18 + .reply-of {
  19 + font-size: 12px;
  20 + color: #B5B5B5;
  21 + margin-left: 5px;
  22 + i {
  23 + font-size: 10px;
  24 + }
  25 + }
  26 + h4 {
  27 + font-size: 16px;
  28 + }
17 } 29 }
18 noosfero-profile-image { 30 noosfero-profile-image {
19 img { 31 img {
@@ -27,8 +39,24 @@ @@ -27,8 +39,24 @@
27 font-size: 1.7em; 39 font-size: 1.7em;
28 } 40 }
29 } 41 }
  42 + // Limit identation of replies
30 .comments { 43 .comments {
31 margin-left: 30px; 44 margin-left: 30px;
  45 + .comments .comments {
  46 + margin-left: 0px;
  47 + .comment {
  48 + margin-left: 0px;
  49 + }
  50 + }
  51 + }
  52 + .tooltip-inner {
  53 + max-width: 350px;
  54 + text-align: left;
  55 + .reply-tooltip {
  56 + .comment {
  57 + margin: 5px;
  58 + }
  59 + }
32 } 60 }
33 } 61 }
34 } 62 }
src/app/article/comment/comments.component.ts
1 -import { Inject, Input, Output, Component, provide, EventEmitter } from 'ng-forward';  
2 -import {INgForwardJQuery} from "ng-forward/cjs/util/jqlite-extensions";  
3 -  
4 - 1 +import { Inject, Input, Component, provide } from 'ng-forward';
5 import { PostCommentComponent } from "./post-comment/post-comment.component"; 2 import { PostCommentComponent } from "./post-comment/post-comment.component";
6 import { CommentService } from "../../../lib/ng-noosfero-api/http/comment.service"; 3 import { CommentService } from "../../../lib/ng-noosfero-api/http/comment.service";
7 import { CommentComponent } from "./comment.component"; 4 import { CommentComponent } from "./comment.component";
@@ -19,12 +16,13 @@ export class CommentsComponent { @@ -19,12 +16,13 @@ export class CommentsComponent {
19 @Input() showForm = true; 16 @Input() showForm = true;
20 @Input() article: noosfero.Article; 17 @Input() article: noosfero.Article;
21 @Input() parent: noosfero.CommentViewModel; 18 @Input() parent: noosfero.CommentViewModel;
22 -  
23 protected page = 1; 19 protected page = 1;
24 protected perPage = 5; 20 protected perPage = 5;
25 protected total = 0; 21 protected total = 0;
26 22
27 - constructor(protected commentService: CommentService) { } 23 + newComment = <noosfero.Comment>{};
  24 +
  25 + constructor(protected commentService: CommentService, private $scope: ng.IScope) { }
28 26
29 ngOnInit() { 27 ngOnInit() {
30 if (this.parent) { 28 if (this.parent) {
@@ -43,7 +41,7 @@ export class CommentsComponent { @@ -43,7 +41,7 @@ export class CommentsComponent {
43 this.comments.forEach((comment: noosfero.CommentViewModel) => { 41 this.comments.forEach((comment: noosfero.CommentViewModel) => {
44 comment.__show_reply = false; 42 comment.__show_reply = false;
45 }); 43 });
46 - if (this.parent) { 44 + if (this.parent) {
47 this.parent.__show_reply = false; 45 this.parent.__show_reply = false;
48 } 46 }
49 } 47 }
src/app/article/comment/comments.html
1 <div class="comments"> 1 <div class="comments">
2 - <noosfero-post-comment (comment-saved)="ctrl.commentAdded($event.detail)" ng-if="ctrl.showForm" [article]="ctrl.article" [parent]="ctrl.parent"></noosfero-post-comment> 2 + <noosfero-post-comment (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 | orderBy: 'created_at':true" [comment]="comment" [article]="ctrl.article"></noosfero-comment> 5 <noosfero-comment ng-repeat="comment in ctrl.comments | orderBy: 'created_at':true" [comment]="comment" [article]="ctrl.article"></noosfero-comment>
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
@@ -7,6 +7,8 @@ const htmlTemplate: string = &#39;&lt;noosfero-post-comment [article]=&quot;ctrl.article&quot; [r @@ -7,6 +7,8 @@ const htmlTemplate: string = &#39;&lt;noosfero-post-comment [article]=&quot;ctrl.article&quot; [r
7 describe("Components", () => { 7 describe("Components", () => {
8 describe("Post Comment Component", () => { 8 describe("Post Comment Component", () => {
9 9
  10 + let properties = { article: { id: 1, accept_comments: true } };
  11 +
10 beforeEach(angular.mock.module("templates")); 12 beforeEach(angular.mock.module("templates"));
11 13
12 let commentService = jasmine.createSpyObj("commentService", ["createInArticle"]); 14 let commentService = jasmine.createSpyObj("commentService", ["createInArticle"]);
@@ -19,7 +21,7 @@ describe(&quot;Components&quot;, () =&gt; { @@ -19,7 +21,7 @@ describe(&quot;Components&quot;, () =&gt; {
19 21
20 @Component({ selector: 'test-container-component', directives: [PostCommentComponent], template: htmlTemplate, providers: providers }) 22 @Component({ selector: 'test-container-component', directives: [PostCommentComponent], template: htmlTemplate, providers: providers })
21 class ContainerComponent { 23 class ContainerComponent {
22 - article = { id: 1 }; 24 + article = properties['article'];
23 comment = { id: 2 }; 25 comment = { id: 2 };
24 } 26 }
25 27
@@ -30,6 +32,14 @@ describe(&quot;Components&quot;, () =&gt; { @@ -30,6 +32,14 @@ describe(&quot;Components&quot;, () =&gt; {
30 }); 32 });
31 }); 33 });
32 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 +
33 it("emit an event when create comment", done => { 43 it("emit an event when create comment", done => {
34 helpers.createComponentFromClass(ContainerComponent).then(fixture => { 44 helpers.createComponentFromClass(ContainerComponent).then(fixture => {
35 let component: PostCommentComponent = fixture.debugElement.componentViewChildren[0].componentInstance; 45 let component: PostCommentComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
src/app/article/comment/post-comment/post-comment.component.ts
@@ -2,20 +2,23 @@ import { Inject, Input, Output, EventEmitter, Component } from &#39;ng-forward&#39;; @@ -2,20 +2,23 @@ import { Inject, Input, Output, EventEmitter, Component } from &#39;ng-forward&#39;;
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"; 4 import { SessionService } from "../../../login";
  5 +import { CommentFormHotspotComponent } from "../../../hotspot/comment-form-hotspot.component";
5 6
6 @Component({ 7 @Component({
7 selector: 'noosfero-post-comment', 8 selector: 'noosfero-post-comment',
8 templateUrl: 'app/article/comment/post-comment/post-comment.html', 9 templateUrl: 'app/article/comment/post-comment/post-comment.html',
9 - outputs: ['commentSaved'] 10 + outputs: ['commentSaved'],
  11 + directives: [CommentFormHotspotComponent]
10 }) 12 })
11 @Inject(CommentService, NotificationService, SessionService) 13 @Inject(CommentService, NotificationService, SessionService)
12 export class PostCommentComponent { 14 export class PostCommentComponent {
13 15
  16 + public static EVENT_COMMENT_RECEIVED = "comment.received";
  17 +
14 @Input() article: noosfero.Article; 18 @Input() article: noosfero.Article;
15 @Input() parent: noosfero.Comment; 19 @Input() parent: noosfero.Comment;
16 @Output() commentSaved: EventEmitter<Comment> = new EventEmitter<Comment>(); 20 @Output() commentSaved: EventEmitter<Comment> = new EventEmitter<Comment>();
17 -  
18 - comment = <noosfero.Comment>{}; 21 + @Input() comment = <noosfero.Comment>{};
19 private currentUser: noosfero.User; 22 private currentUser: noosfero.User;
20 23
21 constructor(private commentService: CommentService, 24 constructor(private commentService: CommentService,
src/app/article/comment/post-comment/post-comment.html
1 -<form class="clearfix post-comment"> 1 +<form class="clearfix post-comment" ng-if="ctrl.article.accept_comments">
2 <div class="form-group"> 2 <div class="form-group">
3 <div class="comment media"> 3 <div class="comment media">
4 <div class="media-left"> 4 <div class="media-left">
@@ -8,6 +8,7 @@ @@ -8,6 +8,7 @@
8 </div> 8 </div>
9 <div class="media-body"> 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> 10 <textarea class="form-control custom-control" rows="1" ng-model="ctrl.comment.body" placeholder="{{'comment.post.placeholder' | translate}}"></textarea>
  11 + <noosfero-hotspot-comment-form [comment]="ctrl.comment" [parent]="ctrl.parent"></noosfero-hotspot-comment-form>
11 <button ng-show="ctrl.comment.body" type="submit" class="btn btn-default pull-right ng-hide" ng-click="ctrl.save()">{{"comment.post" | translate}}</button> 12 <button ng-show="ctrl.comment.body" type="submit" class="btn btn-default pull-right ng-hide" ng-click="ctrl.save()">{{"comment.post" | translate}}</button>
12 </div> 13 </div>
13 </div> 14 </div>
src/app/article/comment/post-comment/post-comment.scss
1 .comments { 1 .comments {
2 .post-comment { 2 .post-comment {
3 .media { 3 .media {
4 - border-top: 2px solid #F3F3F3;  
5 padding-top: 10px; 4 padding-top: 10px;
6 .media-left { 5 .media-left {
7 padding: 10px 0; 6 padding: 10px 0;
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"> 1 <ul class="nav navbar-nav">
2 <li ng-show="vm.profile"> 2 <li ng-show="vm.profile">
3 - <a href="#" role="button" ui-sref="main.profile.cms({profile: vm.profile.identifier})"> 3 + <a ng-show="vm.parentId" href="#" role="button" ui-sref="main.cms({profile: vm.profile.identifier, parent_id: vm.parentId})">
  4 + <i class="fa fa-file fa-fw fa-lg"></i> {{"navbar.content_viewer_actions.new_post" | translate}}
  5 + </a>
  6 + </li>
  7 + <li ng-show="vm.profile">
  8 + <a href="#" role="button" ui-sref="main.cms({profile: vm.profile.identifier, parent_id: vm.parentId, type: 'CommentParagraphPlugin::Discussion'})">
4 <i class="fa fa-file fa-fw fa-lg"></i> {{"navbar.content_viewer_actions.new_post" | translate}} 9 <i class="fa fa-file fa-fw fa-lg"></i> {{"navbar.content_viewer_actions.new_post" | translate}}
5 </a> 10 </a>
6 </li> 11 </li>
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/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
@@ -5,7 +5,7 @@ import {noosferoAngularRunBlock} from &quot;./index.run&quot;; @@ -5,7 +5,7 @@ import {noosferoAngularRunBlock} from &quot;./index.run&quot;;
5 import {MainComponent} from "./main/main.component"; 5 import {MainComponent} from "./main/main.component";
6 import {bootstrap, bundle} from "ng-forward"; 6 import {bootstrap, bundle} from "ng-forward";
7 7
8 -import {AUTH_EVENTS} from "./login/auth-events"; 8 +import {AuthEvents} from "./login/auth-events";
9 import {AuthController} from "./login/auth.controller"; 9 import {AuthController} from "./login/auth.controller";
10 10
11 import {Navbar} from "./layout/navbar/navbar"; 11 import {Navbar} from "./layout/navbar/navbar";
@@ -14,16 +14,17 @@ declare var moment: any; @@ -14,16 +14,17 @@ declare var moment: any;
14 14
15 let noosferoApp: any = bundle("noosferoApp", MainComponent, ["ngAnimate", "ngCookies", "ngStorage", "ngTouch", 15 let noosferoApp: any = bundle("noosferoApp", MainComponent, ["ngAnimate", "ngCookies", "ngStorage", "ngTouch",
16 "ngSanitize", "ngMessages", "ngAria", "restangular", 16 "ngSanitize", "ngMessages", "ngAria", "restangular",
17 - "ui.router", "ui.bootstrap", "toastr",  
18 - "angularMoment", "angular.filter", "akoenig.deckgrid", 17 + "ui.router", "ui.bootstrap", "toastr", "ngCkeditor",
  18 + "angular-bind-html-compile", "angularMoment", "angular.filter", "akoenig.deckgrid",
19 "angular-timeline", "duScroll", "oitozero.ngSweetAlert", 19 "angular-timeline", "duScroll", "oitozero.ngSweetAlert",
20 - "pascalprecht.translate", "tmh.dynamicLocale", "angularLoad"]).publish(); 20 + "pascalprecht.translate", "tmh.dynamicLocale", "angularLoad",
  21 + "angular-click-outside"]).publish();
21 22
22 NoosferoApp.angularModule = noosferoApp; 23 NoosferoApp.angularModule = noosferoApp;
23 24
24 25
25 NoosferoApp.addConstants("moment", moment); 26 NoosferoApp.addConstants("moment", moment);
26 -NoosferoApp.addConstants("AUTH_EVENTS", AUTH_EVENTS); 27 +NoosferoApp.addConstants("AuthEvents", AuthEvents);
27 28
28 NoosferoApp.addConfig(noosferoModuleConfig); 29 NoosferoApp.addConfig(noosferoModuleConfig);
29 NoosferoApp.run(noosferoAngularRunBlock); 30 NoosferoApp.run(noosferoAngularRunBlock);
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-block/communities-block.component.spec.ts
@@ -1,51 +0,0 @@ @@ -1,51 +0,0 @@
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-block/communities-block.component.ts
@@ -1,24 +0,0 @@ @@ -1,24 +0,0 @@
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-block/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-block/communities-block.html
@@ -1,5 +0,0 @@ @@ -1,5 +0,0 @@
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-block/communities-block.scss
@@ -1,16 +0,0 @@ @@ -1,16 +0,0 @@
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/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/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/link-list/link-list.component.spec.ts
@@ -1,65 +0,0 @@ @@ -1,65 +0,0 @@
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.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 -});  
66 \ No newline at end of file 0 \ No newline at end of file
src/app/layout/blocks/link-list/link-list.component.ts
@@ -1,20 +0,0 @@ @@ -1,20 +0,0 @@
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.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.html
@@ -1,7 +0,0 @@ @@ -1,7 +0,0 @@
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.scss
@@ -1,34 +0,0 @@ @@ -1,34 +0,0 @@
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/main-block/index.ts
@@ -1,2 +0,0 @@ @@ -1,2 +0,0 @@
1 -/* Module Index Entry - generated using the script npm run generate-index */  
2 -export * from "./main-block.component";  
src/app/layout/blocks/main-block/main-block.component.spec.ts
@@ -1,40 +0,0 @@ @@ -1,40 +0,0 @@
1 -import {TestComponentBuilder} from 'ng-forward/cjs/testing/test-component-builder';  
2 -import {Input, provide, Component, StateConfig} from 'ng-forward';  
3 -  
4 -import {MainBlockComponent} from './main-block.component';  
5 -import {NoosferoApp} from '../../../index.module';  
6 -  
7 -const tcb = new TestComponentBuilder();  
8 -  
9 -const htmlTemplate: string = '<noosfero-main-block [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-main-block>';  
10 -  
11 -describe("Components", () => {  
12 - describe("Main Block 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 Block will be load on our tests  
18 - beforeEach(angular.mock.module("templates"));  
19 -  
20 - it("check if the main block has a tag with ui-view attribute", done => {  
21 -  
22 - // Creating a container component (BlockContainerComponent) to include  
23 - // the component under test (Block)  
24 - @Component({ selector: 'test-container-component', template: htmlTemplate, directives: [MainBlockComponent] })  
25 - class BlockContainerComponent {  
26 - }  
27 -  
28 - // uses the TestComponentBuilder instance to initialize the component  
29 - tcb.createAsync(BlockContainerComponent).then(fixture => {  
30 - // and here we can inspect and run the test assertions  
31 - // let myComponent: MainBlockComponent = fixture.componentInstance;  
32 -  
33 - // assure the block object inside the Block matches  
34 - // the provided through the parent component  
35 - expect(fixture.debugElement.queryAll('[ui-view="mainBlockContent"]').length).toEqual(1);  
36 - done();  
37 - });  
38 - });  
39 - });  
40 -});  
41 \ No newline at end of file 0 \ No newline at end of file
src/app/layout/blocks/main-block/main-block.component.ts
@@ -1,10 +0,0 @@ @@ -1,10 +0,0 @@
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-block/main-block.html'  
7 -})  
8 -export class MainBlockComponent {  
9 -  
10 -}  
src/app/layout/blocks/main-block/main-block.html
@@ -1 +0,0 @@ @@ -1 +0,0 @@
1 -<div ui-view="mainBlockContent" autoscroll></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,40 @@ @@ -0,0 +1,40 @@
  1 +import {TestComponentBuilder} from 'ng-forward/cjs/testing/test-component-builder';
  2 +import {Input, provide, Component, StateConfig} from 'ng-forward';
  3 +
  4 +import {MainBlockComponent} from './main-block.component';
  5 +import {NoosferoApp} from '../../../index.module';
  6 +
  7 +const tcb = new TestComponentBuilder();
  8 +
  9 +const htmlTemplate: string = '<noosfero-main-block [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-main-block>';
  10 +
  11 +describe("Components", () => {
  12 + describe("Main Block 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 Block will be load on our tests
  18 + beforeEach(angular.mock.module("templates"));
  19 +
  20 + it("check if the main block has a tag with ui-view attribute", done => {
  21 +
  22 + // Creating a container component (BlockContainerComponent) to include
  23 + // the component under test (Block)
  24 + @Component({ selector: 'test-container-component', template: htmlTemplate, directives: [MainBlockComponent] })
  25 + class BlockContainerComponent {
  26 + }
  27 +
  28 + // uses the TestComponentBuilder instance to initialize the component
  29 + tcb.createAsync(BlockContainerComponent).then(fixture => {
  30 + // and here we can inspect and run the test assertions
  31 + // let myComponent: MainBlockComponent = fixture.componentInstance;
  32 +
  33 + // assure the block object inside the Block matches
  34 + // the provided through the parent component
  35 + expect(fixture.debugElement.queryAll('[ui-view="mainBlockContent"]').length).toEqual(1);
  36 + done();
  37 + });
  38 + });
  39 + });
  40 +});
0 \ No newline at end of file 41 \ No newline at end of file
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 +}
src/app/layout/blocks/main/main-block.html 0 → 100644
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 +<div ui-view="mainBlockContent" autoscroll></div>
src/app/layout/blocks/members-block/index.ts
@@ -1,2 +0,0 @@ @@ -1,2 +0,0 @@
1 -/* Module Index Entry - generated using the script npm run generate-index */  
2 -export * from "./members-block.component";  
src/app/layout/blocks/members-block/members-block.component.spec.ts
@@ -1,49 +0,0 @@ @@ -1,49 +0,0 @@
1 -import {TestComponentBuilder} from 'ng-forward/cjs/testing/test-component-builder';  
2 -import {Provider, Input, provide, Component} from 'ng-forward';  
3 -import {MembersBlockComponent} from './members-block.component';  
4 -import {ComponentTestHelper, createClass} from './../../../../spec/component-test-helper';  
5 -  
6 -const htmlTemplate: string = '<noosfero-members-block [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-members-block>';  
7 -  
8 -const tcb = new TestComponentBuilder();  
9 -  
10 -describe("Components", () => {  
11 - describe("Members Block Component", () => {  
12 -  
13 - let helper: ComponentTestHelper;  
14 -  
15 - let providers = [  
16 - new Provider('ProfileService', {  
17 - useValue: {  
18 - getProfileMembers: (profileId: number, filters: any): any => {  
19 - return Promise.resolve({ data: { people: [{ identifier: "person1" }] } });  
20 - }  
21 - }  
22 - }),  
23 - ];  
24 -  
25 - beforeEach(angular.mock.module("templates"));  
26 -  
27 - beforeEach( (done) => {  
28 - // Custom properties for the component  
29 - let properties = { owner: { id: 1 } };  
30 - // Create the component bed for the test.  
31 - let cls = createClass({  
32 - template: htmlTemplate,  
33 - directives: [MembersBlockComponent],  
34 - providers: providers,  
35 - properties: properties  
36 - });  
37 - helper = new ComponentTestHelper(cls, done);  
38 - });  
39 -  
40 - it("get members of the block owner", () => {  
41 - expect(helper.component.members[0].identifier).toEqual("person1");  
42 - });  
43 -  
44 - it("render the profile image for each member", () => {  
45 - expect(helper.all("noosfero-profile-image").length).toEqual(1);  
46 - });  
47 -  
48 - });  
49 -});  
50 \ No newline at end of file 0 \ No newline at end of file
src/app/layout/blocks/members-block/members-block.component.ts
@@ -1,25 +0,0 @@ @@ -1,25 +0,0 @@
1 -import {Input, Inject, Component} from "ng-forward";  
2 -import {ProfileService} from "../../../../lib/ng-noosfero-api/http/profile.service";  
3 -  
4 -@Component({  
5 - selector: "noosfero-members-block",  
6 - templateUrl: 'app/layout/blocks/members-block/members-block.html',  
7 -})  
8 -@Inject(ProfileService)  
9 -export class MembersBlockComponent {  
10 -  
11 - @Input() block: noosfero.Block;  
12 - @Input() owner: noosfero.Profile;  
13 -  
14 - members: any = [];  
15 -  
16 - constructor(private profileService: ProfileService) {  
17 -  
18 - }  
19 -  
20 - ngOnInit() {  
21 - this.profileService.getProfileMembers(this.owner.id, { per_page: 6 }).then((response: any) => {  
22 - this.members = response.data.people;  
23 - });  
24 - }  
25 -}  
src/app/layout/blocks/members-block/members-block.html
@@ -1,5 +0,0 @@ @@ -1,5 +0,0 @@
1 -<div class="members-block">  
2 - <a ng-repeat="member in ctrl.members" ui-sref="main.profile.home({profile: member.identifier})" class="member">  
3 - <noosfero-profile-image [profile]="member"></noosfero-profile-image>  
4 - </a>  
5 -</div>  
src/app/layout/blocks/members-block/members-block.scss
@@ -1,17 +0,0 @@ @@ -1,17 +0,0 @@
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/members/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 "./members-block.component";
src/app/layout/blocks/members/members-block.component.spec.ts 0 → 100644
@@ -0,0 +1,49 @@ @@ -0,0 +1,49 @@
  1 +import {TestComponentBuilder} from 'ng-forward/cjs/testing/test-component-builder';
  2 +import {Provider, Input, provide, Component} from 'ng-forward';
  3 +import {MembersBlockComponent} from './members-block.component';
  4 +import {ComponentTestHelper, createClass} from './../../../../spec/component-test-helper';
  5 +
  6 +const htmlTemplate: string = '<noosfero-members-block [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-members-block>';
  7 +
  8 +const tcb = new TestComponentBuilder();
  9 +
  10 +describe("Components", () => {
  11 + describe("Members Block Component", () => {
  12 +
  13 + let helper: ComponentTestHelper<MembersBlockComponent>;
  14 +
  15 + let providers = [
  16 + new Provider('ProfileService', {
  17 + useValue: {
  18 + getProfileMembers: (profileId: number, filters: any): any => {
  19 + return Promise.resolve({ data: { people: [{ identifier: "person1" }] } });
  20 + }
  21 + }
  22 + }),
  23 + ];
  24 +
  25 + beforeEach(angular.mock.module("templates"));
  26 +
  27 + beforeEach((done) => {
  28 + // Custom properties for the component
  29 + let properties = { owner: { id: 1 } };
  30 + // Create the component bed for the test.
  31 + let cls = createClass({
  32 + template: htmlTemplate,
  33 + directives: [MembersBlockComponent],
  34 + providers: providers,
  35 + properties: properties
  36 + });
  37 + helper = new ComponentTestHelper<MembersBlockComponent>(cls, done);
  38 + });
  39 +
  40 + it("get members of the block owner", () => {
  41 + expect(helper.component.members[0].identifier).toEqual("person1");
  42 + });
  43 +
  44 + it("render the profile image for each member", () => {
  45 + expect(helper.all("noosfero-profile-image").length).toEqual(1);
  46 + });
  47 +
  48 + });
  49 +});
src/app/layout/blocks/members/members-block.component.ts 0 → 100644
@@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
  1 +import {Input, Inject, Component} from "ng-forward";
  2 +import {ProfileService} from "../../../../lib/ng-noosfero-api/http/profile.service";
  3 +
  4 +@Component({
  5 + selector: "noosfero-members-block",
  6 + templateUrl: 'app/layout/blocks/members/members-block.html',
  7 +})
  8 +@Inject(ProfileService)
  9 +export class MembersBlockComponent {
  10 +
  11 + @Input() block: noosfero.Block;
  12 + @Input() owner: noosfero.Profile;
  13 +
  14 + members: any = [];
  15 +
  16 + constructor(private profileService: ProfileService) {
  17 +
  18 + }
  19 +
  20 + ngOnInit() {
  21 + this.profileService.getProfileMembers(this.owner.id, { per_page: 6 }).then((response: any) => {
  22 + this.members = response.data.people;
  23 + });
  24 + }
  25 +}
src/app/layout/blocks/members/members-block.html 0 → 100644
@@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
  1 +<div class="members-block">
  2 + <a ng-repeat="member in ctrl.members" ui-sref="main.profile.home({profile: member.identifier})" class="member">
  3 + <noosfero-profile-image [profile]="member"></noosfero-profile-image>
  4 + </a>
  5 +</div>
src/app/layout/blocks/members/members-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/people-block/index.ts
@@ -1,2 +0,0 @@ @@ -1,2 +0,0 @@
1 -/* Module Index Entry - generated using the script npm run generate-index */  
2 -export * from "./people-block.component";  
src/app/layout/blocks/people-block/people-block.component.spec.ts
@@ -1,72 +0,0 @@ @@ -1,72 +0,0 @@
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 {PeopleBlockComponent} from './people-block.component';  
6 -  
7 -const htmlTemplate: string = '<noosfero-people-block [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-people-block>';  
8 -  
9 -describe("Components", () => {  
10 -  
11 - describe("People Block Component", () => {  
12 - let serviceMock = {  
13 - getEnvironmentPeople: (filters: any): any => {  
14 - return Promise.resolve([{ identifier: "person1" }]);  
15 - }  
16 - };  
17 - let providers = [new Provider('EnvironmentService', { useValue: serviceMock })];  
18 -  
19 - let helper: ComponentTestHelper;  
20 -  
21 - beforeEach( angular.mock.module("templates") );  
22 -  
23 - /**  
24 - * The beforeEach procedure will initialize the helper and parse  
25 - * the component according to the given providers. Unfortunetly, in  
26 - * this mode, the providers and properties given to the construtor  
27 - * can't be overriden.  
28 - */  
29 - beforeEach( (done) => {  
30 - // Create the component bed for the test. Optionally, this could be done  
31 - // in each test if one needs customization of these parameters per test  
32 - let cls = createClass({  
33 - template: htmlTemplate,  
34 - directives: [PeopleBlockComponent],  
35 - providers: providers,  
36 - properties: {}  
37 - });  
38 - helper = new ComponentTestHelper(cls, done);  
39 - });  
40 -  
41 - /**  
42 - * By default the helper will have the component, with all properties  
43 - * ready to be used. Here the mock provider 'EnvironmentService' will  
44 - * return the given array with one person.  
45 - */  
46 - it("get block with one people", () => {  
47 - expect(helper.component.people[0].identifier).toEqual("person1");  
48 - });  
49 -  
50 - /**  
51 - * There are helper functions to access the JQuery DOM like this.  
52 - */  
53 - it("render the profile image for each person", () => {  
54 - expect(helper.all("noosfero-profile-image").length).toEqual(1);  
55 - });  
56 -  
57 - /**  
58 - * The main debugElement element is also available  
59 - */  
60 - it("render the main noosfero people block", () => {  
61 - expect(helper.debugElement.children().length).toEqual(1, "The people-block should have a div children");  
62 - });  
63 -  
64 - /**  
65 - * Just another example of a JQuery DOM helper function  
66 - */  
67 - it("render the noosfero people block div", () => {  
68 - let div = helper.findChildren("noosfero-people-block", "div");  
69 - expect(div.className).toBe('people-block', "The class should be people-block");  
70 - });  
71 - });  
72 -});  
73 \ No newline at end of file 0 \ No newline at end of file
src/app/layout/blocks/people-block/people-block.component.ts
@@ -1,26 +0,0 @@ @@ -1,26 +0,0 @@
1 -import {Input, Inject, Component} from "ng-forward";  
2 -import {EnvironmentService} from "../../../../lib/ng-noosfero-api/http/environment.service";  
3 -  
4 -@Component({  
5 - selector: "noosfero-people-block",  
6 - templateUrl: 'app/layout/blocks/people-block/people-block.html',  
7 -})  
8 -@Inject(EnvironmentService)  
9 -export class PeopleBlockComponent {  
10 -  
11 - @Input() block: noosfero.Block;  
12 - @Input() owner: noosfero.Environment;  
13 - private type: string = "people";  
14 -  
15 - people: noosfero.Person[] = [];  
16 -  
17 - constructor(private environmentService: EnvironmentService) {  
18 - }  
19 -  
20 - ngOnInit() {  
21 - this.environmentService.getEnvironmentPeople({ limit: '6' }).then((people: noosfero.Person[]) => {  
22 - this.people = people;  
23 - });  
24 - }  
25 -  
26 -}  
src/app/layout/blocks/people-block/people-block.html
@@ -1,5 +0,0 @@ @@ -1,5 +0,0 @@
1 -<div class="{{ctrl.type}}-block">  
2 - <a ng-repeat="person in ctrl.people" ui-sref="main.profile.home({profile: person.identifier})" class="person">  
3 - <noosfero-profile-image [profile]="person"></noosfero-profile-image>  
4 - </a>  
5 -</div>  
src/app/layout/blocks/people-block/people-block.scss
@@ -1,17 +0,0 @@ @@ -1,17 +0,0 @@
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/people/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 "./people-block.component";
src/app/layout/blocks/people/people-block.component.spec.ts 0 → 100644
@@ -0,0 +1,72 @@ @@ -0,0 +1,72 @@
  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 {PeopleBlockComponent} from './people-block.component';
  6 +
  7 +const htmlTemplate: string = '<noosfero-people-block [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-people-block>';
  8 +
  9 +describe("Components", () => {
  10 +
  11 + describe("People Block Component", () => {
  12 + let serviceMock = {
  13 + getEnvironmentPeople: (filters: any): any => {
  14 + return Promise.resolve([{ identifier: "person1" }]);
  15 + }
  16 + };
  17 + let providers = [new Provider('EnvironmentService', { useValue: serviceMock })];
  18 +
  19 + let helper: ComponentTestHelper<PeopleBlockComponent>;
  20 +
  21 + beforeEach(angular.mock.module("templates"));
  22 +
  23 + /**
  24 + * The beforeEach procedure will initialize the helper and parse
  25 + * the component according to the given providers. Unfortunetly, in
  26 + * this mode, the providers and properties given to the construtor
  27 + * can't be overriden.
  28 + */
  29 + beforeEach((done) => {
  30 + // Create the component bed for the test. Optionally, this could be done
  31 + // in each test if one needs customization of these parameters per test
  32 + let cls = createClass({
  33 + template: htmlTemplate,
  34 + directives: [PeopleBlockComponent],
  35 + providers: providers,
  36 + properties: {}
  37 + });
  38 + helper = new ComponentTestHelper<PeopleBlockComponent>(cls, done);
  39 + });
  40 +
  41 + /**
  42 + * By default the helper will have the component, with all properties
  43 + * ready to be used. Here the mock provider 'EnvironmentService' will
  44 + * return the given array with one person.
  45 + */
  46 + it("get block with one people", () => {
  47 + expect(helper.component.people[0].identifier).toEqual("person1");
  48 + });
  49 +
  50 + /**
  51 + * There are helper functions to access the JQuery DOM like this.
  52 + */
  53 + it("render the profile image for each person", () => {
  54 + expect(helper.all("noosfero-profile-image").length).toEqual(1);
  55 + });
  56 +
  57 + /**
  58 + * The main debugElement element is also available
  59 + */
  60 + it("render the main noosfero people block", () => {
  61 + expect(helper.debugElement.children().length).toEqual(1, "The people-block should have a div children");
  62 + });
  63 +
  64 + /**
  65 + * Just another example of a JQuery DOM helper function
  66 + */
  67 + it("render the noosfero people block div", () => {
  68 + let div = helper.findChildren("noosfero-people-block", "div");
  69 + expect(div.className).toBe('people-block', "The class should be people-block");
  70 + });
  71 + });
  72 +});
src/app/layout/blocks/people/people-block.component.ts 0 → 100644
@@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
  1 +import {Input, Inject, Component} from "ng-forward";
  2 +import {EnvironmentService} from "../../../../lib/ng-noosfero-api/http/environment.service";
  3 +
  4 +@Component({
  5 + selector: "noosfero-people-block",
  6 + templateUrl: 'app/layout/blocks/people/people-block.html',
  7 +})
  8 +@Inject(EnvironmentService)
  9 +export class PeopleBlockComponent {
  10 +
  11 + @Input() block: noosfero.Block;
  12 + @Input() owner: noosfero.Environment;
  13 + private type: string = "people";
  14 +
  15 + people: noosfero.Person[] = [];
  16 +
  17 + constructor(private environmentService: EnvironmentService) {
  18 + }
  19 +
  20 + ngOnInit() {
  21 + this.environmentService.getEnvironmentPeople({ limit: '6' }).then((people: noosfero.Person[]) => {
  22 + this.people = people;
  23 + });
  24 + }
  25 +
  26 +}
src/app/layout/blocks/people/people-block.html 0 → 100644
@@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
  1 +<div class="{{ctrl.type}}-block">
  2 + <a ng-repeat="person in ctrl.people" ui-sref="main.profile.home({profile: person.identifier})" class="person">
  3 + <noosfero-profile-image [profile]="person"></noosfero-profile-image>
  4 + </a>
  5 +</div>
src/app/layout/blocks/people/people-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/profile-image-block/index.ts
@@ -1,2 +0,0 @@ @@ -1,2 +0,0 @@
1 -/* Module Index Entry - generated using the script npm run generate-index */  
2 -export * from "./profile-image-block.component";  
src/app/layout/blocks/profile-image-block/profile-image-block.component.spec.ts
@@ -1,46 +0,0 @@ @@ -1,46 +0,0 @@
1 -import {TestComponentBuilder, ComponentFixture} from 'ng-forward/cjs/testing/test-component-builder';  
2 -import {Pipe, Input, provide, Component} from 'ng-forward';  
3 -  
4 -import {ProfileImageBlockComponent} from './profile-image-block.component';  
5 -  
6 -import * as helpers from "./../../../../spec/helpers";  
7 -  
8 -const tcb = new TestComponentBuilder();  
9 -  
10 -const htmlTemplate: string = '<noosfero-profile-image-block [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-profile-image-block>';  
11 -  
12 -describe("Components", () => {  
13 -  
14 - describe("Profile Image Block Component", () => {  
15 -  
16 - beforeEach(angular.mock.module("templates"));  
17 -  
18 - @Component({  
19 - selector: 'test-container-component',  
20 - template: htmlTemplate,  
21 - directives: [ProfileImageBlockComponent],  
22 - providers: helpers.provideFilters("translateFilter")  
23 - })  
24 - class BlockContainerComponent {  
25 - block = { type: 'Block' };  
26 - owner = { name: 'profile-name' };  
27 - constructor() {  
28 - }  
29 - }  
30 -  
31 - it("show image if present", () => {  
32 - helpers.tcb.createAsync(BlockContainerComponent).then(fixture => {  
33 - let elProfile = fixture.debugElement.componentViewChildren[0];  
34 - expect(elProfile.query('div.profile-image-block').length).toEqual(1);  
35 - });  
36 - });  
37 -  
38 - it("has link to the profile", () => {  
39 - helpers.tcb.createAsync(BlockContainerComponent).then(fixture => {  
40 - let elProfile = fixture.debugElement.componentViewChildren[0];  
41 - expect(elProfile.query('a.settings-link').length).toEqual(1);  
42 - });  
43 - });  
44 -  
45 - });  
46 -});  
src/app/layout/blocks/profile-image-block/profile-image-block.component.ts
@@ -1,14 +0,0 @@ @@ -1,14 +0,0 @@
1 -import {Inject, Input, Component} from "ng-forward";  
2 -import {ProfileImageComponent} from "./../../../profile/image/image.component";  
3 -  
4 -@Component({  
5 - selector: "noosfero-profile-image-block",  
6 - templateUrl: 'app/layout/blocks/profile-image-block/profile-image-block.html',  
7 - directives: [ProfileImageComponent]  
8 -})  
9 -export class ProfileImageBlockComponent {  
10 -  
11 - @Input() block: noosfero.Block;  
12 - @Input() owner: noosfero.Profile;  
13 -  
14 -}  
src/app/layout/blocks/profile-image-block/profile-image-block.html
@@ -1,6 +0,0 @@ @@ -1,6 +0,0 @@
1 -<div class="center-block text-center profile-image-block">  
2 - <a ui-sref="main.profile.info({profile: ctrl.owner.identifier})">  
3 - <noosfero-profile-image [profile]="ctrl.owner"></noosfero-profile-image>  
4 - </a>  
5 - <a class="settings-link" target="_self" ui-sref="main.profile.settings({profile: ctrl.owner.identifier})">{{"blocks.profile_image.control_panel" | translate}}</a>  
6 -</div>  
src/app/layout/blocks/profile-image-block/profile-image-block.scss
@@ -1,5 +0,0 @@ @@ -1,5 +0,0 @@
1 -.profile-image-block {  
2 - .settings-link {  
3 - display: block;  
4 - }  
5 -}  
src/app/layout/blocks/profile-image/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 "./profile-image-block.component";
src/app/layout/blocks/profile-image/profile-image-block.component.spec.ts 0 → 100644
@@ -0,0 +1,46 @@ @@ -0,0 +1,46 @@
  1 +import {TestComponentBuilder, ComponentFixture} from 'ng-forward/cjs/testing/test-component-builder';
  2 +import {Pipe, Input, provide, Component} from 'ng-forward';
  3 +
  4 +import {ProfileImageBlockComponent} from './profile-image-block.component';
  5 +
  6 +import * as helpers from "./../../../../spec/helpers";
  7 +
  8 +const tcb = new TestComponentBuilder();
  9 +
  10 +const htmlTemplate: string = '<noosfero-profile-image-block [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-profile-image-block>';
  11 +
  12 +describe("Components", () => {
  13 +
  14 + describe("Profile Image Block Component", () => {
  15 +
  16 + beforeEach(angular.mock.module("templates"));
  17 +
  18 + @Component({
  19 + selector: 'test-container-component',
  20 + template: htmlTemplate,
  21 + directives: [ProfileImageBlockComponent],
  22 + providers: helpers.provideFilters("translateFilter")
  23 + })
  24 + class BlockContainerComponent {
  25 + block = { type: 'Block' };
  26 + owner = { name: 'profile-name' };
  27 + constructor() {
  28 + }
  29 + }
  30 +
  31 + it("show image if present", () => {
  32 + helpers.tcb.createAsync(BlockContainerComponent).then(fixture => {
  33 + let elProfile = fixture.debugElement.componentViewChildren[0];
  34 + expect(elProfile.query('div.profile-image-block').length).toEqual(1);
  35 + });
  36 + });
  37 +
  38 + it("has link to the profile", () => {
  39 + helpers.tcb.createAsync(BlockContainerComponent).then(fixture => {
  40 + let elProfile = fixture.debugElement.componentViewChildren[0];
  41 + expect(elProfile.query('a.settings-link').length).toEqual(1);
  42 + });
  43 + });
  44 +
  45 + });
  46 +});
src/app/layout/blocks/profile-image/profile-image-block.component.ts 0 → 100644
@@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
  1 +import {Inject, Input, Component} from "ng-forward";
  2 +import {ProfileImageComponent} from "./../../../profile/image/image.component";
  3 +
  4 +@Component({
  5 + selector: "noosfero-profile-image-block",
  6 + templateUrl: 'app/layout/blocks/profile-image/profile-image-block.html',
  7 + directives: [ProfileImageComponent]
  8 +})
  9 +export class ProfileImageBlockComponent {
  10 +
  11 + @Input() block: noosfero.Block;
  12 + @Input() owner: noosfero.Profile;
  13 +
  14 +}
src/app/layout/blocks/profile-image/profile-image-block.html 0 → 100644
@@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
  1 +<div class="center-block text-center profile-image-block">
  2 + <a ui-sref="main.profile.info({profile: ctrl.owner.identifier})">
  3 + <noosfero-profile-image [profile]="ctrl.owner"></noosfero-profile-image>
  4 + </a>
  5 + <a class="settings-link" target="_self" ui-sref="main.profile.settings({profile: ctrl.owner.identifier})">{{"blocks.profile_image.control_panel" | translate}}</a>
  6 +</div>
src/app/layout/blocks/profile-image/profile-image-block.scss 0 → 100644
@@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
  1 +.profile-image-block {
  2 + .settings-link {
  3 + display: block;
  4 + }
  5 +}
src/app/layout/blocks/raw-html/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 "./raw-html.component"; 2 +export * from "./raw-html-block.component";
src/app/layout/blocks/raw-html/raw-html-block.component.spec.ts 0 → 100644
@@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
  1 +import {TestComponentBuilder} from 'ng-forward/cjs/testing/test-component-builder';
  2 +import {Component} from 'ng-forward';
  3 +
  4 +import {RawHTMLBlockComponent} from './raw-html-block.component';
  5 +
  6 +const tcb = new TestComponentBuilder();
  7 +
  8 +const htmlTemplate: string = '<noosfero-raw-html-block [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-raw-html-block>';
  9 +
  10 +describe("Components", () => {
  11 +
  12 + describe("Raw Html Block Component", () => {
  13 +
  14 + beforeEach(angular.mock.module("templates"));
  15 + beforeEach(angular.mock.module("ngSanitize"));
  16 +
  17 + it("display html stored in block settings", done => {
  18 +
  19 + @Component({
  20 + selector: 'test-container-component',
  21 + template: htmlTemplate,
  22 + directives: [RawHTMLBlockComponent],
  23 + })
  24 + class CustomBlockType {
  25 + block: any = { settings: { html: '<em>block content</em>' } };
  26 + owner: any = { name: 'profile-name' };
  27 + }
  28 + tcb.createAsync(CustomBlockType).then(fixture => {
  29 + expect(fixture.debugElement.query(".raw-html-block em").text().trim()).toEqual('block content');
  30 + done();
  31 + });
  32 + });
  33 +
  34 + });
  35 +
  36 +});
src/app/layout/blocks/raw-html/raw-html-block.component.ts 0 → 100644
@@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
  1 +import {Component, Input} from "ng-forward";
  2 +
  3 +@Component({
  4 + selector: "noosfero-raw-html-block",
  5 + templateUrl: 'app/layout/blocks/raw-html/raw-html-block.html'
  6 +})
  7 +
  8 +export class RawHTMLBlockComponent {
  9 +
  10 + @Input() block: any;
  11 + @Input() owner: any;
  12 +
  13 + html: string;
  14 +
  15 + ngOnInit() {
  16 + this.html = this.block.settings.html;
  17 + }
  18 +}
src/app/layout/blocks/raw-html/raw-html-block.html 0 → 100644
@@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
  1 +<div class="raw-html-block" ng-bind-html="ctrl.html">
  2 +</div>
src/app/layout/blocks/raw-html/raw-html.component.spec.ts
@@ -1,36 +0,0 @@ @@ -1,36 +0,0 @@
1 -import {TestComponentBuilder} from 'ng-forward/cjs/testing/test-component-builder';  
2 -import {Component} from 'ng-forward';  
3 -  
4 -import {RawHTMLBlockComponent} from './raw-html.component';  
5 -  
6 -const tcb = new TestComponentBuilder();  
7 -  
8 -const htmlTemplate: string = '<noosfero-raw-htmlblock [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-raw-htmlblock>';  
9 -  
10 -describe("Components", () => {  
11 -  
12 - describe("Raw Html Block Component", () => {  
13 -  
14 - beforeEach(angular.mock.module("templates"));  
15 - beforeEach(angular.mock.module("ngSanitize"));  
16 -  
17 - it("display html stored in block settings", done => {  
18 -  
19 - @Component({  
20 - selector: 'test-container-component',  
21 - template: htmlTemplate,  
22 - directives: [RawHTMLBlockComponent],  
23 - })  
24 - class CustomBlockType {  
25 - block: any = { settings: { html: '<em>block content</em>' } };  
26 - owner: any = { name: 'profile-name' };  
27 - }  
28 - tcb.createAsync(CustomBlockType).then(fixture => {  
29 - expect(fixture.debugElement.query(".raw-html-block em").text().trim()).toEqual('block content');  
30 - done();  
31 - });  
32 - });  
33 -  
34 - });  
35 -  
36 -});  
src/app/layout/blocks/raw-html/raw-html.component.ts
@@ -1,18 +0,0 @@ @@ -1,18 +0,0 @@
1 -import {Component, Input} from "ng-forward";  
2 -  
3 -@Component({  
4 - selector: "noosfero-raw-htmlblock",  
5 - templateUrl: 'app/layout/blocks/raw-html/raw-html.html'  
6 -})  
7 -  
8 -export class RawHTMLBlockComponent {  
9 -  
10 - @Input() block: any;  
11 - @Input() owner: any;  
12 -  
13 - html: string;  
14 -  
15 - ngOnInit() {  
16 - this.html = this.block.settings.html;  
17 - }  
18 -}  
src/app/layout/blocks/raw-html/raw-html.html
@@ -1,2 +0,0 @@ @@ -1,2 +0,0 @@
1 -<div class="raw-html-block" ng-bind-html="ctrl.html">  
2 -</div>  
src/app/layout/blocks/recent-documents/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 "./recent-documents.component"; 2 +export * from "./recent-documents-block.component";
src/app/layout/blocks/recent-documents/recent-documents-block.component.spec.ts 0 → 100644
@@ -0,0 +1,80 @@ @@ -0,0 +1,80 @@
  1 +import {TestComponentBuilder} from 'ng-forward/cjs/testing/test-component-builder';
  2 +import {Provider, Input, provide, Component} from 'ng-forward';
  3 +import {provideFilters} from '../../../../spec/helpers';
  4 +import {RecentDocumentsBlockComponent} from './recent-documents-block.component';
  5 +
  6 +const htmlTemplate: string = '<noosfero-recent-documents-block [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-recent-documents-block>';
  7 +
  8 +const tcb = new TestComponentBuilder();
  9 +
  10 +describe("Components", () => {
  11 + describe("Recent Documents Block Component", () => {
  12 +
  13 + let settingsObj = {};
  14 + let mockedArticleService = {
  15 + getByProfile: (profile: noosfero.Profile, filters: any): any => {
  16 + return Promise.resolve({ data: [{ name: "article1" }], headers: (name: string) => { return name; } });
  17 + }
  18 + };
  19 + let profile = { name: 'profile-name' };
  20 + beforeEach(angular.mock.module("templates"));
  21 +
  22 + let state = jasmine.createSpyObj("state", ["go"]);
  23 +
  24 +
  25 + function getProviders() {
  26 + return [
  27 + new Provider('$state', { useValue: state }),
  28 + new Provider('ArticleService', {
  29 + useValue: mockedArticleService
  30 + }),
  31 + ].concat(provideFilters("truncateFilter", "stripTagsFilter"));
  32 + }
  33 + let componentClass: any = null;
  34 +
  35 + function getComponent() {
  36 + @Component({ selector: 'test-container-component', template: htmlTemplate, directives: [RecentDocumentsBlockComponent], providers: getProviders() })
  37 + class BlockContainerComponent {
  38 + block = { type: 'Block', settings: settingsObj };
  39 + owner = profile;
  40 + constructor() {
  41 + }
  42 + }
  43 + return BlockContainerComponent;
  44 + }
  45 +
  46 +
  47 + it("get recent documents from the article service", done => {
  48 + tcb.createAsync(getComponent()).then(fixture => {
  49 + let recentDocumentsBlock: RecentDocumentsBlockComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
  50 + expect(recentDocumentsBlock.documents).toEqual([{ name: "article1" }]);
  51 + done();
  52 + });
  53 + });
  54 +
  55 + it("go to article page when open a document", done => {
  56 + tcb.createAsync(getComponent()).then(fixture => {
  57 + let recentDocumentsBlock: RecentDocumentsBlockComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
  58 + recentDocumentsBlock.openDocument({ path: "path", profile: { identifier: "identifier" } });
  59 + expect(state.go).toHaveBeenCalledWith("main.profile.page", { page: "path", profile: "identifier" });
  60 + done();
  61 + });
  62 + });
  63 +
  64 + it("it uses default limit 5 if not defined on block", done => {
  65 + settingsObj = null;
  66 + mockedArticleService = jasmine.createSpyObj("mockedArticleService", ["getByProfile"]);
  67 + (<any>mockedArticleService).mocked = true;
  68 + let thenMocked = jasmine.createSpy("then");
  69 + mockedArticleService.getByProfile = jasmine.createSpy("getByProfile").and.returnValue({then: thenMocked});
  70 + let getByProfileFunct = mockedArticleService.getByProfile;
  71 + tcb.createAsync(getComponent()).then(fixture => {
  72 + let recentDocumentsBlock: RecentDocumentsBlockComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
  73 + recentDocumentsBlock.openDocument({ path: "path", profile: { identifier: "identifier" } });
  74 + expect(getByProfileFunct).toHaveBeenCalledWith(profile, { content_type: 'TinyMceArticle', per_page: 5 });
  75 + done();
  76 + });
  77 + });
  78 +
  79 + });
  80 +});
src/app/layout/blocks/recent-documents/recent-documents-block.component.ts 0 → 100644
@@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
  1 +import {Component, Inject, Input} from "ng-forward";
  2 +import {ArticleService} from "../../../../lib/ng-noosfero-api/http/article.service";
  3 +
  4 +@Component({
  5 + selector: "noosfero-recent-documents-block",
  6 + templateUrl: 'app/layout/blocks/recent-documents/recent-documents-block.html'
  7 +})
  8 +@Inject(ArticleService, "$state")
  9 +export class RecentDocumentsBlockComponent {
  10 +
  11 + @Input() block: any;
  12 + @Input() owner: any;
  13 +
  14 + profile: any;
  15 + documents: any;
  16 +
  17 + documentsLoaded: boolean = false;
  18 +
  19 + constructor(private articleService: ArticleService, private $state: any) {
  20 + }
  21 +
  22 + ngOnInit() {
  23 + this.profile = this.owner;
  24 + this.documents = [];
  25 +
  26 + let limit = ((this.block && this.block.settings) ? this.block.settings.limit : null) || 5;
  27 + // FIXME get all text articles
  28 + // FIXME make the getByProfile a generic method where we tell the type passing a class TinyMceArticle
  29 + // and the promise should be of type TinyMceArticle[], per example
  30 + this.articleService.getByProfile(this.profile, { content_type: 'TinyMceArticle', per_page: limit })
  31 + .then((result: noosfero.RestResult<noosfero.Article[]>) => {
  32 + this.documents = <noosfero.Article[]>result.data;
  33 + this.documentsLoaded = true;
  34 + });
  35 + }
  36 +
  37 + openDocument(article: any) {
  38 + this.$state.go("main.profile.page", { page: article.path, profile: article.profile.identifier });
  39 + }
  40 +
  41 +}
src/app/layout/blocks/recent-documents/recent-documents-block.html 0 → 100644
@@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
  1 +<div deckgrid source="ctrl.documents" class="deckgrid">
  2 + <div class="a-card panel media" ng-click="mother.ctrl.openDocument(card);">
  3 + <div class="author media-left" ng-show="card.author.image">
  4 + <img ng-src="{{card.author.image.url}}" class="img-circle">
  5 + </div>
  6 + <div class="header media-body">
  7 + <h5 class="title media-heading" ng-bind="card.title"></h5>
  8 +
  9 + <div class="subheader">
  10 + <span class="time">
  11 + <i class="fa fa-clock-o"></i> <span am-time-ago="card.created_at | dateFormat"></span>
  12 + </span>
  13 + </div>
  14 + </div>
  15 + <img ng-show="card.image" ng-src="{{card.image.url}}" class="img-responsive article-image">
  16 + <div class="post-lead" ng-bind-html="card.body | stripTags | truncate: 100: '...': true"></div>
  17 + </div>
  18 +</div>
src/app/layout/blocks/recent-documents/recent-documents-block.scss 0 → 100644
@@ -0,0 +1,65 @@ @@ -0,0 +1,65 @@
  1 +.block.recentdocumentsblock {
  2 + .deckgrid[deckgrid]::before {
  3 + font-size: 0; /* See https://github.com/akoenig/angular-deckgrid/issues/14#issuecomment-35728861 */
  4 + visibility: hidden;
  5 + }
  6 + .author {
  7 + img {
  8 + width: 30px;
  9 + height: 30px;
  10 + }
  11 + }
  12 + .header {
  13 + .subheader {
  14 + color: #C1C1C1;
  15 + font-size: 10px;
  16 + }
  17 + }
  18 + .post-lead {
  19 + color: #8E8E8E;
  20 + font-size: 14px;
  21 + }
  22 + .article-image {
  23 + margin: 10px 0;
  24 + }
  25 +}
  26 +
  27 +.col-md-2-5 {
  28 + .deckgrid[deckgrid]::before {
  29 + content: '1 .deck-column';
  30 + }
  31 +}
  32 +
  33 +.col-md-7 {
  34 + .block.recentdocumentsblock {
  35 + background-color: transparent;
  36 + border: 0;
  37 +
  38 + .deckgrid[deckgrid]::before {
  39 + content: '3 .deck-column';
  40 + }
  41 +
  42 + .panel-heading {
  43 + display: none;
  44 + }
  45 + .panel-body {
  46 + padding: 0;
  47 + }
  48 +
  49 + .deckgrid {
  50 + .column {
  51 + float: left;
  52 + }
  53 +
  54 + .deck-column {
  55 + @extend .col-md-4;
  56 + padding: 0;
  57 +
  58 + .a-card {
  59 + padding: 10px;
  60 + margin: 3px;
  61 + }
  62 + }
  63 + }
  64 + }
  65 +}
src/app/layout/blocks/recent-documents/recent-documents.component.spec.ts
@@ -1,80 +0,0 @@ @@ -1,80 +0,0 @@
1 -import {TestComponentBuilder} from 'ng-forward/cjs/testing/test-component-builder';  
2 -import {Provider, Input, provide, Component} from 'ng-forward';  
3 -import {provideFilters} from '../../../../spec/helpers';  
4 -import {RecentDocumentsBlockComponent} from './recent-documents.component';  
5 -  
6 -const htmlTemplate: string = '<noosfero-recent-documents-block [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-recent-documents-block>';  
7 -  
8 -const tcb = new TestComponentBuilder();  
9 -  
10 -describe("Components", () => {  
11 - describe("Recent Documents Block Component", () => {  
12 -  
13 - let settingsObj = {};  
14 - let mockedArticleService = {  
15 - getByProfile: (profile: noosfero.Profile, filters: any): any => {  
16 - return Promise.resolve({ data: [{ name: "article1" }], headers: (name: string) => { return name; } });  
17 - }  
18 - };  
19 - let profile = { name: 'profile-name' };  
20 - beforeEach(angular.mock.module("templates"));  
21 -  
22 - let state = jasmine.createSpyObj("state", ["go"]);  
23 -  
24 -  
25 - function getProviders() {  
26 - return [  
27 - new Provider('$state', { useValue: state }),  
28 - new Provider('ArticleService', {  
29 - useValue: mockedArticleService  
30 - }),  
31 - ].concat(provideFilters("truncateFilter", "stripTagsFilter"));  
32 - }  
33 - let componentClass: any = null;  
34 -  
35 - function getComponent() {  
36 - @Component({ selector: 'test-container-component', template: htmlTemplate, directives: [RecentDocumentsBlockComponent], providers: getProviders() })  
37 - class BlockContainerComponent {  
38 - block = { type: 'Block', settings: settingsObj };  
39 - owner = profile;  
40 - constructor() {  
41 - }  
42 - }  
43 - return BlockContainerComponent;  
44 - }  
45 -  
46 -  
47 - it("get recent documents from the article service", done => {  
48 - tcb.createAsync(getComponent()).then(fixture => {  
49 - let recentDocumentsBlock: RecentDocumentsBlockComponent = fixture.debugElement.componentViewChildren[0].componentInstance;  
50 - expect(recentDocumentsBlock.documents).toEqual([{ name: "article1" }]);  
51 - done();  
52 - });  
53 - });  
54 -  
55 - it("go to article page when open a document", done => {  
56 - tcb.createAsync(getComponent()).then(fixture => {  
57 - let recentDocumentsBlock: RecentDocumentsBlockComponent = fixture.debugElement.componentViewChildren[0].componentInstance;  
58 - recentDocumentsBlock.openDocument({ path: "path", profile: { identifier: "identifier" } });  
59 - expect(state.go).toHaveBeenCalledWith("main.profile.page", { page: "path", profile: "identifier" });  
60 - done();  
61 - });  
62 - });  
63 -  
64 - it("it uses default limit 5 if not defined on block", done => {  
65 - settingsObj = null;  
66 - mockedArticleService = jasmine.createSpyObj("mockedArticleService", ["getByProfile"]);  
67 - (<any>mockedArticleService).mocked = true;  
68 - let thenMocked = jasmine.createSpy("then");  
69 - mockedArticleService.getByProfile = jasmine.createSpy("getByProfile").and.returnValue({then: thenMocked});  
70 - let getByProfileFunct = mockedArticleService.getByProfile;  
71 - tcb.createAsync(getComponent()).then(fixture => {  
72 - let recentDocumentsBlock: RecentDocumentsBlockComponent = fixture.debugElement.componentViewChildren[0].componentInstance;  
73 - recentDocumentsBlock.openDocument({ path: "path", profile: { identifier: "identifier" } });  
74 - expect(getByProfileFunct).toHaveBeenCalledWith(profile, { content_type: 'TinyMceArticle', per_page: 5 });  
75 - done();  
76 - });  
77 - });  
78 -  
79 - });  
80 -});  
81 \ No newline at end of file 0 \ No newline at end of file
src/app/layout/blocks/recent-documents/recent-documents.component.ts
@@ -1,41 +0,0 @@ @@ -1,41 +0,0 @@
1 -import {Component, Inject, Input} from "ng-forward";  
2 -import {ArticleService} from "../../../../lib/ng-noosfero-api/http/article.service";  
3 -  
4 -@Component({  
5 - selector: "noosfero-recent-documents-block",  
6 - templateUrl: 'app/layout/blocks/recent-documents/recent-documents.html'  
7 -})  
8 -@Inject(ArticleService, "$state")  
9 -export class RecentDocumentsBlockComponent {  
10 -  
11 - @Input() block: any;  
12 - @Input() owner: any;  
13 -  
14 - profile: any;  
15 - documents: any;  
16 -  
17 - documentsLoaded: boolean = false;  
18 -  
19 - constructor(private articleService: ArticleService, private $state: any) {  
20 - }  
21 -  
22 - ngOnInit() {  
23 - this.profile = this.owner;  
24 - this.documents = [];  
25 -  
26 - let limit = ((this.block && this.block.settings) ? this.block.settings.limit : null) || 5;  
27 - // FIXME get all text articles  
28 - // FIXME make the getByProfile a generic method where we tell the type passing a class TinyMceArticle  
29 - // and the promise should be of type TinyMceArticle[], per example  
30 - this.articleService.getByProfile(this.profile, { content_type: 'TinyMceArticle', per_page: limit })  
31 - .then((result: noosfero.RestResult<noosfero.Article[]>) => {  
32 - this.documents = <noosfero.Article[]>result.data;  
33 - this.documentsLoaded = true;  
34 - });  
35 - }  
36 -  
37 - openDocument(article: any) {  
38 - this.$state.go("main.profile.page", { page: article.path, profile: article.profile.identifier });  
39 - }  
40 -  
41 -}  
src/app/layout/blocks/recent-documents/recent-documents.html
@@ -1,18 +0,0 @@ @@ -1,18 +0,0 @@
1 -<div deckgrid source="ctrl.documents" class="deckgrid">  
2 - <div class="a-card panel media" ng-click="mother.ctrl.openDocument(card);">  
3 - <div class="author media-left" ng-show="card.author.image">  
4 - <img ng-src="{{card.author.image.url}}" class="img-circle">  
5 - </div>  
6 - <div class="header media-body">  
7 - <h5 class="title media-heading" ng-bind="card.title"></h5>  
8 -  
9 - <div class="subheader">  
10 - <span class="time">  
11 - <i class="fa fa-clock-o"></i> <span am-time-ago="card.created_at | dateFormat"></span>  
12 - </span>  
13 - </div>  
14 - </div>  
15 - <img ng-show="card.image" ng-src="{{card.image.url}}" class="img-responsive article-image">  
16 - <div class="post-lead" ng-bind-html="card.body | stripTags | truncate: 100: '...': true"></div>  
17 - </div>  
18 -</div>  
src/app/layout/blocks/recent-documents/recent-documents.scss
@@ -1,65 +0,0 @@ @@ -1,65 +0,0 @@
1 -.block.recentdocumentsblock {  
2 - .deckgrid[deckgrid]::before {  
3 - font-size: 0; /* See https://github.com/akoenig/angular-deckgrid/issues/14#issuecomment-35728861 */  
4 - visibility: hidden;  
5 - }  
6 - .author {  
7 - img {  
8 - width: 30px;  
9 - height: 30px;  
10 - }  
11 - }  
12 - .header {  
13 - .subheader {  
14 - color: #C1C1C1;  
15 - font-size: 10px;  
16 - }  
17 - }  
18 - .post-lead {  
19 - color: #8E8E8E;  
20 - font-size: 14px;  
21 - }  
22 - .article-image {  
23 - margin: 10px 0;  
24 - }  
25 -}  
26 -  
27 -.col-md-2-5 {  
28 - .deckgrid[deckgrid]::before {  
29 - content: '1 .deck-column';  
30 - }  
31 -}  
32 -  
33 -.col-md-7 {  
34 - .block.recentdocumentsblock {  
35 - background-color: transparent;  
36 - border: 0;  
37 -  
38 - .deckgrid[deckgrid]::before {  
39 - content: '3 .deck-column';  
40 - }  
41 -  
42 - .panel-heading {  
43 - display: none;  
44 - }  
45 - .panel-body {  
46 - padding: 0;  
47 - }  
48 -  
49 - .deckgrid {  
50 - .column {  
51 - float: left;  
52 - }  
53 -  
54 - .deck-column {  
55 - @extend .col-md-4;  
56 - padding: 0;  
57 -  
58 - .a-card {  
59 - padding: 10px;  
60 - margin: 3px;  
61 - }  
62 - }  
63 - }  
64 - }  
65 -}  
src/app/layout/blocks/statistics/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 "./statistics-block.component";
src/app/layout/blocks/statistics/statistics-block.component.spec.ts 0 → 100644
@@ -0,0 +1,53 @@ @@ -0,0 +1,53 @@
  1 +import {ComponentTestHelper, createClass} from './../../../../spec/component-test-helper';
  2 +import {StatisticsBlockComponent} from './statistics-block.component';
  3 +import * as helpers from "../../../../spec/helpers";
  4 +
  5 +const htmlTemplate: string = '<noosfero-statistics-block [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-statistics-block>';
  6 +
  7 +describe("Components", () => {
  8 +
  9 + describe("Statistics Block Component", () => {
  10 + let helper: ComponentTestHelper<StatisticsBlockComponent>;
  11 + beforeEach(angular.mock.module("templates"));
  12 +
  13 + beforeEach((done) => {
  14 + let cls = createClass({
  15 + template: htmlTemplate,
  16 + directives: [StatisticsBlockComponent],
  17 + providers: helpers.provideFilters("translateFilter"),
  18 + properties: {
  19 + block: {
  20 + statistics: [
  21 + {
  22 + name: "users",
  23 + display: true,
  24 + quantity: 10
  25 + },
  26 + {
  27 + name: "communities",
  28 + display: true,
  29 + quantity: 20
  30 + },
  31 + {
  32 + name: "hits",
  33 + display: false,
  34 + quantity: null
  35 + }
  36 + ]
  37 + }
  38 + }
  39 + });
  40 + helper = new ComponentTestHelper<StatisticsBlockComponent>(cls, done);
  41 + });
  42 +
  43 + it("shows statistics marked with display equals 'true'", () => {
  44 + expect(helper.debugElement.queryAll("li.statistic").length).toEqual(2);
  45 + expect(helper.debugElement.query("span.users").text()).toEqual("10");
  46 + expect(helper.debugElement.query("span.communities").text()).toEqual("20");
  47 + });
  48 +
  49 + it("does not shows statistics marked with display equals 'false'", () => {
  50 + expect(helper.debugElement.queryAll("span.hits").length).toEqual(0);
  51 + });
  52 + });
  53 +});
src/app/layout/blocks/statistics/statistics-block.component.ts 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  1 +import {Input, Inject, Component} from "ng-forward";
  2 +
  3 +@Component({
  4 + selector: "noosfero-statistics-block",
  5 + templateUrl: 'app/layout/blocks/statistics/statistics-block.html'
  6 +})
  7 +
  8 +export class StatisticsBlockComponent {
  9 + @Input() block: noosfero.StatisticsBlock;
  10 + @Input() owner: any;
  11 +}
src/app/layout/blocks/statistics/statistics-block.html 0 → 100644
@@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
  1 +<div class="statistics-block">
  2 + <ul class="list-group">
  3 + <li ng-repeat="counter in ctrl.block.statistics" class="list-group-item statistic {{counter.name}}" ng-if="counter.display" >
  4 + <span class="badge {{counter.name}}">{{counter.quantity}}</span>
  5 + {{"statistics." + counter.name | translate}}
  6 + </li>
  7 +
  8 + </ul>
  9 +</div>
src/app/layout/blocks/statistics/statistics.scss 0 → 100644
@@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
  1 +.statistics-block {
  2 +// background-color: blue;
  3 +}
src/app/layout/boxes/boxes.component.spec.ts
@@ -7,7 +7,6 @@ import { @@ -7,7 +7,6 @@ import {
7 quickCreateComponent, 7 quickCreateComponent,
8 provideEmptyObjects, 8 provideEmptyObjects,
9 createProviderToValue, 9 createProviderToValue,
10 - getAngularServiceFactory,  
11 provideFilters 10 provideFilters
12 } from "../../../spec/helpers"; 11 } from "../../../spec/helpers";
13 12
src/app/layout/navbar/navbar.html
1 <nav class="navbar navbar-static-top navbar-inverse"> 1 <nav class="navbar navbar-static-top navbar-inverse">
2 <div class="container-fluid"> 2 <div class="container-fluid">
3 <div class="navbar-header"> 3 <div class="navbar-header">
4 - <button type="button" class="navbar-toggle collapsed" ng-click="isCollapsed = !isCollapsed"> 4 + <button type="button" class="navbar-toggle collapsed" (click)="ctrl.toggleCollapse()" ng-show="ctrl.showHamburguer">
5 <span class="sr-only">{{"navbar.toggle_menu" | translate}}</span> 5 <span class="sr-only">{{"navbar.toggle_menu" | translate}}</span>
6 - <span class="icon-bar"></span>  
7 - <span class="icon-bar"></span>  
8 - <span class="icon-bar"></span> 6 + <i class="fa fa-bars"></i>
9 </button> 7 </button>
10 <a class="navbar-brand" ui-sref="main.environment.home"> 8 <a class="navbar-brand" ui-sref="main.environment.home">
11 - <span class="noosfero-logo"> </span> 9 + <span class="noosfero-logo"> </span>
12 <span class="noosfero-name">{{"noosfero.name" | translate}}</span> 10 <span class="noosfero-name">{{"noosfero.name" | translate}}</span>
13 </a> 11 </a>
14 </div> 12 </div>
src/app/layout/navbar/navbar.scss
1 .navbar { 1 .navbar {
2 2
  3 + margin-bottom: 0px;
  4 +
3 .container-fluid { 5 .container-fluid {
4 - padding-right: 12%;  
5 - padding-left: 12%;  
6 - @media (max-width: 978px) {  
7 - padding-right: 2%;  
8 - padding-left: 2%;  
9 - }  
10 6
11 .navbar-brand { 7 .navbar-brand {
12 .noosfero-logo { 8 .noosfero-logo {
13 - background:url("../assets/images/logo-noosfero.png") no-repeat; 9 + background: url("../assets/images/logo-noosfero.png") no-repeat;
14 padding: 0px 62px 64px 15px; 10 padding: 0px 62px 64px 15px;
15 } 11 }
16 } 12 }
@@ -28,5 +24,19 @@ @@ -28,5 +24,19 @@
28 } 24 }
29 } 25 }
30 } 26 }
  27 +
  28 + .navbar-toggle {
  29 + display: block;
  30 + color: #fff;
  31 + padding: 4px 12px;
  32 + margin: 10px 10px;
  33 + text-align: center;
  34 + }
  35 +
  36 + @media (max-width: $break-sm-max) {
  37 + .navbar-toggle .fa-bars{
  38 + font-size: 14pt;
  39 + }
  40 + }
31 } 41 }
32 } 42 }
src/app/layout/navbar/navbar.spec.ts
1 import * as helpers from "./../../../spec/helpers"; 1 import * as helpers from "./../../../spec/helpers";
2 import {Navbar} from "./navbar"; 2 import {Navbar} from "./navbar";
3 3
4 -import {Injectable, Provider, provide} from "ng-forward"; 4 +import {Injectable, Provider, provide, EventEmitter} from "ng-forward";
5 5
6 import {ComponentFixture} from 'ng-forward/cjs/testing/test-component-builder'; 6 import {ComponentFixture} from 'ng-forward/cjs/testing/test-component-builder';
7 7
8 -import {SessionService, AuthService, AuthController, IAuthEvents, AUTH_EVENTS} from "./../../login"; 8 +import {SessionService, AuthService, AuthController, AuthEvents} from "./../../login";
9 9
  10 +import events from 'ng-forward/cjs/events/events';
10 11
11 describe("Components", () => { 12 describe("Components", () => {
12 13
@@ -53,22 +54,19 @@ describe(&quot;Components&quot;, () =&gt; { @@ -53,22 +54,19 @@ describe(&quot;Components&quot;, () =&gt; {
53 provide('$uibModal', { 54 provide('$uibModal', {
54 useValue: $modal 55 useValue: $modal
55 }), 56 }),
56 - provide('AuthService', { 57 + provide(AuthService, {
57 useValue: authService 58 useValue: authService
58 }), 59 }),
59 helpers.provideEmptyObjects('moment'), 60 helpers.provideEmptyObjects('moment'),
60 provide('$state', { 61 provide('$state', {
61 useValue: stateService 62 useValue: stateService
62 }), 63 }),
63 - provide("$scope", {  
64 - useValue: scope  
65 - }),  
66 provide('SessionService', { 64 provide('SessionService', {
67 useValue: sessionService 65 useValue: sessionService
68 }), 66 }),
69 - provide('AUTH_EVENTS', { 67 + provide('AuthEvents', {
70 useValue: { 68 useValue: {
71 - AUTH_EVENTS 69 + AuthEvents
72 } 70 }
73 }), 71 }),
74 provide('TranslatorService', { 72 provide('TranslatorService', {
@@ -146,7 +144,7 @@ describe(&quot;Components&quot;, () =&gt; { @@ -146,7 +144,7 @@ describe(&quot;Components&quot;, () =&gt; {
146 144
147 145
148 it('closes the modal after login', (done: Function) => { 146 it('closes the modal after login', (done: Function) => {
149 - modalInstance = jasmine.createSpyObj("modalInstance", ["close"]); 147 + modalInstance = {};
150 modalInstance.close = jasmine.createSpy("close"); 148 modalInstance.close = jasmine.createSpy("close");
151 149
152 $modal.open = () => { 150 $modal.open = () => {
@@ -155,19 +153,21 @@ describe(&quot;Components&quot;, () =&gt; { @@ -155,19 +153,21 @@ describe(&quot;Components&quot;, () =&gt; {
155 153
156 buildComponent().then((fixture: ComponentFixture) => { 154 buildComponent().then((fixture: ComponentFixture) => {
157 let navbarComp: Navbar = <Navbar>fixture.debugElement.componentViewChildren[0].componentInstance; 155 let navbarComp: Navbar = <Navbar>fixture.debugElement.componentViewChildren[0].componentInstance;
158 - let localScope: ng.IScope = navbarComp["$scope"];  
159 156
160 navbarComp.openLogin(); 157 navbarComp.openLogin();
161 - localScope.$emit(AUTH_EVENTS.loginSuccess); 158 + let successEvent: string = AuthEvents[AuthEvents.loginSuccess];
  159 +
  160 + (<any>navbarComp.authService)[successEvent].next(user);
  161 +
162 expect(modalInstance.close).toHaveBeenCalled(); 162 expect(modalInstance.close).toHaveBeenCalled();
163 done(); 163 done();
164 }); 164 });
  165 +
165 }); 166 });
166 167
167 it('updates current user on logout', (done: Function) => { 168 it('updates current user on logout', (done: Function) => {
168 buildComponent().then((fixture: ComponentFixture) => { 169 buildComponent().then((fixture: ComponentFixture) => {
169 let navbarComp: Navbar = <Navbar>fixture.debugElement.componentViewChildren[0].componentInstance; 170 let navbarComp: Navbar = <Navbar>fixture.debugElement.componentViewChildren[0].componentInstance;
170 - let localScope: ng.IScope = navbarComp["$scope"];  
171 171
172 // init navbar currentUser with some user 172 // init navbar currentUser with some user
173 navbarComp["currentUser"] = user; 173 navbarComp["currentUser"] = user;
@@ -176,12 +176,14 @@ describe(&quot;Components&quot;, () =&gt; { @@ -176,12 +176,14 @@ describe(&quot;Components&quot;, () =&gt; {
176 // and emmit the 'logoutSuccess' event 176 // and emmit the 'logoutSuccess' event
177 // just what happens when user logsout 177 // just what happens when user logsout
178 sessionService.currentUser = () => { return null; }; 178 sessionService.currentUser = () => { return null; };
179 - localScope.$emit(AUTH_EVENTS.logoutSuccess);  
180 - expect(navbarComp["currentUser"]).toBeNull(); 179 + let successEvent: string = AuthEvents[AuthEvents.logoutSuccess];
  180 +
  181 + (<any>navbarComp.authService)[successEvent].next(user);
  182 +
181 done(); 183 done();
  184 + expect(navbarComp["currentUser"]).toBeNull();
  185 +
182 }); 186 });
183 }); 187 });
184 -  
185 -  
186 }); 188 });
187 }); 189 });
src/app/layout/navbar/navbar.ts
1 -import {Component, Inject} from "ng-forward"; 1 +import {Component, Inject, EventEmitter, Input} from "ng-forward";
2 import {LanguageSelectorComponent} from "../language-selector/language-selector.component"; 2 import {LanguageSelectorComponent} from "../language-selector/language-selector.component";
3 -  
4 -  
5 -import {SessionService, AuthService, AuthController, IAuthEvents, AUTH_EVENTS} from "./../../login"; 3 +import {SessionService, AuthService, AuthController, AuthEvents} from "./../../login";
  4 +import {SidebarNotificationService} from "../sidebar/sidebar.notification.service";
  5 +import {BodyStateClassesService} from '../services/body-state-classes.service';
6 6
7 @Component({ 7 @Component({
8 selector: "acme-navbar", 8 selector: "acme-navbar",
9 templateUrl: "app/layout/navbar/navbar.html", 9 templateUrl: "app/layout/navbar/navbar.html",
10 directives: [LanguageSelectorComponent], 10 directives: [LanguageSelectorComponent],
11 - providers: [AuthService, SessionService] 11 + providers: [AuthService, SessionService, SidebarNotificationService]
12 }) 12 })
13 -@Inject("$uibModal", AuthService, "SessionService", "$scope", "$state") 13 +@Inject("$uibModal", AuthService, "SessionService", "$state", SidebarNotificationService, BodyStateClassesService)
14 export class Navbar { 14 export class Navbar {
15 15
16 private currentUser: noosfero.User; 16 private currentUser: noosfero.User;
17 private modalInstance: any = null; 17 private modalInstance: any = null;
  18 +
  19 + public showHamburguer: boolean = false;
  20 +
18 /** 21 /**
19 * 22 *
20 */ 23 */
21 constructor( 24 constructor(
22 private $uibModal: any, 25 private $uibModal: any,
23 - private authService: AuthService, 26 + public authService: AuthService,
24 private session: SessionService, 27 private session: SessionService,
25 - private $scope: ng.IScope,  
26 - private $state: ng.ui.IStateService 28 + private $state: ng.ui.IStateService,
  29 + private sidebarNotificationService: SidebarNotificationService,
  30 + private bodyStateService: BodyStateClassesService
27 ) { 31 ) {
28 this.currentUser = this.session.currentUser(); 32 this.currentUser = this.session.currentUser();
29 33
30 - this.$scope.$on(AUTH_EVENTS.loginSuccess, () => { 34 + this.showHamburguer = this.authService.isAuthenticated();
  35 + this.bodyStateService.addContentClass(!this.sidebarNotificationService.sidebarVisible);
  36 +
  37 + this.authService.subscribe(AuthEvents[AuthEvents.loginSuccess], () => {
31 if (this.modalInstance) { 38 if (this.modalInstance) {
32 this.modalInstance.close(); 39 this.modalInstance.close();
33 this.modalInstance = null; 40 this.modalInstance = null;
34 } 41 }
35 42
36 - this.$state.go(this.$state.current, {}, { reload: true }); // TODO move to auth 43 + this.currentUser = this.session.currentUser();
  44 + this.showHamburguer = true;
  45 +
  46 + this.$state.go(this.$state.current, {}, { reload: true }); // TODO move to auth
37 }); 47 });
38 48
39 - this.$scope.$on(AUTH_EVENTS.logoutSuccess, () => { 49 + this.authService.subscribe(AuthEvents[AuthEvents.logoutSuccess], () => {
40 this.currentUser = this.session.currentUser(); 50 this.currentUser = this.session.currentUser();
41 }); 51 });
  52 +
  53 + }
  54 +
  55 + public toggleCollapse() {
  56 + this.sidebarNotificationService.alternateVisibility();
  57 +
  58 + this.bodyStateService.addContentClass(!this.sidebarNotificationService.sidebarVisible);
42 } 59 }
43 60
44 openLogin() { 61 openLogin() {
@@ -55,8 +72,6 @@ export class Navbar { @@ -55,8 +72,6 @@ export class Navbar {
55 this.$state.go(this.$state.current, {}, { reload: true }); // TODO move to auth 72 this.$state.go(this.$state.current, {}, { reload: true }); // TODO move to auth
56 }; 73 };
57 74
58 -  
59 -  
60 activate() { 75 activate() {
61 if (!this.currentUser) { 76 if (!this.currentUser) {
62 this.openLogin(); 77 this.openLogin();
src/app/layout/scss/_bootstrap-overrides.scss
@@ -54,3 +54,19 @@ @@ -54,3 +54,19 @@
54 .tabs-wrapper.tabs-no-header .tab-content { 54 .tabs-wrapper.tabs-no-header .tab-content {
55 padding: 0 20px 20px; 55 padding: 0 20px 20px;
56 } 56 }
  57 +
  58 +/* LABELS */
  59 +.label {
  60 + @include border-radius($border-radius-base);
  61 + font-size: 0.875em;
  62 + font-weight: 600;
  63 +}
  64 +.label-default,
  65 +.label.label-large {
  66 + font-size: 1em;
  67 + padding: 0.4em 0.8em 0.5em;
  68 +}
  69 +.label.label-circle {
  70 + @include border-radius(50%);
  71 + padding: 4px !important;
  72 +}
src/app/layout/scss/_sidebar.scss 0 → 100644
@@ -0,0 +1,311 @@ @@ -0,0 +1,311 @@
  1 +#col-left {
  2 + position: relative;
  3 + color: #003940;
  4 + height: 100%;
  5 +
  6 + a {
  7 + color: #e1e1e1;
  8 + }
  9 + a:hover,
  10 + .nav-active a.nav-link,
  11 + a.active {
  12 + color: #fff;
  13 + }
  14 + * {
  15 + outline: none;
  16 + }
  17 +}
  18 +#nav-col {
  19 + padding: 0;
  20 + z-index: 100;
  21 + position: absolute;
  22 + background: #2c3e50;
  23 + width: 220px;
  24 + border-right: 2px solid #e7ebee;
  25 + height: 200%;
  26 +
  27 + @media (max-width: $break-sm-max) {
  28 + position: relative;
  29 + width: auto;
  30 + }
  31 +}
  32 +#sidebar-nav {
  33 + max-height: 100%;
  34 + padding-left: 0;
  35 + padding-right: 0;
  36 +
  37 + .nav {
  38 + > li {
  39 + margin: 0;
  40 +
  41 + &.nav-header {
  42 + color: lighten(#2c3e50, 40%);
  43 + font-size: 0.8em;
  44 + padding: 12px 15px 6px 14px;
  45 + border-top: 2px solid darken(#2c3e50, 4%);
  46 +
  47 + &.nav-header-first {
  48 + padding-top: 4px;
  49 + border-top: 0;
  50 + }
  51 + }
  52 +
  53 + > a {
  54 + color: #fff;
  55 + height: 44px;
  56 + line-height: 28px;
  57 + @include transition(border-color 0.1s ease-in-out 0s, background-color 0.1s ease-in-out 0s, box-shadow 0.1s ease-in-out 0s);
  58 + overflow: hidden;
  59 + padding: 8px 15px 8px 20px;
  60 + border-left: 0 solid transparent;
  61 +
  62 + &:hover {
  63 + border-left-color: $primary-color;
  64 + }
  65 + > i {
  66 + position: absolute;
  67 + margin-top: 6px;
  68 + }
  69 + > span {
  70 + margin-left: 35px;
  71 + font-size: 0.875em;
  72 + font-weight: 700;
  73 +
  74 + &.label {
  75 + font-size: 0.75em;
  76 + margin: 5px 0 0 0;
  77 + padding: 4px 0.6em;
  78 + }
  79 + &.label.label-circle {
  80 + margin-right: 5px;
  81 + }
  82 + }
  83 + }
  84 + &.open > a {
  85 + border-bottom-color: #252525;
  86 + outline: none;
  87 + text-decoration: none;
  88 + }
  89 + &.active > .submenu > li.active > .submenu {
  90 + display: block;
  91 + }
  92 + }
  93 + li {
  94 + @import "sidebar/submenu";
  95 +
  96 + &.active > .submenu {
  97 + display: block;
  98 + }
  99 + }
  100 + }
  101 +}
  102 +.navbar-nav .open .dropdown-menu {
  103 + background-color: #FFFFFF;
  104 + border: none;
  105 + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.176);
  106 + position: absolute;
  107 +}
  108 +#user-left-box {
  109 + padding: 20px 15px 20px 25px;
  110 +
  111 + img {
  112 + @include border-radius(18%);
  113 + border: 3px solid #fff;
  114 + float: left;
  115 + width: 70px;
  116 + }
  117 + .user-box {
  118 + color: #fff;
  119 + float: left;
  120 + padding-left: 15px;
  121 + padding-top: 18px;
  122 +
  123 + > .name {
  124 + display: block;
  125 + font-size: 1em;
  126 + font-weight: 600;
  127 + line-height: 1.2;
  128 +
  129 + > a {
  130 + color: #fff;
  131 +
  132 + &:hover,
  133 + &:focus {
  134 + color: #E1E1E1;
  135 + text-decoration: none;
  136 + }
  137 + }
  138 + }
  139 + > .status {
  140 + display: block;
  141 + font-size: 0.75em;
  142 + padding-top: 3px;
  143 + }
  144 + > .status > i {
  145 + color: $green-color;
  146 + margin-right: 4px;
  147 + }
  148 + }
  149 + &.dropdown {
  150 + .dropdown-menu {
  151 + top: 55px;
  152 + left: 30px;
  153 +
  154 + a {
  155 + color: #707070;
  156 + font-size: 0.875em;
  157 +
  158 + &:hover {
  159 + background-color: #f6f6f6;
  160 + color: #707070;
  161 + }
  162 + }
  163 + }
  164 + }
  165 +}
  166 +@media (min-width: $break-sm-min) {
  167 + .nav-small {
  168 + #nav-col {
  169 + width: 64px;
  170 + }
  171 + #content-wrapper {
  172 + margin-left: 64px;
  173 + }
  174 + #nav-col {
  175 + #user-left-box {
  176 + display: none;
  177 + }
  178 + #sidebar-nav {
  179 + .nav > li > a {
  180 + padding-left: 15px !important;
  181 + padding-right: 15px;
  182 + text-align: center;
  183 +
  184 + > i {
  185 + position: relative;
  186 + font-size: 1.25em;
  187 + }
  188 + > span {
  189 + display: none;
  190 + }
  191 + }
  192 + .nav > li.nav-header {
  193 + display: none;
  194 + }
  195 + .nav li > a.dropdown-toggle > .drop-icon {
  196 + display: none;
  197 + }
  198 + .nav .submenu > li > a.dropdown-toggle > .drop-icon {
  199 + display: block;
  200 + }
  201 + .nav li .submenu {
  202 + left: 64px;
  203 + position: absolute;
  204 + top: 0;
  205 + width: 210px;
  206 +
  207 + > li > a {
  208 + padding-left: 28px;
  209 + }
  210 + }
  211 + .nav > .active > .submenu > li > .submenu {
  212 + left: auto;
  213 + position: relative;
  214 + top: auto;
  215 + width: 100%;
  216 +
  217 + a {
  218 + padding-left: 48px
  219 + }
  220 + }
  221 + }
  222 + }
  223 +
  224 + #nav-col-submenu {
  225 + @import "sidebar/submenu";
  226 +
  227 + .submenu {
  228 + position: absolute;
  229 + top: 60px;
  230 + left: 64px;
  231 + width: 210px;
  232 +
  233 + > li > a {
  234 + padding-left: 28px;
  235 +
  236 + &.dropdown-toggle > .drop-icon {
  237 + display: block;
  238 + }
  239 + }
  240 + }
  241 + > .submenu {
  242 + display: block !important;
  243 + }
  244 + .submenu > li > .submenu,
  245 + .submenu > li > .submenu {
  246 + left: auto;
  247 + position: relative;
  248 + top: auto;
  249 + width: 100%;
  250 +
  251 + a {
  252 + padding-left: 48px
  253 + }
  254 + }
  255 + }
  256 + }
  257 +}
  258 +@media (max-width: $break-sm-max) {
  259 +
  260 + #sidebar-nav.navbar-collapse {
  261 + max-height: 336px;
  262 + }
  263 +}
  264 +
  265 +/* Extracted from original _header.scss */
  266 +#sidebar-nav .nav a {
  267 + > i {
  268 + font-size: 1.125em;
  269 + }
  270 + > .open > &, .open > &:focus {
  271 + background: inherit;
  272 + }
  273 + &:hover, .open > &:hover {
  274 + background: darken(#2c3e50, 4%);
  275 + color: #fff;
  276 + outline: none;
  277 + }
  278 +}
  279 +
  280 +#sidebar-nav .nav-pills > li.active > a,
  281 +#sidebar-nav .nav-pills > li.active > a:hover,
  282 +#sidebar-nav .nav-pills > li.active > a:focus,
  283 +.nav-pills > li.open > a,
  284 +.nav-pills > li.open > a:hover,
  285 +.nav-pills > li.open > a:focus,
  286 +#sidebar-nav .nav-pills > li.open > a,
  287 +#sidebar-nav .nav-pills > li.open > a:hover,
  288 +#sidebar-nav .nav-pills > li > a:focus,
  289 +.nav-small #nav-col #sidebar-nav .nav-pills > li.open > a {
  290 + background-color: darken(#2c3e50, 4%);
  291 + color: #fff;
  292 + border-left-color: $primary-color;
  293 +}
  294 +
  295 +#user-left-box {
  296 + .profile-image-wrap {
  297 + float:left;
  298 + width: 70px;
  299 + height: 70px;
  300 + }
  301 + i.profile-image {
  302 + border-radius: 18%;
  303 + border: 3px solid #fff;
  304 + background-color: #fff;
  305 + text-align: center;
  306 + }
  307 +}
  308 +
  309 +.submenu-count {
  310 + margin-right: 20px !important;
  311 +}
src/app/layout/scss/sidebar/_submenu.scss 0 → 100644
@@ -0,0 +1,52 @@ @@ -0,0 +1,52 @@
  1 +a.dropdown-toggle > .drop-icon {
  2 + color: #868b98;
  3 + font-size: 12px;
  4 + margin-top: -6px;
  5 + position: absolute;
  6 + right: 25px;
  7 + top: 50%;
  8 + @include transition(transform 0.2s ease-in-out 0.1s);
  9 +}
  10 +&.open a.dropdown-toggle > .drop-icon,
  11 +&.active > a.dropdown-toggle > .drop-icon {
  12 + color: #fff;
  13 + transform:rotate(90deg);
  14 +}
  15 +
  16 +.submenu {
  17 + background: darken(#2c3e50, 4%);
  18 + padding-left: 0;
  19 + margin: 0;
  20 + list-style: none;
  21 +
  22 + li {
  23 + position: relative;
  24 +
  25 + > a {
  26 + display: block;
  27 + font-size: 0.875em;
  28 + line-height: 38px;
  29 + padding-left: 60px;
  30 + color: #fff;
  31 + outline: none;
  32 + text-decoration: none;
  33 + @include transition(border-color 0.1s ease-in-out 0s, background-color 0.1s ease-in-out 0s, box-shadow 0.1s ease-in-out 0s);
  34 +
  35 + &:hover {
  36 + background-color: #1f2c39;
  37 + }
  38 + }
  39 + &:first-of-type > a {
  40 + border-top: 0;
  41 + }
  42 + > a:hover,
  43 + > a:focus,
  44 + > a.active,
  45 + &.active > a,
  46 + &.open > a {
  47 + text-decoration: none;
  48 + color: #fff;
  49 + background-color: darken(#2c3e50, 7%);
  50 + }
  51 + }
  52 +}
src/app/layout/scss/skins/_whbl.scss 0 → 100644
@@ -0,0 +1,310 @@ @@ -0,0 +1,310 @@
  1 +$whbl-nav-col-bg: #ffffff; //f3f5f6
  2 +$whbl-sidebar-linkColor: #484848;
  3 +$whbl-primary-color: #1E96D0; //blue
  4 +$whbl-font-color: #16191c;
  5 +
  6 +.skin-whbl {
  7 + #header-navbar {
  8 + background-color: $whbl-primary-color;
  9 + }
  10 + .navbar > .container .navbar-brand {
  11 + background-color: transparent;
  12 + width: 221px;
  13 + }
  14 + #nav-col,
  15 + #page-wrapper {
  16 + background-color: $whbl-nav-col-bg;
  17 + }
  18 + #sidebar-nav .nav > li > a {
  19 + color: $whbl-sidebar-linkColor;
  20 + /* border-bottom: 1px solid #e7ebee; */
  21 + }
  22 + #sidebar-nav .nav > .open > .submenu > li > .submenu,
  23 + #sidebar-nav .nav > .active > .submenu > li > .submenu,
  24 + #sidebar-nav .nav li .submenu > li.open a,
  25 + #nav-col-submenu .submenu > li > .submenu,
  26 + #nav-col-submenu li .submenu > li.open > a {
  27 + background-color: darken($whbl-nav-col-bg, 8%);
  28 + }
  29 + .nav-pills > li.active > a,
  30 + .nav-pills > li.active > a:hover,
  31 + .nav-pills > li.active > a:focus,
  32 + #sidebar-nav .nav-pills > li.active > a,
  33 + #sidebar-nav .nav-pills > li.active > a:hover,
  34 + #sidebar-nav .nav-pills > li.active > a:focus,
  35 + .nav-pills > li.open > a,
  36 + .nav-pills > li.open > a:hover,
  37 + .nav-pills > li.open > a:focus,
  38 + #sidebar-nav .nav-pills > li.open > a,
  39 + #sidebar-nav .nav-pills > li.open > a:hover,
  40 + #sidebar-nav .nav-pills > li.open > a:focus,
  41 + #sidebar-nav .nav-pills > li > a:focus {
  42 + background-color: darken($whbl-nav-col-bg, 4%);
  43 + border-color: $whbl-primary-color;
  44 + border-bottom-color: #e7ebee;
  45 + color: $whbl-sidebar-linkColor;
  46 + }
  47 + #sidebar-nav .nav-pills > li.active > a > i {
  48 + color: #2980b9;
  49 + }
  50 + #sidebar-nav .nav > li > a:hover {
  51 + background-color: darken($whbl-nav-col-bg, 4%);
  52 + border-color: $whbl-primary-color;
  53 + border-bottom-color: #e7ebee;
  54 + color: $whbl-sidebar-linkColor;
  55 + }
  56 +
  57 + #sidebar-nav .nav > li .submenu li > a:hover {
  58 + color: $whbl-font-color;
  59 + font-weight: bold;
  60 + }
  61 + #header-navbar .nav > li > a {
  62 + color: #fff;
  63 + }
  64 + #header-navbar .nav > li > a:hover,
  65 + #header-navbar .nav > li > a:focus,
  66 + #header-navbar .nav .open > a,
  67 + #header-navbar .nav .open > a:hover,
  68 + #header-navbar .nav .open > a:focus {
  69 + background-color: #2980b9;
  70 + }
  71 + #sidebar-nav .nav li .submenu,
  72 + #nav-col-submenu .submenu {
  73 + background-color: darken($whbl-nav-col-bg, 4%);
  74 + }
  75 + #sidebar-nav .nav li .submenu > li > a,
  76 + .nav-small #nav-col-submenu .submenu > li > a {
  77 + color: $whbl-font-color;
  78 + }
  79 + #sidebar-nav .nav > .open > .submenu > .open > a,
  80 + #sidebar-nav .nav > .active > .submenu > .open > a,
  81 + #sidebar-nav .nav > .active > .submenu > .active > a,
  82 + #nav-col-submenu .submenu > .open > a,
  83 + #nav-col-submenu .submenu > .active > a {
  84 + border-bottom-color: transparent;
  85 + box-shadow: 0 -1px 0 transparent inset;
  86 + }
  87 + #sidebar-nav .nav > .open > .submenu > .open > a {
  88 + border-bottom-color: #dcdfe6;
  89 + box-shadow: none;
  90 + }
  91 + #sidebar-nav .nav li.open > a.dropdown-toggle > .drop-icon,
  92 + #sidebar-nav .nav li.active > a.dropdown-toggle > .drop-icon {
  93 + color: $whbl-font-color;
  94 + }
  95 + #sidebar-nav .nav li .submenu > li > a:hover,
  96 + #sidebar-nav .nav li .submenu > li > a.active,
  97 + #sidebar-nav .nav li .submenu > li.active > a {
  98 + background-color: darken($whbl-nav-col-bg, 8%);
  99 + }
  100 + .navbar > .container .navbar-brand {
  101 + color: #fff;
  102 + }
  103 + .navbar-toggle {
  104 + color: #fff;
  105 + }
  106 + .graph-box {
  107 + background-color: $whbl-primary-color !important;
  108 + }
  109 + .content-wrapper {
  110 + background-color: #f9f9f9;
  111 + height: 100%;
  112 + margin-top: 0;
  113 + margin-bottom: 0;
  114 + position: relative;
  115 + min-height: 1200px;
  116 + padding: 15px 15px 35px 15px;
  117 + margin-left: 220px;
  118 + // border-left: 2px solid #e7ebee;
  119 + }
  120 + #user-left-box {
  121 +
  122 + .user-box {
  123 + color: $whbl-font-color;
  124 + > a {
  125 + color: $whbl-font-color;
  126 + }
  127 + > .name {
  128 + > a {
  129 + color: $whbl-font-color;
  130 +
  131 + &:hover, &:focus {
  132 + color: $whbl-font-color;
  133 + }
  134 + }
  135 + }
  136 + }
  137 + .user-box a:hover,
  138 + .user-box a:focus {
  139 + color: darken($whbl-font-color, 20%);
  140 + }
  141 + }
  142 + #sidebar-nav .nav > li.nav-header {
  143 + border-top-color: #e7ebee;
  144 + color: darken($whbl-nav-col-bg, 35%);
  145 + }
  146 + .nav-tabs {
  147 + background-color: #f9f9f9;
  148 + }
  149 + h1 {
  150 + color: $whbl-primary-color;
  151 + }
  152 + #header-navbar .nav > li > a:hover,
  153 + #header-navbar .nav > li > a:focus,
  154 + #header-navbar .nav .open > a,
  155 + #header-navbar .nav .open > a:hover,
  156 + #header-navbar .nav .open > a:focus,
  157 + .navbar-toggle:hover,
  158 + .navbar-toggle:focus,
  159 + .mobile-search.active > .btn {
  160 + background-color: #2980b9;
  161 + }
  162 + .main-box {
  163 + border: 1px solid $main-bg-color;
  164 + }
  165 + a,
  166 + .fc-state-default,
  167 + .jvectormap-zoomin,
  168 + .jvectormap-zoomout,
  169 + #user-profile .profile-details ul > li > span {
  170 + color: $whbl-primary-color;
  171 + }
  172 + a:hover,
  173 + a:focus,
  174 + .widget-users li > .details > .name > a:hover,
  175 + .widget-todo .actions > a:hover {
  176 + color: $whbl-primary-color;
  177 + }
  178 + .table a.table-link:hover {
  179 + color: #2980b9;
  180 + }
  181 + .pagination {
  182 + > li {
  183 + > a,
  184 + > span,
  185 + > a:hover,
  186 + > span:hover,
  187 + > a:focus,
  188 + > span:focus,
  189 + > a:active,
  190 + > span:active {
  191 + color: $whbl-primary-color;
  192 + }
  193 + }
  194 + > .active {
  195 + > a,
  196 + > span,
  197 + > a:hover,
  198 + > span:hover,
  199 + > a:focus,
  200 + > span:focus {
  201 + background-color: $whbl-primary-color;
  202 + border-color: $whbl-primary-color;
  203 + color: #fff;
  204 + }
  205 + }
  206 + }
  207 + .notifications-list {
  208 + .item-footer {
  209 + background-color: #272d33;
  210 +
  211 + a:hover {
  212 + background-color: #0f1114;
  213 + }
  214 + }
  215 + }
  216 + .btn-primary,
  217 + .btn-default,
  218 + .btn-info,
  219 + .btn-success,
  220 + .btn-warning,
  221 + .btn-danger,
  222 + .btn-primary:hover,
  223 + .btn-default:hover,
  224 + .btn-info:hover,
  225 + .btn-success:hover,
  226 + .btn-warning:hover,
  227 + .btn-danger:hover {
  228 + color: #fff;
  229 + }
  230 + .btn-primary {
  231 + background-color: $whbl-primary-color;
  232 + border-color: #2980b9;
  233 + }
  234 + .btn-primary:hover,
  235 + .btn-primary:focus,
  236 + .btn-primary:active,
  237 + .btn-primary.active,
  238 + .open .dropdown-toggle.btn-primary {
  239 + background-color: #2980b9;
  240 + border-color: #216897;
  241 + }
  242 + .btn-success {
  243 + background-color: $whbl-primary-color;
  244 + border-color: #2980b9;
  245 + }
  246 + .btn-success:hover,
  247 + .btn-success:focus,
  248 + .btn-success:active,
  249 + .btn-success.active,
  250 + .open .dropdown-toggle.btn-success {
  251 + background-color: #2980b9;
  252 + border-color: #1c5c87;
  253 + }
  254 + h1 {
  255 + color: $whbl-primary-color;
  256 + }
  257 + .widget-users li > .details > .time {
  258 + color: $whbl-primary-color;
  259 + }
  260 + blockquote,
  261 + blockquote.pull-right {
  262 + border-color: $whbl-primary-color;
  263 + }
  264 + a.list-group-item.active,
  265 + a.list-group-item.active:hover,
  266 + a.list-group-item.active:focus {
  267 + background-color: $whbl-primary-color;
  268 + border-color: $whbl-primary-color;
  269 + }
  270 + .nav .caret {
  271 + border-bottom-color: $whbl-primary-color;
  272 + border-top-color: $whbl-primary-color;
  273 + }
  274 + .panel-default > .panel-heading,
  275 + .notifications-list .item-footer {
  276 + background-color: $whbl-primary-color;
  277 + }
  278 + .notifications-list .item-footer a:hover {
  279 + background-color: #2980b9;
  280 + }
  281 + #invoice-companies .invoice-dates .invoice-number > span,
  282 + .notifications-list .item a .time {
  283 + color: $whbl-primary-color;
  284 + }
  285 + .table thead > tr > th > a:hover span {
  286 + color: $whbl-primary-color;
  287 + border-color: $whbl-primary-color;
  288 + }
  289 + .pace .pace-progress {
  290 + background-color: #fff;
  291 + }
  292 +}
  293 +.rtl.skin-whbl #content-wrapper {
  294 + border-left: 0;
  295 + border-right: 2px solid #e7ebee;
  296 +}
  297 +
  298 +@media (max-width: $break-sm-max) {
  299 + .skin-whbl {
  300 + #logo.navbar-brand > img.normal-logo.logo-white {
  301 + display: block;
  302 + }
  303 + #logo.navbar-brand > img.normal-logo.logo-black {
  304 + display: none;
  305 + }
  306 + .navbar > .container .navbar-brand {
  307 + background-color: $whbl-primary-color;
  308 + }
  309 + }
  310 +}
src/app/layout/services/body-state-classes.service.spec.ts
1 import * as helpers from '../../../spec/helpers'; 1 import * as helpers from '../../../spec/helpers';
2 import {BodyStateClassesService} from "./body-state-classes.service"; 2 import {BodyStateClassesService} from "./body-state-classes.service";
3 import {AuthService} from "./../../login/auth.service"; 3 import {AuthService} from "./../../login/auth.service";
4 -import {AUTH_EVENTS} from "./../../login/auth-events"; 4 +import {AuthEvents} from "./../../login/auth-events";
  5 +
  6 +import {EventEmitter} from 'ng-forward';
  7 +
5 8
6 describe("BodyStateClasses Service", () => { 9 describe("BodyStateClasses Service", () => {
  10 +
7 let currentStateName = "main"; 11 let currentStateName = "main";
8 let bodyStateClasseService: BodyStateClassesService; 12 let bodyStateClasseService: BodyStateClassesService;
9 let $rootScope: ng.IRootScopeService = <any>{}, 13 let $rootScope: ng.IRootScopeService = <any>{},
@@ -13,17 +17,15 @@ describe(&quot;BodyStateClasses Service&quot;, () =&gt; { @@ -13,17 +17,15 @@ describe(&quot;BodyStateClasses Service&quot;, () =&gt; {
13 name: currentStateName 17 name: currentStateName
14 } 18 }
15 }, 19 },
16 - authService: AuthService, 20 + authService: any = helpers.mocks.authService,
17 bodyEl: { className: string }, 21 bodyEl: { className: string },
18 bodyElJq: any; 22 bodyElJq: any;
19 23
20 -  
21 let getService = (): BodyStateClassesService => { 24 let getService = (): BodyStateClassesService => {
22 return new BodyStateClassesService($rootScope, $document, $state, authService); 25 return new BodyStateClassesService($rootScope, $document, $state, authService);
23 }; 26 };
24 27
25 beforeEach(() => { 28 beforeEach(() => {
26 - authService = <any>{};  
27 authService.isAuthenticated = jasmine.createSpy("isAuthenticated").and.returnValue(true); 29 authService.isAuthenticated = jasmine.createSpy("isAuthenticated").and.returnValue(true);
28 bodyEl = { className: "" }; 30 bodyEl = { className: "" };
29 bodyElJq = [bodyEl]; 31 bodyElJq = [bodyEl];
@@ -58,8 +60,9 @@ describe(&quot;BodyStateClasses Service&quot;, () =&gt; { @@ -58,8 +60,9 @@ describe(&quot;BodyStateClasses Service&quot;, () =&gt; {
58 60
59 it("should capture loginSuccess event and add noosfero-user-logged class to the body element", () => { 61 it("should capture loginSuccess event and add noosfero-user-logged class to the body element", () => {
60 let userLoggedClassName = BodyStateClassesService.USER_LOGGED_CLASSNAME; 62 let userLoggedClassName = BodyStateClassesService.USER_LOGGED_CLASSNAME;
61 - $rootScope = <any>helpers.mocks.scopeWithEvents(); 63 +
62 bodyElJq.addClass = jasmine.createSpy("addClass"); 64 bodyElJq.addClass = jasmine.createSpy("addClass");
  65 + bodyElJq.removeClass = jasmine.createSpy("removeClass");
63 authService.isAuthenticated = jasmine.createSpy("isAuthenticated").and.returnValue(false); 66 authService.isAuthenticated = jasmine.createSpy("isAuthenticated").and.returnValue(false);
64 let service = getService(); 67 let service = getService();
65 68
@@ -68,11 +71,11 @@ describe(&quot;BodyStateClasses Service&quot;, () =&gt; { @@ -68,11 +71,11 @@ describe(&quot;BodyStateClasses Service&quot;, () =&gt; {
68 // triggers the service start 71 // triggers the service start
69 service.start(); 72 service.start();
70 // check if the the body element addClass was not called yet, 73 // check if the the body element addClass was not called yet,
71 - // because the user is not authenticated 74 + // because the user is not authenticated
72 expect(bodyElJq.addClass).not.toHaveBeenCalledWith(userLoggedClassName); 75 expect(bodyElJq.addClass).not.toHaveBeenCalledWith(userLoggedClassName);
73 76
74 // emit the event loginSuccess 77 // emit the event loginSuccess
75 - $rootScope.$emit(AUTH_EVENTS.loginSuccess); 78 + authService.loginSuccess.next(null);
76 79
77 // and check now if the addClass was called passing the userLogged class 80 // and check now if the addClass was called passing the userLogged class
78 // to the body Element 81 // to the body Element
@@ -83,7 +86,6 @@ describe(&quot;BodyStateClasses Service&quot;, () =&gt; { @@ -83,7 +86,6 @@ describe(&quot;BodyStateClasses Service&quot;, () =&gt; {
83 let userLoggedClassName = BodyStateClassesService.USER_LOGGED_CLASSNAME; 86 let userLoggedClassName = BodyStateClassesService.USER_LOGGED_CLASSNAME;
84 87
85 authService.isAuthenticated = jasmine.createSpy("isAuthenticated").and.returnValue(true); 88 authService.isAuthenticated = jasmine.createSpy("isAuthenticated").and.returnValue(true);
86 - $rootScope = <any>helpers.mocks.scopeWithEvents();  
87 89
88 bodyElJq.addClass = jasmine.createSpy("addClass"); 90 bodyElJq.addClass = jasmine.createSpy("addClass");
89 bodyElJq.removeClass = jasmine.createSpy("removeClass"); 91 bodyElJq.removeClass = jasmine.createSpy("removeClass");
@@ -95,11 +97,11 @@ describe(&quot;BodyStateClasses Service&quot;, () =&gt; { @@ -95,11 +97,11 @@ describe(&quot;BodyStateClasses Service&quot;, () =&gt; {
95 service.start(); 97 service.start();
96 98
97 // check if the the body element addClass was called 99 // check if the the body element addClass was called
98 - // because the user is already authenticated 100 + // because the user is already authenticated
99 expect(bodyElJq.addClass).toHaveBeenCalledWith(userLoggedClassName); 101 expect(bodyElJq.addClass).toHaveBeenCalledWith(userLoggedClassName);
100 102
101 // emit the event logoutSuccess 103 // emit the event logoutSuccess
102 - $rootScope.$emit(AUTH_EVENTS.logoutSuccess); 104 + authService.logoutSuccess.next(null);
103 105
104 // and check now if the removeClass was called passing the userLogged class 106 // and check now if the removeClass was called passing the userLogged class
105 // to the body Element 107 // to the body Element
@@ -126,9 +128,44 @@ describe(&quot;BodyStateClasses Service&quot;, () =&gt; { @@ -126,9 +128,44 @@ describe(&quot;BodyStateClasses Service&quot;, () =&gt; {
126 expect(bodyEl.className).toEqual(BodyStateClassesService.ROUTE_STATE_CLASSNAME_PREFIX + currentStateName); 128 expect(bodyEl.className).toEqual(BodyStateClassesService.ROUTE_STATE_CLASSNAME_PREFIX + currentStateName);
127 129
128 // emit the event $stateChangeSuccess 130 // emit the event $stateChangeSuccess
129 - $rootScope.$emit("$stateChangeSuccess", null, {name: "new-route"}); 131 + $rootScope.$emit("$stateChangeSuccess", null, { name: "new-route" });
130 132
131 // and check now if the bodyEl has a class indicating the new state route 133 // and check now if the bodyEl has a class indicating the new state route
132 expect(bodyEl.className).toEqual(BodyStateClassesService.ROUTE_STATE_CLASSNAME_PREFIX + "new-route"); 134 expect(bodyEl.className).toEqual(BodyStateClassesService.ROUTE_STATE_CLASSNAME_PREFIX + "new-route");
133 }); 135 });
134 -});  
135 \ No newline at end of file 136 \ No newline at end of file
  137 +
  138 + it("add a css class theme skin to body element", () => {
  139 + let service = getService();
  140 + let skinClass: string = 'skin-test';
  141 +
  142 + bodyElJq.addClass = jasmine.createSpy("addClass");
  143 + bodyElJq.removeClass = jasmine.createSpy("removeClass");
  144 + service["bodyElement"] = bodyElJq;
  145 +
  146 + service.start({
  147 + skin: skinClass
  148 + });
  149 +
  150 + expect(bodyElJq.addClass).toHaveBeenCalledWith(skinClass);
  151 + });
  152 +
  153 + it("add a css class to content wrapper element", () => {
  154 + let service = getService();
  155 +
  156 + let contentWrapperMock = jasmine.createSpyObj("contentWrapperMock", ["addClass", "removeClass"]);
  157 + service["getContentWrapper"] = jasmine.createSpy("getContentWrapper").and.returnValue(contentWrapperMock);
  158 + service.addContentClass(true);
  159 +
  160 + expect(contentWrapperMock.addClass).toHaveBeenCalledWith(BodyStateClassesService.CONTENT_WRAPPER_FULL);
  161 + });
  162 +
  163 + it("remove a css class from content wrapper element", () => {
  164 + let service = getService();
  165 +
  166 + let contentWrapperMock = jasmine.createSpyObj("contentWrapperMock", ["addClass", "removeClass"]);
  167 + service["getContentWrapper"] = jasmine.createSpy("getContentWrapper").and.returnValue(contentWrapperMock);
  168 + service.addContentClass(false);
  169 +
  170 + expect(contentWrapperMock.removeClass).toHaveBeenCalledWith(BodyStateClassesService.CONTENT_WRAPPER_FULL);
  171 + });
  172 +});
src/app/layout/services/body-state-classes.service.ts
1 import {Directive, Inject, Injectable} from "ng-forward"; 1 import {Directive, Inject, Injectable} from "ng-forward";
2 -import {AUTH_EVENTS} from "./../../login/auth-events"; 2 +import {AuthEvents} from "./../../login/auth-events";
3 import {AuthService} from "./../../login/auth.service"; 3 import {AuthService} from "./../../login/auth.service";
4 import {HtmlUtils} from "../html-utils"; 4 import {HtmlUtils} from "../html-utils";
  5 +import {INgForwardJQuery} from 'ng-forward/cjs/util/jqlite-extensions';
  6 +
  7 +export interface StartParams {
  8 + skin?: string;
  9 +}
5 10
6 /** 11 /**
7 * This is a service which adds classes to the body element 12 * This is a service which adds classes to the body element
@@ -9,18 +14,23 @@ import {HtmlUtils} from &quot;../html-utils&quot;; @@ -9,18 +14,23 @@ import {HtmlUtils} from &quot;../html-utils&quot;;
9 * eg: 14 * eg:
10 * User Logged: 15 * User Logged:
11 * - noosfero-user-logged 16 * - noosfero-user-logged
12 - * Route States: 17 + * Route States:
13 * - noosfero-route-main 18 * - noosfero-route-main
14 * - noosfero-route-main.profile.info 19 * - noosfero-route-main.profile.info
  20 + *
  21 + * Show the all content in full mode:
  22 + * - full-content
15 */ 23 */
16 @Injectable() 24 @Injectable()
17 @Inject("$rootScope", "$document", "$state", AuthService) 25 @Inject("$rootScope", "$document", "$state", AuthService)
18 export class BodyStateClassesService { 26 export class BodyStateClassesService {
19 27
20 private started: boolean = false; 28 private started: boolean = false;
  29 + private skin: string;
21 30
22 public static get USER_LOGGED_CLASSNAME(): string { return "noosfero-user-logged"; } 31 public static get USER_LOGGED_CLASSNAME(): string { return "noosfero-user-logged"; }
23 public static get ROUTE_STATE_CLASSNAME_PREFIX(): string { return "noosfero-route-"; } 32 public static get ROUTE_STATE_CLASSNAME_PREFIX(): string { return "noosfero-route-"; }
  33 + public static get CONTENT_WRAPPER_FULL(): string { return "full-content"; }
24 34
25 private bodyElement: ng.IAugmentedJQuery = null; 35 private bodyElement: ng.IAugmentedJQuery = null;
26 36
@@ -33,14 +43,37 @@ export class BodyStateClassesService { @@ -33,14 +43,37 @@ export class BodyStateClassesService {
33 43
34 } 44 }
35 45
36 - start() { 46 + start(config?: StartParams) {
37 if (!this.started) { 47 if (!this.started) {
38 this.setupUserLoggedClassToggle(); 48 this.setupUserLoggedClassToggle();
39 this.setupStateClassToggle(); 49 this.setupStateClassToggle();
  50 +
  51 + if (config) {
  52 + this.setThemeSkin(config.skin);
  53 + }
40 this.started = true; 54 this.started = true;
41 } 55 }
42 } 56 }
43 57
  58 + setThemeSkin(skin: string) {
  59 + this.getBodyElement().addClass(skin);
  60 + }
  61 +
  62 + addContentClass(addClass: boolean, className?: string): BodyStateClassesService {
  63 +
  64 + let fullContentClass: string = className || BodyStateClassesService.CONTENT_WRAPPER_FULL;
  65 + let contentWrapper = this.getContentWrapper();
  66 +
  67 + if (contentWrapper) {
  68 + if (addClass) {
  69 + contentWrapper.addClass(fullContentClass);
  70 + } else {
  71 + contentWrapper.removeClass(fullContentClass);
  72 + }
  73 + }
  74 + return this;
  75 + }
  76 +
44 private getStateChangeSuccessHandlerFunction(bodyElement: ng.IAugmentedJQuery): (event: ng.IAngularEvent, toState: ng.ui.IState) => void { 77 private getStateChangeSuccessHandlerFunction(bodyElement: ng.IAugmentedJQuery): (event: ng.IAngularEvent, toState: ng.ui.IState) => void {
45 let self = this; 78 let self = this;
46 return (event: ng.IAngularEvent, toState: ng.ui.IState) => { 79 return (event: ng.IAngularEvent, toState: ng.ui.IState) => {
@@ -65,7 +98,7 @@ export class BodyStateClassesService { @@ -65,7 +98,7 @@ export class BodyStateClassesService {
65 98
66 /** 99 /**
67 * Setup the initial state of the user-logged css class 100 * Setup the initial state of the user-logged css class
68 - * and adds events handlers to switch this class when the login events happens 101 + * and adds events handlers to switch this class when the login events happens
69 */ 102 */
70 private setupUserLoggedClassToggle() { 103 private setupUserLoggedClassToggle() {
71 let bodyElement = this.getBodyElement(); 104 let bodyElement = this.getBodyElement();
@@ -76,18 +109,18 @@ export class BodyStateClassesService { @@ -76,18 +109,18 @@ export class BodyStateClassesService {
76 bodyElement.addClass(BodyStateClassesService.USER_LOGGED_CLASSNAME); 109 bodyElement.addClass(BodyStateClassesService.USER_LOGGED_CLASSNAME);
77 } 110 }
78 111
79 - // listen to the AUTH_EVENTS.loginSuccess and AUTH_EVENTS.logoutSuccess  
80 - // to switch the css class which indicates user logged in  
81 - this.$rootScope.$on(AUTH_EVENTS.loginSuccess, () => { 112 + // to switch the css class which indicates user logged in
  113 + this.authService.subscribe(AuthEvents[AuthEvents.loginSuccess], () => {
82 bodyElement.addClass(BodyStateClassesService.USER_LOGGED_CLASSNAME); 114 bodyElement.addClass(BodyStateClassesService.USER_LOGGED_CLASSNAME);
83 }); 115 });
84 - this.$rootScope.$on(AUTH_EVENTS.logoutSuccess, () => { 116 +
  117 + this.authService.subscribe(AuthEvents[AuthEvents.logoutSuccess], () => {
85 bodyElement.removeClass(BodyStateClassesService.USER_LOGGED_CLASSNAME); 118 bodyElement.removeClass(BodyStateClassesService.USER_LOGGED_CLASSNAME);
86 }); 119 });
87 } 120 }
88 121
89 /** 122 /**
90 - * Returns the user 'body' html Element 123 + * Returns the user 'body' html Element
91 */ 124 */
92 private getBodyElement(): ng.IAugmentedJQuery { 125 private getBodyElement(): ng.IAugmentedJQuery {
93 if (this.bodyElement === null) { 126 if (this.bodyElement === null) {
@@ -95,4 +128,9 @@ export class BodyStateClassesService { @@ -95,4 +128,9 @@ export class BodyStateClassesService {
95 } 128 }
96 return this.bodyElement; 129 return this.bodyElement;
97 } 130 }
98 -}  
99 \ No newline at end of file 131 \ No newline at end of file
  132 +
  133 + private getContentWrapper(selector?: string): INgForwardJQuery {
  134 + let doc = <INgForwardJQuery>angular.element(this.$document);
  135 + return doc.query(selector || '.content-wrapper');
  136 + }
  137 +}
src/app/layout/sidebar/sidebar-section.component.ts 0 → 100644
@@ -0,0 +1,126 @@ @@ -0,0 +1,126 @@
  1 +import {Component, Input} from "ng-forward";
  2 +
  3 +@Component({
  4 + selector: 'sidebar-section',
  5 + templateUrl: 'app/layout/sidebar/sidebar-section.html'
  6 +})
  7 +/**
  8 + * @ngdoc object
  9 + * @name sidebar.SidebarSectionComponent
  10 + * @description
  11 + * This is a widget to render sections to
  12 + * SidebarComponent.
  13 + *
  14 + * <b>Usage example:</b>
  15 + * @example
  16 + * <pre>
  17 + * let section: SidebarSectionComponent = new SidebarSectionComponent('MySection');
  18 + * section.addItem({
  19 + * title: 'Friends',
  20 + * count: 0,
  21 + * url: '#',
  22 + * className: 'active',
  23 + * icon: 'fa-users', //A font-awesome icon class
  24 + * subitems: [
  25 + * { title: 'Example' }
  26 + * ]
  27 + * });
  28 + * </pre>
  29 + */
  30 +export class SidebarSectionComponent {
  31 +
  32 + /**
  33 + * @ngdoc property
  34 + * @name name
  35 + * @propertyOf sidebar.SidebarComponent
  36 + * @description
  37 + * The name of the section
  38 + */
  39 + @Input()
  40 + public name: string;
  41 +
  42 + /**
  43 + * @ngdoc property
  44 + * @name items
  45 + * @propertyOf sidebar.SidebarComponent
  46 + * @description
  47 + * Array of items to render into this sidebar menu
  48 + */
  49 + @Input()
  50 + public items: any[] = [
  51 + {
  52 + title: 'Friends',
  53 + count: 0,
  54 + url: '#',
  55 + className: 'active',
  56 + icon: 'fa-users'
  57 + }
  58 + ];
  59 +
  60 + /**
  61 + * @ngdoc method
  62 + * @name constructor
  63 + * @methodOf sidebar.SidebarSectionComponent
  64 + * @param {string} name The name of the section (optional)
  65 + * @description
  66 + * The constructor for this component. The name of section
  67 + * can be assigned here, optionally
  68 + */
  69 + constructor(name?: string) {
  70 + this.name = name;
  71 + }
  72 +
  73 + /**
  74 + * @ngdoc method
  75 + * @name addItem
  76 + * @methodOf sidebar.SidebarSectionComponent
  77 + * @param {Object} item Literal object with properties to render a menu item
  78 + * @returns {SidebarSectionComponent} This own component type, using the "Fluent Interface" pattern
  79 + * @description
  80 + * Use this method to add new items for a section instance
  81 + *
  82 + * <b>Usage example:</b>
  83 + * @example
  84 + * <pre>
  85 + * section.addItem({
  86 + * title: 'Friends',
  87 + * count: 0,
  88 + * url: '#',
  89 + * className: 'active',
  90 + * icon: 'fa-users', //A font-awesome icon class
  91 + * subitems: [
  92 + * { title: 'Example' } //A subitem literal object
  93 + * ]
  94 + * });
  95 + * </pre>
  96 + */
  97 + addItem(item: any): SidebarSectionComponent {
  98 + this.items.push(item);
  99 + return this;
  100 + }
  101 +
  102 + /**
  103 + * @ngdoc method
  104 + * @name setName
  105 + * @methodOf sidebar.SidebarSectionComponent
  106 + * @param {string} name The name of the section
  107 + * @returns {SidebarSectionComponent} This own component type, using the "Fluent Interface" pattern
  108 + * @description
  109 + * Change the name of the section assigned on constructor
  110 + *
  111 + * <b>Usage example:</b>
  112 + * @example
  113 + * <pre>
  114 + * section.setName('MyAnotherSection')
  115 + * .addItem({
  116 + * //Item here
  117 + * ...
  118 + * });
  119 + * </pre>
  120 + */
  121 + setName(name: string): SidebarSectionComponent {
  122 + this.name = name;
  123 + return this;
  124 + }
  125 +
  126 +}
src/app/layout/sidebar/sidebar-section.html 0 → 100644
@@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
  1 +<ul class="nav nav-pills nav-stacked">
  2 + <li class="nav-header nav-header-first hidden-sm hidden-xs">
  3 + {{ctrl.name}}
  4 + </li>
  5 +
  6 + <li ng-click="widgetExpanded = !widgetExpanded" ng-repeat="item in ctrl.items" class="{{item.className}}">
  7 + <a href="{{item.url}}" class="dropdown-toggle">
  8 + <i class="fa {{item.icon}}"></i>
  9 + <span>{{item.title}}</span>
  10 + <span class="label label-primary label-circle pull-right" ng-class="{'submenu-count': item.subitems}" ng-show="item.count != undefined">{{item.count}}</span>
  11 + <i class="fa fa-angle-right drop-icon" ng-show="item.subitems"></i>
  12 + </a>
  13 + <ul class="submenu" ng-show="widgetExpanded && item.subitems">
  14 + <li ng-repeat="subitem in item.subitems">
  15 + <a href="{{subitem.url}}">
  16 + {{subitem.title}}
  17 + </a>
  18 + </li>
  19 + </ul>
  20 + </li>
  21 +</ul>
src/app/layout/sidebar/sidebar.component.spec.ts 0 → 100644
@@ -0,0 +1,86 @@ @@ -0,0 +1,86 @@
  1 +import {provide} from 'ng-forward';
  2 +import {ComponentTestHelper, createClass} from '../../../spec/component-test-helper';
  3 +import {providers} from 'ng-forward/cjs/testing/providers';
  4 +import {SidebarComponent} from './sidebar.component';
  5 +import {SidebarSectionComponent} from './sidebar-section.component';
  6 +import * as helpers from '../../../spec/helpers';
  7 +
  8 +const htmlTemplate: string = '<sidebar [visible]="false"></sidebar>';
  9 +
  10 +describe('Sidebar Component', () => {
  11 +
  12 + let helper: ComponentTestHelper<SidebarComponent>;
  13 + let notifyService: any = {
  14 + event: Function,
  15 + subscribe: (fn: (visible: boolean) => void) => {
  16 + notifyService.event = fn;
  17 + },
  18 + next: (value: any) => {
  19 + notifyService.event(value);
  20 + },
  21 + setVisibility: (visible: boolean) => {
  22 + notifyService.event(visible);
  23 + }
  24 + };
  25 +
  26 + let sessionService: any = {
  27 + currentUser: (): any => {
  28 + return {
  29 + person: { name: 'test' }
  30 + };
  31 + }
  32 + };
  33 +
  34 +
  35 + beforeEach(angular.mock.module("templates"));
  36 +
  37 + beforeEach((done: Function) => {
  38 + providers((provide: any) => {
  39 + return <any>[
  40 + provide('SidebarNotificationService', {
  41 + useValue: notifyService
  42 + }),
  43 + provide('SessionService', {
  44 + useValue: sessionService
  45 + }),
  46 + provide('SidebarSectionComponent', {
  47 + useValue: SidebarSectionComponent
  48 + })
  49 + ];
  50 + });
  51 +
  52 + let cls = createClass({
  53 + template: htmlTemplate,
  54 + directives: [SidebarComponent],
  55 + properties: {
  56 + visible: false
  57 + }
  58 + });
  59 +
  60 + helper = new ComponentTestHelper<SidebarComponent>(cls, done);
  61 + });
  62 +
  63 + it('render the sidebar html content', () => {
  64 + expect(helper.all('div#nav-col').length).toEqual(1);
  65 + });
  66 +
  67 + it('show sidebar only if a service emit a visibility event', () => {
  68 +
  69 + notifyService.setVisibility(true);
  70 + expect(helper.component.isVisible()).toBeTruthy();
  71 + });
  72 +
  73 + it('show user name into sidebar', () => {
  74 +
  75 + notifyService.setVisibility(true);
  76 + expect(helper.component.user.name).toEqual(sessionService.currentUser().person.name);
  77 + expect(helper.debugElement.query('div.user-box .name a').text()).toMatch(sessionService.currentUser().person.name);
  78 + });
  79 +
  80 + it('show sidebar section with a menu itens', () => {
  81 +
  82 + notifyService.setVisibility(true);
  83 + expect(helper.debugElement.query('li.active a span').text()).toMatch('Friends');
  84 + });
  85 +
  86 +});
src/app/layout/sidebar/sidebar.component.ts 0 → 100644
@@ -0,0 +1,104 @@ @@ -0,0 +1,104 @@
  1 +import {Component, Inject, Input} from "ng-forward";
  2 +import {SidebarNotificationService} from "./sidebar.notification.service";
  3 +import {SessionService} from '../../login/session.service';
  4 +import {SidebarSectionComponent} from './sidebar-section.component';
  5 +
  6 +@Component({
  7 + selector: 'sidebar',
  8 + templateUrl: 'app/layout/sidebar/sidebar.html',
  9 + directives: [SidebarSectionComponent]
  10 +})
  11 +@Inject(SidebarNotificationService, SessionService)
  12 +/**
  13 + * @ngdoc object
  14 + * @name sidebar.SidebarComponent
  15 + * @requires [SidebarNotificationService, SessionService]
  16 + * @description
  17 + * This is a widget to a sidebar with visible control.
  18 + * Needs a SidebarSectionComponent to show sections/items/subitems
  19 + * menu
  20 + *
  21 + * <b>Usage example:</b>
  22 + * @example
  23 + * <pre>
  24 + * let sidebar: SidebarComponent = new SidebarComponent(SidebarNotificationService, SessionService);
  25 + * sidebar.visible = true;
  26 + * </pre>
  27 + */
  28 +export class SidebarComponent {
  29 +
  30 + /**
  31 + * @ngdoc property
  32 + * @name visible
  33 + * @propertyOf sidebar.SidebarComponent
  34 + * @description
  35 + * Controls if this component is show/hide
  36 + */
  37 + @Input()
  38 + private visible: boolean = false;
  39 +
  40 + /**
  41 + * @ngdoc property
  42 + * @name showStatus
  43 + * @propertyOf sidebar.SidebarComponent
  44 + * @description
  45 + * Controls the show/hide state of the circle user status
  46 + */
  47 + @Input('showstatus')
  48 + public showStatus: boolean = false;
  49 +
  50 + /**
  51 + * @ngdoc property
  52 + * @name user
  53 + * @propertyOf sidebar.SidebarComponent
  54 + * @description
  55 + * The user data to show into sidebar
  56 + */
  57 + @Input()
  58 + public user: { name: string } = {
  59 + name: ''
  60 + };
  61 +
  62 + /**
  63 + * @ngdoc method
  64 + * @name constructor
  65 + * @methodOf sidebar.SidebarComponent
  66 + * @param {SidebarNotificationService} notificationService The service that emmits events to show/hide this component
  67 + * @param {SessionService} session The service that loads the user data when user is logged
  68 + * @description
  69 + * The constructor for this component. Loads the dependencies services
  70 + */
  71 + constructor(private notificationService: SidebarNotificationService, private session: SessionService) { }
  72 +
  73 + /**
  74 + * @ngdoc method
  75 + * @name ngOnInit
  76 + * @methodOf sidebar.SidebarComponent
  77 + * @description
  78 + * Check the initial visibility when this component is loaded
  79 + */
  80 + ngOnInit() {
  81 +
  82 + let userData: any = this.session.currentUser();
  83 + if (userData) {
  84 + this.user = userData.person;
  85 + }
  86 +
  87 + this.notificationService.setVisibility(this.visible);
  88 + this.notificationService.subscribe((visible: boolean) => {
  89 + this.visible = visible;
  90 + });
  91 + }
  92 +
  93 + /**
  94 + * @ngdoc method
  95 + * @name isVisible
  96 + * @methodOf sidebar.SidebarComponent
  97 + * @returns {boolean} True, whether this component is visible, otherwise returns false
  98 + * @description
  99 + * Verify whether sidebar is visible or not
  100 + */
  101 + isVisible(): boolean {
  102 + return <boolean>this.visible;
  103 + }
  104 +}
src/app/layout/sidebar/sidebar.html 0 → 100644
@@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
  1 +<div id="nav-col" ng-show="ctrl.isVisible()" ng-class="{'sidebar-hide':!ctrl.isVisible()}">
  2 + <section id="col-left" class="col-left-nano">
  3 + <div id="col-left-inner" class="col-left-nano-content">
  4 + <div id="user-left-box" class="clearfix hidden-sm hidden-xs dropdown profile2-dropdown">
  5 + <noosfero-profile-image [profile]="ctrl.user"></noosfero-profile-image>
  6 + <div class="user-box">
  7 + <span class="name">
  8 + <a href="#" class="dropdown-toggle" data-toggle="dropdown">
  9 + {{ctrl.user.name}}
  10 + </a>
  11 + </span>
  12 + <span class="status" ng-show="ctrl.showStatus">
  13 + <i class="fa fa-circle"></i> {{ctrl.user.status}}
  14 + </span>
  15 + </div>
  16 + </div>
  17 +
  18 + <div class="collapse navbar-collapse navbar-ex1-collapse" id="sidebar-nav">
  19 + <sidebar-section [name]='Navigation'></sidebar-section>
  20 + </div>
  21 + </div>
  22 + </section>
  23 + </div>
src/app/layout/sidebar/sidebar.notification.service.ts 0 → 100644
@@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
  1 +import {Injectable, EventEmitter} from "ng-forward";
  2 +
  3 +
  4 +@Injectable()
  5 +export class SidebarNotificationService {
  6 + private alternateVisibilityEvent: EventEmitter<boolean> = new EventEmitter<boolean>();
  7 + public sidebarVisible: boolean = false;
  8 +
  9 + getCurrentVisibility() {
  10 + return this.sidebarVisible;
  11 + }
  12 +
  13 + alternateVisibility() {
  14 + this.sidebarVisible = !this.sidebarVisible;
  15 + this.alternateVisibilityEvent.next(this.sidebarVisible);
  16 + }
  17 +
  18 + setVisibility(visibility: boolean) {
  19 + this.sidebarVisible = visibility;
  20 + this.alternateVisibilityEvent.next(this.sidebarVisible);
  21 + }
  22 +
  23 + subscribe(fn: (visible: boolean) => void) {
  24 + this.alternateVisibilityEvent.subscribe(fn);
  25 + }
  26 +
  27 +}
src/app/login/auth-events.ts
1 -export interface IAuthEvents {  
2 - loginSuccess: string;  
3 - loginFailed: string;  
4 - logoutSuccess: string; 1 +export enum AuthEvents {
  2 + loginSuccess, loginFailed, logoutSuccess
5 } 3 }
6 -  
7 -export const AUTH_EVENTS: IAuthEvents = {  
8 - loginSuccess: "auth-login-success",  
9 - loginFailed: "auth-login-failed",  
10 - logoutSuccess: "auth-logout-success"  
11 -};  
12 \ No newline at end of file 4 \ No newline at end of file
src/app/login/auth.service.spec.ts
1 -import {AuthService, AUTH_EVENTS} from "./"; 1 +import {AuthService, AuthEvents} from "./";
  2 +import {SessionService} from './session.service';
  3 +
  4 +import {Injectable, Provider, provide, EventEmitter} from "ng-forward";
  5 +import {ComponentFixture} from 'ng-forward/cjs/testing/test-component-builder';
  6 +
  7 +import {getAngularServiceFactory, AngularServiceFactory} from "../../spec/helpers";
2 8
3 describe("Services", () => { 9 describe("Services", () => {
4 10
@@ -15,23 +21,26 @@ describe(&quot;Services&quot;, () =&gt; { @@ -15,23 +21,26 @@ describe(&quot;Services&quot;, () =&gt; {
15 $translateProvider.translations('en', {}); 21 $translateProvider.translations('en', {});
16 })); 22 }));
17 23
18 - beforeEach(inject((_$httpBackend_: ng.IHttpBackendService, _$rootScope_: ng.IRootScopeService, _AuthService_: AuthService) => {  
19 - $httpBackend = _$httpBackend_;  
20 - authService = _AuthService_;  
21 - $rootScope = _$rootScope_; 24 + beforeEach(() => {
22 25
23 user = <noosfero.User>{ 26 user = <noosfero.User>{
24 id: 1, 27 id: 1,
25 login: "user" 28 login: "user"
26 }; 29 };
27 - }));  
28 - 30 + });
29 31
30 describe("Succesffull login", () => { 32 describe("Succesffull login", () => {
31 33
  34 + let factory: AngularServiceFactory;
  35 + let authService: AuthService;
  36 + let $log: ng.ILogService;
  37 + let $http: ng.IHttpBackendService;
  38 +
32 beforeEach(() => { 39 beforeEach(() => {
33 credentials = { username: "user", password: "password" }; 40 credentials = { username: "user", password: "password" };
34 - 41 + factory = getAngularServiceFactory();
  42 + authService = factory.getAngularService("AuthService");
  43 + $httpBackend = factory.getHttpBackendService();
35 $httpBackend.expectPOST("/api/v1/login", "login=user&password=password").respond(200, { user: user }); 44 $httpBackend.expectPOST("/api/v1/login", "login=user&password=password").respond(200, { user: user });
36 }); 45 });
37 46
@@ -44,20 +53,15 @@ describe(&quot;Services&quot;, () =&gt; { @@ -44,20 +53,15 @@ describe(&quot;Services&quot;, () =&gt; {
44 expect($httpBackend.verifyNoOutstandingRequest()); 53 expect($httpBackend.verifyNoOutstandingRequest());
45 }); 54 });
46 55
47 -  
48 - it("should emit event loggin successful with user logged data", () => {  
49 -  
50 - authService.login(credentials);  
51 -  
52 - let eventEmmited: boolean = false;  
53 - $rootScope.$on(AUTH_EVENTS.loginSuccess, (event: ng.IAngularEvent, userThroughEvent: noosfero.User) => {  
54 - eventEmmited = true; 56 + it("should emit event loggin successful with user logged data", (done: Function) => {
  57 + let successEvent: any = AuthEvents[AuthEvents.loginSuccess];
  58 + (<any>authService)[successEvent].subscribe((userThroughEvent: noosfero.User): any => {
55 expect(userThroughEvent).toEqual(user); 59 expect(userThroughEvent).toEqual(user);
  60 + done();
56 }); 61 });
57 - 62 + authService.login(credentials);
58 $httpBackend.flush(); 63 $httpBackend.flush();
59 64
60 - expect(eventEmmited).toBeTruthy(AUTH_EVENTS.loginSuccess + " was not emmited!");  
61 }); 65 });
62 66
63 it("should return the current logged in user", () => { 67 it("should return the current logged in user", () => {
@@ -74,6 +78,5 @@ describe(&quot;Services&quot;, () =&gt; { @@ -74,6 +78,5 @@ describe(&quot;Services&quot;, () =&gt; {
74 }); 78 });
75 }); 79 });
76 80
77 -  
78 }); 81 });
79 }); 82 });
src/app/login/auth.service.ts
1 -import {Injectable, Inject} from "ng-forward"; 1 +import {Injectable, Inject, EventEmitter} from "ng-forward";
2 2
3 import {NoosferoRootScope, UserResponse} from "./../shared/models/interfaces"; 3 import {NoosferoRootScope, UserResponse} from "./../shared/models/interfaces";
4 import {SessionService} from "./session.service"; 4 import {SessionService} from "./session.service";
5 5
6 -import {AUTH_EVENTS, IAuthEvents} from "./auth-events"; 6 +import {AuthEvents} from "./auth-events";
7 7
8 @Injectable() 8 @Injectable()
9 -@Inject("$q", "$http", "$rootScope", "SessionService", "$log", "AUTH_EVENTS") 9 +@Inject("$http", SessionService, "$log")
10 export class AuthService { 10 export class AuthService {
11 11
12 - constructor(private $q: ng.IQService,  
13 - private $http: ng.IHttpService,  
14 - private $rootScope: NoosferoRootScope,  
15 - private sessionService: SessionService,  
16 - private $log: ng.ILogService,  
17 - private auth_events: IAuthEvents) { 12 + public loginSuccess: EventEmitter<noosfero.User> = new EventEmitter<noosfero.User>();
  13 + public loginFailed: EventEmitter<ng.IHttpPromiseCallbackArg<any>> = new EventEmitter<ng.IHttpPromiseCallbackArg<any>>();
  14 + public logoutSuccess: EventEmitter<noosfero.User> = new EventEmitter<noosfero.User>();
18 15
  16 + constructor(private $http: ng.IHttpService,
  17 + private sessionService: SessionService,
  18 + private $log: ng.ILogService) {
19 } 19 }
20 20
21 loginFromCookie() { 21 loginFromCookie() {
@@ -27,8 +27,8 @@ export class AuthService { @@ -27,8 +27,8 @@ export class AuthService {
27 private loginSuccessCallback(response: ng.IHttpPromiseCallbackArg<UserResponse>) { 27 private loginSuccessCallback(response: ng.IHttpPromiseCallbackArg<UserResponse>) {
28 this.$log.debug('AuthService.login [SUCCESS] response', response); 28 this.$log.debug('AuthService.login [SUCCESS] response', response);
29 let currentUser: noosfero.User = this.sessionService.create(response.data); 29 let currentUser: noosfero.User = this.sessionService.create(response.data);
30 - this.$rootScope.currentUser = currentUser;  
31 - this.$rootScope.$broadcast(this.auth_events.loginSuccess, currentUser); 30 + this.loginSuccess.next(currentUser);
  31 +
32 return currentUser; 32 return currentUser;
33 } 33 }
34 34
@@ -40,15 +40,15 @@ export class AuthService { @@ -40,15 +40,15 @@ export class AuthService {
40 40
41 private loginFailedCallback(response: ng.IHttpPromiseCallbackArg<any>): any { 41 private loginFailedCallback(response: ng.IHttpPromiseCallbackArg<any>): any {
42 this.$log.debug('AuthService.login [FAIL] response', response); 42 this.$log.debug('AuthService.login [FAIL] response', response);
43 - this.$rootScope.$broadcast(this.auth_events.loginFailed);  
44 - // return $q.reject(response); 43 + this.loginFailed.next(response);
45 return null; 44 return null;
46 } 45 }
47 46
48 public logout() { 47 public logout() {
  48 + let user: noosfero.User = this.sessionService.currentUser();
49 this.sessionService.destroy(); 49 this.sessionService.destroy();
50 - this.$rootScope.currentUser = undefined;  
51 - this.$rootScope.$broadcast(this.auth_events.logoutSuccess); 50 +
  51 + this.logoutSuccess.next(user);
52 this.$http.jsonp('/account/logout'); // FIXME logout from noosfero to sync login state 52 this.$http.jsonp('/account/logout'); // FIXME logout from noosfero to sync login state
53 } 53 }
54 54
@@ -66,4 +66,14 @@ export class AuthService { @@ -66,4 +66,14 @@ export class AuthService {
66 } 66 }
67 return (this.isAuthenticated() && authorizedRoles.indexOf(this.sessionService.currentUser().userRole) !== -1); 67 return (this.isAuthenticated() && authorizedRoles.indexOf(this.sessionService.currentUser().userRole) !== -1);
68 } 68 }
69 -}  
70 \ No newline at end of file 69 \ No newline at end of file
  70 +
  71 + subscribe(eventName: string, fn: Function) {
  72 +
  73 + let event: EventEmitter<any> = <EventEmitter<any>>(<any>this)[eventName];
  74 + if (event) {
  75 + event.subscribe(fn);
  76 + } else {
  77 + throw new Error(`The event: ${eventName} not exists`);
  78 + }
  79 + }
  80 +}
src/app/main/main.component.ts
  1 +import * as plugins from "../../plugins";
1 import {bundle, Component, StateConfig, Inject} from "ng-forward"; 2 import {bundle, Component, StateConfig, Inject} from "ng-forward";
2 import {ArticleBlogComponent} from "./../article/types/blog/blog.component"; 3 import {ArticleBlogComponent} from "./../article/types/blog/blog.component";
3 4
@@ -8,15 +9,18 @@ import {BoxesComponent} from &quot;../layout/boxes/boxes.component&quot;; @@ -8,15 +9,18 @@ import {BoxesComponent} from &quot;../layout/boxes/boxes.component&quot;;
8 import {BlockComponent} from "../layout/blocks/block.component"; 9 import {BlockComponent} from "../layout/blocks/block.component";
9 import {EnvironmentComponent} from "../environment/environment.component"; 10 import {EnvironmentComponent} from "../environment/environment.component";
10 import {EnvironmentHomeComponent} from "../environment/environment-home.component"; 11 import {EnvironmentHomeComponent} from "../environment/environment-home.component";
11 -import {PeopleBlockComponent} from "../layout/blocks/people-block/people-block.component"; 12 +import {PeopleBlockComponent} from "../layout/blocks/people/people-block.component";
  13 +import {LinkListBlockComponent} from "./../layout/blocks/link-list/link-list-block.component";
  14 +import {RecentDocumentsBlockComponent} from "../layout/blocks/recent-documents/recent-documents-block.component";
  15 +import {ProfileImageBlockComponent} from "../layout/blocks/profile-image/profile-image-block.component";
  16 +import {RawHTMLBlockComponent} from "../layout/blocks/raw-html/raw-html-block.component";
  17 +import {StatisticsBlockComponent} from "../layout/blocks/statistics/statistics-block.component";
  18 +
  19 +import {MembersBlockComponent} from "./../layout/blocks/members/members-block.component";
  20 +import {CommunitiesBlockComponent} from "./../layout/blocks/communities/communities-block.component";
  21 +
12 import {LoginBlockComponent} from "../layout/blocks/login-block/login-block.component"; 22 import {LoginBlockComponent} from "../layout/blocks/login-block/login-block.component";
13 -import {LinkListBlockComponent} from "./../layout/blocks/link-list/link-list.component";  
14 -import {RecentDocumentsBlockComponent} from "../layout/blocks/recent-documents/recent-documents.component";  
15 -import {ProfileImageBlockComponent} from "../layout/blocks/profile-image-block/profile-image-block.component";  
16 -import {RawHTMLBlockComponent} from "../layout/blocks/raw-html/raw-html.component";  
17 23
18 -import {MembersBlockComponent} from "./../layout/blocks/members-block/members-block.component";  
19 -import {CommunitiesBlockComponent} from "./../layout/blocks/communities-block/communities-block.component";  
20 import {NoosferoTemplate} from "../shared/pipes/noosfero-template.filter"; 24 import {NoosferoTemplate} from "../shared/pipes/noosfero-template.filter";
21 import {DateFormat} from "../shared/pipes/date-format.filter"; 25 import {DateFormat} from "../shared/pipes/date-format.filter";
22 26
@@ -29,7 +33,10 @@ import {BodyStateClassesService} from &quot;./../layout/services/body-state-classes.s @@ -29,7 +33,10 @@ import {BodyStateClassesService} from &quot;./../layout/services/body-state-classes.s
29 33
30 import {Navbar} from "../layout/navbar/navbar"; 34 import {Navbar} from "../layout/navbar/navbar";
31 35
32 -import {MainBlockComponent} from "../layout/blocks/main-block/main-block.component"; 36 +import {SidebarComponent} from "../layout/sidebar/sidebar.component";
  37 +
  38 +import {MainBlockComponent} from "../layout/blocks/main/main-block.component";
  39 +import {HtmlEditorComponent} from "../shared/components/html-editor/html-editor.component";
33 40
34 41
35 /** 42 /**
@@ -49,8 +56,13 @@ import {MainBlockComponent} from &quot;../layout/blocks/main-block/main-block.compone @@ -49,8 +56,13 @@ import {MainBlockComponent} from &quot;../layout/blocks/main-block/main-block.compone
49 }) 56 })
50 @Inject(BodyStateClassesService) 57 @Inject(BodyStateClassesService)
51 export class MainContentComponent { 58 export class MainContentComponent {
  59 +
  60 + public themeSkin: string = 'skin-whbl';
  61 +
52 constructor(private bodyStateClassesService: BodyStateClassesService) { 62 constructor(private bodyStateClassesService: BodyStateClassesService) {
53 - bodyStateClassesService.start(); 63 + bodyStateClassesService.start({
  64 + skin: this.themeSkin
  65 + });
54 } 66 }
55 } 67 }
56 68
@@ -71,22 +83,24 @@ export class EnvironmentContent { @@ -71,22 +83,24 @@ export class EnvironmentContent {
71 * NoosferoTemplate, DateFormat, RawHTMLBlock 83 * NoosferoTemplate, DateFormat, RawHTMLBlock
72 * @description 84 * @description
73 * The Main controller for the Noosfero Angular Theme application. 85 * The Main controller for the Noosfero Angular Theme application.
74 - * 86 + *
75 * The main route '/' is defined as the URL for this controller, which routes 87 * The main route '/' is defined as the URL for this controller, which routes
76 * requests to the {@link main.MainContentComponent} controller and also, the '/profile' route, 88 * requests to the {@link main.MainContentComponent} controller and also, the '/profile' route,
77 - * which routes requests to the {@link profile.Profile} controller. See {@link profile.Profile}  
78 - * for more details on how various Noosfero profiles are rendered. 89 + * which routes requests to the {@link profile.Profile} controller. See {@link profile.Profile}
  90 + * for more details on how various Noosfero profiles are rendered.
79 */ 91 */
80 @Component({ 92 @Component({
81 selector: 'main', 93 selector: 'main',
82 template: '<div ng-view></div>', 94 template: '<div ng-view></div>',
83 directives: [ 95 directives: [
84 ArticleBlogComponent, ArticleViewComponent, BoxesComponent, BlockComponent, 96 ArticleBlogComponent, ArticleViewComponent, BoxesComponent, BlockComponent,
85 - EnvironmentComponent, PeopleBlockComponent, LoginBlockComponent,  
86 - LinkListBlockComponent, CommunitiesBlockComponent,  
87 - MainBlockComponent, RecentDocumentsBlockComponent, Navbar, ProfileImageBlockComponent,  
88 - MembersBlockComponent, NoosferoTemplate, DateFormat, RawHTMLBlockComponent  
89 - ], 97 + EnvironmentComponent, PeopleBlockComponent,
  98 + LinkListBlockComponent, CommunitiesBlockComponent, HtmlEditorComponent,
  99 + MainBlockComponent, RecentDocumentsBlockComponent, Navbar, SidebarComponent, ProfileImageBlockComponent,
  100 + MembersBlockComponent, NoosferoTemplate, DateFormat, RawHTMLBlockComponent, StatisticsBlockComponent,
  101 + LoginBlockComponent
  102 + ].concat(plugins.mainComponents).concat(plugins.hotspots),
  103 +
90 providers: [AuthService, SessionService, NotificationService, BodyStateClassesService] 104 providers: [AuthService, SessionService, NotificationService, BodyStateClassesService]
91 }) 105 })
92 @StateConfig([ 106 @StateConfig([
src/app/main/main.html
1 <acme-navbar></acme-navbar> 1 <acme-navbar></acme-navbar>
2 -<div ui-view="content"></div> 2 +<!-- Sidebar Left -->
  3 +<sidebar [visible]="false"></sidebar>
  4 +<div class="content-wrapper full-content" ui-view="content"></div>
src/app/main/main.scss 0 → 100644
@@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
  1 +.content-wrapper {
  2 + margin-top: 32px;
  3 +}
  4 +
  5 +.full-content {
  6 + margin-left: 0px !important;
  7 +}
src/app/profile/image/image.html
1 -<span title="{{ctrl.profile.name}}"> 1 +<span class="profile-image-wrap" title="{{ctrl.profile.name}}">
2 <img ng-if="ctrl.profile.image" ng-src="{{ctrl.profile.image.url}}" class="img-responsive profile-image"> 2 <img ng-if="ctrl.profile.image" ng-src="{{ctrl.profile.image.url}}" class="img-responsive profile-image">
3 <i ng-if="!ctrl.profile.image" class="fa {{ctrl.defaultIcon}} fa-5x profile-image"></i> 3 <i ng-if="!ctrl.profile.image" class="fa {{ctrl.defaultIcon}} fa-5x profile-image"></i>
4 </span> 4 </span>
src/app/profile/profile.component.ts
1 import {StateConfig, Component, Inject, provide} from 'ng-forward'; 1 import {StateConfig, Component, Inject, provide} from 'ng-forward';
2 import {ProfileInfoComponent} from './info/profile-info.component'; 2 import {ProfileInfoComponent} from './info/profile-info.component';
3 import {ProfileHomeComponent} from './profile-home.component'; 3 import {ProfileHomeComponent} from './profile-home.component';
4 -import {BasicEditorComponent} from '../article/basic-editor.component'; 4 +import {BasicEditorComponent} from '../article/cms/basic-editor/basic-editor.component';
  5 +import {CmsComponent} from '../article/cms/cms.component';
5 import {ContentViewerComponent} from "../article/content-viewer/content-viewer.component"; 6 import {ContentViewerComponent} from "../article/content-viewer/content-viewer.component";
6 import {ContentViewerActionsComponent} from "../article/content-viewer/content-viewer-actions.component"; 7 import {ContentViewerActionsComponent} from "../article/content-viewer/content-viewer-actions.component";
7 import {ActivitiesComponent} from "./activities/activities.component"; 8 import {ActivitiesComponent} from "./activities/activities.component";
@@ -45,13 +46,25 @@ import {MyProfileComponent} from &quot;./myprofile.component&quot;; @@ -45,13 +46,25 @@ import {MyProfileComponent} from &quot;./myprofile.component&quot;;
45 component: MyProfileComponent 46 component: MyProfileComponent
46 }, 47 },
47 { 48 {
48 - name: 'main.profile.cms',  
49 - url: "^/myprofile/:profile/cms",  
50 - component: BasicEditorComponent, 49 + name: 'main.cms',
  50 + url: "^/myprofile/:profile/cms?parent_id&type",
  51 + component: CmsComponent,
51 views: { 52 views: {
52 - "mainBlockContent": {  
53 - templateUrl: "app/article/basic-editor.html",  
54 - controller: BasicEditorComponent, 53 + "content": {
  54 + templateUrl: "app/article/cms/cms.html",
  55 + controller: CmsComponent,
  56 + controllerAs: "vm"
  57 + }
  58 + }
  59 + },
  60 + {
  61 + name: 'main.cmsEdit',
  62 + url: "^/myprofile/:profile/cms/edit/:id",
  63 + component: CmsComponent,
  64 + views: {
  65 + "content": {
  66 + templateUrl: "app/article/cms/cms.html",
  67 + controller: CmsComponent,
55 controllerAs: "vm" 68 controllerAs: "vm"
56 } 69 }
57 } 70 }
@@ -98,7 +111,7 @@ export class ProfileComponent { @@ -98,7 +111,7 @@ export class ProfileComponent {
98 }).then((response: restangular.IResponse) => { 111 }).then((response: restangular.IResponse) => {
99 this.boxes = response.data.boxes; 112 this.boxes = response.data.boxes;
100 }).catch(() => { 113 }).catch(() => {
101 - $state.transitionTo('main'); 114 + $state.transitionTo('main.environment.home');
102 notificationService.error({ message: "notification.profile.not_found" }); 115 notificationService.error({ message: "notification.profile.not_found" });
103 }); 116 });
104 } 117 }
src/app/profile/profile.html
1 <div class="profile-container"> 1 <div class="profile-container">
  2 + <div class="profile-header" ng-bind-html="vm.profile.custom_header"></div>
2 <div class="row"> 3 <div class="row">
3 <noosfero-boxes [boxes]="vm.boxes" [owner]="vm.profile"></noosfero-boxes> 4 <noosfero-boxes [boxes]="vm.boxes" [owner]="vm.profile"></noosfero-boxes>
4 </div> 5 </div>
  6 + <div class="profile-footer" ng-bind-html="vm.profile.custom_footer"></div>
5 </div> 7 </div>
src/app/shared/components/html-editor/html-editor.component.spec.ts 0 → 100644
@@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
  1 +import {ComponentTestHelper, createClass} from './../../../../spec/component-test-helper';
  2 +import {HtmlEditorComponent} from "./html-editor.component";
  3 +
  4 +const htmlTemplate: string = '<html-editor [(value)]="ctrl.value" [options]="ctrl.options"></html-editor>';
  5 +
  6 +describe("Components", () => {
  7 + describe("Html Editor Component", () => {
  8 +
  9 + let helper: ComponentTestHelper<HtmlEditorComponent>;
  10 + beforeEach(angular.mock.module("templates"));
  11 +
  12 + beforeEach((done) => {
  13 + let properties = { value: "value" };
  14 + let cls = createClass({
  15 + template: htmlTemplate,
  16 + directives: [HtmlEditorComponent],
  17 + properties: properties
  18 + });
  19 + helper = new ComponentTestHelper<HtmlEditorComponent>(cls, done);
  20 + });
  21 +
  22 + it("render a textarea", () => {
  23 + expect(helper.find("textarea").length).toEqual(1);
  24 + });
  25 + });
  26 +});
src/app/shared/components/html-editor/html-editor.component.ts 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  1 +import {Component, Input} from "ng-forward";
  2 +
  3 +@Component({
  4 + selector: 'html-editor',
  5 + templateUrl: "app/shared/components/html-editor/html-editor.html",
  6 +})
  7 +export class HtmlEditorComponent {
  8 +
  9 + @Input() options: any = {};
  10 + @Input() value: any;
  11 +}
src/app/shared/components/html-editor/html-editor.html 0 → 100644
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 +<textarea ckeditor="ctrl.options" class="form-control" ng-model="ctrl.value"></textarea>
src/app/shared/services/notification.service.ts
@@ -25,7 +25,7 @@ export class NotificationService { @@ -25,7 +25,7 @@ export class NotificationService {
25 25
26 httpError(status: number, data: any): boolean { 26 httpError(status: number, data: any): boolean {
27 this.error({ message: `notification.http_error.${status}.message` }); 27 this.error({ message: `notification.http_error.${status}.message` });
28 - return true; // return true to indicate that the error was already handled 28 + return false; // return true to indicate that the error was already handled
29 } 29 }
30 30
31 success({ 31 success({
src/index.html
@@ -27,6 +27,10 @@ @@ -27,6 +27,10 @@
27 27
28 <div ui-view></div> 28 <div ui-view></div>
29 29
  30 + <script>
  31 + CKEDITOR_BASEPATH='/bower_components/ng-ckeditor/libs/ckeditor/';
  32 + </script>
  33 +
30 <!-- build:js(src) scripts/vendor.js --> 34 <!-- build:js(src) scripts/vendor.js -->
31 <!-- bower:js --> 35 <!-- bower:js -->
32 <!-- run `gulp inject` to automatically populate bower script dependencies --> 36 <!-- run `gulp inject` to automatically populate bower script dependencies -->
@@ -42,5 +46,7 @@ @@ -42,5 +46,7 @@
42 <!-- endinject --> 46 <!-- endinject -->
43 <!-- endbuild --> 47 <!-- endbuild -->
44 48
  49 + <script src="/bower_components/ng-ckeditor/libs/ckeditor/ckeditor.js"></script>
  50 + <script src="/bower_components/ng-ckeditor/ng-ckeditor.min.js"></script>
45 </body> 51 </body>
46 </html> 52 </html>
src/languages/en.json
@@ -35,5 +35,24 @@ @@ -35,5 +35,24 @@
35 "comment.pagination.more": "More", 35 "comment.pagination.more": "More",
36 "comment.post.success.title": "Good job!", 36 "comment.post.success.title": "Good job!",
37 "comment.post.success.message": "Comment saved!", 37 "comment.post.success.message": "Comment saved!",
38 - "comment.reply": "reply" 38 + "comment.reply": "reply",
  39 + "article.actions.edit": "Edit",
  40 + "article.basic_editor.title": "Title",
  41 + "article.basic_editor.body": "Body",
  42 + "article.basic_editor.save": "Save",
  43 + "article.basic_editor.save.failed": "This article could not be saved",
  44 + "article.basic_editor.cancel": "Cancel",
  45 + "article.basic_editor.success.title": "Good job!",
  46 + "article.basic_editor.success.message": "Article saved!",
  47 + "article.basic_editor.visibility": "Visibility",
  48 + "article.basic_editor.visibility.public": "Public",
  49 + "article.basic_editor.visibility.private": "Private",
  50 + "statistics.users": "Users",
  51 + "statistics.enterprises": "Enterprises",
  52 + "statistics.products": "Products",
  53 + "statistics.communities": "Communities",
  54 + "statistics.categories": "Categories",
  55 + "statistics.tags": "Tags",
  56 + "statistics.comments": "Comments",
  57 + "statistics.hits": "Hits"
39 } 58 }
src/languages/pt.json
@@ -35,5 +35,24 @@ @@ -35,5 +35,24 @@
35 "comment.pagination.more": "Mais", 35 "comment.pagination.more": "Mais",
36 "comment.post.success.title": "Bom trabalho!", 36 "comment.post.success.title": "Bom trabalho!",
37 "comment.post.success.message": "Comentário salvo com sucesso!", 37 "comment.post.success.message": "Comentário salvo com sucesso!",
38 - "comment.reply": "responder" 38 + "comment.reply": "responder",
  39 + "article.actions.edit": "Editar",
  40 + "article.basic_editor.title": "Título",
  41 + "article.basic_editor.body": "Corpo",
  42 + "article.basic_editor.save": "Salvar",
  43 + "article.basic_editor.save.failed": "O artigo não pode ser salvo",
  44 + "article.basic_editor.cancel": "Cancelar",
  45 + "article.basic_editor.success.title": "Bom trabalho!",
  46 + "article.basic_editor.success.message": "Artigo salvo com sucesso!",
  47 + "article.basic_editor.visibility": "Visibilidade",
  48 + "article.basic_editor.visibility.public": "Público",
  49 + "article.basic_editor.visibility.private": "Privado",
  50 + "statistics.users": "Usuários",
  51 + "statistics.enterprises": "Empreendimentos",
  52 + "statistics.products": "Produtos",
  53 + "statistics.communities": "Comunidades",
  54 + "statistics.categories": "Categorias",
  55 + "statistics.tags": "Tags",
  56 + "statistics.comments": "Comentários",
  57 + "statistics.hits": "Acessos"
39 } 58 }
src/lib/ng-noosfero-api/http/article.service.ts
@@ -22,6 +22,23 @@ export class ArticleService extends RestangularService&lt;noosfero.Article&gt; { @@ -22,6 +22,23 @@ export class ArticleService extends RestangularService&lt;noosfero.Article&gt; {
22 }; 22 };
23 } 23 }
24 24
  25 + updateArticle(article: noosfero.Article) {
  26 + let headers = {
  27 + 'Content-Type': 'application/json'
  28 + };
  29 + let deferred = this.$q.defer<noosfero.RestResult<noosfero.Article>>();
  30 + // TODO dynamically copy the selected attributes to update
  31 + let attributesToUpdate: any = {
  32 + article: {
  33 + name: article.name, body: article.body, published: article.published,
  34 + start_date: article['start_date'], end_date: article['end_date']
  35 + }
  36 + };
  37 + let restRequest: ng.IPromise<noosfero.RestResult<noosfero.Article>> = this.getElement(article.id).customPOST(attributesToUpdate, null, null, headers);
  38 + restRequest.then(this.getHandleSuccessFunction(deferred))
  39 + .catch(this.getHandleErrorFunction(deferred));
  40 + return deferred.promise;
  41 + }
25 42
26 createInProfile(profile: noosfero.Profile, article: noosfero.Article): ng.IPromise<noosfero.RestResult<noosfero.Article>> { 43 createInProfile(profile: noosfero.Profile, article: noosfero.Article): ng.IPromise<noosfero.RestResult<noosfero.Article>> {
27 let profileElement = this.profileService.get(<number>profile.id); 44 let profileElement = this.profileService.get(<number>profile.id);
@@ -32,6 +49,14 @@ export class ArticleService extends RestangularService&lt;noosfero.Article&gt; { @@ -32,6 +49,14 @@ export class ArticleService extends RestangularService&lt;noosfero.Article&gt; {
32 return this.create(article, <noosfero.RestModel>profileElement, null, headers); 49 return this.create(article, <noosfero.RestModel>profileElement, null, headers);
33 } 50 }
34 51
  52 + createInParent(parentId: number, article: noosfero.Article): ng.IPromise<noosfero.RestResult<noosfero.Article>> {
  53 + let headers = {
  54 + 'Content-Type': 'application/json'
  55 + };
  56 +
  57 + let parent = this.getElement(parentId);
  58 + return this.create(article, parent, null, headers, true, "children");
  59 + }
35 60
36 getAsCollectionChildrenOf<C>(rootElement: noosfero.Environment | noosfero.Article | noosfero.Profile, path: string, queryParams?: any, headers?: any): restangular.ICollectionPromise<C> { 61 getAsCollectionChildrenOf<C>(rootElement: noosfero.Environment | noosfero.Article | noosfero.Profile, path: string, queryParams?: any, headers?: any): restangular.ICollectionPromise<C> {
37 return rootElement.getList<C>(path, queryParams, headers); 62 return rootElement.getList<C>(path, queryParams, headers);
@@ -77,5 +102,4 @@ export class ArticleService extends RestangularService&lt;noosfero.Article&gt; { @@ -77,5 +102,4 @@ export class ArticleService extends RestangularService&lt;noosfero.Article&gt; {
77 } 102 }
78 103
79 104
80 -  
81 } 105 }
src/lib/ng-noosfero-api/http/restangular_service.spec.ts
@@ -207,4 +207,27 @@ describe(&quot;Restangular Service - base Class&quot;, () =&gt; { @@ -207,4 +207,27 @@ describe(&quot;Restangular Service - base Class&quot;, () =&gt; {
207 $httpBackend.flush(); 207 $httpBackend.flush();
208 }); 208 });
209 209
210 -});  
211 \ No newline at end of file 210 \ No newline at end of file
  211 + it("post('customPath', rootObject) calls POST /rootObjects/1/customPath", (done) => {
  212 + let rootObj: RootObjectModel = rootObjectRestService.getElement(1);
  213 + $httpBackend.expectPOST("/api/v1/rootObjects/1/customPath").respond(201, { object: { attr: 1, rootId: 1 } });
  214 + objectRestService.post('customPath', rootObj).then((result: noosfero.RestResult<ObjectModel>) => {
  215 + expect(result.data).toBeDefined();
  216 + expect((<any>result.data).attr).toEqual(1);
  217 + expect((<any>result.data).rootId).toEqual(1);
  218 + done();
  219 + });
  220 + $httpBackend.flush();
  221 + });
  222 +
  223 + it("post('customPath') calls POST /objects/customPath", (done) => {
  224 + $httpBackend.expectPOST("/api/v1/objects/customPath").respond(201, { object: { attr: 1, rootId: 1 } });
  225 + objectRestService.post('customPath').then((result: noosfero.RestResult<ObjectModel>) => {
  226 + expect(result.data).toBeDefined();
  227 + expect((<any>result.data).attr).toEqual(1);
  228 + expect((<any>result.data).rootId).toEqual(1);
  229 + done();
  230 + });
  231 + $httpBackend.flush();
  232 + });
  233 +
  234 +});
src/lib/ng-noosfero-api/http/restangular_service.ts
@@ -11,6 +11,8 @@ @@ -11,6 +11,8 @@
11 export abstract class RestangularService<T extends noosfero.RestModel> { 11 export abstract class RestangularService<T extends noosfero.RestModel> {
12 12
13 private baseResource: restangular.IElement; 13 private baseResource: restangular.IElement;
  14 + private currentPromise: ng.IDeferred<T>;
  15 +
14 /** 16 /**
15 * Creates an instance of RestangularService. 17 * Creates an instance of RestangularService.
16 * 18 *
@@ -20,6 +22,7 @@ export abstract class RestangularService&lt;T extends noosfero.RestModel&gt; { @@ -20,6 +22,7 @@ export abstract class RestangularService&lt;T extends noosfero.RestModel&gt; {
20 */ 22 */
21 constructor(protected restangularService: restangular.IService, protected $q: ng.IQService, protected $log: ng.ILogService) { 23 constructor(protected restangularService: restangular.IService, protected $q: ng.IQService, protected $log: ng.ILogService) {
22 this.baseResource = restangularService.all(this.getResourcePath()); 24 this.baseResource = restangularService.all(this.getResourcePath());
  25 + this.resetCurrent();
23 // TODO 26 // TODO
24 // this.restangularService.setResponseInterceptor((data, operation, what, url, response, deferred) => { 27 // this.restangularService.setResponseInterceptor((data, operation, what, url, response, deferred) => {
25 // let transformedData: any = data; 28 // let transformedData: any = data;
@@ -32,6 +35,18 @@ export abstract class RestangularService&lt;T extends noosfero.RestModel&gt; { @@ -32,6 +35,18 @@ export abstract class RestangularService&lt;T extends noosfero.RestModel&gt; {
32 // }); 35 // });
33 } 36 }
34 37
  38 + public resetCurrent() {
  39 + this.currentPromise = this.$q.defer();
  40 + }
  41 +
  42 + public getCurrent(): ng.IPromise<T> {
  43 + return this.currentPromise.promise;
  44 + }
  45 +
  46 + public setCurrent(object: T) {
  47 + this.currentPromise.resolve(object);
  48 + }
  49 +
35 protected extractData(response: restangular.IResponse): noosfero.RestResult<T> { 50 protected extractData(response: restangular.IResponse): noosfero.RestResult<T> {
36 let dataKey: string; 51 let dataKey: string;
37 if (response.data && this.getDataKeys()) { 52 if (response.data && this.getDataKeys()) {
@@ -221,7 +236,7 @@ export abstract class RestangularService&lt;T extends noosfero.RestModel&gt; { @@ -221,7 +236,7 @@ export abstract class RestangularService&lt;T extends noosfero.RestModel&gt; {
221 * Creates a new Resource into the resource collection 236 * Creates a new Resource into the resource collection
222 * calls POST /resourcePath 237 * calls POST /resourcePath
223 */ 238 */
224 - public create(obj: T, rootElement?: noosfero.RestModel, queryParams?: any, headers?: any, isSub: boolean = true): ng.IPromise<noosfero.RestResult<T>> { 239 + public create(obj: T, rootElement?: noosfero.RestModel, queryParams?: any, headers?: any, isSub: boolean = true, path?: string): ng.IPromise<noosfero.RestResult<T>> {
225 let deferred = this.$q.defer<noosfero.RestResult<T>>(); 240 let deferred = this.$q.defer<noosfero.RestResult<T>>();
226 241
227 let restRequest: ng.IPromise<noosfero.RestResult<T>>; 242 let restRequest: ng.IPromise<noosfero.RestResult<T>>;
@@ -233,8 +248,9 @@ export abstract class RestangularService&lt;T extends noosfero.RestModel&gt; { @@ -233,8 +248,9 @@ export abstract class RestangularService&lt;T extends noosfero.RestModel&gt; {
233 data = obj; 248 data = obj;
234 } 249 }
235 250
  251 + let subpath = path || this.getResourcePath();
236 if (rootElement) { 252 if (rootElement) {
237 - restRequest = rootElement.all(this.getResourcePath()).post(data, queryParams, headers); 253 + restRequest = rootElement.all(subpath).post(data, queryParams, headers);
238 } else { 254 } else {
239 restRequest = this.baseResource.post(data, queryParams, headers); 255 restRequest = this.baseResource.post(data, queryParams, headers);
240 } 256 }
@@ -245,6 +261,22 @@ export abstract class RestangularService&lt;T extends noosfero.RestModel&gt; { @@ -245,6 +261,22 @@ export abstract class RestangularService&lt;T extends noosfero.RestModel&gt; {
245 return deferred.promise; 261 return deferred.promise;
246 } 262 }
247 263
  264 +
  265 + public post(path: string, rootElement?: restangular.IElement, data?: any, headers?: any): ng.IPromise<noosfero.RestResult<T>> {
  266 + let deferred = this.$q.defer<noosfero.RestResult<T>>();
  267 + let restRequest: ng.IPromise<any>;
  268 +
  269 + if (rootElement) {
  270 + restRequest = rootElement.customPOST(data, path, headers);
  271 + } else {
  272 + restRequest = this.baseResource.customPOST(data, path, headers);
  273 + }
  274 + restRequest
  275 + .then(this.getHandleSuccessFunction(deferred))
  276 + .catch(this.getHandleErrorFunction(deferred));
  277 + return deferred.promise;
  278 + }
  279 +
248 /** 280 /**
249 * Returns a Restangular IElement representing the 281 * Returns a Restangular IElement representing the
250 */ 282 */
src/lib/ng-noosfero-api/interfaces/article.ts
1 1
2 namespace noosfero { 2 namespace noosfero {
3 - export interface Article extends RestModel { 3 + export interface Article extends RestModel {
4 path: string; 4 path: string;
5 profile: Profile; 5 profile: Profile;
6 type: string; 6 type: string;
  7 + parent: Article;
  8 + body: string;
  9 + title: string;
  10 + name: string;
  11 + published: boolean;
  12 + setting: any;
  13 + start_date: string;
  14 + end_date: string;
7 } 15 }
8 -}  
9 \ No newline at end of file 16 \ No newline at end of file
  17 +}
src/lib/ng-noosfero-api/interfaces/statistics_block.ts 0 → 100644
@@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
  1 +namespace noosfero {
  2 + interface StatisticInfo {
  3 + name: string;
  4 + quantity: number;
  5 + display: boolean;
  6 + }
  7 + export interface StatisticsBlock extends Block {
  8 + statistics: StatisticInfo[];
  9 + }
  10 +}
src/plugins/comment_paragraph/allow-comment/allow-comment.component.spec.ts 0 → 100644
@@ -0,0 +1,78 @@ @@ -0,0 +1,78 @@
  1 +import {AllowCommentComponent} from "./allow-comment.component";
  2 +import {ComponentTestHelper, createClass} from '../../../spec/component-test-helper';
  3 +import * as helpers from "../../../spec/helpers";
  4 +import {Provider} from 'ng-forward';
  5 +import {ComponentFixture} from 'ng-forward/cjs/testing/test-component-builder';
  6 +
  7 +let htmlTemplate = '<comment-paragraph-plugin-allow-comment [content]="ctrl.content" [paragraph-uuid]="ctrl.paragraphUuid" [article]="ctrl.article"></comment-paragraph-plugin-allow-comment>';
  8 +
  9 +describe("Components", () => {
  10 + describe("Allow Comment Component", () => {
  11 +
  12 + let serviceMock = {
  13 + commentParagraphCount: () => {
  14 + return Promise.resolve(5);
  15 + }
  16 + };
  17 + let functionToggleCommentParagraph: Function;
  18 + let eventServiceMock = {
  19 + // toggleCommentParagraph
  20 + subscribeToggleCommentParagraph: (fn: Function) => {
  21 + functionToggleCommentParagraph = fn;
  22 + }
  23 + };
  24 +
  25 + let providers = [
  26 + new Provider('CommentParagraphService', { useValue: serviceMock }),
  27 + new Provider('CommentParagraphEventService', { useValue: eventServiceMock })
  28 + ];
  29 + let helper: ComponentTestHelper<AllowCommentComponent>;
  30 +
  31 + beforeEach(angular.mock.module("templates"));
  32 +
  33 + beforeEach((done) => {
  34 + let cls = createClass({
  35 + template: htmlTemplate,
  36 + directives: [AllowCommentComponent],
  37 + providers: providers,
  38 + properties: {
  39 + content: "",
  40 + paragraphUuid: "uuid",
  41 + article: {
  42 + setting: {
  43 + comment_paragraph_plugin_activate: true
  44 + }
  45 + }
  46 + }
  47 + });
  48 + helper = new ComponentTestHelper<AllowCommentComponent>(cls, done);
  49 + });
  50 +
  51 + it('update comments count', () => {
  52 + expect(helper.component.commentsCount).toEqual(5);
  53 + });
  54 +
  55 + it('display paragraph content', () => {
  56 + expect(helper.all(".paragraph .paragraph-content").length).toEqual(1);
  57 + });
  58 +
  59 + it('display button to side comments', () => {
  60 + expect(helper.all(".paragraph .actions a").length).toEqual(1);
  61 + });
  62 +
  63 + it('set display to true when click in show paragraph', () => {
  64 + helper.component.showParagraphComments();
  65 + expect(helper.component.display).toBeTruthy();
  66 + });
  67 +
  68 + it('set display to false when click in hide paragraph', () => {
  69 + helper.component.hideParagraphComments();
  70 + expect(helper.component.display).toBeFalsy();
  71 + });
  72 +
  73 + it('update article when receive a toogle paragraph event', () => {
  74 + functionToggleCommentParagraph({ id: 2 });
  75 + expect(helper.component.article.id).toEqual(2);
  76 + });
  77 + });
  78 +});
src/plugins/comment_paragraph/allow-comment/allow-comment.component.ts 0 → 100644
@@ -0,0 +1,45 @@ @@ -0,0 +1,45 @@
  1 +import {Component, Input, Inject} from "ng-forward";
  2 +import {SideCommentsComponent} from "../side-comments/side-comments.component";
  3 +import {CommentParagraphEventService} from "../events/comment-paragraph-event.service";
  4 +import {CommentParagraphService} from "../http/comment-paragraph.service";
  5 +
  6 +@Component({
  7 + selector: "comment-paragraph-plugin-allow-comment",
  8 + templateUrl: "plugins/comment_paragraph/allow-comment/allow-comment.html",
  9 + directives: [SideCommentsComponent]
  10 +})
  11 +@Inject("$scope", CommentParagraphEventService, CommentParagraphService)
  12 +export class AllowCommentComponent {
  13 +
  14 + @Input() content: string;
  15 + @Input() paragraphUuid: string;
  16 + @Input() article: noosfero.Article;
  17 + commentsCount: number;
  18 + display = false;
  19 +
  20 + constructor(private $scope: ng.IScope,
  21 + private commentParagraphEventService: CommentParagraphEventService,
  22 + private commentParagraphService: CommentParagraphService) { }
  23 +
  24 + ngOnInit() {
  25 + this.commentParagraphEventService.subscribeToggleCommentParagraph((article: noosfero.Article) => {
  26 + this.article = article;
  27 + this.$scope.$apply();
  28 + });
  29 + this.commentParagraphService.commentParagraphCount(this.article, this.paragraphUuid).then((count: number) => {
  30 + this.commentsCount = count;
  31 + });
  32 + }
  33 +
  34 + isActivated() {
  35 + return this.article && this.article.setting && this.article.setting.comment_paragraph_plugin_activate;
  36 + }
  37 +
  38 + showParagraphComments() {
  39 + this.display = true;
  40 + }
  41 +
  42 + hideParagraphComments() {
  43 + this.display = false;
  44 + }
  45 +}
src/plugins/comment_paragraph/allow-comment/allow-comment.html 0 → 100644
@@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
  1 +<div class="paragraph" ng-class="{'active' : ctrl.display}">
  2 + <div class="paragraph-content" ng-bind-html="ctrl.content" ng-class="{'active' : ctrl.display}"></div>
  3 + <div ng-if="ctrl.isActivated()" class="actions">
  4 + <a href="#" popover-placement="right-top" popover-trigger="none"
  5 + uib-popover-template="'plugins/comment_paragraph/allow-comment/popover.html'"
  6 + (click)="ctrl.showParagraphComments()" popover-is-open="ctrl.display">
  7 + <div class="arrow_box" ng-class="{'active' : ctrl.display}">
  8 + <span class="count">{{ctrl.commentsCount > 0 ? ctrl.commentsCount : '+'}}</span>
  9 + </div>
  10 + </a>
  11 + </div>
  12 +</div>
src/plugins/comment_paragraph/allow-comment/allow-comment.scss 0 → 100644
@@ -0,0 +1,62 @@ @@ -0,0 +1,62 @@
  1 +$balloon-selected-color: #50BF68;
  2 +$balloon-color: #c4c4c4;
  3 +
  4 +comment-paragraph-plugin-allow-comment {
  5 + .paragraph {
  6 + width: 100%;
  7 + &.active {
  8 + width: 80%;
  9 + }
  10 + .popover {
  11 + &.right > .arrow {
  12 + }
  13 + }
  14 + .paragraph-content {
  15 + width: 95%;
  16 + display: inline-block;
  17 + }
  18 + .actions {
  19 + width: 3%;
  20 + display: inline-block;
  21 + vertical-align: top;
  22 + margin-left: 10px;
  23 + .popover {
  24 + width: 100%;
  25 + max-width: 330px;
  26 + }
  27 + .count {
  28 + font-size: 14px;
  29 + font-weight: bold;
  30 + color: white;
  31 + text-align: center;
  32 + width: 100%;
  33 + display: inline-block;
  34 + }
  35 + .arrow_box {
  36 + position: relative;
  37 + background: $balloon-color;
  38 + margin-top: 5px;
  39 + width: 25px;
  40 + border-radius: 2px;
  41 + &:after {
  42 + top: 100%;
  43 + left: 50%;
  44 + border: solid transparent;
  45 + content: " ";
  46 + position: absolute;
  47 + pointer-events: none;
  48 + border-color: rgba(196, 196, 196, 0);
  49 + border-top-color: $balloon-color;
  50 + border-width: 6px;
  51 + margin-left: -6px;
  52 + }
  53 + &:hover, &.active {
  54 + background: $balloon-selected-color;
  55 + &:after {
  56 + border-top-color: $balloon-selected-color;
  57 + }
  58 + }
  59 + }
  60 + }
  61 + }
  62 +}
src/plugins/comment_paragraph/allow-comment/popover.html 0 → 100644
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 +<comment-paragraph-side-comments id="side-comments-{{ctrl.paragraphUuid}}" click-outside="ctrl.hideParagraphComments()" [article]="ctrl.article" [paragraph-uuid]="ctrl.paragraphUuid"></comment-paragraph-side-comments>
src/plugins/comment_paragraph/article/cms/discussion-editor/discussion-editor.component.ts 0 → 100644
@@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
  1 +import {Component, Input, Inject} from 'ng-forward';
  2 +
  3 +@Component({
  4 + selector: 'comment-paragraph-plugin-discussion-editor',
  5 + templateUrl: "plugins/comment_paragraph/article/cms/discussion-editor/discussion-editor.html"
  6 +})
  7 +@Inject("$scope")
  8 +export class DiscussionEditorComponent {
  9 +
  10 + @Input() article: noosfero.Article;
  11 + start_date: Date;
  12 + end_date: Date;
  13 +
  14 + constructor(private $scope: ng.IScope) {
  15 + this.convertDate('start_date');
  16 + this.convertDate('end_date');
  17 + }
  18 +
  19 + convertDate(attributeName: string) {
  20 + this.$scope.$watch(() => {
  21 + return (<any>this)[attributeName];
  22 + }, () => {
  23 + if ((<any>this)[attributeName]) {
  24 + (<any>this.article)[attributeName] = (<any>this)[attributeName].toISOString();
  25 + }
  26 + });
  27 + }
  28 +
  29 + ngOnInit() {
  30 + if (this.article.start_date) {
  31 + this.start_date = new Date(this.article.start_date);
  32 + } else {
  33 + this.start_date = moment().toDate();
  34 + }
  35 + if (this.article.end_date) {
  36 + this.end_date = new Date(this.article.end_date);
  37 + }
  38 + }
  39 +}
src/plugins/comment_paragraph/article/cms/discussion-editor/discussion-editor.html 0 → 100644
@@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
  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 +
  8 + <div class="form-inline">
  9 + <span class="start-date discussion-date">
  10 + <label for="startDateInput">{{"comment-paragraph-plugin.discussion.editor.start_date.label" | translate}}</label>
  11 + <input id="startDateInput" type="text" class="form-control" uib-datepicker-popup ng-model="ctrl.start_date"
  12 + is-open="startDateOpened" ng-required="true" close-text="Close" />
  13 + <span class="input-group-btn date-popup-button">
  14 + <button type="button" class="btn btn-default" ng-click="startDateOpened = true">
  15 + <i class="fa fa-calendar fa-fw"></i>
  16 + </button>
  17 + </span>
  18 + </span>
  19 + <span class="end-date discussion-date">
  20 + <label for="endDateInput">{{"comment-paragraph-plugin.discussion.editor.end_date.label" | translate}}</label>
  21 + <input id="endDateInput" type="text" class="form-control" uib-datepicker-popup ng-model="ctrl.end_date"
  22 + is-open="endDateOpened" ng-required="true" close-text="Close" />
  23 + <span class="date-popup-button">
  24 + <button type="button" class="btn btn-default" ng-click="endDateOpened = true">
  25 + <i class="fa fa-calendar fa-fw"></i>
  26 + </button>
  27 + </span>
  28 + </span>
  29 + </div>
  30 + </div>
  31 + <div class="form-group">
  32 + <label for="bodyInput">{{"article.basic_editor.body" | translate}}</label>
  33 + <html-editor [(value)]="ctrl.article.body"></html-editor>
  34 + </div>
  35 +</form>
src/plugins/comment_paragraph/article/cms/discussion-editor/discussion.scss 0 → 100644
@@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
  1 +comment-paragraph-plugin-discussion-editor {
  2 + .discussion-date {
  3 + [uib-datepicker-popup-wrap] {
  4 + display: inline-block;
  5 + }
  6 + .date-popup-button {
  7 + @extend .input-group-btn;
  8 + display: inline-block;
  9 + }
  10 + }
  11 + .end-date {
  12 + margin-left: 60px;
  13 + }
  14 +}
src/plugins/comment_paragraph/block/discussion/discussion-block.component.ts 0 → 100644
@@ -0,0 +1,52 @@ @@ -0,0 +1,52 @@
  1 +import {Component, Inject, Input} from "ng-forward";
  2 +import {ArticleService} from "../../../../lib/ng-noosfero-api/http/article.service";
  3 +
  4 +@Component({
  5 + selector: "noosfero-comment-paragraph-plugin-discussion-block",
  6 + templateUrl: 'plugins/comment_paragraph/block/discussion/discussion-block.html'
  7 +})
  8 +@Inject(ArticleService, "$state")
  9 +export class DiscussionBlockComponent {
  10 +
  11 + @Input() block: any;
  12 + @Input() owner: any;
  13 +
  14 + profile: any;
  15 + documents: any;
  16 +
  17 + documentsLoaded: boolean = false;
  18 +
  19 + constructor(private articleService: ArticleService, private $state: any) { }
  20 +
  21 + ngOnInit() {
  22 + this.profile = this.owner;
  23 + this.documents = [];
  24 +
  25 + let limit = ((this.block && this.block.settings) ? this.block.settings.limit : null) || 50;
  26 + let params: any = { content_type: 'CommentParagraphPlugin::Discussion', per_page: limit, order: 'start_date DESC' };
  27 + let now = new Date().toISOString();
  28 + switch (this.block.settings['discussion_status']) {
  29 + case 0:
  30 + params['from_start_date'] = now;
  31 + break;
  32 + case 1:
  33 + params['until_start_date'] = now;
  34 + params['from_end_date'] = now;
  35 + break;
  36 + case 2:
  37 + params['until_end_date'] = now;
  38 + break;
  39 + }
  40 + console.log(this.block.settings['discussion_status']);
  41 + this.articleService.getByProfile(this.profile, params)
  42 + .then((result: noosfero.RestResult<noosfero.Article[]>) => {
  43 + this.documents = <noosfero.Article[]>result.data;
  44 + this.documentsLoaded = true;
  45 + });
  46 + }
  47 +
  48 + openDocument(article: any) {
  49 + this.$state.go("main.profile.page", { page: article.path, profile: article.profile.identifier });
  50 + }
  51 +
  52 +}
src/plugins/comment_paragraph/block/discussion/discussion-block.html 0 → 100644
@@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
  1 +<div deckgrid source="ctrl.documents" class="deckgrid">
  2 + <div class="a-card panel media" ng-click="mother.ctrl.openDocument(card);">
  3 + <div class="author media-left" ng-show="card.author.image">
  4 + <img ng-src="{{card.author.image.url}}" class="img-circle">
  5 + </div>
  6 + <div class="header media-body">
  7 + <h5 class="title media-heading" ng-bind="card.title"></h5>
  8 +
  9 + <div class="subheader">
  10 + <span class="time">
  11 + <i class="fa fa-clock-o"></i>
  12 + <span class="start-date date" ng-show="card.start_date">
  13 + <span class="description">{{"comment-paragraph-plugin.discussion.editor.start_date.label" | translate}}</span>
  14 + <span class="value">{{card.start_date | amDateFormat:'DD/MM/YYYY'}}</span>
  15 + </span>
  16 + <span class="end-date date" ng-show="card.end_date">
  17 + <span class="description">{{"comment-paragraph-plugin.discussion.editor.end_date.label" | translate}}</span>
  18 + <span class="value">{{card.end_date | amDateFormat:'DD/MM/YYYY'}}</span>
  19 + </span>
  20 + </span>
  21 + </div>
  22 + </div>
  23 + <img ng-show="card.image" ng-src="{{card.image.url}}" class="img-responsive article-image">
  24 + <div class="post-lead" ng-bind-html="card.body | stripTags | truncate: 100: '...': true"></div>
  25 + </div>
  26 +</div>
src/plugins/comment_paragraph/block/discussion/discussion-block.scss 0 → 100644
@@ -0,0 +1,68 @@ @@ -0,0 +1,68 @@
  1 +.block {
  2 + noosfero-comment-paragraph-plugin-discussion-block {
  3 + .deckgrid[deckgrid]::before {
  4 + content: '4 .column.column-1-4';
  5 + font-size: 0;
  6 + visibility: hidden;
  7 + }
  8 + .author {
  9 + img {
  10 + width: 30px;
  11 + height: 30px;
  12 + }
  13 + }
  14 + .header {
  15 + .subheader {
  16 + color: #C1C1C1;
  17 + font-size: 10px;
  18 + }
  19 + }
  20 + .post-lead {
  21 + color: #8E8E8E;
  22 + font-size: 14px;
  23 + }
  24 + .article-image {
  25 + margin: 10px 0;
  26 + }
  27 + }
  28 +
  29 + .col-md-2-5 {
  30 + .deckgrid[deckgrid]::before {
  31 + content: '1 .deck-column';
  32 + }
  33 + }
  34 +
  35 + .col-md-7 {
  36 + .block.recentdocumentsblock {
  37 + background-color: transparent;
  38 + border: 0;
  39 +
  40 + .deckgrid[deckgrid]::before {
  41 + content: '3 .deck-column';
  42 + }
  43 +
  44 + .panel-heading {
  45 + display: none;
  46 + }
  47 + .panel-body {
  48 + padding: 0;
  49 + }
  50 +
  51 + .deckgrid {
  52 + .column {
  53 + float: left;
  54 + }
  55 +
  56 + .deck-column {
  57 + @extend .col-md-4;
  58 + padding: 0;
  59 +
  60 + .a-card {
  61 + padding: 10px;
  62 + margin: 3px;
  63 + }
  64 + }
  65 + }
  66 + }
  67 + }
  68 +}
src/plugins/comment_paragraph/events/comment-paragraph-event.service.spec.ts 0 → 100644
@@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
  1 +import {CommentParagraphEventService} from "./comment-paragraph-event.service";
  2 +import {ComponentTestHelper, createClass} from '../../../spec/component-test-helper';
  3 +import * as helpers from "../../../spec/helpers";
  4 +import {Provider} from 'ng-forward';
  5 +import {ComponentFixture} from 'ng-forward/cjs/testing/test-component-builder';
  6 +
  7 +describe("Services", () => {
  8 + describe("Comment Paragraph Event Service", () => {
  9 + let eventService: CommentParagraphEventService;
  10 +
  11 + beforeEach(() => {
  12 + eventService = new CommentParagraphEventService();
  13 + eventService['toggleCommentParagraphEmitter'] = jasmine.createSpyObj("toggleCommentParagraphEmitter", ["next", "subscribe"]);
  14 + });
  15 +
  16 + it('subscribe to toggle comment paragraph event', () => {
  17 + eventService['toggleCommentParagraphEmitter'].subscribe = jasmine.createSpy("subscribe");
  18 + eventService.subscribeToggleCommentParagraph(() => { });
  19 + expect(eventService['toggleCommentParagraphEmitter'].subscribe).toHaveBeenCalled();
  20 + });
  21 +
  22 + it('emit event when toggle comment paragraph', () => {
  23 + eventService['toggleCommentParagraphEmitter'].subscribe = jasmine.createSpy("next");
  24 + eventService.toggleCommentParagraph(<noosfero.Article>{});
  25 + expect(eventService['toggleCommentParagraphEmitter'].next).toHaveBeenCalled();
  26 + });
  27 + });
  28 +});
src/plugins/comment_paragraph/events/comment-paragraph-event.service.ts 0 → 100644
@@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
  1 +import {Injectable, EventEmitter} from "ng-forward";
  2 +
  3 +@Injectable()
  4 +export class CommentParagraphEventService {
  5 +
  6 + private toggleCommentParagraphEmitter: EventEmitter<noosfero.Article>;
  7 +
  8 + constructor() {
  9 + this.toggleCommentParagraphEmitter = new EventEmitter();
  10 + }
  11 +
  12 + toggleCommentParagraph(article: noosfero.Article) {
  13 + this.toggleCommentParagraphEmitter.next(article);
  14 + }
  15 +
  16 + subscribeToggleCommentParagraph(fn: (article: noosfero.Article) => void) {
  17 + this.toggleCommentParagraphEmitter.subscribe(fn);
  18 + }
  19 +}
src/plugins/comment_paragraph/hotspot/article-content/article-content.component.ts 0 → 100644
@@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
  1 +import { Input, Inject, Component } from "ng-forward";
  2 +import {Hotspot} from "../../../../app/hotspot/hotspot.decorator";
  3 +
  4 +@Component({
  5 + selector: "comment-paragraph-article-content-hotspot",
  6 + templateUrl: "plugins/comment_paragraph/hotspot/article-content/article-content.html",
  7 +})
  8 +@Hotspot("article_extra_content")
  9 +export class CommentParagraphArticleContentHotspotComponent {
  10 +
  11 + @Input() article: noosfero.Article;
  12 +
  13 + isDiscussion() {
  14 + return this.article.type === "CommentParagraphPlugin::Discussion";
  15 + }
  16 +}
src/plugins/comment_paragraph/hotspot/article-content/article-content.html 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  1 +<div class="discussion-header" ng-if="ctrl.isDiscussion()">
  2 + <span class="description">{{"comment-paragraph-plugin.discussion.header" | translate}}</span>
  3 + <span class="start-date date" ng-if="ctrl.article.start_date">
  4 + <span class="description">{{"comment-paragraph-plugin.discussion.editor.start_date.label" | translate}}</span>
  5 + <span class="value">{{ctrl.article.start_date | amDateFormat:'DD/MM/YYYY'}}</span>
  6 + </span>
  7 + <span class="end-date date" ng-if="ctrl.article.end_date">
  8 + <span class="description">{{"comment-paragraph-plugin.discussion.editor.end_date.label" | translate}}</span>
  9 + <span class="value">{{ctrl.article.end_date | amDateFormat:'DD/MM/YYYY'}}</span>
  10 + </span>
  11 +</div>
src/plugins/comment_paragraph/hotspot/article-content/article-content.scss 0 → 100644
@@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
  1 +.discussion-header {
  2 + @extend .pull-right;
  3 + margin-bottom: 20px;
  4 + .date {
  5 + text-transform: lowercase;
  6 + font-size: 16px;
  7 + .description {
  8 + font-weight: bold;
  9 + color: #BFBFBF;
  10 + }
  11 + .value {
  12 + font-size: 15px;
  13 + color: #969696;
  14 + }
  15 + }
  16 + .description {
  17 + font-weight: bold;
  18 + color: #BFBFBF;
  19 + }
  20 +}
src/plugins/comment_paragraph/hotspot/comment-paragraph-article-button.component.spec.ts 0 → 100644
@@ -0,0 +1,85 @@ @@ -0,0 +1,85 @@
  1 +import {CommentParagraphArticleButtonHotspotComponent} from "./comment-paragraph-article-button.component";
  2 +import {ComponentTestHelper, createClass} from '../../../spec/component-test-helper';
  3 +import * as helpers from "../../../spec/helpers";
  4 +import {Provider} from 'ng-forward';
  5 +import {ComponentFixture} from 'ng-forward/cjs/testing/test-component-builder';
  6 +
  7 +let htmlTemplate = '<comment-paragraph-article-button-hotspot [article]="ctrl.article"></comment-paragraph-article-button-hotspot>';
  8 +
  9 +describe("Components", () => {
  10 + describe("Comment Paragraph Article Button Hotspot Component", () => {
  11 +
  12 + let serviceMock = jasmine.createSpyObj("CommentParagraphService", ["deactivateCommentParagraph", "activateCommentParagraph"]);
  13 + let eventServiceMock = jasmine.createSpyObj("CommentParagraphEventService", ["toggleCommentParagraph"]);
  14 +
  15 + let providers = [
  16 + new Provider('CommentParagraphService', { useValue: serviceMock }),
  17 + new Provider('CommentParagraphEventService', { useValue: eventServiceMock })
  18 + ].concat(helpers.provideFilters('translateFilter'));
  19 + let helper: ComponentTestHelper<CommentParagraphArticleButtonHotspotComponent>;
  20 +
  21 + beforeEach(angular.mock.module("templates"));
  22 +
  23 + beforeEach((done) => {
  24 + let cls = createClass({
  25 + template: htmlTemplate,
  26 + directives: [CommentParagraphArticleButtonHotspotComponent],
  27 + providers: providers,
  28 + properties: {
  29 + article: {}
  30 + }
  31 + });
  32 + helper = new ComponentTestHelper<CommentParagraphArticleButtonHotspotComponent>(cls, done);
  33 + });
  34 +
  35 + it('emit event when deactivate comment paragraph in an article', () => {
  36 + serviceMock.deactivateCommentParagraph = jasmine.createSpy("deactivateCommentParagraph").and.returnValue(
  37 + { then: (fn: Function) => { fn({ data: {} }); } }
  38 + );
  39 + eventServiceMock.toggleCommentParagraph = jasmine.createSpy("toggleCommentParagraph");
  40 + helper.component.deactivateCommentParagraph();
  41 +
  42 + expect(serviceMock.deactivateCommentParagraph).toHaveBeenCalled();
  43 + expect(eventServiceMock.toggleCommentParagraph).toHaveBeenCalled();
  44 + });
  45 +
  46 + it('emit event when activate comment paragraph in an article', () => {
  47 + serviceMock.activateCommentParagraph = jasmine.createSpy("activateCommentParagraph").and.returnValue(
  48 + { then: (fn: Function) => { fn({ data: {} }); } }
  49 + );
  50 + eventServiceMock.toggleCommentParagraph = jasmine.createSpy("toggleCommentParagraph");
  51 + helper.component.activateCommentParagraph();
  52 +
  53 + expect(serviceMock.activateCommentParagraph).toHaveBeenCalled();
  54 + expect(eventServiceMock.toggleCommentParagraph).toHaveBeenCalled();
  55 + });
  56 +
  57 + it('return true when comment paragraph is active', () => {
  58 + helper.component.article = <noosfero.Article>{ setting: { comment_paragraph_plugin_activate: true } };
  59 + helper.detectChanges();
  60 + expect(helper.component.isActivated()).toBeTruthy();
  61 + });
  62 +
  63 + it('return false when comment paragraph is not active', () => {
  64 + expect(helper.component.isActivated()).toBeFalsy();
  65 + });
  66 +
  67 + it('return false when article has no setting attribute', () => {
  68 + helper.component.article = <noosfero.Article>{};
  69 + helper.detectChanges();
  70 + expect(helper.component.isActivated()).toBeFalsy();
  71 + });
  72 +
  73 + it('display activate button when comment paragraph is not active', () => {
  74 + expect(helper.all('.comment-paragraph-activate').length).toEqual(1);
  75 + expect(helper.all('.comment-paragraph-deactivate').length).toEqual(0);
  76 + });
  77 +
  78 + it('display deactivate button when comment paragraph is active', () => {
  79 + helper.component.article = <noosfero.Article>{ setting: { comment_paragraph_plugin_activate: true } };
  80 + helper.detectChanges();
  81 + expect(helper.all('.comment-paragraph-deactivate').length).toEqual(1);
  82 + expect(helper.all('.comment-paragraph-activate').length).toEqual(0);
  83 + });
  84 + });
  85 +});
src/plugins/comment_paragraph/hotspot/comment-paragraph-article-button.component.ts 0 → 100644
@@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
  1 +import { Input, Inject, Component } from "ng-forward";
  2 +import {Hotspot} from "../../../app/hotspot/hotspot.decorator";
  3 +import {CommentParagraphService} from "../http/comment-paragraph.service";
  4 +import {CommentParagraphEventService} from "../events/comment-paragraph-event.service";
  5 +
  6 +@Component({
  7 + selector: "comment-paragraph-article-button-hotspot",
  8 + templateUrl: "plugins/comment_paragraph/hotspot/comment-paragraph-article-button.html",
  9 +})
  10 +@Inject("$scope", CommentParagraphService, CommentParagraphEventService)
  11 +@Hotspot("article_extra_toolbar_buttons")
  12 +export class CommentParagraphArticleButtonHotspotComponent {
  13 +
  14 + @Input() article: noosfero.Article;
  15 +
  16 + constructor(private $scope: ng.IScope,
  17 + private commentParagraphService: CommentParagraphService,
  18 + private commentParagraphEventService: CommentParagraphEventService) { }
  19 +
  20 + deactivateCommentParagraph() {
  21 + this.toggleCommentParagraph(this.commentParagraphService.deactivateCommentParagraph(this.article));
  22 + }
  23 +
  24 + activateCommentParagraph() {
  25 + this.toggleCommentParagraph(this.commentParagraphService.activateCommentParagraph(this.article));
  26 + }
  27 +
  28 + isActivated() {
  29 + return this.article && this.article.setting && this.article.setting.comment_paragraph_plugin_activate;
  30 + }
  31 +
  32 + private toggleCommentParagraph(promise: ng.IPromise<noosfero.RestResult<noosfero.Article>>) {
  33 + promise.then((result: noosfero.RestResult<noosfero.Article>) => {
  34 + this.article = result.data;
  35 + this.commentParagraphEventService.toggleCommentParagraph(this.article);
  36 + });
  37 + }
  38 +}
src/plugins/comment_paragraph/hotspot/comment-paragraph-article-button.html 0 → 100644
@@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
  1 +<a href='#' class="btn btn-default btn-xs comment-paragraph-activate" (click)="ctrl.activateCommentParagraph()"
  2 + ng-if="!ctrl.article.setting.comment_paragraph_plugin_activate">
  3 + <i class="fa fa-fw fa-plus"></i> {{"comment-paragraph-plugin.title" | translate}}
  4 +</a>
  5 +<a href='#' class="btn btn-default btn-xs comment-paragraph-deactivate" (click)="ctrl.deactivateCommentParagraph()"
  6 + ng-if="ctrl.article.setting.comment_paragraph_plugin_activate">
  7 + <i class="fa fa-fw fa-minus"></i> {{"comment-paragraph-plugin.title" | translate}}
  8 +</a>
src/plugins/comment_paragraph/hotspot/comment-paragraph-form.component.ts 0 → 100644
@@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
  1 +import { Inject, Input, Component } from "ng-forward";
  2 +import {Hotspot} from "../../../app/hotspot/hotspot.decorator";
  3 +
  4 +@Component({
  5 + selector: "comment-paragraph-form-hotspot",
  6 + template: "<span></span>",
  7 +})
  8 +@Hotspot("comment_form_extra_contents")
  9 +@Inject("$scope")
  10 +export class CommentParagraphFormHotspotComponent {
  11 +
  12 + @Input() comment: noosfero.Comment;
  13 + @Input() parent: noosfero.Comment;
  14 +
  15 + constructor(private $scope: ng.IScope) { }
  16 +
  17 + ngOnInit() {
  18 + this.$scope.$watch(() => {
  19 + return this.parent;
  20 + }, () => {
  21 + if (this.parent && (<any>this.parent).paragraph_uuid) {
  22 + (<any>this.comment).paragraph_uuid = (<any>this.parent).paragraph_uuid;
  23 + }
  24 + });
  25 + }
  26 +}
src/plugins/comment_paragraph/hotspot/comment-paragraph-from.component.spec.ts 0 → 100644
@@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
  1 +import {CommentParagraphFormHotspotComponent} from "./comment-paragraph-form.component";
  2 +import {ComponentTestHelper, createClass} from '../../../spec/component-test-helper';
  3 +import * as helpers from "../../../spec/helpers";
  4 +import {ComponentFixture} from 'ng-forward/cjs/testing/test-component-builder';
  5 +
  6 +let htmlTemplate = '<comment-paragraph-form-hotspot [comment]="ctrl.comment" [parent]="ctrl.parent"></comment-paragraph-form-hotspot>';
  7 +
  8 +describe("Components", () => {
  9 + describe("Comment Paragraph Form Hotspot Component", () => {
  10 +
  11 + let helper: ComponentTestHelper<CommentParagraphFormHotspotComponent>;
  12 +
  13 + beforeEach(angular.mock.module("templates"));
  14 +
  15 + beforeEach((done) => {
  16 + let cls = createClass({
  17 + template: htmlTemplate,
  18 + directives: [CommentParagraphFormHotspotComponent],
  19 + providers: [],
  20 + properties: {
  21 + comment: {}
  22 + }
  23 + });
  24 + helper = new ComponentTestHelper<CommentParagraphFormHotspotComponent>(cls, done);
  25 + });
  26 +
  27 + it('set paragraph uuid when parent has it setted', () => {
  28 + helper.component.parent = <any>{ paragraph_uuid: 'uuid' };
  29 + helper.detectChanges();
  30 + expect((<any>helper.component.comment).paragraph_uuid).toEqual('uuid');
  31 + });
  32 + });
  33 +});
src/plugins/comment_paragraph/http/comment-paragraph.service.spec.ts 0 → 100644
@@ -0,0 +1,90 @@ @@ -0,0 +1,90 @@
  1 +import {CommentParagraphService} from "./comment-paragraph.service";
  2 +
  3 +
  4 +describe("Services", () => {
  5 +
  6 + describe("Comment Paragraph Service", () => {
  7 +
  8 + let $httpBackend: ng.IHttpBackendService;
  9 + let commentParagraphService: CommentParagraphService;
  10 +
  11 + beforeEach(angular.mock.module("noosferoApp", ($translateProvider: angular.translate.ITranslateProvider) => {
  12 + $translateProvider.translations('en', {});
  13 + }));
  14 +
  15 + beforeEach(inject((_$httpBackend_: ng.IHttpBackendService, _CommentParagraphService_: CommentParagraphService) => {
  16 + $httpBackend = _$httpBackend_;
  17 + commentParagraphService = _CommentParagraphService_;
  18 + }));
  19 +
  20 +
  21 + describe("Succesfull requests", () => {
  22 +
  23 + it("should return paragraph comments by article", (done) => {
  24 + let articleId = 1;
  25 + $httpBackend.expectGET(`/api/v1/articles/${articleId}/comment_paragraph_plugin/comments?without_reply=true`).respond(200, { comments: [{ body: "comment1" }] });
  26 + commentParagraphService.getByArticle(<noosfero.Article>{ id: articleId }).then((result: noosfero.RestResult<noosfero.Comment[]>) => {
  27 + expect(result.data).toEqual([{ body: "comment1" }]);
  28 + done();
  29 + });
  30 + $httpBackend.flush();
  31 + });
  32 +
  33 + it("should create a paragraph comment", (done) => {
  34 + let articleId = 1;
  35 + $httpBackend.expectPOST(`/api/v1/articles/${articleId}/comment_paragraph_plugin/comments`).respond(200, { comment: { body: "comment1" } });
  36 + commentParagraphService.createInArticle(<noosfero.Article>{ id: articleId }, <noosfero.Comment>{ body: "comment1" }).then((result: noosfero.RestResult<noosfero.Comment>) => {
  37 + expect(result.data).toEqual({ body: "comment1" });
  38 + done();
  39 + });
  40 + $httpBackend.flush();
  41 + });
  42 +
  43 + it("activate paragraph comments for an article", (done) => {
  44 + let articleId = 1;
  45 + $httpBackend.expectPOST(`/api/v1/articles/${articleId}/comment_paragraph_plugin/activate`).respond(200, { article: { title: "article1" } });
  46 + commentParagraphService.activateCommentParagraph(<noosfero.Article>{ id: articleId }).then((result: noosfero.RestResult<noosfero.Article>) => {
  47 + expect(result.data).toEqual({ title: "article1" });
  48 + done();
  49 + });
  50 + $httpBackend.flush();
  51 + });
  52 +
  53 + it("deactivate paragraph comments for an article", (done) => {
  54 + let articleId = 1;
  55 + $httpBackend.expectPOST(`/api/v1/articles/${articleId}/comment_paragraph_plugin/deactivate`).respond(200, { article: { title: "article1" } });
  56 + commentParagraphService.deactivateCommentParagraph(<noosfero.Article>{ id: articleId }).then((result: noosfero.RestResult<noosfero.Article>) => {
  57 + expect(result.data).toEqual({ title: "article1" });
  58 + done();
  59 + });
  60 + $httpBackend.flush();
  61 + });
  62 +
  63 + it("return counts for paragraph comments", (done) => {
  64 + let articleId = 1;
  65 + $httpBackend.expectGET(`/api/v1/articles/${articleId}/comment_paragraph_plugin/comments/count`).respond(200, { '1': 5, '2': 6 });
  66 + commentParagraphService.commentParagraphCount(<noosfero.Article>{ id: articleId }, '1').then((count: number) => {
  67 + expect(count).toEqual(5);
  68 + done();
  69 + });
  70 + $httpBackend.flush();
  71 + });
  72 +
  73 + it("reset promise when comment paragraph counts fails", (done) => {
  74 + let articleId = 1;
  75 + commentParagraphService['articleService'] = jasmine.createSpyObj("articleService", ["getElement"]);
  76 + commentParagraphService['articleService'].getElement = jasmine.createSpy("getElement").and.returnValue(
  77 + {
  78 + customGET: (path: string) => {
  79 + return Promise.reject({});
  80 + }
  81 + }
  82 + );
  83 + commentParagraphService.commentParagraphCount(<noosfero.Article>{ id: articleId }, '1').catch(() => {
  84 + expect(commentParagraphService['commentParagraphCountsPromise']).toBeNull();
  85 + done();
  86 + });
  87 + });
  88 + });
  89 + });
  90 +});
src/plugins/comment_paragraph/http/comment-paragraph.service.ts 0 → 100644
@@ -0,0 +1,64 @@ @@ -0,0 +1,64 @@
  1 +import { Injectable, Inject } from "ng-forward";
  2 +import {RestangularService} from "../../../lib/ng-noosfero-api/http/restangular_service";
  3 +import {ArticleService} from "../../../lib/ng-noosfero-api/http/article.service";
  4 +
  5 +@Injectable()
  6 +@Inject("Restangular", "$q", "$log", ArticleService)
  7 +export class CommentParagraphService extends RestangularService<noosfero.Comment> {
  8 +
  9 + private commentParagraphCountsPromise: ng.IPromise<any>;
  10 +
  11 + constructor(Restangular: restangular.IService, $q: ng.IQService, $log: ng.ILogService, protected articleService: ArticleService) {
  12 + super(Restangular, $q, $log);
  13 + }
  14 +
  15 + getResourcePath() {
  16 + return "comment_paragraph_plugin/comments";
  17 + }
  18 +
  19 + getDataKeys() {
  20 + return {
  21 + singular: 'comment',
  22 + plural: 'comments'
  23 + };
  24 + }
  25 +
  26 + getByArticle(article: noosfero.Article, params: any = {}): ng.IPromise<noosfero.RestResult<noosfero.Comment[]>> {
  27 + params['without_reply'] = true;
  28 + let articleElement = this.articleService.getElement(<number>article.id);
  29 + return this.list(articleElement, params);
  30 + }
  31 +
  32 + createInArticle(article: noosfero.Article, comment: noosfero.Comment): ng.IPromise<noosfero.RestResult<noosfero.Comment>> {
  33 + let articleElement = this.articleService.getElement(<number>article.id);
  34 + return this.create(comment, articleElement, null, { 'Content-Type': 'application/json' }, false);
  35 + }
  36 +
  37 + activateCommentParagraph(article: noosfero.Article) {
  38 + let articleElement = this.articleService.getElement(<number>article.id);
  39 + return this.articleService.post("comment_paragraph_plugin/activate", articleElement);
  40 + }
  41 +
  42 + deactivateCommentParagraph(article: noosfero.Article) {
  43 + let articleElement = this.articleService.getElement(<number>article.id);
  44 + return this.articleService.post("comment_paragraph_plugin/deactivate", articleElement);
  45 + }
  46 +
  47 + commentParagraphCount(article: noosfero.Article, paragraphUuid: string) {
  48 + return this.commentParagraphCounts(article).then((counts: any) => {
  49 + return counts[paragraphUuid];
  50 + });
  51 + }
  52 +
  53 + private commentParagraphCounts(article: noosfero.Article) {
  54 + if (!this.commentParagraphCountsPromise) {
  55 + let articleElement = this.articleService.getElement(<number>article.id);
  56 + this.commentParagraphCountsPromise = articleElement.customGET("comment_paragraph_plugin/comments/count").then((response: restangular.IResponse) => {
  57 + return response.data;
  58 + }).catch(() => {
  59 + this.commentParagraphCountsPromise = null;
  60 + });
  61 + }
  62 + return this.commentParagraphCountsPromise;
  63 + }
  64 +}
src/plugins/comment_paragraph/index.ts 0 → 100644
@@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
  1 +import {AllowCommentComponent} from "./allow-comment/allow-comment.component";
  2 +import {CommentParagraphArticleButtonHotspotComponent} from "./hotspot/comment-paragraph-article-button.component";
  3 +import {CommentParagraphFormHotspotComponent} from "./hotspot/comment-paragraph-form.component";
  4 +import {DiscussionEditorComponent} from "./article/cms/discussion-editor/discussion-editor.component";
  5 +import {CommentParagraphArticleContentHotspotComponent} from "./hotspot/article-content/article-content.component";
  6 +import {DiscussionBlockComponent} from "./block/discussion/discussion-block.component";
  7 +
  8 +export let mainComponents: any = [AllowCommentComponent, DiscussionEditorComponent, DiscussionBlockComponent];
  9 +export let hotspots: any = [CommentParagraphArticleButtonHotspotComponent, CommentParagraphFormHotspotComponent, CommentParagraphArticleContentHotspotComponent];
src/plugins/comment_paragraph/languages/en.json 0 → 100644
@@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
  1 +{
  2 + "comment-paragraph-plugin.title": "Paragraph Comments",
  3 + "comment-paragraph-plugin.discussion.editor.start_date.label": "From",
  4 + "comment-paragraph-plugin.discussion.editor.end_date.label": "To",
  5 + "comment-paragraph-plugin.discussion.header": "Open for comments"
  6 +}
src/plugins/comment_paragraph/languages/pt.json 0 → 100644
@@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
  1 +{
  2 + "comment-paragraph-plugin.title": "Comentários por Parágrafo",
  3 + "comment-paragraph-plugin.discussion.editor.start_date.label": "De",
  4 + "comment-paragraph-plugin.discussion.editor.end_date.label": "Até",
  5 + "comment-paragraph-plugin.discussion.header": "Aberto para comentários"
  6 +}
src/plugins/comment_paragraph/side-comments/side-comments.component.spec.ts 0 → 100644
@@ -0,0 +1,51 @@ @@ -0,0 +1,51 @@
  1 +import {SideCommentsComponent} from "./side-comments.component";
  2 +import {ComponentTestHelper, createClass} from '../../../spec/component-test-helper';
  3 +import * as helpers from "../../../spec/helpers";
  4 +import {Provider} from 'ng-forward';
  5 +import {ComponentFixture} from 'ng-forward/cjs/testing/test-component-builder';
  6 +
  7 +let htmlTemplate = '<comment-paragraph-side-comments [article]="ctrl.article" [paragraph-uuid]="ctrl.paragraphUuid"></comment-paragraph-side-comments>';
  8 +
  9 +describe("Components", () => {
  10 + describe("Side Comments Component", () => {
  11 +
  12 + let serviceMock = jasmine.createSpyObj("CommentParagraphService", ["getByArticle"]);
  13 + serviceMock.getByArticle = jasmine.createSpy("getByArticle").and.returnValue(Promise.resolve({ data: {} }));
  14 +
  15 + let commentServiceMock = {};
  16 + let postCommentEventService = jasmine.createSpyObj("postCommentEventService", ["emit", "subscribe"]);
  17 + postCommentEventService.subscribe = jasmine.createSpy("subscribe");
  18 +
  19 + let providers = [
  20 + new Provider('CommentParagraphService', { useValue: serviceMock }),
  21 + new Provider('CommentService', { useValue: commentServiceMock }),
  22 + new Provider('PostCommentEventService', { useValue: postCommentEventService })
  23 + ];
  24 + let helper: ComponentTestHelper<SideCommentsComponent>;
  25 +
  26 + beforeEach(angular.mock.module("templates"));
  27 +
  28 + beforeEach((done) => {
  29 + let cls = createClass({
  30 + template: htmlTemplate,
  31 + directives: [SideCommentsComponent],
  32 + providers: providers,
  33 + properties: {
  34 + paragraphUuid: "uuid",
  35 + article: {}
  36 + }
  37 + });
  38 + helper = new ComponentTestHelper<SideCommentsComponent>(cls, done);
  39 + });
  40 +
  41 + it('call service to load paragraph comments', () => {
  42 + helper.component.loadComments();
  43 + expect(serviceMock.getByArticle).toHaveBeenCalled();
  44 + });
  45 +
  46 + it('set paragraph uuid in new comment object', () => {
  47 + let comment = <any>helper.component.newComment;
  48 + expect(comment['paragraph_uuid']).toEqual('uuid');
  49 + });
  50 + });
  51 +});
src/plugins/comment_paragraph/side-comments/side-comments.component.ts 0 → 100644
@@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
  1 +import {Component, Inject, Input, Output} from "ng-forward";
  2 +import {CommentsComponent} from "../../../app/article/comment/comments.component";
  3 +import {CommentService} from "../../../lib/ng-noosfero-api/http/comment.service";
  4 +import {CommentParagraphService} from "../http/comment-paragraph.service";
  5 +
  6 +@Component({
  7 + selector: "comment-paragraph-side-comments",
  8 + templateUrl: 'app/article/comment/comments.html',
  9 +})
  10 +@Inject(CommentService, "$scope", CommentParagraphService)
  11 +export class SideCommentsComponent extends CommentsComponent {
  12 +
  13 + @Input() article: noosfero.Article;
  14 + @Input() paragraphUuid: string;
  15 +
  16 + constructor(commentService: CommentService,
  17 + $scope: ng.IScope,
  18 + private commentParagraphService: CommentParagraphService) {
  19 + super(commentService, $scope);
  20 + }
  21 +
  22 + ngOnInit() {
  23 + super.ngOnInit();
  24 + (<any>this.newComment).paragraph_uuid = this.paragraphUuid;
  25 + }
  26 +
  27 + loadComments() {
  28 + return this.commentParagraphService.getByArticle(this.article, { page: this.page, per_page: this.perPage, paragraph_uuid: this.paragraphUuid });
  29 + }
  30 +}
src/plugins/comment_paragraph/side-comments/side-comments.scss 0 → 100644
@@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
  1 +comment-paragraph-side-comments {
  2 + .comments {
  3 + min-height: 20px;
  4 + border-top: 0;
  5 + .comment {
  6 + margin: 0;
  7 + &.media {
  8 + border-top: 0;
  9 + }
  10 + .comments {
  11 + margin-left: 0;
  12 + }
  13 + }
  14 + }
  15 +}
src/plugins/index.ts 0 → 100644
@@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
  1 +import * as commentParagraph from "./comment_paragraph";
  2 +
  3 +export let mainComponents: any = [];
  4 +mainComponents = mainComponents.concat(commentParagraph.mainComponents);
  5 +
  6 +export let hotspots: any = [];
  7 +hotspots = hotspots.concat(commentParagraph.hotspots);
src/spec/component-test-helper.ts
@@ -36,7 +36,7 @@ export class ComponentTestHelper&lt;T extends any&gt; { @@ -36,7 +36,7 @@ export class ComponentTestHelper&lt;T extends any&gt; {
36 * @propertyOf spec.ComponentTestHelper 36 * @propertyOf spec.ComponentTestHelper
37 * @description 37 * @description
38 * The NgForward TestComponentBuilder 38 * The NgForward TestComponentBuilder
39 - */ 39 + */
40 tcb: TestComponentBuilder; 40 tcb: TestComponentBuilder;
41 /** 41 /**
42 * @ngdoc property 42 * @ngdoc property
@@ -69,6 +69,8 @@ export class ComponentTestHelper&lt;T extends any&gt; { @@ -69,6 +69,8 @@ export class ComponentTestHelper&lt;T extends any&gt; {
69 this.init(done); 69 this.init(done);
70 } 70 }
71 71
  72 + fixture: any;
  73 +
72 /** 74 /**
73 * @ngdoc method 75 * @ngdoc method
74 * @name init 76 * @name init
@@ -80,7 +82,8 @@ export class ComponentTestHelper&lt;T extends any&gt; { @@ -80,7 +82,8 @@ export class ComponentTestHelper&lt;T extends any&gt; {
80 let promisse = this.tcb.createAsync(this.mockComponent) as Promise<ComponentFixture>; 82 let promisse = this.tcb.createAsync(this.mockComponent) as Promise<ComponentFixture>;
81 return promisse.then((fixture: any) => { 83 return promisse.then((fixture: any) => {
82 // Fire all angular events and parsing 84 // Fire all angular events and parsing
83 - fixture.detectChanges(); 85 + this.fixture = fixture;
  86 + this.detectChanges();
84 // The main debug element 87 // The main debug element
85 this.debugElement = fixture.debugElement; 88 this.debugElement = fixture.debugElement;
86 this.component = <T>this.debugElement.componentViewChildren[0].componentInstance; 89 this.component = <T>this.debugElement.componentViewChildren[0].componentInstance;
@@ -97,6 +100,17 @@ export class ComponentTestHelper&lt;T extends any&gt; { @@ -97,6 +100,17 @@ export class ComponentTestHelper&lt;T extends any&gt; {
97 100
98 /** 101 /**
99 * @ngdoc method 102 * @ngdoc method
  103 + * @name detectChanges
  104 + * @methodOf spec.ComponentTestHelper
  105 + * @description
  106 + * Detect changes in component
  107 + */
  108 + detectChanges() {
  109 + this.fixture.detectChanges();
  110 + }
  111 +
  112 + /**
  113 + * @ngdoc method
100 * @name all 114 * @name all
101 * @methodOf spec.ComponentTestHelper 115 * @methodOf spec.ComponentTestHelper
102 * @description 116 * @description
src/spec/helpers.ts
@@ -73,6 +73,7 @@ class AngularServiceHookComponent { @@ -73,6 +73,7 @@ class AngularServiceHookComponent {
73 } 73 }
74 } 74 }
75 75
  76 +
76 /** 77 /**
77 * This helper class allows get angular services to be used in integration tests 78 * This helper class allows get angular services to be used in integration tests
78 * i.e: '$http', '$q', '$location', etc... 79 * i.e: '$http', '$q', '$location', etc...
src/spec/mocks.ts
1 const DEBUG = false; 1 const DEBUG = false;
2 2
3 -let log = (message: string, ...args: any[]) => { 3 +let log = (message: string, ...args: any[]) => {
4 if (DEBUG) { 4 if (DEBUG) {
5 console.log(message); 5 console.log(message);
6 } 6 }
@@ -30,7 +30,7 @@ class ScopeWithEvents { @@ -30,7 +30,7 @@ class ScopeWithEvents {
30 } 30 }
31 } 31 }
32 } 32 }
33 -export var mocks = { 33 +export var mocks: any = {
34 scopeWithEvents: (): ScopeWithEvents => new ScopeWithEvents(), 34 scopeWithEvents: (): ScopeWithEvents => new ScopeWithEvents(),
35 modalInstance: { 35 modalInstance: {
36 close: () => { } 36 close: () => { }
@@ -41,8 +41,38 @@ export var mocks = { @@ -41,8 +41,38 @@ export var mocks = {
41 } 41 }
42 }, 42 },
43 authService: { 43 authService: {
  44 + loginSuccess: {
  45 + event: Function,
  46 + subscribe: (fn: Function) => {
  47 + mocks.authService['loginSuccess'].event = fn;
  48 + },
  49 + next: (param: any) => {
  50 + mocks.authService['loginSuccess'].event(param);
  51 + }
  52 + },
  53 + loginFailed: {
  54 + event: Function,
  55 + subscribe: (fn: Function) => {
  56 + mocks.authService['loginFailed'].event = fn;
  57 + },
  58 + next: (param: any) => {
  59 + mocks.authService['loginFailed'].event(param);
  60 + }
  61 + },
  62 + logoutSuccess: {
  63 + event: Function,
  64 + subscribe: (fn: Function) => {
  65 + mocks.authService['logoutSuccess'].event = fn;
  66 + },
  67 + next: (param: any) => {
  68 + mocks.authService['logoutSuccess'].event(param);
  69 + }
  70 + },
44 logout: () => { }, 71 logout: () => { },
45 - login: () => { } 72 + subscribe: (eventName: string, fn: Function) => {
  73 + mocks.authService[eventName].subscribe(fn);
  74 + },
  75 + isAuthenticated: () => { }
46 }, 76 },
47 articleService: { 77 articleService: {
48 getByProfile: (profileId: number, params?: any) => { 78 getByProfile: (profileId: number, params?: any) => {
@@ -71,7 +101,9 @@ export var mocks = { @@ -71,7 +101,9 @@ export var mocks = {
71 return { 101 return {
72 then: (func?: Function) => { if (func) func(); } 102 then: (func?: Function) => { if (func) func(); }
73 }; 103 };
74 - } 104 + },
  105 + setCurrent: (article: noosfero.Article) => { },
  106 + getCurrent: () => { return Promise.resolve({}); }
75 }, 107 },
76 environmentService: { 108 environmentService: {
77 getEnvironmentPeople: (params: any) => { 109 getEnvironmentPeople: (params: any) => {
themes/participa-consulta/app/navbar.scss 0 → 100644
@@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
  1 +.navbar {
  2 + min-height: 123px;
  3 +/* background:url("../assets/images/redebrasil/bg-header.png") repeat-x;*/
  4 + background-color: #f9c404;
  5 + background-image: -moz-radial-gradient(center, ellipse cover, #fcdd4e 1%, #f9c404 100%);
  6 + background-image: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(1%, #fcdd4e), color-stop(100%, #f9c404));
  7 + background-image: -webkit-radial-gradient(center, ellipse cover, #fcdd4e 1%, #f9c404 100%);
  8 + background-image: -o-radial-gradient(center, ellipse cover, #fcdd4e 1%, #f9c404 100%);
  9 + background-image: -ms-radial-gradient(center, ellipse cover, #fcdd4e 1%, #f9c404 100%);
  10 + background-image: radial-gradient(ellipse at center, #fcdd4e 1%, #f9c404 100%);
  11 +
  12 + .container-fluid {
  13 + .navbar-brand {
  14 + .noosfero-logo {
  15 + display: none;
  16 + }
  17 + .noosfero-name {
  18 + color: #03316f;
  19 + font-size: 40px;
  20 + font-weight: 800;
  21 + line-height: 1em;
  22 + letter-spacing: -0.05em;
  23 + }
  24 + }
  25 + }
  26 +}
themes/participa-consulta/app/participa-consulta.scss 0 → 100644
@@ -0,0 +1,37 @@ @@ -0,0 +1,37 @@
  1 +body > .ng-scope {
  2 + min-height: 100%;
  3 +}
  4 +
  5 +html, body {
  6 + height:100%;
  7 +}
  8 +
  9 +.box-default{
  10 + border: none;
  11 + border-radius: 3px 3px 0 0;
  12 + font-family: "Ubuntu Mediun";
  13 + font-size: 15px;
  14 + font-variant: normal;
  15 + font-weight: normal;
  16 + line-height: 30px;
  17 + padding: 15px 15px 15px 55px;
  18 + text-transform: capitalize;
  19 +}
  20 +
  21 +.skin-whbl .membersblock > .panel-heading{
  22 + @extend .box-default;
  23 + background: #DAE1C4 url("../assets/images/participa-consulta/pessoas.png") no-repeat 15px center;
  24 + background-size: 30px 30px;
  25 + color: #4F9CAC;
  26 +}
  27 +
  28 +.skin-whbl .statisticsblock > .panel-heading{
  29 + @extend .box-default;
  30 + background: #69677C url("../assets/images/participa-consulta/indicadores.png") no-repeat 15px center;
  31 + background-size: 30px 30px;
  32 + color: #DAE1C4;
  33 +}
  34 +
  35 +.profile-header{
  36 + text-align: center;
  37 +}
themes/participa-consulta/assets/images/participa-consulta/indicadores.png 0 → 100644

1004 Bytes

themes/participa-consulta/assets/images/participa-consulta/pessoas.png 0 → 100644

1.27 KB