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