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 | 34 | "angular-dynamic-locale": "^0.1.30", |
35 | 35 | "angular-i18n": "^1.5.0", |
36 | 36 | "angular-load": "^0.4.1", |
37 | - "angular-translate-interpolation-messageformat": "^2.10.0" | |
37 | + "angular-translate-interpolation-messageformat": "^2.10.0", | |
38 | + "angular-bind-html-compile": "^1.2.1", | |
39 | + "angular-click-outside": "^2.7.1" | |
38 | 40 | }, |
39 | 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 | 46 | "overrides": { |
43 | 47 | "bootstrap-sass": { | ... | ... |
gulp/build.js
... | ... | @@ -6,6 +6,7 @@ var rename = require('gulp-rename'); |
6 | 6 | var insert = require('gulp-insert'); |
7 | 7 | var merge = require('merge-stream'); |
8 | 8 | var conf = require('./conf'); |
9 | +var languages = require('./languages'); | |
9 | 10 | |
10 | 11 | var themeName = conf.paths.theme.replace('-', ' '); |
11 | 12 | themeName = themeName.charAt(0).toUpperCase() + themeName.slice(1); |
... | ... | @@ -16,25 +17,31 @@ var $ = require('gulp-load-plugins')({ |
16 | 17 | }); |
17 | 18 | |
18 | 19 | gulp.task('partials', function () { |
19 | - var srcPaths = [path.join(conf.paths.tmp, '/serve/app/**/*.html')]; | |
20 | - conf.paths.allSources.forEach(function(src) { | |
21 | - srcPaths.push(path.join(src, '/app/**/*.html')); | |
20 | + var merged = merge(); | |
21 | + ['app', conf.paths.plugins].forEach(function(partialPath) { | |
22 | + var srcPaths = [path.join(conf.paths.tmp, '/serve/app/**/*.html')]; | |
23 | + conf.paths.allSources.forEach(function(src) { | |
24 | + srcPaths.push(path.join(src, partialPath, '/**/*.html')); | |
25 | + }); | |
26 | + merged.add(gulp.src(srcPaths) | |
27 | + .pipe($.minifyHtml({ | |
28 | + empty: true, | |
29 | + spare: true, | |
30 | + quotes: true | |
31 | + })) | |
32 | + .pipe($.angularTemplatecache('templateCacheHtml-'+partialPath+'.js', { | |
33 | + module: 'noosferoApp', | |
34 | + root: partialPath | |
35 | + })) | |
36 | + .pipe(gulp.dest(conf.paths.tmp + '/partials/'))); | |
22 | 37 | }); |
23 | - return gulp.src(srcPaths) | |
24 | - .pipe($.minifyHtml({ | |
25 | - empty: true, | |
26 | - spare: true, | |
27 | - quotes: true | |
28 | - })) | |
29 | - .pipe($.angularTemplatecache('templateCacheHtml.js', { | |
30 | - module: 'noosferoApp', | |
31 | - root: 'app' | |
32 | - })) | |
33 | - .pipe(gulp.dest(conf.paths.tmp + '/partials/')); | |
38 | + return merged; | |
34 | 39 | }); |
35 | 40 | |
36 | 41 | gulp.task('html', ['inject', 'partials'], function () { |
37 | - var partialsInjectFile = gulp.src(path.join(conf.paths.tmp, '/partials/templateCacheHtml.js'), { read: false }); | |
42 | + var partialsInjectFile = gulp.src([ | |
43 | + path.join(conf.paths.tmp, '/partials/templateCacheHtml-app.js'), | |
44 | + path.join(conf.paths.tmp, '/partials/templateCacheHtml-plugins.js')], { read: false }); | |
38 | 45 | var partialsInjectOptions = { |
39 | 46 | starttag: '<!-- inject:partials -->', |
40 | 47 | ignorePath: path.join(conf.paths.tmp, '/partials'), |
... | ... | @@ -73,6 +80,8 @@ gulp.task('html', ['inject', 'partials'], function () { |
73 | 80 | .pipe($.useref()) |
74 | 81 | .pipe($.revReplace({prefix: noosferoThemePrefix})) |
75 | 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 | 85 | .pipe($.minifyHtml({ |
77 | 86 | empty: true, |
78 | 87 | spare: true, |
... | ... | @@ -93,6 +102,10 @@ gulp.task('fonts', function () { |
93 | 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 | 109 | gulp.task('locale', function () { |
97 | 110 | return gulp.src([ |
98 | 111 | path.join("bower_components/angular-i18n", '*.js'), |
... | ... | @@ -124,6 +137,10 @@ gulp.task('clean-docs', [], function() { |
124 | 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 | 144 | gulp.task('noosfero', ['html'], function () { |
128 | 145 | var layouts = gulp.src('layouts/**/*') |
129 | 146 | .pipe(gulp.dest(path.join(conf.paths.dist, "layouts"))); |
... | ... | @@ -136,4 +153,4 @@ gulp.task('noosfero', ['html'], function () { |
136 | 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 | 15 | */ |
16 | 16 | exports.paths = { |
17 | 17 | src: 'src', |
18 | + plugins: 'plugins', | |
18 | 19 | dist: 'dist', |
19 | 20 | tmp: '.tmp', |
20 | 21 | e2e: 'e2e', |
21 | 22 | docs: 'docs', |
22 | - themes: 'themes' | |
23 | + themes: 'themes', | |
24 | + languages: 'languages' | |
23 | 25 | }; |
24 | 26 | exports.configTheme = function(theme) { |
25 | 27 | exports.paths.theme = theme || "angular-default"; |
... | ... | @@ -34,7 +36,7 @@ exports.configTheme(argv.theme); |
34 | 36 | * to inject css preprocessor deps and js files in karma |
35 | 37 | */ |
36 | 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 | 40 | directory: 'bower_components' |
39 | 41 | }; |
40 | 42 | ... | ... |
... | ... | @@ -0,0 +1,29 @@ |
1 | +'use strict'; | |
2 | + | |
3 | +var path = require('path'); | |
4 | +var gulp = require('gulp'); | |
5 | +var merge = require('merge-stream'); | |
6 | +var conf = require('./conf'); | |
7 | +var mergeJson = require('gulp-merge-json'); | |
8 | +var glob = require("glob"); | |
9 | + | |
10 | +exports.pluginLanguages = function(dest) { | |
11 | + var merged = merge(); | |
12 | + glob(path.join(conf.paths.src, conf.paths.languages, "*.json"), function (er, files) { | |
13 | + files.forEach(function(file) { | |
14 | + merged.add(exports.pluginLanguage(file, dest)); | |
15 | + }); | |
16 | + }); | |
17 | + return merged; | |
18 | +} | |
19 | + | |
20 | +exports.pluginLanguage = function(file, dest) { | |
21 | + var language = file.split('/').pop().replace('\.json',''); | |
22 | + return gulp.src(path.join(conf.paths.src, '**', conf.paths.languages, language+'.json')) | |
23 | + .pipe(mergeJson(path.join(conf.paths.languages, language+'.json'))) | |
24 | + .pipe(gulp.dest(dest)) | |
25 | +} | |
26 | + | |
27 | +gulp.task('serve-languages', function() { | |
28 | + return exports.pluginLanguages(path.join(conf.paths.tmp, '/serve')); | |
29 | +}); | ... | ... |
gulp/server.js
... | ... | @@ -45,7 +45,7 @@ browserSync.use(browserSyncSpa({ |
45 | 45 | selector: '[ng-app]'// Only needed for angular apps |
46 | 46 | })); |
47 | 47 | |
48 | -gulp.task('serve', ['watch'], function () { | |
48 | +gulp.task('serve', ['serve-languages', 'watch'], function () { | |
49 | 49 | var srcPaths = [path.join(conf.paths.tmp, '/serve')]; |
50 | 50 | conf.paths.allSources.reverse().forEach(function(src) { |
51 | 51 | srcPaths.push(src); | ... | ... |
gulp/styles.js
... | ... | @@ -31,6 +31,7 @@ var buildStyles = function() { |
31 | 31 | ]; |
32 | 32 | conf.paths.allSources.forEach(function(src) { |
33 | 33 | srcPaths.push(path.join(src, '/app/**/*.scss')); |
34 | + srcPaths.push(path.join(src, conf.paths.plugins, '/**/*.scss')); | |
34 | 35 | }); |
35 | 36 | var injectFiles = gulp.src(srcPaths, { read: false }); |
36 | 37 | ... | ... |
gulp/watch.js
... | ... | @@ -3,6 +3,7 @@ |
3 | 3 | var path = require('path'); |
4 | 4 | var gulp = require('gulp'); |
5 | 5 | var conf = require('./conf'); |
6 | +var languages = require('./languages'); | |
6 | 7 | |
7 | 8 | var browserSync = require('browser-sync'); |
8 | 9 | |
... | ... | @@ -16,7 +17,8 @@ gulp.task('watch', ['inject'], function () { |
16 | 17 | |
17 | 18 | gulp.watch([ |
18 | 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 | 22 | ], function(event) { |
21 | 23 | if(isOnlyChange(event)) { |
22 | 24 | gulp.start('styles-reload'); |
... | ... | @@ -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 | 42 | var watchPaths = []; |
37 | 43 | conf.paths.allSources.forEach(function(src) { |
38 | 44 | watchPaths.push(path.join(src, '/app/**/*.html')); |
45 | + watchPaths.push(path.join(src, conf.paths.plugins, '/**/*.html')); | |
39 | 46 | }); |
40 | 47 | gulp.watch(watchPaths, function(event) { |
41 | 48 | browserSync.reload(event.path); | ... | ... |
karma.conf.js
layouts/angular-layout.html.erb
1 | -<% if params[:angular_theme_old] || request.fullpath.start_with?('/myprofile') %> | |
1 | +<% if params[:angular_theme_old] %> | |
2 | 2 | <%= render file: Rails.root.join("app/views/layouts/application-ng.html.erb"), use_full_path: false %> |
3 | 3 | <% else %> |
4 | 4 | <%= from_theme_include(current_theme, "index") %> | ... | ... |
package.json
src/app/article/article-default-view.component.ts
1 | 1 | import { bundle, Input, Inject, Component, Directive } from 'ng-forward'; |
2 | 2 | import {ArticleBlogComponent} from "./types/blog/blog.component"; |
3 | 3 | import {CommentsComponent} from "./comment/comments.component"; |
4 | +import {MacroDirective} from "./macro/macro.directive"; | |
5 | +import {ArticleToolbarHotspotComponent} from "../hotspot/article-toolbar-hotspot.component"; | |
6 | +import {ArticleContentHotspotComponent} from "../hotspot/article-content-hotspot.component"; | |
4 | 7 | |
5 | 8 | /** |
6 | 9 | * @ngdoc controller |
... | ... | @@ -30,7 +33,9 @@ export class ArticleDefaultViewComponent { |
30 | 33 | @Component({ |
31 | 34 | selector: 'noosfero-article', |
32 | 35 | template: 'not-used', |
33 | - directives: [ArticleDefaultViewComponent, ArticleBlogComponent, CommentsComponent] | |
36 | + directives: [ArticleDefaultViewComponent, ArticleBlogComponent, | |
37 | + CommentsComponent, MacroDirective, ArticleToolbarHotspotComponent, | |
38 | + ArticleContentHotspotComponent] | |
34 | 39 | }) |
35 | 40 | @Inject("$element", "$scope", "$injector", "$compile") |
36 | 41 | export class ArticleViewComponent { |
... | ... | @@ -40,7 +45,8 @@ export class ArticleViewComponent { |
40 | 45 | directiveName: string; |
41 | 46 | |
42 | 47 | ngOnInit() { |
43 | - let specificDirective = 'noosfero' + this.article.type; | |
48 | + let articleType = this.article.type.replace(/::/, ''); | |
49 | + let specificDirective = 'noosfero' + articleType; | |
44 | 50 | this.directiveName = "noosfero-default-article"; |
45 | 51 | if (this.$injector.has(specificDirective + 'Directive')) { |
46 | 52 | this.directiveName = specificDirective.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); |
... | ... | @@ -53,6 +59,5 @@ export class ArticleViewComponent { |
53 | 59 | private $scope: ng.IScope, |
54 | 60 | private $injector: ng.auto.IInjectorService, |
55 | 61 | private $compile: ng.ICompileService) { |
56 | - | |
57 | 62 | } |
58 | 63 | } | ... | ... |
src/app/article/article.html
... | ... | @@ -4,6 +4,10 @@ |
4 | 4 | </div> |
5 | 5 | |
6 | 6 | <div class="sub-header clearfix"> |
7 | + <a href="#" class="btn btn-default btn-xs" ui-sref="main.cmsEdit({profile: ctrl.profile.identifier, id: ctrl.article.id})"> | |
8 | + <i class="fa fa-pencil-square-o fa-fw fa-lg"></i> {{"article.actions.edit" | translate}} | |
9 | + </a> | |
10 | + <noosfero-hotspot-article-toolbar [article]="ctrl.article"></noosfero-hotspot-article-toolbar> | |
7 | 11 | <div class="page-info pull-right small text-muted"> |
8 | 12 | <span class="time"> |
9 | 13 | <i class="fa fa-clock-o"></i> <span am-time-ago="ctrl.article.created_at | dateFormat"></span> |
... | ... | @@ -16,9 +20,9 @@ |
16 | 20 | </span> |
17 | 21 | </div> |
18 | 22 | </div> |
19 | - | |
23 | + <noosfero-hotspot-article-content [article]="ctrl.article"></noosfero-hotspot-article-content> | |
20 | 24 | <div class="page-body"> |
21 | - <div ng-bind-html="ctrl.article.body"></div> | |
25 | + <div bind-html-compile="ctrl.article.body"></div> | |
22 | 26 | </div> |
23 | 27 | |
24 | 28 | <noosfero-comments [article]="ctrl.article"></noosfero-comments> | ... | ... |
src/app/article/basic-editor.component.spec.ts
... | ... | @@ -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 | -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 | -<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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 | 9 | |
10 | 10 | @Input() comment: noosfero.CommentViewModel; |
11 | 11 | @Input() article: noosfero.Article; |
12 | + @Input() displayActions = true; | |
13 | + @Input() displayReplies = true; | |
12 | 14 | |
13 | 15 | showReply() { |
14 | 16 | return this.comment && this.comment.__show_reply === true; | ... | ... |
src/app/article/comment/comment.html
... | ... | @@ -9,13 +9,19 @@ |
9 | 9 | <a class="pull-left" ui-sref="main.profile.home({profile: ctrl.comment.author.identifier})"> |
10 | 10 | <h4 class="media-heading">{{ctrl.comment.author.name}}</h4> |
11 | 11 | </a> |
12 | + <span class="reply-of" ng-if="ctrl.comment.reply_of" uib-tooltip-template="'app/article/comment/comment-reply-tooltip.html'"> | |
13 | + <i class="fa fa-fw fa-mail-forward"></i> | |
14 | + <span class="author">{{ctrl.comment.reply_of.author.name}}</span> | |
15 | + </span> | |
12 | 16 | <span class="date" am-time-ago="ctrl.comment.created_at | dateFormat"></span> |
13 | 17 | </div> |
14 | 18 | <div class="title">{{ctrl.comment.title}}</div> |
15 | 19 | <div class="body">{{ctrl.comment.body}}</div> |
16 | - <a href="#" (click)="ctrl.reply()" class="small text-muted"> | |
17 | - {{"comment.reply" | translate}} | |
18 | - </a> | |
20 | + <div class="actions" ng-if="ctrl.displayActions"> | |
21 | + <a href="#" (click)="ctrl.reply()" class="small text-muted" ng-if="ctrl.article.accept_comments"> | |
22 | + {{"comment.reply" | translate}} | |
23 | + </a> | |
24 | + </div> | |
19 | 25 | </div> |
20 | - <noosfero-comments [show-form]="ctrl.showReply()" [article]="ctrl.article" [parent]="ctrl.comment"></noosfero-comments> | |
26 | + <noosfero-comments [show-form]="ctrl.showReply()" [article]="ctrl.article" [parent]="ctrl.comment" ng-if="ctrl.displayReplies"></noosfero-comments> | |
21 | 27 | </div> | ... | ... |
src/app/article/comment/comment.scss
... | ... | @@ -5,6 +5,7 @@ |
5 | 5 | @extend .text-muted; |
6 | 6 | @extend .small; |
7 | 7 | margin-left: 8px; |
8 | + font-size: 12px; | |
8 | 9 | } |
9 | 10 | .title { |
10 | 11 | font-weight: bold; |
... | ... | @@ -13,7 +14,18 @@ |
13 | 14 | min-width: 40px; |
14 | 15 | } |
15 | 16 | .media-body { |
16 | - padding: 0 10px 10px 10px; | |
17 | + padding: 0 10px 10px 0; | |
18 | + .reply-of { | |
19 | + font-size: 12px; | |
20 | + color: #B5B5B5; | |
21 | + margin-left: 5px; | |
22 | + i { | |
23 | + font-size: 10px; | |
24 | + } | |
25 | + } | |
26 | + h4 { | |
27 | + font-size: 16px; | |
28 | + } | |
17 | 29 | } |
18 | 30 | noosfero-profile-image { |
19 | 31 | img { |
... | ... | @@ -27,8 +39,24 @@ |
27 | 39 | font-size: 1.7em; |
28 | 40 | } |
29 | 41 | } |
42 | + // Limit identation of replies | |
30 | 43 | .comments { |
31 | 44 | margin-left: 30px; |
45 | + .comments .comments { | |
46 | + margin-left: 0px; | |
47 | + .comment { | |
48 | + margin-left: 0px; | |
49 | + } | |
50 | + } | |
51 | + } | |
52 | + .tooltip-inner { | |
53 | + max-width: 350px; | |
54 | + text-align: left; | |
55 | + .reply-tooltip { | |
56 | + .comment { | |
57 | + margin: 5px; | |
58 | + } | |
59 | + } | |
32 | 60 | } |
33 | 61 | } |
34 | 62 | } | ... | ... |
src/app/article/comment/comments.component.ts
1 | -import { Inject, Input, Output, Component, provide, EventEmitter } from 'ng-forward'; | |
2 | -import {INgForwardJQuery} from "ng-forward/cjs/util/jqlite-extensions"; | |
3 | - | |
4 | - | |
1 | +import { Inject, Input, Component, provide } from 'ng-forward'; | |
5 | 2 | import { PostCommentComponent } from "./post-comment/post-comment.component"; |
6 | 3 | import { CommentService } from "../../../lib/ng-noosfero-api/http/comment.service"; |
7 | 4 | import { CommentComponent } from "./comment.component"; |
... | ... | @@ -19,12 +16,13 @@ export class CommentsComponent { |
19 | 16 | @Input() showForm = true; |
20 | 17 | @Input() article: noosfero.Article; |
21 | 18 | @Input() parent: noosfero.CommentViewModel; |
22 | - | |
23 | 19 | protected page = 1; |
24 | 20 | protected perPage = 5; |
25 | 21 | protected total = 0; |
26 | 22 | |
27 | - constructor(protected commentService: CommentService) { } | |
23 | + newComment = <noosfero.Comment>{}; | |
24 | + | |
25 | + constructor(protected commentService: CommentService, private $scope: ng.IScope) { } | |
28 | 26 | |
29 | 27 | ngOnInit() { |
30 | 28 | if (this.parent) { |
... | ... | @@ -43,7 +41,7 @@ export class CommentsComponent { |
43 | 41 | this.comments.forEach((comment: noosfero.CommentViewModel) => { |
44 | 42 | comment.__show_reply = false; |
45 | 43 | }); |
46 | - if (this.parent) { | |
44 | + if (this.parent) { | |
47 | 45 | this.parent.__show_reply = false; |
48 | 46 | } |
49 | 47 | } | ... | ... |
src/app/article/comment/comments.html
1 | 1 | <div class="comments"> |
2 | - <noosfero-post-comment (comment-saved)="ctrl.commentAdded($event.detail)" ng-if="ctrl.showForm" [article]="ctrl.article" [parent]="ctrl.parent"></noosfero-post-comment> | |
2 | + <noosfero-post-comment (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 | 4 | <div class="comments-list"> |
5 | 5 | <noosfero-comment ng-repeat="comment in ctrl.comments | orderBy: 'created_at':true" [comment]="comment" [article]="ctrl.article"></noosfero-comment> | ... | ... |
src/app/article/comment/post-comment/post-comment.component.spec.ts
... | ... | @@ -7,6 +7,8 @@ const htmlTemplate: string = '<noosfero-post-comment [article]="ctrl.article" [r |
7 | 7 | describe("Components", () => { |
8 | 8 | describe("Post Comment Component", () => { |
9 | 9 | |
10 | + let properties = { article: { id: 1, accept_comments: true } }; | |
11 | + | |
10 | 12 | beforeEach(angular.mock.module("templates")); |
11 | 13 | |
12 | 14 | let commentService = jasmine.createSpyObj("commentService", ["createInArticle"]); |
... | ... | @@ -19,7 +21,7 @@ describe("Components", () => { |
19 | 21 | |
20 | 22 | @Component({ selector: 'test-container-component', directives: [PostCommentComponent], template: htmlTemplate, providers: providers }) |
21 | 23 | class ContainerComponent { |
22 | - article = { id: 1 }; | |
24 | + article = properties['article']; | |
23 | 25 | comment = { id: 2 }; |
24 | 26 | } |
25 | 27 | |
... | ... | @@ -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 | 43 | it("emit an event when create comment", done => { |
34 | 44 | helpers.createComponentFromClass(ContainerComponent).then(fixture => { |
35 | 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 | 2 | import { CommentService } from "../../../../lib/ng-noosfero-api/http/comment.service"; |
3 | 3 | import { NotificationService } from "../../../shared/services/notification.service"; |
4 | 4 | import { SessionService } from "../../../login"; |
5 | +import { CommentFormHotspotComponent } from "../../../hotspot/comment-form-hotspot.component"; | |
5 | 6 | |
6 | 7 | @Component({ |
7 | 8 | selector: 'noosfero-post-comment', |
8 | 9 | templateUrl: 'app/article/comment/post-comment/post-comment.html', |
9 | - outputs: ['commentSaved'] | |
10 | + outputs: ['commentSaved'], | |
11 | + directives: [CommentFormHotspotComponent] | |
10 | 12 | }) |
11 | 13 | @Inject(CommentService, NotificationService, SessionService) |
12 | 14 | export class PostCommentComponent { |
13 | 15 | |
16 | + public static EVENT_COMMENT_RECEIVED = "comment.received"; | |
17 | + | |
14 | 18 | @Input() article: noosfero.Article; |
15 | 19 | @Input() parent: noosfero.Comment; |
16 | 20 | @Output() commentSaved: EventEmitter<Comment> = new EventEmitter<Comment>(); |
17 | - | |
18 | - comment = <noosfero.Comment>{}; | |
21 | + @Input() comment = <noosfero.Comment>{}; | |
19 | 22 | private currentUser: noosfero.User; |
20 | 23 | |
21 | 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 | 2 | <div class="form-group"> |
3 | 3 | <div class="comment media"> |
4 | 4 | <div class="media-left"> |
... | ... | @@ -8,6 +8,7 @@ |
8 | 8 | </div> |
9 | 9 | <div class="media-body"> |
10 | 10 | <textarea class="form-control custom-control" rows="1" ng-model="ctrl.comment.body" placeholder="{{'comment.post.placeholder' | translate}}"></textarea> |
11 | + <noosfero-hotspot-comment-form [comment]="ctrl.comment" [parent]="ctrl.parent"></noosfero-hotspot-comment-form> | |
11 | 12 | <button ng-show="ctrl.comment.body" type="submit" class="btn btn-default pull-right ng-hide" ng-click="ctrl.save()">{{"comment.post" | translate}}</button> |
12 | 13 | </div> |
13 | 14 | </div> | ... | ... |
src/app/article/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 | 1 | import {Input, Component, provide} from 'ng-forward'; |
4 | 2 | |
5 | 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 | 5 | import {ContentViewerActionsComponent} from './content-viewer-actions.component'; |
9 | 6 | |
10 | 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 | 9 | |
13 | 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 | 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 | 54 | let profile: any = { |
49 | 55 | id: 1, |
50 | 56 | identifier: 'the-profile-test', |
51 | 57 | type: 'Person' |
52 | 58 | }; |
53 | - | |
54 | 59 | helpers.mocks.profileService.getCurrentProfile = () => { |
55 | 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 | 1 | import {Component, Inject, provide} from "ng-forward"; |
2 | 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 | 5 | @Component({ |
5 | 6 | selector: "content-viewer-actions", |
6 | 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 | 14 | export class ContentViewerActionsComponent { |
11 | 15 | |
12 | 16 | article: noosfero.Article; |
13 | 17 | profile: noosfero.Profile; |
18 | + parentId: number; | |
14 | 19 | |
15 | - constructor(profileService: ProfileService) { | |
20 | + constructor(profileService: ProfileService, articleService: ArticleService) { | |
16 | 21 | profileService.getCurrentProfile().then((profile: noosfero.Profile) => { |
17 | 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 | 28 | } |
29 | 29 | |
30 | 30 | activate() { |
31 | - this.profileService.getCurrentProfile().then((profile: noosfero.Profile) => { | |
31 | + this.profileService.getCurrentProfile().then((profile: noosfero.Profile) => { | |
32 | 32 | this.profile = profile; |
33 | 33 | return this.articleService.getArticleByProfileAndPath(this.profile, this.$stateParams["page"]); |
34 | 34 | }).then((result: noosfero.RestResult<any>) => { |
35 | 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 | 1 | <ul class="nav navbar-nav"> |
2 | 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 | 9 | <i class="fa fa-file fa-fw fa-lg"></i> {{"navbar.content_viewer_actions.new_post" | translate}} |
5 | 10 | </a> |
6 | 11 | </li> | ... | ... |
src/app/article/index.ts
... | ... | @@ -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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 | 28 | $default-bg-hover-color: #f8f8f8; |
29 | 29 | $red-color: #e84e40; |
30 | 30 | $red-color-dark: #dd191d; |
31 | +$green-color: #8bc34a; | |
32 | +$green-color-dark: #689f38; | |
31 | 33 | |
32 | 34 | //GRID - media queries breakpoints |
33 | 35 | $break-xxs-min: 420px; |
34 | 36 | $break-xs-min: 768px; |
37 | +$break-sm-min: 992px; | |
35 | 38 | |
36 | 39 | $break-xxs-max: ($break-xxs-min - 1); |
40 | +$break-sm-max: ($break-sm-min - 1); | |
37 | 41 | $break-xs-max: ($break-xs-min - 1); |
38 | 42 | |
39 | 43 | |
... | ... | @@ -76,4 +80,5 @@ h1, h2, h3, h4, h5 { |
76 | 80 | @import "layout/scss/mixins"; |
77 | 81 | @import "layout/scss/bootstrap-overrides"; |
78 | 82 | @import "layout/scss/layout"; |
83 | +@import "layout/scss/sidebar"; | |
79 | 84 | @import "layout/scss/tables"; | ... | ... |
src/app/index.ts
... | ... | @@ -5,7 +5,7 @@ import {noosferoAngularRunBlock} from "./index.run"; |
5 | 5 | import {MainComponent} from "./main/main.component"; |
6 | 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 | 9 | import {AuthController} from "./login/auth.controller"; |
10 | 10 | |
11 | 11 | import {Navbar} from "./layout/navbar/navbar"; |
... | ... | @@ -14,16 +14,17 @@ declare var moment: any; |
14 | 14 | |
15 | 15 | let noosferoApp: any = bundle("noosferoApp", MainComponent, ["ngAnimate", "ngCookies", "ngStorage", "ngTouch", |
16 | 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 | 19 | "angular-timeline", "duScroll", "oitozero.ngSweetAlert", |
20 | - "pascalprecht.translate", "tmh.dynamicLocale", "angularLoad"]).publish(); | |
20 | + "pascalprecht.translate", "tmh.dynamicLocale", "angularLoad", | |
21 | + "angular-click-outside"]).publish(); | |
21 | 22 | |
22 | 23 | NoosferoApp.angularModule = noosferoApp; |
23 | 24 | |
24 | 25 | |
25 | 26 | NoosferoApp.addConstants("moment", moment); |
26 | -NoosferoApp.addConstants("AUTH_EVENTS", AUTH_EVENTS); | |
27 | +NoosferoApp.addConstants("AuthEvents", AuthEvents); | |
27 | 28 | |
28 | 29 | NoosferoApp.addConfig(noosferoModuleConfig); |
29 | 30 | NoosferoApp.run(noosferoAngularRunBlock); | ... | ... |
src/app/layout/blocks/block.component.ts
... | ... | @@ -11,7 +11,7 @@ export class BlockComponent { |
11 | 11 | @Input() owner: any; |
12 | 12 | |
13 | 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 | 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 | -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 | -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
src/app/layout/blocks/communities/communities-block.component.spec.ts
0 → 100644
... | ... | @@ -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 @@ |
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
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 @@ |
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 @@ |
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 @@ |
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 | -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 | 0 | \ No newline at end of file |
src/app/layout/blocks/link-list/link-list.component.ts
... | ... | @@ -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 | -.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 | -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 | 0 | \ No newline at end of file |
src/app/layout/blocks/main-block/main-block.component.ts
... | ... | @@ -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 | -<div ui-view="mainBlockContent" autoscroll></div> |
... | ... | @@ -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 | 41 | \ No newline at end of file | ... | ... |
... | ... | @@ -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 @@ |
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 | -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 | 0 | \ No newline at end of file |
src/app/layout/blocks/members-block/members-block.component.ts
... | ... | @@ -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 | -.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 @@ |
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 @@ |
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 @@ |
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 | -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 | 0 | \ No newline at end of file |
src/app/layout/blocks/people-block/people-block.component.ts
... | ... | @@ -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 | -.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 @@ |
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 @@ |
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 @@ |
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 | -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 | -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 | -<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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 | -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 | -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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 | -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 | 0 | \ No newline at end of file |
src/app/layout/blocks/recent-documents/recent-documents.component.ts
... | ... | @@ -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 | -<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 | -.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 @@ |
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 @@ |
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 @@ |
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
src/app/layout/navbar/navbar.html
1 | 1 | <nav class="navbar navbar-static-top navbar-inverse"> |
2 | 2 | <div class="container-fluid"> |
3 | 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 | 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 | 7 | </button> |
10 | 8 | <a class="navbar-brand" ui-sref="main.environment.home"> |
11 | - <span class="noosfero-logo"> </span> | |
9 | + <span class="noosfero-logo"> </span> | |
12 | 10 | <span class="noosfero-name">{{"noosfero.name" | translate}}</span> |
13 | 11 | </a> |
14 | 12 | </div> | ... | ... |
src/app/layout/navbar/navbar.scss
1 | 1 | .navbar { |
2 | 2 | |
3 | + margin-bottom: 0px; | |
4 | + | |
3 | 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 | 7 | .navbar-brand { |
12 | 8 | .noosfero-logo { |
13 | - background:url("../assets/images/logo-noosfero.png") no-repeat; | |
9 | + background: url("../assets/images/logo-noosfero.png") no-repeat; | |
14 | 10 | padding: 0px 62px 64px 15px; |
15 | 11 | } |
16 | 12 | } |
... | ... | @@ -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 | 1 | import * as helpers from "./../../../spec/helpers"; |
2 | 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 | 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 | 12 | describe("Components", () => { |
12 | 13 | |
... | ... | @@ -53,22 +54,19 @@ describe("Components", () => { |
53 | 54 | provide('$uibModal', { |
54 | 55 | useValue: $modal |
55 | 56 | }), |
56 | - provide('AuthService', { | |
57 | + provide(AuthService, { | |
57 | 58 | useValue: authService |
58 | 59 | }), |
59 | 60 | helpers.provideEmptyObjects('moment'), |
60 | 61 | provide('$state', { |
61 | 62 | useValue: stateService |
62 | 63 | }), |
63 | - provide("$scope", { | |
64 | - useValue: scope | |
65 | - }), | |
66 | 64 | provide('SessionService', { |
67 | 65 | useValue: sessionService |
68 | 66 | }), |
69 | - provide('AUTH_EVENTS', { | |
67 | + provide('AuthEvents', { | |
70 | 68 | useValue: { |
71 | - AUTH_EVENTS | |
69 | + AuthEvents | |
72 | 70 | } |
73 | 71 | }), |
74 | 72 | provide('TranslatorService', { |
... | ... | @@ -146,7 +144,7 @@ describe("Components", () => { |
146 | 144 | |
147 | 145 | |
148 | 146 | it('closes the modal after login', (done: Function) => { |
149 | - modalInstance = jasmine.createSpyObj("modalInstance", ["close"]); | |
147 | + modalInstance = {}; | |
150 | 148 | modalInstance.close = jasmine.createSpy("close"); |
151 | 149 | |
152 | 150 | $modal.open = () => { |
... | ... | @@ -155,19 +153,21 @@ describe("Components", () => { |
155 | 153 | |
156 | 154 | buildComponent().then((fixture: ComponentFixture) => { |
157 | 155 | let navbarComp: Navbar = <Navbar>fixture.debugElement.componentViewChildren[0].componentInstance; |
158 | - let localScope: ng.IScope = navbarComp["$scope"]; | |
159 | 156 | |
160 | 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 | 162 | expect(modalInstance.close).toHaveBeenCalled(); |
163 | 163 | done(); |
164 | 164 | }); |
165 | + | |
165 | 166 | }); |
166 | 167 | |
167 | 168 | it('updates current user on logout', (done: Function) => { |
168 | 169 | buildComponent().then((fixture: ComponentFixture) => { |
169 | 170 | let navbarComp: Navbar = <Navbar>fixture.debugElement.componentViewChildren[0].componentInstance; |
170 | - let localScope: ng.IScope = navbarComp["$scope"]; | |
171 | 171 | |
172 | 172 | // init navbar currentUser with some user |
173 | 173 | navbarComp["currentUser"] = user; |
... | ... | @@ -176,12 +176,14 @@ describe("Components", () => { |
176 | 176 | // and emmit the 'logoutSuccess' event |
177 | 177 | // just what happens when user logsout |
178 | 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 | 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 | 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 | 7 | @Component({ |
8 | 8 | selector: "acme-navbar", |
9 | 9 | templateUrl: "app/layout/navbar/navbar.html", |
10 | 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 | 14 | export class Navbar { |
15 | 15 | |
16 | 16 | private currentUser: noosfero.User; |
17 | 17 | private modalInstance: any = null; |
18 | + | |
19 | + public showHamburguer: boolean = false; | |
20 | + | |
18 | 21 | /** |
19 | 22 | * |
20 | 23 | */ |
21 | 24 | constructor( |
22 | 25 | private $uibModal: any, |
23 | - private authService: AuthService, | |
26 | + public authService: AuthService, | |
24 | 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 | 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 | 38 | if (this.modalInstance) { |
32 | 39 | this.modalInstance.close(); |
33 | 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 | 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 | 61 | openLogin() { |
... | ... | @@ -55,8 +72,6 @@ export class Navbar { |
55 | 72 | this.$state.go(this.$state.current, {}, { reload: true }); // TODO move to auth |
56 | 73 | }; |
57 | 74 | |
58 | - | |
59 | - | |
60 | 75 | activate() { |
61 | 76 | if (!this.currentUser) { |
62 | 77 | this.openLogin(); | ... | ... |
src/app/layout/scss/_bootstrap-overrides.scss
... | ... | @@ -54,3 +54,19 @@ |
54 | 54 | .tabs-wrapper.tabs-no-header .tab-content { |
55 | 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 @@ |
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 @@ |
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 @@ |
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 | 1 | import * as helpers from '../../../spec/helpers'; |
2 | 2 | import {BodyStateClassesService} from "./body-state-classes.service"; |
3 | 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 | 9 | describe("BodyStateClasses Service", () => { |
10 | + | |
7 | 11 | let currentStateName = "main"; |
8 | 12 | let bodyStateClasseService: BodyStateClassesService; |
9 | 13 | let $rootScope: ng.IRootScopeService = <any>{}, |
... | ... | @@ -13,17 +17,15 @@ describe("BodyStateClasses Service", () => { |
13 | 17 | name: currentStateName |
14 | 18 | } |
15 | 19 | }, |
16 | - authService: AuthService, | |
20 | + authService: any = helpers.mocks.authService, | |
17 | 21 | bodyEl: { className: string }, |
18 | 22 | bodyElJq: any; |
19 | 23 | |
20 | - | |
21 | 24 | let getService = (): BodyStateClassesService => { |
22 | 25 | return new BodyStateClassesService($rootScope, $document, $state, authService); |
23 | 26 | }; |
24 | 27 | |
25 | 28 | beforeEach(() => { |
26 | - authService = <any>{}; | |
27 | 29 | authService.isAuthenticated = jasmine.createSpy("isAuthenticated").and.returnValue(true); |
28 | 30 | bodyEl = { className: "" }; |
29 | 31 | bodyElJq = [bodyEl]; |
... | ... | @@ -58,8 +60,9 @@ describe("BodyStateClasses Service", () => { |
58 | 60 | |
59 | 61 | it("should capture loginSuccess event and add noosfero-user-logged class to the body element", () => { |
60 | 62 | let userLoggedClassName = BodyStateClassesService.USER_LOGGED_CLASSNAME; |
61 | - $rootScope = <any>helpers.mocks.scopeWithEvents(); | |
63 | + | |
62 | 64 | bodyElJq.addClass = jasmine.createSpy("addClass"); |
65 | + bodyElJq.removeClass = jasmine.createSpy("removeClass"); | |
63 | 66 | authService.isAuthenticated = jasmine.createSpy("isAuthenticated").and.returnValue(false); |
64 | 67 | let service = getService(); |
65 | 68 | |
... | ... | @@ -68,11 +71,11 @@ describe("BodyStateClasses Service", () => { |
68 | 71 | // triggers the service start |
69 | 72 | service.start(); |
70 | 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 | 75 | expect(bodyElJq.addClass).not.toHaveBeenCalledWith(userLoggedClassName); |
73 | 76 | |
74 | 77 | // emit the event loginSuccess |
75 | - $rootScope.$emit(AUTH_EVENTS.loginSuccess); | |
78 | + authService.loginSuccess.next(null); | |
76 | 79 | |
77 | 80 | // and check now if the addClass was called passing the userLogged class |
78 | 81 | // to the body Element |
... | ... | @@ -83,7 +86,6 @@ describe("BodyStateClasses Service", () => { |
83 | 86 | let userLoggedClassName = BodyStateClassesService.USER_LOGGED_CLASSNAME; |
84 | 87 | |
85 | 88 | authService.isAuthenticated = jasmine.createSpy("isAuthenticated").and.returnValue(true); |
86 | - $rootScope = <any>helpers.mocks.scopeWithEvents(); | |
87 | 89 | |
88 | 90 | bodyElJq.addClass = jasmine.createSpy("addClass"); |
89 | 91 | bodyElJq.removeClass = jasmine.createSpy("removeClass"); |
... | ... | @@ -95,11 +97,11 @@ describe("BodyStateClasses Service", () => { |
95 | 97 | service.start(); |
96 | 98 | |
97 | 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 | 101 | expect(bodyElJq.addClass).toHaveBeenCalledWith(userLoggedClassName); |
100 | 102 | |
101 | 103 | // emit the event logoutSuccess |
102 | - $rootScope.$emit(AUTH_EVENTS.logoutSuccess); | |
104 | + authService.logoutSuccess.next(null); | |
103 | 105 | |
104 | 106 | // and check now if the removeClass was called passing the userLogged class |
105 | 107 | // to the body Element |
... | ... | @@ -126,9 +128,44 @@ describe("BodyStateClasses Service", () => { |
126 | 128 | expect(bodyEl.className).toEqual(BodyStateClassesService.ROUTE_STATE_CLASSNAME_PREFIX + currentStateName); |
127 | 129 | |
128 | 130 | // emit the event $stateChangeSuccess |
129 | - $rootScope.$emit("$stateChangeSuccess", null, {name: "new-route"}); | |
131 | + $rootScope.$emit("$stateChangeSuccess", null, { name: "new-route" }); | |
130 | 132 | |
131 | 133 | // and check now if the bodyEl has a class indicating the new state route |
132 | 134 | expect(bodyEl.className).toEqual(BodyStateClassesService.ROUTE_STATE_CLASSNAME_PREFIX + "new-route"); |
133 | 135 | }); |
134 | -}); | |
135 | 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 | 1 | import {Directive, Inject, Injectable} from "ng-forward"; |
2 | -import {AUTH_EVENTS} from "./../../login/auth-events"; | |
2 | +import {AuthEvents} from "./../../login/auth-events"; | |
3 | 3 | import {AuthService} from "./../../login/auth.service"; |
4 | 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 | 12 | * This is a service which adds classes to the body element |
... | ... | @@ -9,18 +14,23 @@ import {HtmlUtils} from "../html-utils"; |
9 | 14 | * eg: |
10 | 15 | * User Logged: |
11 | 16 | * - noosfero-user-logged |
12 | - * Route States: | |
17 | + * Route States: | |
13 | 18 | * - noosfero-route-main |
14 | 19 | * - noosfero-route-main.profile.info |
20 | + * | |
21 | + * Show the all content in full mode: | |
22 | + * - full-content | |
15 | 23 | */ |
16 | 24 | @Injectable() |
17 | 25 | @Inject("$rootScope", "$document", "$state", AuthService) |
18 | 26 | export class BodyStateClassesService { |
19 | 27 | |
20 | 28 | private started: boolean = false; |
29 | + private skin: string; | |
21 | 30 | |
22 | 31 | public static get USER_LOGGED_CLASSNAME(): string { return "noosfero-user-logged"; } |
23 | 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 | 35 | private bodyElement: ng.IAugmentedJQuery = null; |
26 | 36 | |
... | ... | @@ -33,14 +43,37 @@ export class BodyStateClassesService { |
33 | 43 | |
34 | 44 | } |
35 | 45 | |
36 | - start() { | |
46 | + start(config?: StartParams) { | |
37 | 47 | if (!this.started) { |
38 | 48 | this.setupUserLoggedClassToggle(); |
39 | 49 | this.setupStateClassToggle(); |
50 | + | |
51 | + if (config) { | |
52 | + this.setThemeSkin(config.skin); | |
53 | + } | |
40 | 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 | 77 | private getStateChangeSuccessHandlerFunction(bodyElement: ng.IAugmentedJQuery): (event: ng.IAngularEvent, toState: ng.ui.IState) => void { |
45 | 78 | let self = this; |
46 | 79 | return (event: ng.IAngularEvent, toState: ng.ui.IState) => { |
... | ... | @@ -65,7 +98,7 @@ export class BodyStateClassesService { |
65 | 98 | |
66 | 99 | /** |
67 | 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 | 103 | private setupUserLoggedClassToggle() { |
71 | 104 | let bodyElement = this.getBodyElement(); |
... | ... | @@ -76,18 +109,18 @@ export class BodyStateClassesService { |
76 | 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 | 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 | 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 | 125 | private getBodyElement(): ng.IAugmentedJQuery { |
93 | 126 | if (this.bodyElement === null) { |
... | ... | @@ -95,4 +128,9 @@ export class BodyStateClassesService { |
95 | 128 | } |
96 | 129 | return this.bodyElement; |
97 | 130 | } |
98 | -} | |
99 | 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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 | 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 | 9 | describe("Services", () => { |
4 | 10 | |
... | ... | @@ -15,23 +21,26 @@ describe("Services", () => { |
15 | 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 | 26 | user = <noosfero.User>{ |
24 | 27 | id: 1, |
25 | 28 | login: "user" |
26 | 29 | }; |
27 | - })); | |
28 | - | |
30 | + }); | |
29 | 31 | |
30 | 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 | 39 | beforeEach(() => { |
33 | 40 | credentials = { username: "user", password: "password" }; |
34 | - | |
41 | + factory = getAngularServiceFactory(); | |
42 | + authService = factory.getAngularService("AuthService"); | |
43 | + $httpBackend = factory.getHttpBackendService(); | |
35 | 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 | 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 | 59 | expect(userThroughEvent).toEqual(user); |
60 | + done(); | |
56 | 61 | }); |
57 | - | |
62 | + authService.login(credentials); | |
58 | 63 | $httpBackend.flush(); |
59 | 64 | |
60 | - expect(eventEmmited).toBeTruthy(AUTH_EVENTS.loginSuccess + " was not emmited!"); | |
61 | 65 | }); |
62 | 66 | |
63 | 67 | it("should return the current logged in user", () => { |
... | ... | @@ -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 | 3 | import {NoosferoRootScope, UserResponse} from "./../shared/models/interfaces"; |
4 | 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 | 8 | @Injectable() |
9 | -@Inject("$q", "$http", "$rootScope", "SessionService", "$log", "AUTH_EVENTS") | |
9 | +@Inject("$http", SessionService, "$log") | |
10 | 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 | 21 | loginFromCookie() { |
... | ... | @@ -27,8 +27,8 @@ export class AuthService { |
27 | 27 | private loginSuccessCallback(response: ng.IHttpPromiseCallbackArg<UserResponse>) { |
28 | 28 | this.$log.debug('AuthService.login [SUCCESS] response', response); |
29 | 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 | 32 | return currentUser; |
33 | 33 | } |
34 | 34 | |
... | ... | @@ -40,15 +40,15 @@ export class AuthService { |
40 | 40 | |
41 | 41 | private loginFailedCallback(response: ng.IHttpPromiseCallbackArg<any>): any { |
42 | 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 | 44 | return null; |
46 | 45 | } |
47 | 46 | |
48 | 47 | public logout() { |
48 | + let user: noosfero.User = this.sessionService.currentUser(); | |
49 | 49 | this.sessionService.destroy(); |
50 | - this.$rootScope.currentUser = undefined; | |
51 | - this.$rootScope.$broadcast(this.auth_events.logoutSuccess); | |
50 | + | |
51 | + this.logoutSuccess.next(user); | |
52 | 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 | 66 | } |
67 | 67 | return (this.isAuthenticated() && authorizedRoles.indexOf(this.sessionService.currentUser().userRole) !== -1); |
68 | 68 | } |
69 | -} | |
70 | 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 | 2 | import {bundle, Component, StateConfig, Inject} from "ng-forward"; |
2 | 3 | import {ArticleBlogComponent} from "./../article/types/blog/blog.component"; |
3 | 4 | |
... | ... | @@ -8,15 +9,18 @@ import {BoxesComponent} from "../layout/boxes/boxes.component"; |
8 | 9 | import {BlockComponent} from "../layout/blocks/block.component"; |
9 | 10 | import {EnvironmentComponent} from "../environment/environment.component"; |
10 | 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 | 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 | 24 | import {NoosferoTemplate} from "../shared/pipes/noosfero-template.filter"; |
21 | 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 | 33 | |
30 | 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 | 56 | }) |
50 | 57 | @Inject(BodyStateClassesService) |
51 | 58 | export class MainContentComponent { |
59 | + | |
60 | + public themeSkin: string = 'skin-whbl'; | |
61 | + | |
52 | 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 | 83 | * NoosferoTemplate, DateFormat, RawHTMLBlock |
72 | 84 | * @description |
73 | 85 | * The Main controller for the Noosfero Angular Theme application. |
74 | - * | |
86 | + * | |
75 | 87 | * The main route '/' is defined as the URL for this controller, which routes |
76 | 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 | 92 | @Component({ |
81 | 93 | selector: 'main', |
82 | 94 | template: '<div ng-view></div>', |
83 | 95 | directives: [ |
84 | 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 | 104 | providers: [AuthService, SessionService, NotificationService, BodyStateClassesService] |
91 | 105 | }) |
92 | 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 | 2 | <img ng-if="ctrl.profile.image" ng-src="{{ctrl.profile.image.url}}" class="img-responsive profile-image"> |
3 | 3 | <i ng-if="!ctrl.profile.image" class="fa {{ctrl.defaultIcon}} fa-5x profile-image"></i> |
4 | 4 | </span> | ... | ... |
src/app/profile/profile.component.ts
1 | 1 | import {StateConfig, Component, Inject, provide} from 'ng-forward'; |
2 | 2 | import {ProfileInfoComponent} from './info/profile-info.component'; |
3 | 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 | 6 | import {ContentViewerComponent} from "../article/content-viewer/content-viewer.component"; |
6 | 7 | import {ContentViewerActionsComponent} from "../article/content-viewer/content-viewer-actions.component"; |
7 | 8 | import {ActivitiesComponent} from "./activities/activities.component"; |
... | ... | @@ -45,13 +46,25 @@ import {MyProfileComponent} from "./myprofile.component"; |
45 | 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 | 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 | 68 | controllerAs: "vm" |
56 | 69 | } |
57 | 70 | } |
... | ... | @@ -98,7 +111,7 @@ export class ProfileComponent { |
98 | 111 | }).then((response: restangular.IResponse) => { |
99 | 112 | this.boxes = response.data.boxes; |
100 | 113 | }).catch(() => { |
101 | - $state.transitionTo('main'); | |
114 | + $state.transitionTo('main.environment.home'); | |
102 | 115 | notificationService.error({ message: "notification.profile.not_found" }); |
103 | 116 | }); |
104 | 117 | } | ... | ... |
src/app/profile/profile.html
1 | 1 | <div class="profile-container"> |
2 | + <div class="profile-header" ng-bind-html="vm.profile.custom_header"></div> | |
2 | 3 | <div class="row"> |
3 | 4 | <noosfero-boxes [boxes]="vm.boxes" [owner]="vm.profile"></noosfero-boxes> |
4 | 5 | </div> |
6 | + <div class="profile-footer" ng-bind-html="vm.profile.custom_footer"></div> | |
5 | 7 | </div> | ... | ... |
src/app/shared/components/html-editor/html-editor.component.spec.ts
0 → 100644
... | ... | @@ -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 @@ |
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 @@ |
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 | 25 | |
26 | 26 | httpError(status: number, data: any): boolean { |
27 | 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 | 31 | success({ | ... | ... |
src/index.html
... | ... | @@ -27,6 +27,10 @@ |
27 | 27 | |
28 | 28 | <div ui-view></div> |
29 | 29 | |
30 | + <script> | |
31 | + CKEDITOR_BASEPATH='/bower_components/ng-ckeditor/libs/ckeditor/'; | |
32 | + </script> | |
33 | + | |
30 | 34 | <!-- build:js(src) scripts/vendor.js --> |
31 | 35 | <!-- bower:js --> |
32 | 36 | <!-- run `gulp inject` to automatically populate bower script dependencies --> |
... | ... | @@ -42,5 +46,7 @@ |
42 | 46 | <!-- endinject --> |
43 | 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 | 51 | </body> |
46 | 52 | </html> | ... | ... |
src/languages/en.json
... | ... | @@ -35,5 +35,24 @@ |
35 | 35 | "comment.pagination.more": "More", |
36 | 36 | "comment.post.success.title": "Good job!", |
37 | 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 | 35 | "comment.pagination.more": "Mais", |
36 | 36 | "comment.post.success.title": "Bom trabalho!", |
37 | 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 | 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 | 43 | createInProfile(profile: noosfero.Profile, article: noosfero.Article): ng.IPromise<noosfero.RestResult<noosfero.Article>> { |
27 | 44 | let profileElement = this.profileService.get(<number>profile.id); |
... | ... | @@ -32,6 +49,14 @@ export class ArticleService extends RestangularService<noosfero.Article> { |
32 | 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 | 61 | getAsCollectionChildrenOf<C>(rootElement: noosfero.Environment | noosfero.Article | noosfero.Profile, path: string, queryParams?: any, headers?: any): restangular.ICollectionPromise<C> { |
37 | 62 | return rootElement.getList<C>(path, queryParams, headers); |
... | ... | @@ -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 | 207 | $httpBackend.flush(); |
208 | 208 | }); |
209 | 209 | |
210 | -}); | |
211 | 210 | \ No newline at end of file |
211 | + it("post('customPath', rootObject) calls POST /rootObjects/1/customPath", (done) => { | |
212 | + let rootObj: RootObjectModel = rootObjectRestService.getElement(1); | |
213 | + $httpBackend.expectPOST("/api/v1/rootObjects/1/customPath").respond(201, { object: { attr: 1, rootId: 1 } }); | |
214 | + objectRestService.post('customPath', rootObj).then((result: noosfero.RestResult<ObjectModel>) => { | |
215 | + expect(result.data).toBeDefined(); | |
216 | + expect((<any>result.data).attr).toEqual(1); | |
217 | + expect((<any>result.data).rootId).toEqual(1); | |
218 | + done(); | |
219 | + }); | |
220 | + $httpBackend.flush(); | |
221 | + }); | |
222 | + | |
223 | + it("post('customPath') calls POST /objects/customPath", (done) => { | |
224 | + $httpBackend.expectPOST("/api/v1/objects/customPath").respond(201, { object: { attr: 1, rootId: 1 } }); | |
225 | + objectRestService.post('customPath').then((result: noosfero.RestResult<ObjectModel>) => { | |
226 | + expect(result.data).toBeDefined(); | |
227 | + expect((<any>result.data).attr).toEqual(1); | |
228 | + expect((<any>result.data).rootId).toEqual(1); | |
229 | + done(); | |
230 | + }); | |
231 | + $httpBackend.flush(); | |
232 | + }); | |
233 | + | |
234 | +}); | ... | ... |
src/lib/ng-noosfero-api/http/restangular_service.ts
... | ... | @@ -11,6 +11,8 @@ |
11 | 11 | export abstract class RestangularService<T extends noosfero.RestModel> { |
12 | 12 | |
13 | 13 | private baseResource: restangular.IElement; |
14 | + private currentPromise: ng.IDeferred<T>; | |
15 | + | |
14 | 16 | /** |
15 | 17 | * Creates an instance of RestangularService. |
16 | 18 | * |
... | ... | @@ -20,6 +22,7 @@ export abstract class RestangularService<T extends noosfero.RestModel> { |
20 | 22 | */ |
21 | 23 | constructor(protected restangularService: restangular.IService, protected $q: ng.IQService, protected $log: ng.ILogService) { |
22 | 24 | this.baseResource = restangularService.all(this.getResourcePath()); |
25 | + this.resetCurrent(); | |
23 | 26 | // TODO |
24 | 27 | // this.restangularService.setResponseInterceptor((data, operation, what, url, response, deferred) => { |
25 | 28 | // let transformedData: any = data; |
... | ... | @@ -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 | 50 | protected extractData(response: restangular.IResponse): noosfero.RestResult<T> { |
36 | 51 | let dataKey: string; |
37 | 52 | if (response.data && this.getDataKeys()) { |
... | ... | @@ -221,7 +236,7 @@ export abstract class RestangularService<T extends noosfero.RestModel> { |
221 | 236 | * Creates a new Resource into the resource collection |
222 | 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 | 240 | let deferred = this.$q.defer<noosfero.RestResult<T>>(); |
226 | 241 | |
227 | 242 | let restRequest: ng.IPromise<noosfero.RestResult<T>>; |
... | ... | @@ -233,8 +248,9 @@ export abstract class RestangularService<T extends noosfero.RestModel> { |
233 | 248 | data = obj; |
234 | 249 | } |
235 | 250 | |
251 | + let subpath = path || this.getResourcePath(); | |
236 | 252 | if (rootElement) { |
237 | - restRequest = rootElement.all(this.getResourcePath()).post(data, queryParams, headers); | |
253 | + restRequest = rootElement.all(subpath).post(data, queryParams, headers); | |
238 | 254 | } else { |
239 | 255 | restRequest = this.baseResource.post(data, queryParams, headers); |
240 | 256 | } |
... | ... | @@ -245,6 +261,22 @@ export abstract class RestangularService<T extends noosfero.RestModel> { |
245 | 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 | 281 | * Returns a Restangular IElement representing the |
250 | 282 | */ | ... | ... |
src/lib/ng-noosfero-api/interfaces/article.ts
1 | 1 | |
2 | 2 | namespace noosfero { |
3 | - export interface Article extends RestModel { | |
3 | + export interface Article extends RestModel { | |
4 | 4 | path: string; |
5 | 5 | profile: Profile; |
6 | 6 | type: string; |
7 | + parent: Article; | |
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 | 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 @@ |
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 @@ |
1 | +import {Component, Input, Inject} from "ng-forward"; | |
2 | +import {SideCommentsComponent} from "../side-comments/side-comments.component"; | |
3 | +import {CommentParagraphEventService} from "../events/comment-paragraph-event.service"; | |
4 | +import {CommentParagraphService} from "../http/comment-paragraph.service"; | |
5 | + | |
6 | +@Component({ | |
7 | + selector: "comment-paragraph-plugin-allow-comment", | |
8 | + templateUrl: "plugins/comment_paragraph/allow-comment/allow-comment.html", | |
9 | + directives: [SideCommentsComponent] | |
10 | +}) | |
11 | +@Inject("$scope", CommentParagraphEventService, CommentParagraphService) | |
12 | +export class AllowCommentComponent { | |
13 | + | |
14 | + @Input() content: string; | |
15 | + @Input() paragraphUuid: string; | |
16 | + @Input() article: noosfero.Article; | |
17 | + commentsCount: number; | |
18 | + display = false; | |
19 | + | |
20 | + constructor(private $scope: ng.IScope, | |
21 | + private commentParagraphEventService: CommentParagraphEventService, | |
22 | + private commentParagraphService: CommentParagraphService) { } | |
23 | + | |
24 | + ngOnInit() { | |
25 | + this.commentParagraphEventService.subscribeToggleCommentParagraph((article: noosfero.Article) => { | |
26 | + this.article = article; | |
27 | + this.$scope.$apply(); | |
28 | + }); | |
29 | + this.commentParagraphService.commentParagraphCount(this.article, this.paragraphUuid).then((count: number) => { | |
30 | + this.commentsCount = count; | |
31 | + }); | |
32 | + } | |
33 | + | |
34 | + isActivated() { | |
35 | + return this.article && this.article.setting && this.article.setting.comment_paragraph_plugin_activate; | |
36 | + } | |
37 | + | |
38 | + showParagraphComments() { | |
39 | + this.display = true; | |
40 | + } | |
41 | + | |
42 | + hideParagraphComments() { | |
43 | + this.display = false; | |
44 | + } | |
45 | +} | ... | ... |
src/plugins/comment_paragraph/allow-comment/allow-comment.html
0 → 100644
... | ... | @@ -0,0 +1,12 @@ |
1 | +<div class="paragraph" ng-class="{'active' : ctrl.display}"> | |
2 | + <div class="paragraph-content" ng-bind-html="ctrl.content" ng-class="{'active' : ctrl.display}"></div> | |
3 | + <div ng-if="ctrl.isActivated()" class="actions"> | |
4 | + <a href="#" popover-placement="right-top" popover-trigger="none" | |
5 | + uib-popover-template="'plugins/comment_paragraph/allow-comment/popover.html'" | |
6 | + (click)="ctrl.showParagraphComments()" popover-is-open="ctrl.display"> | |
7 | + <div class="arrow_box" ng-class="{'active' : ctrl.display}"> | |
8 | + <span class="count">{{ctrl.commentsCount > 0 ? ctrl.commentsCount : '+'}}</span> | |
9 | + </div> | |
10 | + </a> | |
11 | + </div> | |
12 | +</div> | ... | ... |
src/plugins/comment_paragraph/allow-comment/allow-comment.scss
0 → 100644
... | ... | @@ -0,0 +1,62 @@ |
1 | +$balloon-selected-color: #50BF68; | |
2 | +$balloon-color: #c4c4c4; | |
3 | + | |
4 | +comment-paragraph-plugin-allow-comment { | |
5 | + .paragraph { | |
6 | + width: 100%; | |
7 | + &.active { | |
8 | + width: 80%; | |
9 | + } | |
10 | + .popover { | |
11 | + &.right > .arrow { | |
12 | + } | |
13 | + } | |
14 | + .paragraph-content { | |
15 | + width: 95%; | |
16 | + display: inline-block; | |
17 | + } | |
18 | + .actions { | |
19 | + width: 3%; | |
20 | + display: inline-block; | |
21 | + vertical-align: top; | |
22 | + margin-left: 10px; | |
23 | + .popover { | |
24 | + width: 100%; | |
25 | + max-width: 330px; | |
26 | + } | |
27 | + .count { | |
28 | + font-size: 14px; | |
29 | + font-weight: bold; | |
30 | + color: white; | |
31 | + text-align: center; | |
32 | + width: 100%; | |
33 | + display: inline-block; | |
34 | + } | |
35 | + .arrow_box { | |
36 | + position: relative; | |
37 | + background: $balloon-color; | |
38 | + margin-top: 5px; | |
39 | + width: 25px; | |
40 | + border-radius: 2px; | |
41 | + &:after { | |
42 | + top: 100%; | |
43 | + left: 50%; | |
44 | + border: solid transparent; | |
45 | + content: " "; | |
46 | + position: absolute; | |
47 | + pointer-events: none; | |
48 | + border-color: rgba(196, 196, 196, 0); | |
49 | + border-top-color: $balloon-color; | |
50 | + border-width: 6px; | |
51 | + margin-left: -6px; | |
52 | + } | |
53 | + &:hover, &.active { | |
54 | + background: $balloon-selected-color; | |
55 | + &:after { | |
56 | + border-top-color: $balloon-selected-color; | |
57 | + } | |
58 | + } | |
59 | + } | |
60 | + } | |
61 | + } | |
62 | +} | ... | ... |
src/plugins/comment_paragraph/allow-comment/popover.html
0 → 100644
... | ... | @@ -0,0 +1 @@ |
1 | +<comment-paragraph-side-comments id="side-comments-{{ctrl.paragraphUuid}}" click-outside="ctrl.hideParagraphComments()" [article]="ctrl.article" [paragraph-uuid]="ctrl.paragraphUuid"></comment-paragraph-side-comments> | ... | ... |
src/plugins/comment_paragraph/article/cms/discussion-editor/discussion-editor.component.ts
0 → 100644
... | ... | @@ -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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 @@ |
1 | +import {CommentParagraphEventService} from "./comment-paragraph-event.service"; | |
2 | +import {ComponentTestHelper, createClass} from '../../../spec/component-test-helper'; | |
3 | +import * as helpers from "../../../spec/helpers"; | |
4 | +import {Provider} from 'ng-forward'; | |
5 | +import {ComponentFixture} from 'ng-forward/cjs/testing/test-component-builder'; | |
6 | + | |
7 | +describe("Services", () => { | |
8 | + describe("Comment Paragraph Event Service", () => { | |
9 | + let eventService: CommentParagraphEventService; | |
10 | + | |
11 | + beforeEach(() => { | |
12 | + eventService = new CommentParagraphEventService(); | |
13 | + eventService['toggleCommentParagraphEmitter'] = jasmine.createSpyObj("toggleCommentParagraphEmitter", ["next", "subscribe"]); | |
14 | + }); | |
15 | + | |
16 | + it('subscribe to toggle comment paragraph event', () => { | |
17 | + eventService['toggleCommentParagraphEmitter'].subscribe = jasmine.createSpy("subscribe"); | |
18 | + eventService.subscribeToggleCommentParagraph(() => { }); | |
19 | + expect(eventService['toggleCommentParagraphEmitter'].subscribe).toHaveBeenCalled(); | |
20 | + }); | |
21 | + | |
22 | + it('emit event when toggle comment paragraph', () => { | |
23 | + eventService['toggleCommentParagraphEmitter'].subscribe = jasmine.createSpy("next"); | |
24 | + eventService.toggleCommentParagraph(<noosfero.Article>{}); | |
25 | + expect(eventService['toggleCommentParagraphEmitter'].next).toHaveBeenCalled(); | |
26 | + }); | |
27 | + }); | |
28 | +}); | ... | ... |
src/plugins/comment_paragraph/events/comment-paragraph-event.service.ts
0 → 100644
... | ... | @@ -0,0 +1,19 @@ |
1 | +import {Injectable, EventEmitter} from "ng-forward"; | |
2 | + | |
3 | +@Injectable() | |
4 | +export class CommentParagraphEventService { | |
5 | + | |
6 | + private toggleCommentParagraphEmitter: EventEmitter<noosfero.Article>; | |
7 | + | |
8 | + constructor() { | |
9 | + this.toggleCommentParagraphEmitter = new EventEmitter(); | |
10 | + } | |
11 | + | |
12 | + toggleCommentParagraph(article: noosfero.Article) { | |
13 | + this.toggleCommentParagraphEmitter.next(article); | |
14 | + } | |
15 | + | |
16 | + subscribeToggleCommentParagraph(fn: (article: noosfero.Article) => void) { | |
17 | + this.toggleCommentParagraphEmitter.subscribe(fn); | |
18 | + } | |
19 | +} | ... | ... |
src/plugins/comment_paragraph/hotspot/article-content/article-content.component.ts
0 → 100644
... | ... | @@ -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 @@ |
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 @@ |
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 @@ |
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 @@ |
1 | +import { Input, Inject, Component } from "ng-forward"; | |
2 | +import {Hotspot} from "../../../app/hotspot/hotspot.decorator"; | |
3 | +import {CommentParagraphService} from "../http/comment-paragraph.service"; | |
4 | +import {CommentParagraphEventService} from "../events/comment-paragraph-event.service"; | |
5 | + | |
6 | +@Component({ | |
7 | + selector: "comment-paragraph-article-button-hotspot", | |
8 | + templateUrl: "plugins/comment_paragraph/hotspot/comment-paragraph-article-button.html", | |
9 | +}) | |
10 | +@Inject("$scope", CommentParagraphService, CommentParagraphEventService) | |
11 | +@Hotspot("article_extra_toolbar_buttons") | |
12 | +export class CommentParagraphArticleButtonHotspotComponent { | |
13 | + | |
14 | + @Input() article: noosfero.Article; | |
15 | + | |
16 | + constructor(private $scope: ng.IScope, | |
17 | + private commentParagraphService: CommentParagraphService, | |
18 | + private commentParagraphEventService: CommentParagraphEventService) { } | |
19 | + | |
20 | + deactivateCommentParagraph() { | |
21 | + this.toggleCommentParagraph(this.commentParagraphService.deactivateCommentParagraph(this.article)); | |
22 | + } | |
23 | + | |
24 | + activateCommentParagraph() { | |
25 | + this.toggleCommentParagraph(this.commentParagraphService.activateCommentParagraph(this.article)); | |
26 | + } | |
27 | + | |
28 | + isActivated() { | |
29 | + return this.article && this.article.setting && this.article.setting.comment_paragraph_plugin_activate; | |
30 | + } | |
31 | + | |
32 | + private toggleCommentParagraph(promise: ng.IPromise<noosfero.RestResult<noosfero.Article>>) { | |
33 | + promise.then((result: noosfero.RestResult<noosfero.Article>) => { | |
34 | + this.article = result.data; | |
35 | + this.commentParagraphEventService.toggleCommentParagraph(this.article); | |
36 | + }); | |
37 | + } | |
38 | +} | ... | ... |
src/plugins/comment_paragraph/hotspot/comment-paragraph-article-button.html
0 → 100644
... | ... | @@ -0,0 +1,8 @@ |
1 | +<a href='#' class="btn btn-default btn-xs comment-paragraph-activate" (click)="ctrl.activateCommentParagraph()" | |
2 | + ng-if="!ctrl.article.setting.comment_paragraph_plugin_activate"> | |
3 | + <i class="fa fa-fw fa-plus"></i> {{"comment-paragraph-plugin.title" | translate}} | |
4 | +</a> | |
5 | +<a href='#' class="btn btn-default btn-xs comment-paragraph-deactivate" (click)="ctrl.deactivateCommentParagraph()" | |
6 | + ng-if="ctrl.article.setting.comment_paragraph_plugin_activate"> | |
7 | + <i class="fa fa-fw fa-minus"></i> {{"comment-paragraph-plugin.title" | translate}} | |
8 | +</a> | ... | ... |
src/plugins/comment_paragraph/hotspot/comment-paragraph-form.component.ts
0 → 100644
... | ... | @@ -0,0 +1,26 @@ |
1 | +import { Inject, Input, Component } from "ng-forward"; | |
2 | +import {Hotspot} from "../../../app/hotspot/hotspot.decorator"; | |
3 | + | |
4 | +@Component({ | |
5 | + selector: "comment-paragraph-form-hotspot", | |
6 | + template: "<span></span>", | |
7 | +}) | |
8 | +@Hotspot("comment_form_extra_contents") | |
9 | +@Inject("$scope") | |
10 | +export class CommentParagraphFormHotspotComponent { | |
11 | + | |
12 | + @Input() comment: noosfero.Comment; | |
13 | + @Input() parent: noosfero.Comment; | |
14 | + | |
15 | + constructor(private $scope: ng.IScope) { } | |
16 | + | |
17 | + ngOnInit() { | |
18 | + this.$scope.$watch(() => { | |
19 | + return this.parent; | |
20 | + }, () => { | |
21 | + if (this.parent && (<any>this.parent).paragraph_uuid) { | |
22 | + (<any>this.comment).paragraph_uuid = (<any>this.parent).paragraph_uuid; | |
23 | + } | |
24 | + }); | |
25 | + } | |
26 | +} | ... | ... |
src/plugins/comment_paragraph/hotspot/comment-paragraph-from.component.spec.ts
0 → 100644
... | ... | @@ -0,0 +1,33 @@ |
1 | +import {CommentParagraphFormHotspotComponent} from "./comment-paragraph-form.component"; | |
2 | +import {ComponentTestHelper, createClass} from '../../../spec/component-test-helper'; | |
3 | +import * as helpers from "../../../spec/helpers"; | |
4 | +import {ComponentFixture} from 'ng-forward/cjs/testing/test-component-builder'; | |
5 | + | |
6 | +let htmlTemplate = '<comment-paragraph-form-hotspot [comment]="ctrl.comment" [parent]="ctrl.parent"></comment-paragraph-form-hotspot>'; | |
7 | + | |
8 | +describe("Components", () => { | |
9 | + describe("Comment Paragraph Form Hotspot Component", () => { | |
10 | + | |
11 | + let helper: ComponentTestHelper<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 @@ |
1 | +import {CommentParagraphService} from "./comment-paragraph.service"; | |
2 | + | |
3 | + | |
4 | +describe("Services", () => { | |
5 | + | |
6 | + describe("Comment Paragraph Service", () => { | |
7 | + | |
8 | + let $httpBackend: ng.IHttpBackendService; | |
9 | + let commentParagraphService: CommentParagraphService; | |
10 | + | |
11 | + beforeEach(angular.mock.module("noosferoApp", ($translateProvider: angular.translate.ITranslateProvider) => { | |
12 | + $translateProvider.translations('en', {}); | |
13 | + })); | |
14 | + | |
15 | + beforeEach(inject((_$httpBackend_: ng.IHttpBackendService, _CommentParagraphService_: CommentParagraphService) => { | |
16 | + $httpBackend = _$httpBackend_; | |
17 | + commentParagraphService = _CommentParagraphService_; | |
18 | + })); | |
19 | + | |
20 | + | |
21 | + describe("Succesfull requests", () => { | |
22 | + | |
23 | + it("should return paragraph comments by article", (done) => { | |
24 | + let articleId = 1; | |
25 | + $httpBackend.expectGET(`/api/v1/articles/${articleId}/comment_paragraph_plugin/comments?without_reply=true`).respond(200, { comments: [{ body: "comment1" }] }); | |
26 | + commentParagraphService.getByArticle(<noosfero.Article>{ id: articleId }).then((result: noosfero.RestResult<noosfero.Comment[]>) => { | |
27 | + expect(result.data).toEqual([{ body: "comment1" }]); | |
28 | + done(); | |
29 | + }); | |
30 | + $httpBackend.flush(); | |
31 | + }); | |
32 | + | |
33 | + it("should create a paragraph comment", (done) => { | |
34 | + let articleId = 1; | |
35 | + $httpBackend.expectPOST(`/api/v1/articles/${articleId}/comment_paragraph_plugin/comments`).respond(200, { comment: { body: "comment1" } }); | |
36 | + commentParagraphService.createInArticle(<noosfero.Article>{ id: articleId }, <noosfero.Comment>{ body: "comment1" }).then((result: noosfero.RestResult<noosfero.Comment>) => { | |
37 | + expect(result.data).toEqual({ body: "comment1" }); | |
38 | + done(); | |
39 | + }); | |
40 | + $httpBackend.flush(); | |
41 | + }); | |
42 | + | |
43 | + it("activate paragraph comments for an article", (done) => { | |
44 | + let articleId = 1; | |
45 | + $httpBackend.expectPOST(`/api/v1/articles/${articleId}/comment_paragraph_plugin/activate`).respond(200, { article: { title: "article1" } }); | |
46 | + commentParagraphService.activateCommentParagraph(<noosfero.Article>{ id: articleId }).then((result: noosfero.RestResult<noosfero.Article>) => { | |
47 | + expect(result.data).toEqual({ title: "article1" }); | |
48 | + done(); | |
49 | + }); | |
50 | + $httpBackend.flush(); | |
51 | + }); | |
52 | + | |
53 | + it("deactivate paragraph comments for an article", (done) => { | |
54 | + let articleId = 1; | |
55 | + $httpBackend.expectPOST(`/api/v1/articles/${articleId}/comment_paragraph_plugin/deactivate`).respond(200, { article: { title: "article1" } }); | |
56 | + commentParagraphService.deactivateCommentParagraph(<noosfero.Article>{ id: articleId }).then((result: noosfero.RestResult<noosfero.Article>) => { | |
57 | + expect(result.data).toEqual({ title: "article1" }); | |
58 | + done(); | |
59 | + }); | |
60 | + $httpBackend.flush(); | |
61 | + }); | |
62 | + | |
63 | + it("return counts for paragraph comments", (done) => { | |
64 | + let articleId = 1; | |
65 | + $httpBackend.expectGET(`/api/v1/articles/${articleId}/comment_paragraph_plugin/comments/count`).respond(200, { '1': 5, '2': 6 }); | |
66 | + commentParagraphService.commentParagraphCount(<noosfero.Article>{ id: articleId }, '1').then((count: number) => { | |
67 | + expect(count).toEqual(5); | |
68 | + done(); | |
69 | + }); | |
70 | + $httpBackend.flush(); | |
71 | + }); | |
72 | + | |
73 | + it("reset promise when comment paragraph counts fails", (done) => { | |
74 | + let articleId = 1; | |
75 | + commentParagraphService['articleService'] = jasmine.createSpyObj("articleService", ["getElement"]); | |
76 | + commentParagraphService['articleService'].getElement = jasmine.createSpy("getElement").and.returnValue( | |
77 | + { | |
78 | + customGET: (path: string) => { | |
79 | + return Promise.reject({}); | |
80 | + } | |
81 | + } | |
82 | + ); | |
83 | + commentParagraphService.commentParagraphCount(<noosfero.Article>{ id: articleId }, '1').catch(() => { | |
84 | + expect(commentParagraphService['commentParagraphCountsPromise']).toBeNull(); | |
85 | + done(); | |
86 | + }); | |
87 | + }); | |
88 | + }); | |
89 | + }); | |
90 | +}); | ... | ... |
src/plugins/comment_paragraph/http/comment-paragraph.service.ts
0 → 100644
... | ... | @@ -0,0 +1,64 @@ |
1 | +import { Injectable, Inject } from "ng-forward"; | |
2 | +import {RestangularService} from "../../../lib/ng-noosfero-api/http/restangular_service"; | |
3 | +import {ArticleService} from "../../../lib/ng-noosfero-api/http/article.service"; | |
4 | + | |
5 | +@Injectable() | |
6 | +@Inject("Restangular", "$q", "$log", ArticleService) | |
7 | +export class CommentParagraphService extends RestangularService<noosfero.Comment> { | |
8 | + | |
9 | + private commentParagraphCountsPromise: ng.IPromise<any>; | |
10 | + | |
11 | + constructor(Restangular: restangular.IService, $q: ng.IQService, $log: ng.ILogService, protected articleService: ArticleService) { | |
12 | + super(Restangular, $q, $log); | |
13 | + } | |
14 | + | |
15 | + getResourcePath() { | |
16 | + return "comment_paragraph_plugin/comments"; | |
17 | + } | |
18 | + | |
19 | + getDataKeys() { | |
20 | + return { | |
21 | + singular: 'comment', | |
22 | + plural: 'comments' | |
23 | + }; | |
24 | + } | |
25 | + | |
26 | + getByArticle(article: noosfero.Article, params: any = {}): ng.IPromise<noosfero.RestResult<noosfero.Comment[]>> { | |
27 | + params['without_reply'] = true; | |
28 | + let articleElement = this.articleService.getElement(<number>article.id); | |
29 | + return this.list(articleElement, params); | |
30 | + } | |
31 | + | |
32 | + createInArticle(article: noosfero.Article, comment: noosfero.Comment): ng.IPromise<noosfero.RestResult<noosfero.Comment>> { | |
33 | + let articleElement = this.articleService.getElement(<number>article.id); | |
34 | + return this.create(comment, articleElement, null, { 'Content-Type': 'application/json' }, false); | |
35 | + } | |
36 | + | |
37 | + activateCommentParagraph(article: noosfero.Article) { | |
38 | + let articleElement = this.articleService.getElement(<number>article.id); | |
39 | + return this.articleService.post("comment_paragraph_plugin/activate", articleElement); | |
40 | + } | |
41 | + | |
42 | + deactivateCommentParagraph(article: noosfero.Article) { | |
43 | + let articleElement = this.articleService.getElement(<number>article.id); | |
44 | + return this.articleService.post("comment_paragraph_plugin/deactivate", articleElement); | |
45 | + } | |
46 | + | |
47 | + commentParagraphCount(article: noosfero.Article, paragraphUuid: string) { | |
48 | + return this.commentParagraphCounts(article).then((counts: any) => { | |
49 | + return counts[paragraphUuid]; | |
50 | + }); | |
51 | + } | |
52 | + | |
53 | + private commentParagraphCounts(article: noosfero.Article) { | |
54 | + if (!this.commentParagraphCountsPromise) { | |
55 | + let articleElement = this.articleService.getElement(<number>article.id); | |
56 | + this.commentParagraphCountsPromise = articleElement.customGET("comment_paragraph_plugin/comments/count").then((response: restangular.IResponse) => { | |
57 | + return response.data; | |
58 | + }).catch(() => { | |
59 | + this.commentParagraphCountsPromise = null; | |
60 | + }); | |
61 | + } | |
62 | + return this.commentParagraphCountsPromise; | |
63 | + } | |
64 | +} | ... | ... |
... | ... | @@ -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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 | 36 | * @propertyOf spec.ComponentTestHelper |
37 | 37 | * @description |
38 | 38 | * The NgForward TestComponentBuilder |
39 | - */ | |
39 | + */ | |
40 | 40 | tcb: TestComponentBuilder; |
41 | 41 | /** |
42 | 42 | * @ngdoc property |
... | ... | @@ -69,6 +69,8 @@ export class ComponentTestHelper<T extends any> { |
69 | 69 | this.init(done); |
70 | 70 | } |
71 | 71 | |
72 | + fixture: any; | |
73 | + | |
72 | 74 | /** |
73 | 75 | * @ngdoc method |
74 | 76 | * @name init |
... | ... | @@ -80,7 +82,8 @@ export class ComponentTestHelper<T extends any> { |
80 | 82 | let promisse = this.tcb.createAsync(this.mockComponent) as Promise<ComponentFixture>; |
81 | 83 | return promisse.then((fixture: any) => { |
82 | 84 | // Fire all angular events and parsing |
83 | - fixture.detectChanges(); | |
85 | + this.fixture = fixture; | |
86 | + this.detectChanges(); | |
84 | 87 | // The main debug element |
85 | 88 | this.debugElement = fixture.debugElement; |
86 | 89 | this.component = <T>this.debugElement.componentViewChildren[0].componentInstance; |
... | ... | @@ -97,6 +100,17 @@ export class ComponentTestHelper<T extends any> { |
97 | 100 | |
98 | 101 | /** |
99 | 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 | 114 | * @name all |
101 | 115 | * @methodOf spec.ComponentTestHelper |
102 | 116 | * @description | ... | ... |
src/spec/helpers.ts
src/spec/mocks.ts
1 | 1 | const DEBUG = false; |
2 | 2 | |
3 | -let log = (message: string, ...args: any[]) => { | |
3 | +let log = (message: string, ...args: any[]) => { | |
4 | 4 | if (DEBUG) { |
5 | 5 | console.log(message); |
6 | 6 | } |
... | ... | @@ -30,7 +30,7 @@ class ScopeWithEvents { |
30 | 30 | } |
31 | 31 | } |
32 | 32 | } |
33 | -export var mocks = { | |
33 | +export var mocks: any = { | |
34 | 34 | scopeWithEvents: (): ScopeWithEvents => new ScopeWithEvents(), |
35 | 35 | modalInstance: { |
36 | 36 | close: () => { } |
... | ... | @@ -41,8 +41,38 @@ export var mocks = { |
41 | 41 | } |
42 | 42 | }, |
43 | 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 | 71 | logout: () => { }, |
45 | - login: () => { } | |
72 | + subscribe: (eventName: string, fn: Function) => { | |
73 | + mocks.authService[eventName].subscribe(fn); | |
74 | + }, | |
75 | + isAuthenticated: () => { } | |
46 | 76 | }, |
47 | 77 | articleService: { |
48 | 78 | getByProfile: (profileId: number, params?: any) => { |
... | ... | @@ -71,7 +101,9 @@ export var mocks = { |
71 | 101 | return { |
72 | 102 | then: (func?: Function) => { if (func) func(); } |
73 | 103 | }; |
74 | - } | |
104 | + }, | |
105 | + setCurrent: (article: noosfero.Article) => { }, | |
106 | + getCurrent: () => { return Promise.resolve({}); } | |
75 | 107 | }, |
76 | 108 | environmentService: { |
77 | 109 | getEnvironmentPeople: (params: any) => { | ... | ... |
... | ... | @@ -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 @@ |
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