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 | 34 | "angular-dynamic-locale": "^0.1.30", |
35 | 35 | "angular-i18n": "^1.5.0", |
36 | 36 | "angular-load": "^0.4.1", |
37 | - "angular-translate-interpolation-messageformat": "^2.10.0" | |
37 | + "angular-translate-interpolation-messageformat": "^2.10.0", | |
38 | + "angular-bind-html-compile": "^1.2.1", | |
39 | + "angular-click-outside": "^2.7.1", | |
40 | + "ng-ckeditor": "^0.2.1" | |
38 | 41 | }, |
39 | 42 | "devDependencies": { |
40 | 43 | "angular-mocks": "~1.5.0" |
41 | 44 | }, |
42 | 45 | "overrides": { |
46 | + "ng-ckeditor": { | |
47 | + "main": [ | |
48 | + "ng-ckeditor.js", | |
49 | + "libs/ckeditor/ckeditor.js" | |
50 | + ] | |
51 | + }, | |
43 | 52 | "bootstrap-sass": { |
44 | 53 | "main": [ |
45 | 54 | "assets/stylesheets/_bootstrap.scss", | ... | ... |
gulp/build.js
... | ... | @@ -6,6 +6,7 @@ var rename = require('gulp-rename'); |
6 | 6 | var insert = require('gulp-insert'); |
7 | 7 | var merge = require('merge-stream'); |
8 | 8 | var conf = require('./conf'); |
9 | +var languages = require('./languages'); | |
9 | 10 | |
10 | 11 | var themeName = conf.paths.theme.replace('-', ' '); |
11 | 12 | themeName = themeName.charAt(0).toUpperCase() + themeName.slice(1); |
... | ... | @@ -16,25 +17,32 @@ var $ = require('gulp-load-plugins')({ |
16 | 17 | }); |
17 | 18 | |
18 | 19 | gulp.task('partials', function () { |
19 | - var srcPaths = [path.join(conf.paths.tmp, '/serve/app/**/*.html')]; | |
20 | - conf.paths.allSources.forEach(function(src) { | |
21 | - srcPaths.push(path.join(src, '/app/**/*.html')); | |
20 | + var merged = merge(); | |
21 | + ['app', conf.paths.plugins].forEach(function(partialPath) { | |
22 | + var srcPaths = [path.join(conf.paths.tmp, '/serve/app/**/*.html')]; | |
23 | + conf.paths.allSources.forEach(function(src) { | |
24 | + srcPaths.push(path.join(src, partialPath, '/**/*.html')); | |
25 | + }); | |
26 | + merged.add(gulp.src(srcPaths) | |
27 | + .pipe($.minifyHtml({ | |
28 | + empty: true, | |
29 | + spare: true, | |
30 | + quotes: true | |
31 | + })) | |
32 | + .pipe($.angularTemplatecache('templateCacheHtml-'+partialPath+'.js', { | |
33 | + module: '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 | 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 | 46 | var partialsInjectOptions = { |
39 | 47 | starttag: '<!-- inject:partials -->', |
40 | 48 | ignorePath: path.join(conf.paths.tmp, '/partials'), |
... | ... | @@ -73,6 +81,7 @@ gulp.task('html', ['inject', 'partials'], function () { |
73 | 81 | .pipe($.useref()) |
74 | 82 | .pipe($.revReplace({prefix: noosferoThemePrefix})) |
75 | 83 | .pipe(htmlFilter) |
84 | + .pipe($.replace('/bower_components/ng-ckeditor/libs/ckeditor/', noosferoThemePrefix + 'ng-ckeditor/libs/ckeditor/')) | |
76 | 85 | .pipe($.minifyHtml({ |
77 | 86 | empty: true, |
78 | 87 | spare: true, |
... | ... | @@ -93,6 +102,11 @@ gulp.task('fonts', function () { |
93 | 102 | .pipe(gulp.dest(path.join(conf.paths.dist, '/fonts/'))); |
94 | 103 | }); |
95 | 104 | |
105 | +gulp.task('ckeditor', function () { | |
106 | + 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 | 110 | gulp.task('locale', function () { |
97 | 111 | return gulp.src([ |
98 | 112 | path.join("bower_components/angular-i18n", '*.js'), |
... | ... | @@ -124,6 +138,10 @@ gulp.task('clean-docs', [], function() { |
124 | 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 | 145 | gulp.task('noosfero', ['html'], function () { |
128 | 146 | var layouts = gulp.src('layouts/**/*') |
129 | 147 | .pipe(gulp.dest(path.join(conf.paths.dist, "layouts"))); |
... | ... | @@ -136,4 +154,4 @@ gulp.task('noosfero', ['html'], function () { |
136 | 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 | 15 | */ |
16 | 16 | exports.paths = { |
17 | 17 | src: 'src', |
18 | + plugins: 'plugins', | |
18 | 19 | dist: 'dist', |
19 | 20 | tmp: '.tmp', |
20 | 21 | e2e: 'e2e', |
21 | 22 | docs: 'docs', |
22 | - themes: 'themes' | |
23 | + themes: 'themes', | |
24 | + languages: 'languages' | |
23 | 25 | }; |
24 | 26 | exports.configTheme = function(theme) { |
25 | 27 | exports.paths.theme = theme || "angular-default"; | ... | ... |
... | ... | @@ -0,0 +1,29 @@ |
1 | +'use strict'; | |
2 | + | |
3 | +var path = require('path'); | |
4 | +var gulp = require('gulp'); | |
5 | +var merge = require('merge-stream'); | |
6 | +var conf = require('./conf'); | |
7 | +var mergeJson = require('gulp-merge-json'); | |
8 | +var glob = require("glob"); | |
9 | + | |
10 | +exports.pluginLanguages = function(dest) { | |
11 | + var merged = merge(); | |
12 | + glob(path.join(conf.paths.src, conf.paths.languages, "*.json"), function (er, files) { | |
13 | + files.forEach(function(file) { | |
14 | + merged.add(exports.pluginLanguage(file, dest)); | |
15 | + }); | |
16 | + }); | |
17 | + return merged; | |
18 | +} | |
19 | + | |
20 | +exports.pluginLanguage = function(file, dest) { | |
21 | + var language = file.split('/').pop().replace('\.json',''); | |
22 | + return gulp.src(path.join(conf.paths.src, '**', conf.paths.languages, language+'.json')) | |
23 | + .pipe(mergeJson(path.join(conf.paths.languages, language+'.json'))) | |
24 | + .pipe(gulp.dest(dest)) | |
25 | +} | |
26 | + | |
27 | +gulp.task('serve-languages', function() { | |
28 | + return exports.pluginLanguages(path.join(conf.paths.tmp, '/serve')); | |
29 | +}); | ... | ... |
gulp/server.js
... | ... | @@ -45,7 +45,7 @@ browserSync.use(browserSyncSpa({ |
45 | 45 | selector: '[ng-app]'// Only needed for angular apps |
46 | 46 | })); |
47 | 47 | |
48 | -gulp.task('serve', ['watch'], function () { | |
48 | +gulp.task('serve', ['serve-languages', 'watch'], function () { | |
49 | 49 | var srcPaths = [path.join(conf.paths.tmp, '/serve')]; |
50 | 50 | conf.paths.allSources.reverse().forEach(function(src) { |
51 | 51 | srcPaths.push(src); | ... | ... |
gulp/styles.js
... | ... | @@ -31,6 +31,7 @@ var buildStyles = function() { |
31 | 31 | ]; |
32 | 32 | conf.paths.allSources.forEach(function(src) { |
33 | 33 | srcPaths.push(path.join(src, '/app/**/*.scss')); |
34 | + srcPaths.push(path.join(src, conf.paths.plugins, '/**/*.scss')); | |
34 | 35 | }); |
35 | 36 | var injectFiles = gulp.src(srcPaths, { read: false }); |
36 | 37 | ... | ... |
gulp/watch.js
... | ... | @@ -3,6 +3,7 @@ |
3 | 3 | var path = require('path'); |
4 | 4 | var gulp = require('gulp'); |
5 | 5 | var conf = require('./conf'); |
6 | +var languages = require('./languages'); | |
6 | 7 | |
7 | 8 | var browserSync = require('browser-sync'); |
8 | 9 | |
... | ... | @@ -14,10 +15,13 @@ gulp.task('watch', ['inject'], function () { |
14 | 15 | |
15 | 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 | 25 | if(isOnlyChange(event)) { |
22 | 26 | gulp.start('styles-reload'); |
23 | 27 | } else { |
... | ... | @@ -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 | 44 | var watchPaths = []; |
37 | 45 | conf.paths.allSources.forEach(function(src) { |
38 | 46 | watchPaths.push(path.join(src, '/app/**/*.html')); |
47 | + watchPaths.push(path.join(src, conf.paths.plugins, '/**/*.html')); | |
39 | 48 | }); |
40 | 49 | gulp.watch(watchPaths, function(event) { |
41 | 50 | browserSync.reload(event.path); | ... | ... |
karma.conf.js
... | ... | @@ -49,7 +49,7 @@ var _ = require('lodash'); |
49 | 49 | var wiredep = require('wiredep'); |
50 | 50 | |
51 | 51 | var pathSrcHtml = [ |
52 | - path.join('./src/app/**/*.html') | |
52 | + path.join('./src/**/*.html') | |
53 | 53 | ]; |
54 | 54 | |
55 | 55 | var glob = require("glob"); |
... | ... | @@ -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 | 133 | if (coverage) { |
130 | 134 | |
131 | 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 | 2 | <%= render file: Rails.root.join("app/views/layouts/application-ng.html.erb"), use_full_path: false %> |
3 | 3 | <% else %> |
4 | 4 | <%= from_theme_include(current_theme, "index") %> | ... | ... |
package.json
src/app/article/article-default-view-component.spec.ts
1 | 1 | import {Input, provide, Component} from 'ng-forward'; |
2 | 2 | import {ArticleViewComponent, ArticleDefaultViewComponent} from './article-default-view.component'; |
3 | +import {ComponentTestHelper, createClass} from './../../spec/component-test-helper'; | |
3 | 4 | |
4 | 5 | import * as helpers from "../../spec/helpers"; |
5 | 6 | |
... | ... | @@ -9,111 +10,114 @@ const htmlTemplate: string = '<noosfero-article [article]="ctrl.article" [profil |
9 | 10 | |
10 | 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 | 1 | import { bundle, Input, Inject, Component, Directive } from 'ng-forward'; |
2 | 2 | import {ArticleBlogComponent} from "./types/blog/blog.component"; |
3 | 3 | import {CommentsComponent} from "./comment/comments.component"; |
4 | +import {MacroDirective} from "./macro/macro.directive"; | |
5 | +import {ArticleToolbarHotspotComponent} from "../hotspot/article-toolbar-hotspot.component"; | |
6 | +import {ArticleContentHotspotComponent} from "../hotspot/article-content-hotspot.component"; | |
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 | 12 | * @ngdoc controller |
... | ... | @@ -11,13 +17,37 @@ import {CommentsComponent} from "./comment/comments.component"; |
11 | 17 | */ |
12 | 18 | @Component({ |
13 | 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 | 24 | export class ArticleDefaultViewComponent { |
17 | 25 | |
18 | 26 | @Input() article: noosfero.Article; |
19 | 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 | 60 | @Component({ |
31 | 61 | selector: 'noosfero-article', |
32 | 62 | template: 'not-used', |
33 | - directives: [ArticleDefaultViewComponent, ArticleBlogComponent, CommentsComponent] | |
63 | + directives: [ArticleDefaultViewComponent, ArticleBlogComponent, | |
64 | + CommentsComponent, MacroDirective, ArticleToolbarHotspotComponent, | |
65 | + ArticleContentHotspotComponent] | |
34 | 66 | }) |
35 | 67 | @Inject("$element", "$scope", "$injector", "$compile") |
36 | 68 | export class ArticleViewComponent { |
... | ... | @@ -40,7 +72,8 @@ export class ArticleViewComponent { |
40 | 72 | directiveName: string; |
41 | 73 | |
42 | 74 | ngOnInit() { |
43 | - let specificDirective = 'noosfero' + this.article.type; | |
75 | + let articleType = this.article.type.replace(/::/, ''); | |
76 | + let specificDirective = 'noosfero' + articleType; | |
44 | 77 | this.directiveName = "noosfero-default-article"; |
45 | 78 | if (this.$injector.has(specificDirective + 'Directive')) { |
46 | 79 | this.directiveName = specificDirective.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); |
... | ... | @@ -53,6 +86,6 @@ export class ArticleViewComponent { |
53 | 86 | private $scope: ng.IScope, |
54 | 87 | private $injector: ng.auto.IInjectorService, |
55 | 88 | private $compile: ng.ICompileService) { |
56 | - | |
57 | 89 | } |
90 | + | |
58 | 91 | } | ... | ... |
... | ... | @@ -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 | 4 | </div> |
5 | 5 | |
6 | 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 | 16 | <div class="page-info pull-right small text-muted"> |
8 | 17 | <span class="time"> |
9 | 18 | <i class="fa fa-clock-o"></i> <span am-time-ago="ctrl.article.created_at | dateFormat"></span> |
... | ... | @@ -16,9 +25,9 @@ |
16 | 25 | </span> |
17 | 26 | </div> |
18 | 27 | </div> |
19 | - | |
28 | + <noosfero-hotspot-article-content [article]="ctrl.article"></noosfero-hotspot-article-content> | |
20 | 29 | <div class="page-body"> |
21 | - <div ng-bind-html="ctrl.article.body"></div> | |
30 | + <div bind-html-compile="ctrl.article.body"></div> | |
22 | 31 | </div> |
23 | 32 | |
24 | 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 @@ |
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 @@ |
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 @@ |
1 | +<form> | |
2 | + <div class="form-group"> | |
3 | + <label for="titleInput">{{"article.basic_editor.title" | translate}}</label> | |
4 | + <input type="text" class="form-control" id="titleInput" placeholder="{{'article.basic_editor.title' | translate}}" ng-model="ctrl.article.name"> | |
5 | + </div> | |
6 | + <div class="form-group"> | |
7 | + <label for="bodyInput">{{"article.basic_editor.body" | translate}}</label> | |
8 | + <html-editor [(value)]="ctrl.article.body"></html-editor> | |
9 | + </div> | |
10 | +</form> | ... | ... |
src/app/article/cms/basic-options/basic-options.component.ts
0 → 100644
... | ... | @@ -0,0 +1,11 @@ |
1 | +import {Component, Input} from 'ng-forward'; | |
2 | + | |
3 | +@Component({ | |
4 | + selector: 'article-basic-options', | |
5 | + templateUrl: "app/article/cms/basic-options/basic-options.html" | |
6 | +}) | |
7 | +export class BasicOptionsComponent { | |
8 | + | |
9 | + @Input() article: noosfero.Article; | |
10 | + | |
11 | +} | ... | ... |
... | ... | @@ -0,0 +1,15 @@ |
1 | +<div class="side-options"> | |
2 | + <div class="visibility panel panel-default"> | |
3 | + <div class="panel-heading">{{"article.basic_editor.visibility" | translate}}</div> | |
4 | + <div class="panel-body"> | |
5 | + <div> | |
6 | + <input type="radio" ng-model="ctrl.article.published" ng-value="true"> | |
7 | + <i class="fa fa-unlock fa-fw"></i> {{"article.basic_editor.visibility.public" | translate}} | |
8 | + </div> | |
9 | + <div> | |
10 | + <input type="radio" ng-model="ctrl.article.published" ng-value="false"> | |
11 | + <i class="fa fa-lock fa-fw"></i> {{"article.basic_editor.visibility.private" | translate}} | |
12 | + </div> | |
13 | + </div> | |
14 | + </div> | |
15 | +</div> | ... | ... |
... | ... | @@ -0,0 +1,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 @@ |
1 | +import {StateConfig, Component, Inject, provide} from 'ng-forward'; | |
2 | +import {ArticleService} from "../../../lib/ng-noosfero-api/http/article.service"; | |
3 | +import {ProfileService} from "../../../lib/ng-noosfero-api/http/profile.service"; | |
4 | +import {NotificationService} from "../../shared/services/notification.service.ts"; | |
5 | +import {BasicOptionsComponent} from './basic-options/basic-options.component'; | |
6 | +import {BasicEditorComponent} from './basic-editor/basic-editor.component'; | |
7 | +import {ArticleEditorComponent} from './article-editor/article-editor.component'; | |
8 | + | |
9 | +@Component({ | |
10 | + selector: 'article-cms', | |
11 | + templateUrl: "app/article/cms/cms.html", | |
12 | + providers: [ | |
13 | + provide('articleService', { useClass: ArticleService }), | |
14 | + provide('profileService', { useClass: ProfileService }), | |
15 | + provide('notification', { useClass: NotificationService }) | |
16 | + ], | |
17 | + directives: [ArticleEditorComponent, BasicOptionsComponent, BasicEditorComponent] | |
18 | +}) | |
19 | +@Inject(ArticleService, ProfileService, "$state", NotificationService, "$stateParams", "$window") | |
20 | +export class CmsComponent { | |
21 | + | |
22 | + article: noosfero.Article; | |
23 | + parent: noosfero.Article = <noosfero.Article>{}; | |
24 | + | |
25 | + id: number; | |
26 | + parentId: number; | |
27 | + profileIdentifier: string; | |
28 | + | |
29 | + constructor(private articleService: ArticleService, | |
30 | + private profileService: ProfileService, | |
31 | + private $state: ng.ui.IStateService, | |
32 | + private notification: NotificationService, | |
33 | + private $stateParams: ng.ui.IStateParamsService, | |
34 | + private $window: ng.IWindowService) { | |
35 | + | |
36 | + this.parentId = this.$stateParams['parent_id']; | |
37 | + this.profileIdentifier = this.$stateParams["profile"]; | |
38 | + this.id = this.$stateParams['id']; | |
39 | + | |
40 | + if (this.parentId) { | |
41 | + this.articleService.get(this.parentId).then((result: noosfero.RestResult<noosfero.Article>) => { | |
42 | + this.parent = result.data; | |
43 | + }); | |
44 | + } | |
45 | + if (this.id) { | |
46 | + this.articleService.get(this.id).then((result: noosfero.RestResult<noosfero.Article>) => { | |
47 | + this.article = result.data; | |
48 | + this.article.name = this.article.title; // FIXME | |
49 | + }); | |
50 | + } else { | |
51 | + this.article = <noosfero.Article>{ type: this.$stateParams['type'] || "TextArticle", published: true }; | |
52 | + } | |
53 | + } | |
54 | + | |
55 | + save() { | |
56 | + this.profileService.setCurrentProfileByIdentifier(this.profileIdentifier).then((profile: noosfero.Profile) => { | |
57 | + if (this.id) { | |
58 | + return this.articleService.updateArticle(this.article); | |
59 | + } else if (this.parentId) { | |
60 | + return this.articleService.createInParent(this.parentId, this.article); | |
61 | + } else { | |
62 | + return this.articleService.createInProfile(profile, this.article); | |
63 | + } | |
64 | + }).then((response: noosfero.RestResult<noosfero.Article>) => { | |
65 | + let article = (<noosfero.Article>response.data); | |
66 | + this.$state.go('main.profile.page', { page: article.path, profile: article.profile.identifier }); | |
67 | + this.notification.success({ title: "article.basic_editor.success.title", message: "article.basic_editor.success.message" }); | |
68 | + }).catch(() => { | |
69 | + this.notification.error({ message: "article.basic_editor.save.failed" }); | |
70 | + }); | |
71 | + } | |
72 | + | |
73 | + cancel() { | |
74 | + this.$window.history.back(); | |
75 | + } | |
76 | + | |
77 | +} | ... | ... |
... | ... | @@ -0,0 +1,18 @@ |
1 | +<div class="cms"> | |
2 | + <div class="row"> | |
3 | + <div class="col-md-1"></div> | |
4 | + <div class="col-md-8"> | |
5 | + <article-editor ng-if="vm.article" [article]="vm.article"></article-editor> | |
6 | + </div> | |
7 | + <div class="col-md-3"> | |
8 | + <article-basic-options ng-if="vm.article" [article]="vm.article"></article-basic-options> | |
9 | + </div> | |
10 | + </div> | |
11 | + <div class="row"> | |
12 | + <div class="col-md-1"></div> | |
13 | + <div class="col-md-8"> | |
14 | + <button type="submit" class="btn btn-default" ng-click="vm.save()">{{"article.basic_editor.save" | translate}}</button> | |
15 | + <button type="button" class="btn btn-danger" ng-click="vm.cancel()">{{"article.basic_editor.cancel" | translate}}</button> | |
16 | + </div> | |
17 | + </div> | |
18 | +</div> | ... | ... |
src/app/article/comment/comment.component.spec.ts
1 | 1 | import {Provider, provide, Component} from 'ng-forward'; |
2 | 2 | import * as helpers from "../../../spec/helpers"; |
3 | - | |
4 | 3 | import {CommentComponent} from './comment.component'; |
5 | 4 | |
6 | 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 | 7 | describe("Components", () => { |
9 | 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 | 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 | 36 | it("render a comment", done => { |
20 | - helpers.createComponentFromClass(ContainerComponent).then(fixture => { | |
37 | + createComponent().then(fixture => { | |
21 | 38 | expect(fixture.debugElement.queryAll(".comment").length).toEqual(1); |
22 | 39 | done(); |
23 | 40 | }); |
24 | 41 | }); |
25 | 42 | |
26 | 43 | it("not render a post comment tag in the beginning", done => { |
27 | - helpers.createComponentFromClass(ContainerComponent).then(fixture => { | |
44 | + createComponent().then(fixture => { | |
28 | 45 | expect(fixture.debugElement.queryAll("noosfero-post-comment").length).toEqual(0); |
29 | 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 | 52 | let component: CommentComponent = fixture.debugElement.componentViewChildren[0].componentInstance; |
36 | 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 | 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 | 6 | @Component({ |
4 | 7 | selector: 'noosfero-comment', |
8 | + outputs: ['commentRemoved'], | |
5 | 9 | templateUrl: 'app/article/comment/comment.html' |
6 | 10 | }) |
11 | +@Inject(CommentService, NotificationService) | |
7 | 12 | export class CommentComponent { |
8 | 13 | |
9 | - @Input() comment: noosfero.Comment; | |
14 | + @Input() comment: noosfero.CommentViewModel; | |
10 | 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 | 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 | 9 | <a class="pull-left" ui-sref="main.profile.home({profile: ctrl.comment.author.identifier})"> |
10 | 10 | <h4 class="media-heading">{{ctrl.comment.author.name}}</h4> |
11 | 11 | </a> |
12 | + <span class="reply-of" ng-if="ctrl.comment.reply_of" uib-tooltip-template="'app/article/comment/comment-reply-tooltip.html'"> | |
13 | + <i class="fa fa-fw fa-mail-forward"></i> | |
14 | + <span class="author">{{ctrl.comment.reply_of.author.name}}</span> | |
15 | + </span> | |
12 | 16 | <span class="date" am-time-ago="ctrl.comment.created_at | dateFormat"></span> |
13 | - <a href="#" (click)="ctrl.reply()"> | |
14 | - <span class="pull-right small text-muted"> | |
15 | - {{"comment.reply" | translate}} | |
16 | - </span> | |
17 | - </a> | |
18 | 17 | </div> |
19 | 18 | <div class="title">{{ctrl.comment.title}}</div> |
20 | 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 | 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 | 32 | </div> | ... | ... |
src/app/article/comment/comment.scss
... | ... | @@ -5,16 +5,43 @@ |
5 | 5 | @extend .text-muted; |
6 | 6 | @extend .small; |
7 | 7 | margin-left: 8px; |
8 | + font-size: 12px; | |
8 | 9 | } |
9 | 10 | .title { |
10 | 11 | font-weight: bold; |
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 | 29 | .media-left { |
13 | 30 | min-width: 40px; |
14 | 31 | } |
15 | 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 | 46 | noosfero-profile-image { |
20 | 47 | img { |
... | ... | @@ -28,5 +55,24 @@ |
28 | 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 | 17 | commentService.getByArticle = jasmine.createSpy("getByArticle") |
18 | 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 | 37 | it("render comments associated to an article", done => { |
31 | - helpers.createComponentFromClass(ContainerComponent).then(fixture => { | |
38 | + createComponent().then(fixture => { | |
32 | 39 | expect(fixture.debugElement.queryAll("noosfero-comment").length).toEqual(2); |
33 | 40 | done(); |
34 | 41 | }); |
35 | 42 | }); |
36 | 43 | |
37 | 44 | it("render a post comment tag", done => { |
38 | - helpers.createComponentFromClass(ContainerComponent).then(fixture => { | |
45 | + createComponent().then(fixture => { | |
39 | 46 | expect(fixture.debugElement.queryAll("noosfero-post-comment").length).toEqual(1); |
40 | 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 | 56 | expect(fixture.debugElement.queryAll("noosfero-comment").length).toEqual(3); |
49 | 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 | 6 | @Component({ |
7 | 7 | selector: 'noosfero-comments', |
8 | 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 | 13 | export class CommentsComponent { |
13 | 14 | |
14 | - comments: noosfero.Comment[] = []; | |
15 | + comments: noosfero.CommentViewModel[] = []; | |
16 | + @Input() showForm = true; | |
15 | 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 | 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 | 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 | 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 | 8 | </div> | ... | ... |
src/app/article/comment/post-comment/post-comment.component.spec.ts
1 | 1 | import {Provider, provide, Component} from 'ng-forward'; |
2 | 2 | import * as helpers from "../../../../spec/helpers"; |
3 | - | |
4 | 3 | import {PostCommentComponent} from './post-comment.component'; |
5 | 4 | |
6 | 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 | 7 | describe("Components", () => { |
9 | 8 | describe("Post Comment Component", () => { |
10 | 9 | |
10 | + let properties = { article: { id: 1, accept_comments: true } }; | |
11 | + | |
11 | 12 | beforeEach(angular.mock.module("templates")); |
12 | 13 | |
13 | 14 | let commentService = jasmine.createSpyObj("commentService", ["createInArticle"]); |
15 | + let user = {}; | |
14 | 16 | let providers = [ |
15 | 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 | 20 | ].concat(helpers.provideFilters("translateFilter")); |
18 | 21 | |
19 | 22 | @Component({ selector: 'test-container-component', directives: [PostCommentComponent], template: htmlTemplate, providers: providers }) |
20 | 23 | class ContainerComponent { |
21 | - article = { id: 1 }; | |
24 | + article = properties['article']; | |
22 | 25 | comment = { id: 2 }; |
23 | 26 | } |
24 | 27 | |
... | ... | @@ -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 | 43 | it("emit an event when create comment", done => { |
33 | 44 | helpers.createComponentFromClass(ContainerComponent).then(fixture => { |
34 | 45 | let component: PostCommentComponent = fixture.debugElement.componentViewChildren[0].componentInstance; |
46 | + component.commentSaved.next = jasmine.createSpy("next"); | |
35 | 47 | commentService.createInArticle = jasmine.createSpy("createInArticle").and.returnValue(helpers.mocks.promiseResultTemplate({ data: {} })); |
36 | - component["$rootScope"].$emit = jasmine.createSpy("$emit"); | |
37 | 48 | component.save(); |
38 | - expect(component["$rootScope"].$emit).toHaveBeenCalledWith(PostCommentComponent.EVENT_COMMENT_RECEIVED, jasmine.any(Object)); | |
49 | + expect(component.commentSaved.next).toHaveBeenCalled(); | |
39 | 50 | done(); |
40 | 51 | }); |
41 | 52 | }); |
... | ... | @@ -55,9 +66,9 @@ describe("Components", () => { |
55 | 66 | helpers.createComponentFromClass(ContainerComponent).then(fixture => { |
56 | 67 | let component: PostCommentComponent = fixture.debugElement.componentViewChildren[0].componentInstance; |
57 | 68 | component.comment = <any>{ reply_of_id: null }; |
58 | - component.replyOf = <any>{ id: 10 }; | |
69 | + component.parent = <any>{ id: 10 }; | |
59 | 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 | 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 | 2 | import { CommentService } from "../../../../lib/ng-noosfero-api/http/comment.service"; |
3 | 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 | 7 | @Component({ |
6 | 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 | 14 | export class PostCommentComponent { |
11 | 15 | |
12 | 16 | public static EVENT_COMMENT_RECEIVED = "comment.received"; |
13 | 17 | |
14 | 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 | 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 | 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 | 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 | 15 | </div> |
5 | - <button type="submit" class="btn btn-default" ng-click="ctrl.save()">{{"comment.post" | translate}}</button> | |
6 | 16 | </form> | ... | ... |
... | ... | @@ -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 | 1 | import {Input, Component, provide} from 'ng-forward'; |
4 | 2 | |
5 | 3 | import * as helpers from "../../../spec/helpers"; |
6 | - | |
7 | -import {ComponentFixture} from 'ng-forward/cjs/testing/test-component-builder'; | |
4 | +import {ComponentTestHelper, createClass} from '../../../spec/component-test-helper'; | |
8 | 5 | import {ContentViewerActionsComponent} from './content-viewer-actions.component'; |
9 | 6 | |
10 | 7 | // this htmlTemplate will be re-used between the container components in this spec file |
... | ... | @@ -12,56 +9,57 @@ const htmlTemplate: string = '<content-viewer-actions [article]="ctrl.article" [ |
12 | 9 | |
13 | 10 | describe('Content Viewer Actions Component', () => { |
14 | 11 | |
15 | - beforeEach(() => { | |
12 | + let helper: ComponentTestHelper<ContentViewerActionsComponent>; | |
16 | 13 | |
17 | - angular.mock.module("templates"); | |
14 | + beforeEach(angular.mock.module("templates")); | |
18 | 15 | |
19 | - providers((provide: any) => { | |
20 | - return <any>[ | |
21 | - provide('ProfileService', { | |
22 | - useValue: helpers.mocks.profileService | |
23 | - }) | |
24 | - ]; | |
25 | - }); | |
26 | - }); | |
16 | + let providers = [ | |
17 | + provide('ProfileService', { | |
18 | + useValue: helpers.mocks.profileService | |
19 | + }), | |
20 | + provide('ArticleService', { | |
21 | + useValue: helpers.mocks.articleService | |
22 | + }) | |
23 | + ].concat(helpers.provideFilters("translateFilter")); | |
27 | 24 | |
28 | - let buildComponent = (): Promise<ComponentFixture> => { | |
29 | - return helpers.quickCreateComponent({ | |
30 | - providers: [ | |
31 | - helpers.provideEmptyObjects('Restangular'), | |
32 | - helpers.provideFilters('translateFilter') | |
33 | - ], | |
25 | + beforeEach((done) => { | |
26 | + let cls = createClass({ | |
27 | + template: htmlTemplate, | |
34 | 28 | directives: [ContentViewerActionsComponent], |
35 | - template: htmlTemplate | |
29 | + providers: providers | |
36 | 30 | }); |
37 | - }; | |
31 | + helper = new ComponentTestHelper<ContentViewerActionsComponent>(cls, done); | |
32 | + }); | |
38 | 33 | |
39 | - it('renders content viewer actions directive', (done: Function) => { | |
40 | - buildComponent().then((fixture: ComponentFixture) => { | |
41 | - expect(fixture.debugElement.query('content-viewer-actions').length).toEqual(1); | |
34 | + it('renders content viewer actions directive', () => { | |
35 | + expect(helper.all("content-viewer-actions").length).toEqual(1); | |
36 | + }); | |
42 | 37 | |
43 | - done(); | |
44 | - }); | |
38 | + it('return article parent as container when it is not a folder', () => { | |
39 | + let article = <noosfero.Article>({ id: 1, type: 'TextArticle', parent: { id: 2 } }); | |
40 | + expect(helper.component.getArticleContainer(article)).toEqual(2); | |
41 | + }); | |
42 | + | |
43 | + it('return article as container when it is a folder', () => { | |
44 | + let article = <noosfero.Article>({ id: 1, type: 'Folder' }); | |
45 | + expect(helper.component.getArticleContainer(article)).toEqual(1); | |
45 | 46 | }); |
46 | 47 | |
47 | - it('check if profile was loaded', (done: Function) => { | |
48 | + it('return article as container when it is a blog', () => { | |
49 | + let article = <noosfero.Article>({ id: 1, type: 'Blog' }); | |
50 | + expect(helper.component.getArticleContainer(article)).toEqual(1); | |
51 | + }); | |
52 | + | |
53 | + it('check if profile was loaded', () => { | |
48 | 54 | let profile: any = { |
49 | 55 | id: 1, |
50 | 56 | identifier: 'the-profile-test', |
51 | 57 | type: 'Person' |
52 | 58 | }; |
53 | - | |
54 | 59 | helpers.mocks.profileService.getCurrentProfile = () => { |
55 | 60 | return helpers.mocks.promiseResultTemplate(profile); |
56 | 61 | }; |
57 | - | |
58 | - buildComponent().then((fixture: ComponentFixture) => { | |
59 | - let contentViewerComp: ContentViewerActionsComponent = fixture.debugElement.componentViewChildren[0].componentInstance; | |
60 | - | |
61 | - expect(contentViewerComp.profile).toEqual(jasmine.objectContaining(profile)); | |
62 | - | |
63 | - done(); | |
64 | - }); | |
62 | + let component = new ContentViewerActionsComponent(<any>helpers.mocks.profileService, <any>helpers.mocks.articleService); | |
63 | + expect(component.profile).toEqual(jasmine.objectContaining(profile)); | |
65 | 64 | }); |
66 | - | |
67 | 65 | }); | ... | ... |
src/app/article/content-viewer/content-viewer-actions.component.ts
1 | 1 | import {Component, Inject, provide} from "ng-forward"; |
2 | 2 | import {ProfileService} from "../../../lib/ng-noosfero-api/http/profile.service"; |
3 | +import {ArticleService} from "../../../lib/ng-noosfero-api/http/article.service"; | |
3 | 4 | |
4 | 5 | @Component({ |
5 | 6 | selector: "content-viewer-actions", |
6 | 7 | templateUrl: "app/article/content-viewer/navbar-actions.html", |
7 | - providers: [provide('profileService', { useClass: ProfileService })] | |
8 | + providers: [ | |
9 | + provide('profileService', { useClass: ProfileService }), | |
10 | + provide('articleService', { useClass: ArticleService }) | |
11 | + ] | |
8 | 12 | }) |
9 | -@Inject(ProfileService) | |
13 | +@Inject(ProfileService, ArticleService) | |
10 | 14 | export class ContentViewerActionsComponent { |
11 | 15 | |
12 | 16 | article: noosfero.Article; |
13 | 17 | profile: noosfero.Profile; |
18 | + parentId: number; | |
14 | 19 | |
15 | - constructor(profileService: ProfileService) { | |
20 | + constructor(profileService: ProfileService, articleService: ArticleService) { | |
16 | 21 | profileService.getCurrentProfile().then((profile: noosfero.Profile) => { |
17 | 22 | this.profile = profile; |
23 | + return articleService.getCurrent(); | |
24 | + }).then((article: noosfero.Article) => { | |
25 | + this.article = article; | |
26 | + this.parentId = this.getArticleContainer(article); | |
18 | 27 | }); |
19 | 28 | } |
29 | + | |
30 | + getArticleContainer(article: noosfero.Article) { | |
31 | + // FIXME get folder types from api | |
32 | + if (article.type === "Blog" || article.type === "Folder") { | |
33 | + return article.id; | |
34 | + } else if (article.parent) { | |
35 | + return article.parent.id; | |
36 | + } | |
37 | + } | |
20 | 38 | } | ... | ... |
src/app/article/content-viewer/content-viewer.component.ts
... | ... | @@ -28,11 +28,12 @@ export class ContentViewerComponent { |
28 | 28 | } |
29 | 29 | |
30 | 30 | activate() { |
31 | - this.profileService.getCurrentProfile().then((profile: noosfero.Profile) => { | |
31 | + this.profileService.getCurrentProfile().then((profile: noosfero.Profile) => { | |
32 | 32 | this.profile = profile; |
33 | 33 | return this.articleService.getArticleByProfileAndPath(this.profile, this.$stateParams["page"]); |
34 | 34 | }).then((result: noosfero.RestResult<any>) => { |
35 | 35 | this.article = <noosfero.Article>result.data; |
36 | + this.articleService.setCurrent(this.article); | |
36 | 37 | }); |
37 | 38 | } |
38 | 39 | } | ... | ... |
src/app/article/content-viewer/navbar-actions.html
1 | -<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 | 20 | </ul> | ... | ... |
... | ... | @@ -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
... | ... | @@ -0,0 +1,25 @@ |
1 | +import {Input, provide, Component} from 'ng-forward'; | |
2 | +import {MacroDirective} from "./macro.directive"; | |
3 | + | |
4 | +import * as helpers from "../../../spec/helpers"; | |
5 | + | |
6 | +const htmlTemplate: string = '<div data-macro="macro_component" data-macro-custom="custom"></div>'; | |
7 | + | |
8 | +describe("Directives", () => { | |
9 | + | |
10 | + describe("Macro directive", () => { | |
11 | + it("renders a macro component using the name passed in data-macro", (done: Function) => { | |
12 | + helpers.quickCreateComponent({ template: htmlTemplate, directives: [MacroDirective] }).then((fixture) => { | |
13 | + expect(fixture.debugElement.queryAll('macro-component').length).toEqual(1); | |
14 | + done(); | |
15 | + }); | |
16 | + }); | |
17 | + | |
18 | + it("extract custom attributes from macro", (done: Function) => { | |
19 | + helpers.quickCreateComponent({ template: htmlTemplate, directives: [MacroDirective] }).then((fixture) => { | |
20 | + expect(fixture.debugElement.query('macro-component').attr("custom")).toEqual("custom"); | |
21 | + done(); | |
22 | + }); | |
23 | + }); | |
24 | + }); | |
25 | +}); | ... | ... |
... | ... | @@ -0,0 +1,34 @@ |
1 | +import {Directive, Inject} from "ng-forward"; | |
2 | + | |
3 | +@Directive({ | |
4 | + selector: '[macro]', | |
5 | + providers: [] | |
6 | +}) | |
7 | +@Inject('$element', '$scope', '$compile') | |
8 | +export class MacroDirective { | |
9 | + | |
10 | + private macroPrefix = "data-macro"; | |
11 | + | |
12 | + constructor(private $element: any, private $scope: ng.IScope, private $compile: ng.ICompileService) { | |
13 | + let macro = $element[0].attributes[this.macroPrefix].value; | |
14 | + let componentName = this.normalizeName(macro); | |
15 | + let content = $element.html().replace(/"/g, '"'); | |
16 | + let customAttributes = this.extractCustomAttributes($element[0].attributes); | |
17 | + $element.replaceWith($compile(`<${componentName} [article]="ctrl.article" content="${content}" ${customAttributes}></${componentName}>`)($scope)); | |
18 | + } | |
19 | + | |
20 | + extractCustomAttributes(attributes: any) { | |
21 | + let customAttributes = ""; | |
22 | + for (let attr of attributes) { | |
23 | + if (attr.name.startsWith(this.macroPrefix + '-')) { | |
24 | + let name = this.normalizeName(attr.name.replace(this.macroPrefix + '-', '')); | |
25 | + customAttributes += ` ${name}='${attr.value}'`; | |
26 | + } | |
27 | + } | |
28 | + return customAttributes; | |
29 | + } | |
30 | + | |
31 | + normalizeName(name: string) { | |
32 | + return name.replace(/[_\/]/g, '-').toLowerCase(); | |
33 | + } | |
34 | +} | ... | ... |
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 | 10 | // this htmlTemplate will be re-used between the container components in this spec file |
22 | 11 | const htmlTemplate: string = '<noosfero-blog [article]="ctrl.article" [profile]="ctrl.profile"></noosfero-blog>'; |
... | ... | @@ -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 | 46 | let articleService = { |
46 | 47 | getChildren: (article_id: number, filters: {}) => { |
47 | 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 | 59 | providers((provide: any) => { |
79 | 60 | return <any>[ |
... | ... | @@ -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 | 89 | \ No newline at end of file | ... | ... |
src/app/article/types/blog/blog.component.ts
src/app/article/types/blog/blog.html
... | ... | @@ -17,8 +17,8 @@ |
17 | 17 | </div> |
18 | 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 | 21 | boundary-links="true" items-per-page="ctrl.perPage" ng-change="ctrl.loadPage()" |
22 | 22 | first-text="«" last-text="»" previous-text="‹" next-text="›"> |
23 | - </pagination> | |
23 | + </uib-pagination> | |
24 | 24 | </div> | ... | ... |
src/app/environment/environment-home.component.ts
... | ... | @@ -22,7 +22,7 @@ export class EnvironmentHomeComponent { |
22 | 22 | environment: noosfero.Environment; |
23 | 23 | |
24 | 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 | 26 | this.environment = result; |
27 | 27 | }); |
28 | 28 | } | ... | ... |
src/app/environment/environment.component.spec.ts
... | ... | @@ -9,6 +9,7 @@ describe("Components", () => { |
9 | 9 | let environmentServiceMock: any; |
10 | 10 | let notificationMock: any; |
11 | 11 | let $state: any; |
12 | + let defaultEnvironment = <any> {id: 1, name: 'Noosfero' }; | |
12 | 13 | |
13 | 14 | beforeEach(inject((_$rootScope_: ng.IRootScopeService, _$q_: ng.IQService) => { |
14 | 15 | $rootScope = _$rootScope_; |
... | ... | @@ -17,44 +18,40 @@ describe("Components", () => { |
17 | 18 | |
18 | 19 | beforeEach(() => { |
19 | 20 | $state = jasmine.createSpyObj("$state", ["transitionTo"]); |
20 | - environmentServiceMock = jasmine.createSpyObj("environmentServiceMock", ["getByIdentifier", "getBoxes"]); | |
21 | + environmentServiceMock = jasmine.createSpyObj("environmentServiceMock", ["get", "getBoxes"]); | |
21 | 22 | notificationMock = jasmine.createSpyObj("notificationMock", ["error"]); |
22 | 23 | |
23 | - let environmentResponse = $q.defer(); | |
24 | - environmentResponse.resolve({ id: 1 }); | |
25 | 24 | let getBoxesResponse = $q.defer(); |
26 | 25 | getBoxesResponse.resolve({ data: { boxes: [{ id: 2 }] } }); |
27 | 26 | |
28 | - environmentServiceMock.getByIdentifier = jasmine.createSpy('getByIdentifier').and.returnValue(environmentResponse.promise); | |
29 | 27 | environmentServiceMock.getBoxes = jasmine.createSpy("getBoxes").and.returnValue(getBoxesResponse.promise); |
30 | 28 | }); |
31 | 29 | |
32 | 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 | 32 | $rootScope.$apply(); |
35 | - expect(component.environment).toEqual({ id: 1 }); | |
33 | + expect(component.environment).toEqual({ id: 1, name: 'Noosfero' }); | |
36 | 34 | done(); |
37 | 35 | }); |
38 | 36 | |
39 | 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 | 39 | $rootScope.$apply(); |
42 | 40 | expect(environmentServiceMock.getBoxes).toHaveBeenCalled(); |
43 | 41 | expect(component.boxes).toEqual({ data: { boxes: [{ id: 2 }] } }); |
44 | 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 | 46 | let environmentResponse = $q.defer(); |
49 | 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 | 52 | $rootScope.$apply(); |
55 | 53 | |
56 | 54 | expect(notificationMock.error).toHaveBeenCalled(); |
57 | - expect(component.environment).toBeUndefined(); | |
58 | 55 | done(); |
59 | 56 | }); |
60 | 57 | ... | ... |
src/app/environment/environment.component.ts
... | ... | @@ -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 | 35 | export class EnvironmentComponent { |
36 | 36 | |
37 | 37 | boxes: noosfero.Box[]; |
38 | 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
... | ... | @@ -0,0 +1,25 @@ |
1 | +import {Component, Input, Inject} from "ng-forward"; | |
2 | +import * as plugins from "../../plugins"; | |
3 | +import {dasherize} from "ng-forward/cjs/util/helpers"; | |
4 | +import {PluginHotspot} from "./plugin-hotspot"; | |
5 | + | |
6 | +@Component({ | |
7 | + selector: "noosfero-hotspot-article-content", | |
8 | + template: "<span></span>" | |
9 | +}) | |
10 | +@Inject("$element", "$scope", "$compile") | |
11 | +export class ArticleContentHotspotComponent extends PluginHotspot { | |
12 | + | |
13 | + @Input() article: noosfero.Article; | |
14 | + | |
15 | + constructor( | |
16 | + private $element: any, | |
17 | + private $scope: ng.IScope, | |
18 | + private $compile: ng.ICompileService) { | |
19 | + super("article_extra_content"); | |
20 | + } | |
21 | + | |
22 | + addHotspot(directiveName: string) { | |
23 | + this.$element.append(this.$compile('<' + directiveName + ' [article]="ctrl.article"></' + directiveName + '>')(this.$scope)); | |
24 | + } | |
25 | +} | ... | ... |
... | ... | @@ -0,0 +1,25 @@ |
1 | +import {Component, Input, Inject} from "ng-forward"; | |
2 | +import * as plugins from "../../plugins"; | |
3 | +import {dasherize} from "ng-forward/cjs/util/helpers"; | |
4 | +import {PluginHotspot} from "./plugin-hotspot"; | |
5 | + | |
6 | +@Component({ | |
7 | + selector: "noosfero-hotspot-article-toolbar", | |
8 | + template: "<span></span>" | |
9 | +}) | |
10 | +@Inject("$element", "$scope", "$compile") | |
11 | +export class ArticleToolbarHotspotComponent extends PluginHotspot { | |
12 | + | |
13 | + @Input() article: noosfero.Article; | |
14 | + | |
15 | + constructor( | |
16 | + private $element: any, | |
17 | + private $scope: ng.IScope, | |
18 | + private $compile: ng.ICompileService) { | |
19 | + super("article_extra_toolbar_buttons"); | |
20 | + } | |
21 | + | |
22 | + addHotspot(directiveName: string) { | |
23 | + this.$element.append(this.$compile('<' + directiveName + ' [article]="ctrl.article"></' + directiveName + '>')(this.$scope)); | |
24 | + } | |
25 | +} | ... | ... |
... | ... | @@ -0,0 +1,26 @@ |
1 | +import {Component, Input, Inject} from "ng-forward"; | |
2 | +import * as plugins from "../../plugins"; | |
3 | +import {dasherize} from "ng-forward/cjs/util/helpers"; | |
4 | +import {PluginHotspot} from "./plugin-hotspot"; | |
5 | + | |
6 | +@Component({ | |
7 | + selector: "noosfero-hotspot-comment-form", | |
8 | + template: "<span></span>" | |
9 | +}) | |
10 | +@Inject("$element", "$scope", "$compile") | |
11 | +export class CommentFormHotspotComponent extends PluginHotspot { | |
12 | + | |
13 | + @Input() comment: noosfero.Comment; | |
14 | + @Input() parent: noosfero.Comment; | |
15 | + | |
16 | + constructor( | |
17 | + private $element: any, | |
18 | + private $scope: ng.IScope, | |
19 | + private $compile: ng.ICompileService) { | |
20 | + super("comment_form_extra_contents"); | |
21 | + } | |
22 | + | |
23 | + addHotspot(directiveName: string) { | |
24 | + this.$element.append(this.$compile('<' + directiveName + ' [comment]="ctrl.comment" [parent]="ctrl.parent"></' + directiveName + '>')(this.$scope)); | |
25 | + } | |
26 | +} | ... | ... |
... | ... | @@ -0,0 +1,19 @@ |
1 | +import {Component, Input, Inject} from "ng-forward"; | |
2 | +import * as plugins from "../../plugins"; | |
3 | +import {dasherize} from "ng-forward/cjs/util/helpers"; | |
4 | + | |
5 | +export abstract class PluginHotspot { | |
6 | + | |
7 | + constructor(protected hotspot: string) { } | |
8 | + | |
9 | + ngOnInit() { | |
10 | + for (let component of plugins.hotspots) { | |
11 | + if (component.hotspot === this.hotspot) { | |
12 | + let directiveName = dasherize(component.name.replace('Component', '')); | |
13 | + this.addHotspot(directiveName); | |
14 | + } | |
15 | + } | |
16 | + } | |
17 | + | |
18 | + abstract addHotspot(directiveName: string): any; | |
19 | +} | ... | ... |
src/app/index.scss
... | ... | @@ -28,12 +28,16 @@ $primary-color-dark: #0288d1; |
28 | 28 | $default-bg-hover-color: #f8f8f8; |
29 | 29 | $red-color: #e84e40; |
30 | 30 | $red-color-dark: #dd191d; |
31 | +$green-color: #8bc34a; | |
32 | +$green-color-dark: #689f38; | |
31 | 33 | |
32 | 34 | //GRID - media queries breakpoints |
33 | 35 | $break-xxs-min: 420px; |
34 | 36 | $break-xs-min: 768px; |
37 | +$break-sm-min: 992px; | |
35 | 38 | |
36 | 39 | $break-xxs-max: ($break-xxs-min - 1); |
40 | +$break-sm-max: ($break-sm-min - 1); | |
37 | 41 | $break-xs-max: ($break-xs-min - 1); |
38 | 42 | |
39 | 43 | |
... | ... | @@ -76,4 +80,5 @@ h1, h2, h3, h4, h5 { |
76 | 80 | @import "layout/scss/mixins"; |
77 | 81 | @import "layout/scss/bootstrap-overrides"; |
78 | 82 | @import "layout/scss/layout"; |
83 | +@import "layout/scss/sidebar"; | |
79 | 84 | @import "layout/scss/tables"; | ... | ... |
src/app/index.ts
1 | -import {NoosferoApp} from "./index.module"; | |
1 | +import {bootstrap} from "ng-forward"; | |
2 | 2 | import {noosferoModuleConfig} from "./index.config"; |
3 | 3 | import {noosferoAngularRunBlock} from "./index.run"; |
4 | - | |
5 | 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 | 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 | 11 | @Input() owner: any; |
12 | 12 | |
13 | 13 | ngOnInit() { |
14 | - let blockName = (this.block && this.block.type) ? this.block.type.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() : "default-block"; | |
14 | + let blockName = (this.block && this.block.type) ? this.block.type.replace(/::/, '').replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() : "default-block"; | |
15 | 15 | this.$element.replaceWith(this.$compile('<noosfero-' + blockName + ' [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-' + blockName + '>')(this.$scope)); |
16 | 16 | } |
17 | 17 | ... | ... |
src/app/layout/blocks/communities/communities-block.component.spec.ts
0 → 100644
... | ... | @@ -0,0 +1,51 @@ |
1 | +import {TestComponentBuilder} from 'ng-forward/cjs/testing/test-component-builder'; | |
2 | +import {Provider, Input, provide, Component} from 'ng-forward'; | |
3 | + | |
4 | +import {CommunitiesBlockComponent} from './communities-block.component'; | |
5 | + | |
6 | +const htmlTemplate: string = '<noosfero-communities-block [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-communities-block>'; | |
7 | + | |
8 | +const tcb = new TestComponentBuilder(); | |
9 | + | |
10 | +describe("Components", () => { | |
11 | + describe("Communities Block Component", () => { | |
12 | + | |
13 | + beforeEach(angular.mock.module("templates")); | |
14 | + | |
15 | + let state = jasmine.createSpyObj("state", ["go"]); | |
16 | + let providers = [ | |
17 | + new Provider('truncateFilter', { useValue: () => { } }), | |
18 | + new Provider('stripTagsFilter', { useValue: () => { } }), | |
19 | + new Provider('$state', { useValue: state }), | |
20 | + new Provider('CommunityService', { | |
21 | + useValue: { | |
22 | + getByOwner: (owner: any, params: any): any => { | |
23 | + return Promise.resolve({ data: [{ identifier: "community1" }] }); | |
24 | + } | |
25 | + } | |
26 | + }), | |
27 | + ]; | |
28 | + @Component({ selector: 'test-container-component', template: htmlTemplate, directives: [CommunitiesBlockComponent], providers: providers }) | |
29 | + class BlockContainerComponent { | |
30 | + block = { type: 'Block', settings: {} }; | |
31 | + owner = { name: 'profile-name' }; | |
32 | + } | |
33 | + | |
34 | + it("get communities", done => { | |
35 | + tcb.createAsync(BlockContainerComponent).then(fixture => { | |
36 | + let block: CommunitiesBlockComponent = fixture.debugElement.componentViewChildren[0].componentInstance; | |
37 | + expect(block.profiles).toEqual([{ identifier: "community1" }]); | |
38 | + done(); | |
39 | + }); | |
40 | + }); | |
41 | + | |
42 | + it("render the profile image for each community", done => { | |
43 | + tcb.createAsync(BlockContainerComponent).then(fixture => { | |
44 | + fixture.debugElement.getLocal("$rootScope").$apply(); | |
45 | + expect(fixture.debugElement.queryAll("noosfero-profile-image").length).toEqual(1); | |
46 | + done(); | |
47 | + }); | |
48 | + }); | |
49 | + | |
50 | + }); | |
51 | +}); | ... | ... |
src/app/layout/blocks/communities/communities-block.component.ts
0 → 100644
... | ... | @@ -0,0 +1,24 @@ |
1 | +import {Input, Inject, Component} from "ng-forward"; | |
2 | +import {CommunityService} from "../../../../lib/ng-noosfero-api/http/community.service"; | |
3 | + | |
4 | +@Component({ | |
5 | + selector: "noosfero-communities-block", | |
6 | + templateUrl: 'app/layout/blocks/communities/communities-block.html', | |
7 | +}) | |
8 | +@Inject(CommunityService) | |
9 | +export class CommunitiesBlockComponent { | |
10 | + | |
11 | + @Input() block: noosfero.Block; | |
12 | + @Input() owner: noosfero.Profile; | |
13 | + | |
14 | + profiles: any = []; | |
15 | + | |
16 | + constructor(private communityService: CommunityService) { } | |
17 | + | |
18 | + ngOnInit() { | |
19 | + let limit: number = ((this.block && this.block.settings) ? this.block.settings.limit : null) || 5; | |
20 | + this.communityService.getByOwner(this.owner, { limit: limit }).then((result: noosfero.RestResult<noosfero.Community[]>) => { | |
21 | + this.profiles = result.data; | |
22 | + }); | |
23 | + } | |
24 | +} | ... | ... |
src/app/layout/blocks/communities/communities-block.html
0 → 100644
src/app/layout/blocks/communities/communities-block.scss
0 → 100644
src/app/layout/blocks/display-content/display-content-block.component.spec.ts
0 → 100644
... | ... | @@ -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 @@ |
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 @@ |
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 @@ |
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 @@ |
1 | +import {TestComponentBuilder} from 'ng-forward/cjs/testing/test-component-builder'; | |
2 | +import {Pipe, Input, provide, Component} from 'ng-forward'; | |
3 | +import {provideFilters} from '../../../../spec/helpers'; | |
4 | + | |
5 | +import {LinkListBlockComponent} from './link-list-block.component'; | |
6 | + | |
7 | +const tcb = new TestComponentBuilder(); | |
8 | + | |
9 | +const htmlTemplate: string = '<noosfero-link-list-block [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-link-list-block>'; | |
10 | + | |
11 | + | |
12 | +describe("Components", () => { | |
13 | + | |
14 | + describe("Link List Block Component", () => { | |
15 | + | |
16 | + beforeEach(angular.mock.module("templates")); | |
17 | + | |
18 | + it("receives the block and the owner as inputs", done => { | |
19 | + | |
20 | + // Creating a container component (BlockContainerComponent) to include | |
21 | + // the component under test (Block) | |
22 | + @Component({ selector: 'test-container-component', template: htmlTemplate, directives: [LinkListBlockComponent] }) | |
23 | + class BlockContainerComponent { | |
24 | + block = { type: 'Block' }; | |
25 | + owner = { name: 'profile-name' }; | |
26 | + constructor() { | |
27 | + } | |
28 | + } | |
29 | + | |
30 | + // uses the TestComponentBuilder instance to initialize the component | |
31 | + // .overrideView(LinkListBlock, { template: 'asdasdasd', pipes: [NoosferoTemplate] }) | |
32 | + tcb.createAsync(BlockContainerComponent).then(fixture => { | |
33 | + // and here we can inspect and run the test assertions | |
34 | + let myComponent: LinkListBlockComponent = fixture.componentInstance; | |
35 | + | |
36 | + // assure the block object inside the Block matches | |
37 | + // the provided through the parent component | |
38 | + expect(myComponent.block.type).toEqual("Block"); | |
39 | + expect(myComponent.owner.name).toEqual("profile-name"); | |
40 | + done(); | |
41 | + }); | |
42 | + }); | |
43 | + | |
44 | + | |
45 | + it("display links stored in block settings", done => { | |
46 | + | |
47 | + @Component({ | |
48 | + selector: 'test-container-component', | |
49 | + template: htmlTemplate, | |
50 | + directives: [LinkListBlockComponent], | |
51 | + providers: provideFilters("noosferoTemplateFilter") | |
52 | + }) | |
53 | + class CustomBlockType { | |
54 | + block: any = { settings: { links: [{ name: 'link1', address: 'address1' }, { name: 'link2', address: 'address2' }] } }; | |
55 | + owner: any = { name: 'profile-name' }; | |
56 | + } | |
57 | + tcb.createAsync(CustomBlockType).then(fixture => { | |
58 | + expect(fixture.debugElement.queryAll(".link-list-block a").length).toEqual(2); | |
59 | + done(); | |
60 | + }); | |
61 | + }); | |
62 | + | |
63 | + }); | |
64 | + | |
65 | +}); | ... | ... |
src/app/layout/blocks/link-list/link-list-block.component.ts
0 → 100644
... | ... | @@ -0,0 +1,20 @@ |
1 | +import {Component, Input} from "ng-forward"; | |
2 | + | |
3 | +@Component({ | |
4 | + selector: "noosfero-link-list-block", | |
5 | + templateUrl: "app/layout/blocks/link-list/link-list-block.html" | |
6 | +}) | |
7 | +export class LinkListBlockComponent { | |
8 | + | |
9 | + @Input() block: any; | |
10 | + @Input() owner: any; | |
11 | + | |
12 | + links: any; | |
13 | + | |
14 | + ngOnInit() { | |
15 | + if (this.block && this.block.settings) { | |
16 | + this.links = this.block.settings.links; | |
17 | + } | |
18 | + } | |
19 | + | |
20 | +} | ... | ... |
... | ... | @@ -0,0 +1,34 @@ |
1 | +.icon-event { | |
2 | + @extend .fa-calendar; | |
3 | +} | |
4 | +.icon-photos { | |
5 | + @extend .fa-photo; | |
6 | +} | |
7 | +.icon-edit { | |
8 | + @extend .fa-edit; | |
9 | +} | |
10 | +.icon-ok { | |
11 | + @extend .fa-check; | |
12 | +} | |
13 | +.icon-send { | |
14 | + @extend .fa-send-o; | |
15 | +} | |
16 | +.icon-menu-people { | |
17 | + @extend .fa-user; | |
18 | +} | |
19 | +.icon-forum { | |
20 | + @extend .fa-users; | |
21 | +} | |
22 | +.icon-new { | |
23 | + @extend .fa-file-o; | |
24 | +} | |
25 | +.icon-save { | |
26 | + @extend .fa-save; | |
27 | +} | |
28 | + | |
29 | +.link-list-block { | |
30 | + a i { | |
31 | + line-height: 25px; | |
32 | + color: #949494; | |
33 | + } | |
34 | +} | ... | ... |
src/app/layout/blocks/login-block/login-block.component.spec.ts
0 → 100644
... | ... | @@ -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 | 111 | \ No newline at end of file | ... | ... |
src/app/layout/blocks/login-block/login-block.component.ts
0 → 100644
... | ... | @@ -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 @@ |
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 @@ |
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 @@ |
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 | +} | ... | ... |