Compare View

switch
from
...
to
 
Commits (203)
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(&#39;html&#39;, [&#39;inject&#39;, &#39;partials&#39;], 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(&#39;fonts&#39;, 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(&#39;clean-docs&#39;, [], 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(&#39;noosfero&#39;, [&#39;html&#39;], 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(&#39;path&#39;);
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";
... ...
gulp/languages.js 0 → 100644
... ... @@ -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(&#39;watch&#39;, [&#39;inject&#39;], 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(&#39;watch&#39;, [&#39;inject&#39;], 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(&#39;lodash&#39;);
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
... ... @@ -49,6 +49,7 @@
49 49 "gulp-insert": "^0.5.0",
50 50 "gulp-inject": "~3.0.0",
51 51 "gulp-load-plugins": "~0.10.0",
  52 + "gulp-merge-json": "^0.4.0",
52 53 "gulp-minify-css": "~1.2.1",
53 54 "gulp-minify-html": "~1.0.4",
54 55 "gulp-ng-annotate": "~1.1.0",
... ...
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 = &#39;&lt;noosfero-article [article]=&quot;ctrl.article&quot; [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 &quot;./comment/comments.component&quot;;
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 }
... ...
src/app/article/article-view-component.spec.ts 0 → 100644
... ... @@ -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 +import {Component, Input} from 'ng-forward';
  2 +
  3 +@Component({
  4 + selector: 'article-basic-editor',
  5 + templateUrl: "app/article/cms/basic-editor/basic-editor.html"
  6 +})
  7 +export class BasicEditorComponent {
  8 +
  9 + @Input() article: noosfero.Article;
  10 +}
... ...
src/app/article/cms/basic-editor/basic-editor.html 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-editor/basic-editor.scss 0 → 100644
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 +}
... ...
src/app/article/cms/basic-options/basic-options.html 0 → 100644
... ... @@ -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>
... ...
src/app/article/cms/basic-options/basic-options.scss 0 → 100644
... ... @@ -0,0 +1,15 @@
  1 +.side-options {
  2 + margin-top: 25px;
  3 +
  4 + .visibility {
  5 + .panel-heading {
  6 + background-color: transparent;
  7 + font-weight: bold;
  8 + }
  9 + .panel-body {
  10 + i {
  11 + color: #A5A5A5;
  12 + }
  13 + }
  14 + }
  15 +}
... ...
src/app/article/cms/cms.component.spec.ts 0 → 100644
... ... @@ -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 +});
... ...
src/app/article/cms/cms.component.ts 0 → 100644
... ... @@ -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 +}
... ...
src/app/article/cms/cms.html 0 → 100644
... ... @@ -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/cms/cms.scss 0 → 100644
... ... @@ -0,0 +1,4 @@
  1 +.cms {
  2 + @extend .container-fluid;
  3 + padding: 0 1%;
  4 +}
... ...
src/app/article/comment/comment-reply-tooltip.html 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +<div class="reply-tooltip">
  2 + <noosfero-comment [comment]="ctrl.comment.reply_of" [article]="ctrl.article" [display-actions]="false" [display-replies]="false"></noosfero-comment>
  3 +</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 = &#39;&lt;noosfero-comment [article]=&quot;ctrl.article&quot; [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(&quot;Components&quot;, () =&gt; {
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 &quot;./comment.component&quot;;
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/comments.scss 0 → 100644
... ... @@ -0,0 +1,6 @@
  1 +.comments {
  2 + border-top: 2px solid #F3F3F3;
  3 + .comments {
  4 + border-top: 0;
  5 + }
  6 +}
