Commit 49c1ee2611f73c70c3f6873b49c2fa9d369d8af2
Exists in
master
and in
26 other branches
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
bower.json
@@ -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": { |
gulp/build.js
@@ -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('html', ['inject', 'partials'], function () { | @@ -73,6 +80,8 @@ gulp.task('html', ['inject', 'partials'], 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('fonts', function () { | @@ -93,6 +102,10 @@ gulp.task('fonts', 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('clean-docs', [], function() { | @@ -124,6 +137,10 @@ gulp.task('clean-docs', [], 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('noosfero', ['html'], function () { | @@ -136,4 +153,4 @@ gulp.task('noosfero', ['html'], 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']); |
gulp/conf.js
@@ -15,11 +15,13 @@ var path = require('path'); | @@ -15,11 +15,13 @@ var path = require('path'); | ||
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 |
@@ -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 |
gulp/watch.js
@@ -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('watch', ['inject'], function () { | @@ -16,7 +17,8 @@ gulp.task('watch', ['inject'], 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('watch', ['inject'], function () { | @@ -33,9 +35,14 @@ gulp.task('watch', ['inject'], 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); |
karma.conf.js
@@ -49,7 +49,7 @@ var _ = require('lodash'); | @@ -49,7 +49,7 @@ var _ = require('lodash'); | ||
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") %> |
package.json
@@ -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 | +} |
@@ -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-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 | +} |
@@ -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> |
@@ -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 | +}); |
@@ -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 | +} |
@@ -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/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/post-comment/post-comment.component.spec.ts
@@ -7,6 +7,8 @@ const htmlTemplate: string = '<noosfero-post-comment [article]="ctrl.article" [r | @@ -7,6 +7,8 @@ const htmlTemplate: string = '<noosfero-post-comment [article]="ctrl.article" [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("Components", () => { | @@ -19,7 +21,7 @@ describe("Components", () => { | ||
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("Components", () => { | @@ -30,6 +32,14 @@ describe("Components", () => { | ||
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 'ng-forward'; | @@ -2,20 +2,23 @@ import { Inject, Input, Output, EventEmitter, Component } from 'ng-forward'; | ||
2 | import { CommentService } from "../../../../lib/ng-noosfero-api/http/comment.service"; | 2 | import { CommentService } from "../../../../lib/ng-noosfero-api/http/comment.service"; |
3 | import { NotificationService } from "../../../shared/services/notification.service"; | 3 | import { NotificationService } from "../../../shared/services/notification.service"; |
4 | import { SessionService } from "../../../login"; | 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
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 = '<content-viewer-actions [article]="ctrl.article" [ | @@ -12,56 +9,57 @@ const htmlTemplate: string = '<content-viewer-actions [article]="ctrl.article" [ | ||
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"; |
@@ -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 | +}); |
@@ -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, '"'); | ||
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 | +} |
@@ -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 | +} |
@@ -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 | +} |
@@ -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 | +} |
@@ -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 "./index.run"; | @@ -5,7 +5,7 @@ import {noosferoAngularRunBlock} from "./index.run"; | ||
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
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
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/link-list/index.ts
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 | +} |
@@ -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
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
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> |
@@ -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 |
@@ -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 | +} |
@@ -0,0 +1 @@ | @@ -0,0 +1 @@ | ||
1 | +<div ui-view="mainBlockContent" autoscroll></div> |
src/app/layout/blocks/members-block/index.ts
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
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/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 | +} |
@@ -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
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
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/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 | +}); |
@@ -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 | +} |
@@ -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
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
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
src/app/layout/blocks/raw-html/index.ts
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.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
src/app/layout/blocks/recent-documents/index.ts
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/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 | +} |
@@ -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/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("Components", () => { | @@ -53,22 +54,19 @@ describe("Components", () => { | ||
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("Components", () => { | @@ -146,7 +144,7 @@ describe("Components", () => { | ||
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("Components", () => { | @@ -155,19 +153,21 @@ describe("Components", () => { | ||
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("Components", () => { | @@ -176,12 +176,14 @@ describe("Components", () => { | ||
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 | +} |
@@ -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 | +} |
@@ -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 | +} |
@@ -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("BodyStateClasses Service", () => { | @@ -13,17 +17,15 @@ describe("BodyStateClasses Service", () => { | ||
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("BodyStateClasses Service", () => { | @@ -58,8 +60,9 @@ describe("BodyStateClasses Service", () => { | ||
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("BodyStateClasses Service", () => { | @@ -68,11 +71,11 @@ describe("BodyStateClasses Service", () => { | ||
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("BodyStateClasses Service", () => { | @@ -83,7 +86,6 @@ describe("BodyStateClasses Service", () => { | ||
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("BodyStateClasses Service", () => { | @@ -95,11 +97,11 @@ describe("BodyStateClasses Service", () => { | ||
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("BodyStateClasses Service", () => { | @@ -126,9 +128,44 @@ describe("BodyStateClasses Service", () => { | ||
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 "../html-utils"; | @@ -9,18 +14,23 @@ import {HtmlUtils} from "../html-utils"; | ||
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 | +} |
@@ -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 | +} |
@@ -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> |
@@ -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 | +}); |
@@ -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 | +} |
@@ -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> |
@@ -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("Services", () => { | @@ -15,23 +21,26 @@ describe("Services", () => { | ||
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("Services", () => { | @@ -44,20 +53,15 @@ describe("Services", () => { | ||
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("Services", () => { | @@ -74,6 +78,5 @@ describe("Services", () => { | ||
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 "../layout/boxes/boxes.component"; | @@ -8,15 +9,18 @@ import {BoxesComponent} from "../layout/boxes/boxes.component"; | ||
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 "./../layout/services/body-state-classes.s | @@ -29,7 +33,10 @@ import {BodyStateClassesService} from "./../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 "../layout/blocks/main-block/main-block.compone | @@ -49,8 +56,13 @@ import {MainBlockComponent} from "../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
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 "./myprofile.component"; | @@ -45,13 +46,25 @@ import {MyProfileComponent} from "./myprofile.component"; | ||
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 | +} |
@@ -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<noosfero.Article> { | @@ -22,6 +22,23 @@ export class ArticleService extends RestangularService<noosfero.Article> { | ||
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<noosfero.Article> { | @@ -32,6 +49,14 @@ export class ArticleService extends RestangularService<noosfero.Article> { | ||
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<noosfero.Article> { | @@ -77,5 +102,4 @@ export class ArticleService extends RestangularService<noosfero.Article> { | ||
77 | } | 102 | } |
78 | 103 | ||
79 | 104 | ||
80 | - | ||
81 | } | 105 | } |
src/lib/ng-noosfero-api/http/restangular_service.spec.ts
@@ -207,4 +207,27 @@ describe("Restangular Service - base Class", () => { | @@ -207,4 +207,27 @@ describe("Restangular Service - base Class", () => { | ||
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<T extends noosfero.RestModel> { | @@ -20,6 +22,7 @@ export abstract class RestangularService<T extends noosfero.RestModel> { | ||
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<T extends noosfero.RestModel> { | @@ -32,6 +35,18 @@ export abstract class RestangularService<T extends noosfero.RestModel> { | ||
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<T extends noosfero.RestModel> { | @@ -221,7 +236,7 @@ export abstract class RestangularService<T extends noosfero.RestModel> { | ||
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<T extends noosfero.RestModel> { | @@ -233,8 +248,9 @@ export abstract class RestangularService<T extends noosfero.RestModel> { | ||
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<T extends noosfero.RestModel> { | @@ -245,6 +261,22 @@ export abstract class RestangularService<T extends noosfero.RestModel> { | ||
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/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 | +} |
@@ -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]; |
@@ -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 | +} |
@@ -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,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<T extends any> { | @@ -36,7 +36,7 @@ export class ComponentTestHelper<T extends any> { | ||
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<T extends any> { | @@ -69,6 +69,8 @@ export class ComponentTestHelper<T extends any> { | ||
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<T extends any> { | @@ -80,7 +82,8 @@ export class ComponentTestHelper<T extends any> { | ||
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<T extends any> { | @@ -97,6 +100,17 @@ export class ComponentTestHelper<T extends any> { | ||
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) => { |
@@ -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 | +} |
@@ -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