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