... ...
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 = &#39;&lt;noosfero-post-comment [article]=&quot;ctrl.article&quot; [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(&quot;Components&quot;, () =&gt; {
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(&quot;Components&quot;, () =&gt; {
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>
... ...
src/app/article/comment/post-comment/post-comment.scss 0 → 100644
... ... @@ -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 = &#39;&lt;content-viewer-actions [article]=&quot;ctrl.article&quot; [
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>
... ...
src/app/article/content-viewer/navbar-actions.spec.ts 0 → 100644
... ... @@ -0,0 +1,53 @@
  1 +import {TestComponentBuilder} from 'ng-forward/cjs/testing/test-component-builder';
  2 +import {Provider} from 'ng-forward';
  3 +import {ComponentTestHelper, createClass} from "./../../../spec/component-test-helper";
  4 +import {providers} from 'ng-forward/cjs/testing/providers';
  5 +import {ContentViewerActionsComponent} from '././content-viewer-actions.component';
  6 +import * as helpers from "../../../spec/helpers";
  7 +
  8 +const htmlTemplate: string = '<content-viewer-actions [article]="ctrl.article" [profile]="ctrl.profile"></content-viewer-actions>';
  9 +
  10 +describe("Components", () => {
  11 +
  12 + describe("Content Viewer Actions Component", () => {
  13 + let serviceMock = {
  14 + getEnvironmentPeople: (filters: any): any => {
  15 + return Promise.resolve([{ identifier: "person1" }]);
  16 + }
  17 + };
  18 + let providers = [
  19 + new Provider('ArticleService', { useValue: helpers.mocks.articleService }),
  20 + new Provider('ProfileService', { useValue: helpers.mocks.profileService })
  21 + ];
  22 +
  23 + let helper: ComponentTestHelper<ContentViewerActionsComponent>;
  24 +
  25 + beforeEach(angular.mock.module("templates"));
  26 +
  27 + /**
  28 + * The beforeEach procedure will initialize the helper and parse
  29 + * the component according to the given providers. Unfortunetly, in
  30 + * this mode, the providers and properties given to the construtor
  31 + * can't be overriden.
  32 + */
  33 + beforeEach((done) => {
  34 + // Create the component bed for the test. Optionally, this could be done
  35 + // in each test if one needs customization of these parameters per test
  36 + let cls = createClass({
  37 + template: htmlTemplate,
  38 + directives: [ContentViewerActionsComponent],
  39 + providers: providers,
  40 + properties: {}
  41 + });
  42 + helper = new ComponentTestHelper<ContentViewerActionsComponent>(cls, done);
  43 + });
  44 +
  45 + it("render the actions new item menu", () => {
  46 + expect(helper.all("a[class|='btn dropdown-toggle']")[0]).not.toBeNull();
  47 + });
  48 +
  49 + it("render two menu item actions", () => {
  50 + expect(helper.all("ul")[1].find("li").length).toBe(2);
  51 + });
  52 + });
  53 +});
... ...
src/app/article/index.ts
1 1 /* Module Index Entry - generated using the script npm run generate-index */
2 2 export * from "./article-default-view.component";
3   -export * from "./basic-editor.component";
... ...
src/app/article/macro/macro.directive.spec.ts 0 → 100644
... ... @@ -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 +});
... ...
src/app/article/macro/macro.directive.ts 0 → 100644
... ... @@ -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, '&quot;');
  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(&quot;Blog Component&quot;, () =&gt; {
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(&quot;Blog Component&quot;, () =&gt; {
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
... ... @@ -31,7 +31,7 @@ export class ArticleBlogComponent {
31 31  
32 32 loadPage() {
33 33 let filters = {
34   - content_type: "TinyMceArticle",
  34 + content_type: "TextArticle",
35 35 per_page: this.perPage,
36 36 page: this.currentPage
37 37 };
... ...
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(&quot;Components&quot;, () =&gt; {
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(&quot;Components&quot;, () =&gt; {
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 &quot;./environment-home.component&quot;;
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
1 1 <div class="environment-container">
2 2 <div class="row">
3   - <noosfero-boxes [boxes]="vm.boxes" [owner]="vm.environment"></noosfero-boxes>
  3 + <noosfero-boxes ng-if="vm.boxes" [boxes]="vm.boxes" [owner]="vm.environment"></noosfero-boxes>
4 4 </div>
5 5 </div>
... ...
src/app/hotspot/article-content-hotspot.component.ts 0 → 100644
... ... @@ -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 +}
... ...
src/app/hotspot/article-toolbar-hotspot.component.ts 0 → 100644
... ... @@ -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 +}
... ...
src/app/hotspot/comment-form-hotspot.component.ts 0 → 100644
... ... @@ -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 +}
... ...
src/app/hotspot/hotspot.decorator.ts 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +export function Hotspot(hotspotName: string) {
  2 + return (target: any) => {
  3 + target['hotspot'] = hotspotName;
  4 + };
  5 +}
... ...
src/app/hotspot/plugin-hotspot.ts 0 → 100644
... ... @@ -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
... ... @@ -0,0 +1,5 @@
  1 +<div class="communities-block">
  2 + <a ng-repeat="profile in ctrl.profiles" ui-sref="main.profile.home({profile: profile.identifier})" class="profile">
  3 + <noosfero-profile-image [profile]="profile"></noosfero-profile-image>
  4 + </a>
  5 +</div>
... ...
src/app/layout/blocks/communities/communities-block.scss 0 → 100644
... ... @@ -0,0 +1,16 @@
  1 +.communities-block {
  2 + .profile {
  3 + margin: 10px;
  4 + img, i.profile-image {
  5 + width: 60px;
  6 + }
  7 + img {
  8 + display: inline-block;
  9 + vertical-align: top;
  10 + }
  11 + i.profile-image {
  12 + text-align: center;
  13 + font-size: 4.5em;
  14 + }
  15 + }
  16 +}
... ...
src/app/layout/blocks/communities/index.ts 0 → 100644
... ... @@ -0,0 +1,2 @@
  1 +/* Module Index Entry - generated using the script npm run generate-index */
  2 +export * from "./communities-block.component";
... ...
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/display-content/index.ts 0 → 100644
... ... @@ -0,0 +1,2 @@
  1 +/* Module Index Entry - generated using the script npm run generate-index */
  2 +export * from "./display-content-block.component";
... ...
src/app/layout/blocks/link-list/index.ts
1 1 /* Module Index Entry - generated using the script npm run generate-index */
2   -export * from "./link-list.component";
  2 +export * from "./link-list-block.component";
... ...
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 +}
... ...
src/app/layout/blocks/link-list/link-list-block.html 0 → 100644
... ... @@ -0,0 +1,7 @@
  1 +<div class="link-list-block">
  2 + <div ng-repeat="link in ctrl.links">
  3 + <a ng-href="{{link.address | noosferoTemplate:{profile: ctrl.owner.identifier} }}">
  4 + <i class="fa fa-fw icon-{{link.icon}}"></i> <span>{{link.name}}</span>
  5 + </a>
  6 + </div>
  7 +</div>
... ...
src/app/layout/blocks/link-list/link-list-block.scss 0 → 100644
... ... @@ -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/index.ts 0 → 100644
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 +}
... ...
src/app/layout/blocks/login-block/login-block.html 0 → 100644
... ... @@ -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>
... ...
src/app/layout/blocks/main/index.ts 0 → 100644
... ... @@ -0,0 +1,2 @@
  1 +/* Module Index Entry - generated using the script npm run generate-index */
  2 +export * from "./main-block.component";
... ...
src/app/layout/blocks/main/main-block.component.spec.ts 0 → 100644
... ... @@ -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 +});
... ...
src/app/layout/blocks/main/main-block.component.ts 0 → 100644
... ... @@ -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 +}
... ...