Commit 2f5bbc4fccacff192ec721d69f1b8b7bb0aa5f4e

Authored by Victor Costa
2 parents 8382ef57 9d2ce382
Exists in staging

Merge branch 'master' into staging

Showing 86 changed files with 1323 additions and 323 deletions   Show diff stats
README.md
... ... @@ -50,3 +50,31 @@ See some important folders bellow:
50 50 1. Create an app folder inside custom-theme and add your custom scss files
51 51 1. Put the templates that you want to override in the same structure from the main application source, e.g.:
52 52 `src/app/profile/profile.html` will be overriden by `themes/custom-theme/app/profile/profile.html`
  53 +
  54 +## Change skin
  55 +
  56 +- Create a any scss file into your theme folder structure
  57 +- Extend your skin css class from `%skin-base` scss placeholder selector. Something like this:
  58 +
  59 +```sass
  60 +.skin-custom {
  61 + @extend %skin-base
  62 +}
  63 +```
  64 +- Configure application to use the new theme, e.g.:
  65 +`npm config set angular-theme:skin custom-skin`
  66 +
  67 +- Start the application with `npm start` scripts ou make a build
  68 +
  69 +## Development environment
  70 +
  71 +## Known Issues
  72 +
  73 +### Message Translation: angular-i18n
  74 +
  75 + - Plural Interpolation only working when current language is En (English)
  76 +
  77 + `Plural Function not found for locale`
  78 +
  79 + For some reason the messageformat installed on bower_component directory was an older version. Removing the bower_components directory
  80 +and runing `bower install` solved the problem
... ...
bower.json
... ... @@ -35,9 +35,10 @@
35 35 "angular-i18n": "^1.5.0",
36 36 "angular-load": "^0.4.1",
37 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 + "angular-bind-html-compile": "^1.2.1",
  39 + "angular-click-outside": "^2.7.1",
  40 + "ng-ckeditor": "^0.2.1",
  41 + "angular-bootstrap-toggle-switch": "^0.5.6"
41 42 },
42 43 "devDependencies": {
43 44 "angular-mocks": "~1.5.0"
... ... @@ -46,9 +47,7 @@
46 47 "ng-ckeditor": {
47 48 "main": [
48 49 "ng-ckeditor.js",
49   - "libs/ckeditor/lang",
50   - "libs/ckeditor/ckeditor.js",
51   - "libs/ckeditor/config.js"
  50 + "libs/ckeditor/ckeditor.js"
52 51 ]
53 52 },
54 53 "bootstrap-sass": {
... ...
gulp/build.js
... ... @@ -30,7 +30,8 @@ gulp.task('partials', function () {
30 30 quotes: true
31 31 }))
32 32 .pipe($.angularTemplatecache('templateCacheHtml-'+partialPath+'.js', {
33   - module: 'noosferoApp',
  33 + module: 'noosfero.templates.' + partialPath,
  34 + standalone: true,
34 35 root: partialPath
35 36 }))
36 37 .pipe(gulp.dest(conf.paths.tmp + '/partials/')));
... ... @@ -81,7 +82,6 @@ gulp.task('html', ['inject', 'partials'], function () {
81 82 .pipe($.revReplace({prefix: noosferoThemePrefix}))
82 83 .pipe(htmlFilter)
83 84 .pipe($.replace('/bower_components/ng-ckeditor/libs/ckeditor/', noosferoThemePrefix + 'ng-ckeditor/libs/ckeditor/'))
84   - .pipe($.replace('/bower_components/ng-ckeditor/ng-ckeditor.min.js', noosferoThemePrefix + 'ng-ckeditor/ng-ckeditor.min.js'))
85 85 .pipe($.minifyHtml({
86 86 empty: true,
87 87 spare: true,
... ... @@ -103,7 +103,7 @@ gulp.task('fonts', function () {
103 103 });
104 104  
105 105 gulp.task('ckeditor', function () {
106   - conf.wiredep.exclude.push(/ckeditor/); // exclude ckeditor from build to improve performance
  106 + conf.wiredep.exclude.push(/bower_components\/ng-ckeditor\/libs\/ckeditor/); // exclude ckeditor from build to improve performance
107 107 return gulp.src(['bower_components/ng-ckeditor/**/*']).pipe(gulp.dest(path.join(conf.paths.dist, '/ng-ckeditor')));
108 108 });
109 109  
... ...
gulp/conf.js
... ... @@ -27,6 +27,10 @@ exports.configTheme = function(theme) {
27 27 exports.paths.theme = theme || "angular-default";
28 28 exports.paths.allSources = [exports.paths.src, path.join(exports.paths.themes, exports.paths.theme)];
29 29 exports.paths.dist = path.join("dist", exports.paths.theme);
  30 +
  31 + if(argv.skin) {
  32 + exports.paths.skin = argv.skin;
  33 + }
30 34 }
31 35 exports.configTheme(argv.theme);
32 36  
... ...
gulp/inject.js
... ... @@ -3,6 +3,8 @@
3 3 var path = require('path');
4 4 var gulp = require('gulp');
5 5 var conf = require('./conf');
  6 +var map = require('map-stream');
  7 +var transform = require('vinyl-transform');
6 8  
7 9 var $ = require('gulp-load-plugins')();
8 10  
... ... @@ -40,3 +42,34 @@ gulp.task('inject', ['scripts', 'styles'], function () {
40 42 .pipe(wiredep(_.extend({}, conf.wiredep)))
41 43 .pipe(gulp.dest(path.join(conf.paths.tmp, '/serve')));
42 44 });
  45 +
  46 +/**
  47 +* Replace the default theme skin to a npm config
  48 +*
  49 +* Uses "vinyl-transform" + "map-stream" to open the
  50 +* js file and rewrite the source file into the same
  51 +* destination
  52 +*
  53 +* @see https://www.npmjs.com/package/vinyl-transform
  54 +* @see https://www.npmjs.com/package/map-stream
  55 +*/
  56 +gulp.task('inject-skin', function () {
  57 +
  58 + if(conf.paths.skin) {
  59 +
  60 + $.util.log('Configured theme skin:', conf.paths.skin);
  61 +
  62 + var replaceSkin = transform(function(filename) {
  63 + return map(function(file, next) {
  64 + var contents = file.toString();
  65 + contents = contents.replace('skin-whbl', conf.paths.skin);
  66 + return next(null, contents);
  67 + });
  68 + });
  69 +
  70 + gulp.src(path.join(conf.paths.src,'./noosfero.js'))
  71 + .pipe(replaceSkin)
  72 + .pipe(gulp.dest(conf.paths.src));
  73 + }
  74 +
  75 +});
... ...
gulp/scripts.js
... ... @@ -9,7 +9,7 @@ var browserSync = require('browser-sync');
9 9 var $ = require('gulp-load-plugins')();
10 10  
11 11  
12   -gulp.task('scripts-reload', function() {
  12 +gulp.task('scripts-reload', ['inject-skin'], function() {
13 13 return buildScripts()
14 14 .pipe(browserSync.stream());
15 15 });
... ...
package.json
... ... @@ -8,10 +8,11 @@
8 8 "ng-forward": "0.0.1-alpha.12"
9 9 },
10 10 "config": {
11   - "theme": "angular-default"
  11 + "theme": "angular-default",
  12 + "skin": "skin-whbl"
12 13 },
13 14 "scripts": {
14   - "build": "webpack; gulp clean && gulp --theme=$npm_package_config_theme build",
  15 + "build": "webpack; gulp clean && gulp --theme=$npm_package_config_theme --skin=$npm_package_config_skin build",
15 16 "build-all": "gulp clean && for d in ./themes/* ; do (gulp --theme=`basename $d` build); done",
16 17 "webpack": "webpack",
17 18 "karma": "concurrently \"webpack -w\" \"karma start\"",
... ... @@ -23,7 +24,7 @@
23 24 "test": "webpack -w --test",
24 25 "jenkins": "webpack && karma start --single-run",
25 26 "postinstall": "bower install; typings install; cd dev-scripts; typings install; cd ../; npm run fix-jqlite",
26   - "start": "concurrently \"webpack -w\" \"gulp --theme=$npm_package_config_theme serve\"",
  27 + "start": "concurrently \"webpack -w\" \"gulp --theme=$npm_package_config_theme --skin=$npm_package_config_skin serve\"",
27 28 "generate-indexes": "ts-node --project ./dev-scripts ./dev-scripts/generate-index-modules.ts",
28 29 "fix-jqlite": "ts-node --project ./dev-scripts dev-scripts/fix-jqlite.ts"
29 30 },
... ... @@ -46,8 +47,8 @@
46 47 "gulp-eslint": "~1.0.0",
47 48 "gulp-filter": "~3.0.1",
48 49 "gulp-flatten": "~0.2.0",
49   - "gulp-insert": "^0.5.0",
50 50 "gulp-inject": "~3.0.0",
  51 + "gulp-insert": "^0.5.0",
51 52 "gulp-load-plugins": "~0.10.0",
52 53 "gulp-merge-json": "^0.4.0",
53 54 "gulp-minify-css": "~1.2.1",
... ... @@ -81,6 +82,7 @@
81 82 "karma-webpack": "^1.7.0",
82 83 "lodash": "~3.10.1",
83 84 "main-bower-files": "~2.9.0",
  85 + "map-stream": "0.0.6",
84 86 "merge-stream": "^1.0.0",
85 87 "on-build-webpack": "^0.1.0",
86 88 "phantomjs": "~1.9.18",
... ... @@ -96,6 +98,7 @@
96 98 "typescript": "^1.8.10",
97 99 "typings": "^0.6.8",
98 100 "uglify-save-license": "~0.4.1",
  101 + "vinyl-transform": "^1.0.0",
99 102 "webpack": "^1.12.14",
100 103 "wiredep": "~2.2.2",
101 104 "wrench": "~1.5.8",
... ...
src/app/admin/layout-edit/designMode.service.spec.ts 0 → 100644
... ... @@ -0,0 +1,31 @@
  1 +import {DesignModeService} from './designMode.service';
  2 +
  3 +describe('DesignMode Service', () => {
  4 + let service: DesignModeService;
  5 +
  6 + beforeEach(() => {
  7 + service = new DesignModeService();
  8 + });
  9 +
  10 + it('has the designModeOn equals false as default', () => {
  11 + expect(service.isInDesignMode()).toBeFalsy();
  12 + });
  13 +
  14 + it('allows set the designMode value', () => {
  15 + spyOn(service.onToggle, 'next').and.stub();
  16 + service.setInDesignMode(true);
  17 + expect(service.isInDesignMode).toBeTruthy();
  18 + });
  19 +
  20 + it('emits the onToggle event when changing the designModeOn property', () => {
  21 + spyOn(service.onToggle, 'next').and.stub();
  22 + service.setInDesignMode(true);
  23 + expect(service.onToggle.next).toHaveBeenCalled();
  24 + });
  25 +
  26 + it('does not emit onToggle event when there is no change on designModeOn property', () => {
  27 + spyOn(service.onToggle, 'next').and.stub();
  28 + service.setInDesignMode(false);
  29 + expect(service.onToggle.next).not.toHaveBeenCalled();
  30 + });
  31 +});
0 32 \ No newline at end of file
... ...
src/app/admin/layout-edit/designMode.service.ts 0 → 100644
... ... @@ -0,0 +1,23 @@
  1 +import {Component, Injectable, Output, EventEmitter} from 'ng-forward';
  2 +import {BodyStateClassesService} from '../../layout/services/body-state-classes.service';
  3 +
  4 +@Injectable()
  5 +export class DesignModeService {
  6 + @Output() onToggle: EventEmitter<boolean> = new EventEmitter<boolean>();
  7 +
  8 + private designModeOn: boolean = false;
  9 +
  10 + isInDesignMode(): boolean {
  11 + return this.designModeOn;
  12 + }
  13 +
  14 + setInDesignMode(value: boolean) {
  15 + if (this.designModeOn !== value) {
  16 + this.designModeOn = value;
  17 + this.onToggle.next(this.designModeOn);
  18 + }
  19 + }
  20 +
  21 + constructor() {
  22 + }
  23 +}
0 24 \ No newline at end of file
... ...
src/app/admin/layout-edit/designModeToggler.component.spec.ts 0 → 100644
... ... @@ -0,0 +1,59 @@
  1 +import {ComponentTestHelper, createClass} from '../../../spec/component-test-helper';
  2 +import * as helpers from '../../../spec/helpers';
  3 +import {DesignModeTogglerComponent} from './designModeToggler.component';
  4 +import {DesignModeService} from './designMode.service';
  5 +
  6 +describe('DesignModeToggler Component', () => {
  7 + const htmlTemplate: string = '<noosfero-design-toggler></noosfero-design-toggler>';
  8 +
  9 + let helper: ComponentTestHelper<DesignModeTogglerComponent>;
  10 + beforeEach(() => {
  11 + angular.mock.module('templates');
  12 + angular.mock.module('ngSanitize');
  13 + angular.mock.module('toggle-switch');
  14 + });
  15 +
  16 + let designModeService: DesignModeService;
  17 + beforeEach((done) => {
  18 + designModeService = new DesignModeService();
  19 + let cls = createClass({
  20 + template: htmlTemplate,
  21 + directives: [DesignModeTogglerComponent],
  22 + providers: [
  23 + helpers.createProviderToValue('DesignModeService', designModeService)
  24 + ]
  25 + });
  26 + helper = new ComponentTestHelper<DesignModeTogglerComponent>(cls, done);
  27 + });
  28 +
  29 + it('changes css classes representing the switch is on or off', () => {
  30 + expect(helper.debugElement.query('div.switch-animate').hasClass('switch-off')).toBeTruthy();
  31 + expect(helper.debugElement.query('div.switch-animate').hasClass('switch-on')).toBeFalsy();
  32 + helper.component.inDesignMode = true;
  33 + helper.detectChanges();
  34 + expect(helper.debugElement.query('div.switch-animate').hasClass('switch-on')).toBeTruthy();
  35 + expect(helper.debugElement.query('div.switch-animate').hasClass('switch-off')).toBeFalsy();
  36 + });
  37 +
  38 + it('emits event with value "true" when changing inDesignMode to On', (done) => {
  39 + designModeService.onToggle.subscribe((designModeOn: boolean) => {
  40 + expect(designModeOn).toBeTruthy();
  41 + done();
  42 + });
  43 + helper.component.inDesignMode = true;
  44 + helper.detectChanges();
  45 + });
  46 +
  47 + it('emits events with value "false" when changing inDesignMode to Off', (done) => {
  48 + helper.component.inDesignMode = true;
  49 + helper.detectChanges();
  50 +
  51 + designModeService.onToggle.subscribe((designModeOn: boolean) => {
  52 + expect(designModeOn).toBeFalsy();
  53 + done();
  54 + });
  55 +
  56 + helper.component.inDesignMode = false;
  57 + helper.detectChanges();
  58 + });
  59 +});
0 60 \ No newline at end of file
... ...
src/app/admin/layout-edit/designModeToggler.component.ts 0 → 100644
... ... @@ -0,0 +1,24 @@
  1 +import {Component, Inject} from 'ng-forward';
  2 +import {DesignModeService} from './designMode.service';
  3 +@Component({
  4 + selector: 'noosfero-design-toggler',
  5 + templateUrl: 'app/admin/layout-edit/designModeToggler.html'
  6 +})
  7 +@Inject(DesignModeService)
  8 +export class DesignModeTogglerComponent {
  9 +
  10 + icon: string = "&nbsp;<i class='glyphicon glyphicon-wrench'></i>&nbsp;";
  11 +
  12 + constructor(private designModeService: DesignModeService) {
  13 + }
  14 +
  15 + private _inDesignMode: boolean = false;
  16 +
  17 + get inDesignMode(): boolean {
  18 + return this.designModeService.isInDesignMode();
  19 + };
  20 +
  21 + set inDesignMode(value: boolean) {
  22 + this.designModeService.setInDesignMode(value);
  23 + };
  24 +}
0 25 \ No newline at end of file
... ...
src/app/admin/layout-edit/designModeToggler.html 0 → 100644
... ... @@ -0,0 +1,8 @@
  1 +<toggle-switch
  2 + html="true"
  3 + ng-model="ctrl.inDesignMode"
  4 + on-label="{{'designMode.toggle.ON' | translate}}"
  5 + off-label="{{'designMode.toggle.OFF' | translate}}"
  6 + class="switch-small"
  7 + knob-label="{{ ctrl.icon + ('designMode.label' | translate) }}">
  8 +</toggle-switch>
0 9 \ No newline at end of file
... ...
src/app/article/article-default-view-component.spec.ts
... ... @@ -65,16 +65,36 @@ describe(&quot;Components&quot;, () =&gt; {
65 65 expect(state.transitionTo).toHaveBeenCalled();
66 66 });
67 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; ");
  70 + });
  71 +
  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 + });
  75 +
  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('');
  80 + });
  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 + });
  87 +
68 88 /**
69 89 * Execute the delete method on the target component
70 90 */
71 91 function doDeleteArticle() {
72 92 // Create a mock for the notification service confirmation
73   - spyOn(helper.component.notificationService, 'confirmation').and.callFake(function (params: Function) {
  93 + spyOn(helper.component.notificationService, 'confirmation').and.callFake(function(params: Function) {
74 94  
75 95 });
76 96 // Create a mock for the ArticleService removeArticle method
77   - spyOn(helper.component.articleService, 'remove').and.callFake(function (param: noosfero.Article) {
  97 + spyOn(helper.component.articleService, 'remove').and.callFake(function(param: noosfero.Article) {
78 98  
79 99 return {
80 100 catch: () => { }
... ...
src/app/article/article-default-view.component.ts
... ... @@ -6,6 +6,7 @@ import {ArticleToolbarHotspotComponent} from &quot;../hotspot/article-toolbar-hotspot
6 6 import {ArticleContentHotspotComponent} from "../hotspot/article-content-hotspot.component";
7 7 import {ArticleService} from "./../../lib/ng-noosfero-api/http/article.service";
8 8 import { NotificationService } from "./../shared/services/notification.service";
  9 +import {PermissionDirective} from '../shared/components/permission/permission.directive';
9 10  
10 11 /**
11 12 * @ngdoc controller
... ... @@ -16,7 +17,8 @@ import { NotificationService } from &quot;./../shared/services/notification.service&quot;;
16 17 */
17 18 @Component({
18 19 selector: 'noosfero-default-article',
19   - templateUrl: 'app/article/article.html'
  20 + templateUrl: 'app/article/article.html',
  21 + directives: [PermissionDirective]
20 22 })
21 23 @Inject("$state", ArticleService, NotificationService)
22 24 export class ArticleDefaultViewComponent {
... ...
src/app/article/article.html
... ... @@ -4,13 +4,15 @@
4 4 </div>
5 5  
6 6 <div class="sub-header clearfix">
7   - <a href="#" class="btn btn-default btn-xs" ui-sref="main.cmsEdit({profile: ctrl.profile.identifier, id: ctrl.article.id})">
8   - <i class="fa fa-pencil-square-o fa-fw fa-lg"></i> {{"article.actions.edit" | translate}}
9   - </a>
10   - <a href="#" class="btn btn-default btn-xs" ng-click="ctrl.delete()">
11   - <i class="fa fa-trash-o fa-fw fa-lg" ng-click="ctrl.delete()"></i> {{"article.actions.delete" | translate}}
12   - </a>
13   - <noosfero-hotspot-article-toolbar [article]="ctrl.article"></noosfero-hotspot-article-toolbar>
  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>
14 16 <div class="page-info pull-right small text-muted">
15 17 <span class="time">
16 18 <i class="fa fa-clock-o"></i> <span am-time-ago="ctrl.article.created_at | dateFormat"></span>
... ...
src/app/article/comment/comments.component.spec.ts
... ... @@ -19,7 +19,6 @@ describe(&quot;Components&quot;, () =&gt; {
19 19  
20 20 let properties = { article: { id: 1 }, parent: <any>null };
21 21 function createComponent() {
22   - // postCommentEventService = jasmine.createSpyObj("postCommentEventService", ["subscribe"]);
23 22 let providers = [
24 23 helpers.createProviderToValue('CommentService', commentService),
25 24 helpers.createProviderToValue('NotificationService', helpers.mocks.notificationService),
... ...
src/app/article/comment/comments.component.ts
... ... @@ -9,7 +9,7 @@ import { CommentComponent } from &quot;./comment.component&quot;;
9 9 directives: [PostCommentComponent, CommentComponent],
10 10 outputs: ['commentAdded']
11 11 })
12   -@Inject(CommentService, "$element")
  12 +@Inject(CommentService, "$scope")
13 13 export class CommentsComponent {
14 14  
15 15 comments: noosfero.CommentViewModel[] = [];
... ... @@ -32,9 +32,22 @@ export class CommentsComponent {
32 32 }
33 33 }
34 34  
35   - commentAdded(comment: noosfero.Comment): void {
  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 + }
36 48 this.comments.push(comment);
37 49 this.resetShowReply();
  50 + this.$scope.$apply();
38 51 }
39 52  
40 53 commentRemoved(comment: noosfero.Comment): void {
... ... @@ -51,6 +64,7 @@ export class CommentsComponent {
51 64 if (this.parent) {
52 65 this.parent.__show_reply = false;
53 66 }
  67 +
54 68 }
55 69  
56 70 loadComments() {
... ...
src/app/article/content-viewer/content-viewer.component.ts
... ... @@ -14,7 +14,7 @@ import {ProfileService} from &quot;../../../lib/ng-noosfero-api/http/profile.service&quot;
14 14 provide('profileService', { useClass: ProfileService })
15 15 ]
16 16 })
17   -@Inject(ArticleService, ProfileService, "$log", "$stateParams")
  17 +@Inject(ArticleService, ProfileService, "$stateParams")
18 18 export class ContentViewerComponent {
19 19  
20 20 @Input()
... ... @@ -23,7 +23,10 @@ export class ContentViewerComponent {
23 23 @Input()
24 24 profile: noosfero.Profile = null;
25 25  
26   - constructor(private articleService: ArticleService, private profileService: ProfileService, private $log: ng.ILogService, private $stateParams: angular.ui.IStateParamsService) {
  26 + constructor(
  27 + private articleService: ArticleService,
  28 + private profileService: ProfileService,
  29 + private $stateParams: angular.ui.IStateParamsService) {
27 30 this.activate();
28 31 }
29 32  
... ...
src/app/article/content-viewer/navbar-actions.html
1   -<ul class="nav navbar-nav">
  1 +<ul class="nav navbar-nav" permission="vm.profile.permissions" permission-action="allow_edit">
2 2 <li class="dropdown profile-menu" uib-dropdown>
3 3 <a class="btn dropdown-toggle" data-toggle="dropdown" uib-dropdown-toggle>
4 4 {{"navbar.content_viewer_actions.new_item" | translate}}
5 5 <i class="fa fa-caret-down"></i>
6 6 </a>
7 7 <ul class="dropdown-menu" uib-dropdown-menu ng-show="vm.profile">
8   - <li ng-show="vm.parentId">
  8 + <li>
9 9 <a href="#" ui-sref="main.cms({profile: vm.profile.identifier, parent_id: vm.parentId})">
10 10 <i class="fa fa-file fa-fw fa-lg"></i> {{"navbar.content_viewer_actions.new_post" | translate}}
11 11 </a>
... ... @@ -17,6 +17,4 @@
17 17 </li>
18 18 </ul>
19 19 </li>
20   -
21 20 </ul>
22   -
... ...
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
... ... @@ -2,6 +2,7 @@ import {StateConfig, Component, Inject, provide} from &#39;ng-forward&#39;;
2 2 import {EnvironmentService} from "../../lib/ng-noosfero-api/http/environment.service";
3 3 import {NotificationService} from "../shared/services/notification.service";
4 4 import {EnvironmentHomeComponent} from "./environment-home.component";
  5 +import {SearchComponent} from "../search/search.component";
5 6  
6 7 /**
7 8 * @ngdoc controller
... ... @@ -29,23 +30,37 @@ import {EnvironmentHomeComponent} from &quot;./environment-home.component&quot;;
29 30 controllerAs: "vm"
30 31 }
31 32 }
  33 + },
  34 + {
  35 + url: '^/search?query',
  36 + component: SearchComponent,
  37 + name: 'main.environment.search',
  38 + views: {
  39 + "mainBlockContent": {
  40 + templateUrl: "app/search/search.html",
  41 + controller: SearchComponent,
  42 + controllerAs: "ctrl"
  43 + }
  44 + }
32 45 }
33 46 ])
34   -@Inject(EnvironmentService, "$state")
  47 +@Inject(EnvironmentService, "$state", "currentEnvironment")
35 48 export class EnvironmentComponent {
36 49  
37 50 boxes: noosfero.Box[];
38 51 environment: noosfero.Environment;
39 52  
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   - });
  53 + constructor(private environmentService: EnvironmentService, private $state: ng.ui.IStateService, private notificationService: NotificationService, currentEnvironment: noosfero.Environment) {
  54 + this.environment = currentEnvironment;
  55 +
  56 + this.environmentService.getBoxes(this.environment.id)
  57 + .then((boxes: noosfero.Box[]) => {
  58 + this.boxes = boxes;
  59 + }).catch(() => {
  60 + this.$state.transitionTo('main');
  61 + this.notificationService.error({ message: "notification.environment.not_found" });
  62 + });
  63 +
50 64 }
  65 +
51 66 }
... ...
src/app/index.module.ts
... ... @@ -1,97 +0,0 @@
1   -
2   -/**
3   - * @ngdoc service
4   - * @name NoosferoApp
5   - * @description
6   - * The main NoosferoApp module class. It provide helper static methods used by
7   - * the module to initialize the application.
8   - */
9   -export class NoosferoApp {
10   -
11   - /**
12   - * @ngdoc property
13   - * @name appName
14   - * @propertyOf NoosferoApp
15   - * @returns {string} the name of this application ('noosferoApp')
16   - */
17   - static appName: string = "noosferoApp";
18   -
19   - /**
20   - * @ngdoc property
21   - * @name angularModule
22   - * @propertyOf NoosferoApp
23   - * @returns {any} all the modules installed for this application
24   - */
25   - static angularModule: any;
26   -
27   - /**
28   - * @ngdoc method
29   - * @name addConfig
30   - * @methodOf NoosferoApp
31   - * @param {Function} configFunc the configuration function to add
32   - * @descprition adds a configuration function to
33   - * the {@link NoosferoApp#angularModule}
34   - */
35   - static addConfig(configFunc: Function) {
36   - NoosferoApp.angularModule.config(configFunc);
37   - }
38   -
39   - /**
40   - * @ngdoc method
41   - * @name addConstants
42   - * @methodOf NoosferoApp
43   - * @param {string} constantName the constant name
44   - * @param {any} value the constant value
45   - * @description adds a constant to the {@link NoosferoApp#angularModule}
46   - */
47   - static addConstants(constantName: string, value: any) {
48   - NoosferoApp.angularModule.constant(constantName, value);
49   - }
50   -
51   - /**
52   - * @ngdoc method
53   - * @name addService
54   - * @methodOf NoosferoApp
55   - * @param {string} serviceName the service name
56   - * @param {any} value the service value
57   - * @description adds a service to the {@link NoosferoApp#angularModule}
58   - */
59   - static addService(serviceName: string, value: any) {
60   - NoosferoApp.angularModule.service(serviceName, value);
61   - }
62   -
63   - /**
64   - * @ngdoc method
65   - * @name addFactory
66   - * @methodOf NoosferoApp
67   - * @param {string} factoryName the factory name
68   - * @param {any} value the factory value
69   - * @description adds a factory to the {@link NoosferoApp#angularModule}
70   - */
71   - static addFactory(factoryName: string, value: any) {
72   - NoosferoApp.angularModule.factory(factoryName, value);
73   - }
74   -
75   - /**
76   - * @ngdoc method
77   - * @name addController
78   - * @methodOf NoosferoApp
79   - * @param {string} controllerName the controller name
80   - * @param {any} value the controller value
81   - * @description adds a controller to the {@link NoosferoApp#angularModule}
82   - */
83   - static addController(controllerName: string, value: any) {
84   - NoosferoApp.angularModule.controller(controllerName, value);
85   - }
86   -
87   - /**
88   - * @ngdoc method
89   - * @name run
90   - * @methodOf NoosferoApp
91   - * @param {Function} runFunction the function to execute
92   - * @description runs a function using the {@link NoosferoApp#angularModule}
93   - */
94   - static run(runFunction: Function) {
95   - NoosferoApp.angularModule.run(runFunction);
96   - }
97   -}
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 5 import {AuthEvents} from "./login/auth-events";
9   -import {AuthController} from "./login/auth.controller";
10   -
11   -import {Navbar} from "./layout/navbar/navbar";
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", "ngCkeditor",
18   - "angular-bind-html-compile", "angularMoment", "angular.filter", "akoenig.deckgrid",
19   - "angular-timeline", "duScroll", "oitozero.ngSweetAlert",
20   - "pascalprecht.translate", "tmh.dynamicLocale", "angularLoad",
21   - "angular-click-outside"]).publish();
22   -
23   -NoosferoApp.angularModule = noosferoApp;
24   -
25   -
26   -NoosferoApp.addConstants("moment", moment);
27   -NoosferoApp.addConstants("AuthEvents", AuthEvents);
28   -
29   -NoosferoApp.addConfig(noosferoModuleConfig);
30   -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/main/main-block.component.spec.ts
1 1 import {TestComponentBuilder} from 'ng-forward/cjs/testing/test-component-builder';
2 2 import {Input, provide, Component, StateConfig} from 'ng-forward';
3   -
4 3 import {MainBlockComponent} from './main-block.component';
5   -import {NoosferoApp} from '../../../index.module';
  4 +
6 5  
7 6 const tcb = new TestComponentBuilder();
8 7  
... ... @@ -37,4 +36,4 @@ describe(&quot;Components&quot;, () =&gt; {
37 36 });
38 37 });
39 38 });
40   -});
41 39 \ No newline at end of file
  40 +});
... ...
src/app/layout/navbar/navbar.html
1 1 <nav class="navbar navbar-static-top navbar-inverse">
2 2 <div class="container-fluid">
3 3 <div class="navbar-header">
4   - <button type="button" class="navbar-toggle collapsed" (click)="ctrl.toggleCollapse()" ng-show="ctrl.showHamburguer">
  4 + <button type="button" class="navbar-toggle collapsed" (click)="ctrl.toggleCollapse()" ng-show="ctrl.showHamburger">
5 5 <span class="sr-only">{{"navbar.toggle_menu" | translate}}</span>
6 6 <i class="fa fa-bars"></i>
7 7 </button>
8 8 <a class="navbar-brand" ui-sref="main.environment.home">
9 9 <span class="noosfero-logo"> </span>
10   - <span class="noosfero-name">{{"noosfero.name" | translate}}</span>
  10 + <span class="noosfero-name">{{ ctrl.currentEnvironment.name }}</span>
11 11 </a>
12 12 </div>
13 13  
... ... @@ -26,23 +26,29 @@
26 26 <span ng-bind="ctrl.currentUser.person.name"></span> <b class="caret"></b>
27 27 </a>
28 28 <ul class="dropdown-menu" uib-dropdown-menu>
29   - <li>
30   - <a ui-sref="main.profile.info({profile: ctrl.currentUser.person.identifier})"><i class="fa fa-fw fa-user"></i> {{"navbar.profile" | translate}}</a>
31   - </li>
32   - <li>
33   - <a target="_self" ui-sref="main.profile.settings({profile: ctrl.currentUser.person.identifier})"><i class="fa fa-fw fa-gear"></i> {{"navbar.settings" | translate}}</a>
34   - </li>
35   - <li class="divider"></li>
36   - <li>
37   - <a href="#" ng-click="ctrl.logout()"><i class="fa fa-fw fa-power-off"></i> {{"navbar.logout" | translate}}</a>
38   - </li>
  29 + <li>
  30 + <a ui-sref="main.profile.info({profile: ctrl.currentUser.person.identifier})"><i class="fa fa-fw fa-user"></i> {{"navbar.profile" | translate}}</a>
  31 + </li>
  32 + <li>
  33 + <a target="_self" ui-sref="main.profile.settings({profile: ctrl.currentUser.person.identifier})"><i class="fa fa-fw fa-gear"></i> {{"navbar.settings" | translate}}</a>
  34 + </li>
  35 + <li class="divider"></li>
  36 + <li>
  37 + <a href="#" ng-click="ctrl.logout()"><i class="fa fa-fw fa-power-off"></i> {{"navbar.logout" | translate}}</a>
  38 + </li>
39 39 </ul>
40 40 </li>
41 41 </ul>
  42 +
42 43 <ul class="nav navbar-nav navbar-right">
43 44 <language-selector class="nav navbar-nav navbar-right"></language-selector>
44 45 </ul>
45 46 <div ui-view="actions"></div>
  47 + <div class="nav navbar-nav search navbar-right">
  48 + <search-form></search-form>
  49 + </div>
46 50 </div>
47 51 </div>
48 52 </nav>
  53 +<div ui-view="toolbar">
  54 +</div>
49 55 \ No newline at end of file
... ...
src/app/layout/navbar/navbar.scss
... ... @@ -34,6 +34,16 @@
34 34 text-align: center;
35 35 }
36 36  
  37 + .search {
  38 + .search-input {
  39 + height: 45px;
  40 + margin-top: 10px;
  41 + }
  42 + .input-group-btn {
  43 + padding-top: 10px;
  44 + }
  45 + }
  46 +
37 47 @media (max-width: $break-sm-max) {
38 48 .navbar-toggle .fa-bars{
39 49 font-size: 14pt;
... ...
src/app/layout/navbar/navbar.spec.ts
... ... @@ -69,6 +69,11 @@ describe(&quot;Components&quot;, () =&gt; {
69 69 AuthEvents
70 70 }
71 71 }),
  72 + provide('EnvironmentService', {
  73 + useValue: {
  74 + getCurrentEnvironment: () => { return { id: 1, name: 'Nosofero' }; }
  75 + }
  76 + }),
72 77 provide('TranslatorService', {
73 78 useValue: helpers.mocks.translatorService
74 79 })
... ...
src/app/layout/navbar/navbar.ts
1 1 import {Component, Inject, EventEmitter, Input} from "ng-forward";
2 2 import {LanguageSelectorComponent} from "../language-selector/language-selector.component";
3 3 import {SessionService, AuthService, AuthController, AuthEvents} from "./../../login";
  4 +import {EnvironmentService} from "./../../../lib/ng-noosfero-api/http/environment.service";
4 5 import {SidebarNotificationService} from "../sidebar/sidebar.notification.service";
5 6 import {BodyStateClassesService} from '../services/body-state-classes.service';
  7 +import {DesignModeTogglerComponent} from './../../admin/layout-edit/designModeToggler.component';
  8 +import {BootstrapSwitcherComponent, BootstrapSwitcherItem} from './../../shared/components/bootstrap-switcher/bootstrap-switcher.component';
6 9  
7 10 @Component({
8 11 selector: "acme-navbar",
9 12 templateUrl: "app/layout/navbar/navbar.html",
10   - directives: [LanguageSelectorComponent],
11   - providers: [AuthService, SessionService, SidebarNotificationService]
  13 + directives: [LanguageSelectorComponent, DesignModeTogglerComponent, BootstrapSwitcherComponent],
  14 + providers: [AuthService, SessionService, SidebarNotificationService, EnvironmentService]
12 15 })
13   -@Inject("$uibModal", AuthService, "SessionService", "$state", SidebarNotificationService, BodyStateClassesService)
  16 +@Inject("$uibModal", AuthService, "SessionService", "$state", SidebarNotificationService, BodyStateClassesService, EnvironmentService)
14 17 export class Navbar {
15 18  
16 19 private currentUser: noosfero.User;
17 20 private modalInstance: any = null;
18   -
19   - public showHamburguer: boolean = false;
20   -
  21 + public showHamburger: boolean = false;
  22 + public currentEnvironment: noosfero.Environment = <any>{ name: '' };
21 23 /**
22 24 *
23 25 */
... ... @@ -27,11 +29,13 @@ export class Navbar {
27 29 private session: SessionService,
28 30 private $state: ng.ui.IStateService,
29 31 private sidebarNotificationService: SidebarNotificationService,
30   - private bodyStateService: BodyStateClassesService
  32 + private bodyStateService: BodyStateClassesService,
  33 + private environmentService: EnvironmentService
31 34 ) {
32 35 this.currentUser = this.session.currentUser();
  36 + this.currentEnvironment = environmentService.getCurrentEnvironment();
33 37  
34   - this.showHamburguer = this.authService.isAuthenticated();
  38 + this.showHamburger = this.authService.isAuthenticated();
35 39 this.bodyStateService.addContentClass(!this.sidebarNotificationService.sidebarVisible);
36 40  
37 41 this.authService.subscribe(AuthEvents[AuthEvents.loginSuccess], () => {
... ... @@ -41,7 +45,7 @@ export class Navbar {
41 45 }
42 46  
43 47 this.currentUser = this.session.currentUser();
44   - this.showHamburguer = true;
  48 + this.showHamburger = true;
45 49  
46 50 this.$state.go(this.$state.current, {}, { reload: true }); // TODO move to auth
47 51 });
... ...
src/app/layout/scss/_layout.scss
... ... @@ -35,3 +35,24 @@
35 35 padding: 0 20px 20px 20px;
36 36 }
37 37 }
  38 +
  39 +
  40 +body.noosfero-design-on {
  41 +
  42 + div.content-wrapper {
  43 + opacity: 0.5;
  44 + }
  45 +}
  46 +
  47 +ul.nav > li.toggler-container {
  48 + position: relative;
  49 + padding-top: 12px;
  50 +}
  51 +
  52 +.noosfero-main-toolbar {
  53 + padding: 5px;
  54 + @include make-row();
  55 + margin-left: 0px;
  56 + margin-right: 0px;
  57 + background-color: #edecec;
  58 +}
38 59 \ No newline at end of file
... ...
src/app/layout/scss/skins/_whbl.scss
... ... @@ -3,7 +3,7 @@ $whbl-sidebar-linkColor: #484848;
3 3 $whbl-primary-color: #1E96D0; //blue
4 4 $whbl-font-color: #16191c;
5 5  
6   -.skin-whbl {
  6 +%skin-base {
7 7 #header-navbar {
8 8 background-color: $whbl-primary-color;
9 9 }
... ... @@ -180,27 +180,19 @@ $whbl-font-color: #16191c;
180 180 }
181 181 .pagination {
182 182 > li {
183   - > a,
184   - > span,
185   - > a:hover,
186   - > span:hover,
187   - > a:focus,
188   - > span:focus,
189   - > a:active,
190   - > span:active {
191   - color: $whbl-primary-color;
  183 + &.active {
  184 + a {
  185 + background-color: $whbl-primary-color;
  186 + color: #FFF;
  187 + }
192 188 }
193   - }
194   - > .active {
195   - > a,
196   - > span,
197   - > a:hover,
198   - > span:hover,
199   - > a:focus,
200   - > span:focus {
201   - background-color: $whbl-primary-color;
202   - border-color: $whbl-primary-color;
203   - color: #fff;
  189 + > a {
  190 + &:hover {
  191 + background-color: $whbl-primary-color;
  192 + color: #FFF;
  193 + }
  194 + background-color: #FFF;
  195 + color: $whbl-primary-color;
204 196 }
205 197 }
206 198 }
... ... @@ -289,6 +281,31 @@ $whbl-font-color: #16191c;
289 281 .pace .pace-progress {
290 282 background-color: #fff;
291 283 }
  284 +
  285 + noosfero-design-toggler .ats-switch .knob i {
  286 + color: #999999;
  287 + }
  288 +
  289 + .ats-switch .knob {
  290 + padding-right: 15px;
  291 + }
  292 +
  293 + .ats-switch .glyphicon {
  294 + top: 2px;
  295 + }
  296 +
  297 + .ats-switch .switch-left {
  298 + background-color: #41b941;
  299 + font-weight: bold;
  300 + color: white;
  301 + }
  302 +
  303 + .ats-switch .switch-right {
  304 + background-color: #ce3b3b;
  305 + font-weight: bold;
  306 + color: white;
  307 + }
  308 +
292 309 }
293 310 .rtl.skin-whbl #content-wrapper {
294 311 border-left: 0;
... ... @@ -296,7 +313,7 @@ $whbl-font-color: #16191c;
296 313 }
297 314  
298 315 @media (max-width: $break-sm-max) {
299   - .skin-whbl {
  316 + %skin-base {
300 317 #logo.navbar-brand > img.normal-logo.logo-white {
301 318 display: block;
302 319 }
... ... @@ -308,3 +325,7 @@ $whbl-font-color: #16191c;
308 325 }
309 326 }
310 327 }
  328 +
  329 +.skin-whbl {
  330 + @extend %skin-base;
  331 +}
... ...
src/app/layout/services/body-state-classes.service.spec.ts
... ... @@ -4,7 +4,7 @@ import {AuthService} from &quot;./../../login/auth.service&quot;;
4 4 import {AuthEvents} from "./../../login/auth-events";
5 5  
6 6 import {EventEmitter} from 'ng-forward';
7   -
  7 +import {DesignModeService} from './../../admin/layout-edit/designMode.service';
8 8  
9 9 describe("BodyStateClasses Service", () => {
10 10  
... ... @@ -19,10 +19,13 @@ describe(&quot;BodyStateClasses Service&quot;, () =&gt; {
19 19 },
20 20 authService: any = helpers.mocks.authService,
21 21 bodyEl: { className: string },
22   - bodyElJq: any;
  22 + bodyElJq: any,
  23 + designModeService = new DesignModeService();
  24 +
  25 +
23 26  
24 27 let getService = (): BodyStateClassesService => {
25   - return new BodyStateClassesService($rootScope, $document, $state, authService);
  28 + return new BodyStateClassesService($rootScope, $document, $state, authService, designModeService);
26 29 };
27 30  
28 31 beforeEach(() => {
... ... @@ -168,4 +171,30 @@ describe(&quot;BodyStateClasses Service&quot;, () =&gt; {
168 171  
169 172 expect(contentWrapperMock.removeClass).toHaveBeenCalledWith(BodyStateClassesService.CONTENT_WRAPPER_FULL);
170 173 });
  174 +
  175 + it("should add the class noosfero-design-on when designMode is changed to true", () => {
  176 + let fnOnToggle: Function = null;
  177 + designModeService.onToggle = <any> {
  178 + subscribe: (fn: Function) => {
  179 + fnOnToggle = fn;
  180 + },
  181 + next: (value: boolean) => {
  182 + fnOnToggle.apply(designModeService, [value]);
  183 + }
  184 + };
  185 +
  186 + let service = getService();
  187 +
  188 + bodyElJq.addClass = jasmine.createSpy("addClass");
  189 + bodyElJq.removeClass = jasmine.createSpy("removeClass");
  190 + service["bodyElement"] = bodyElJq;
  191 +
  192 + service.start();
  193 +
  194 + debugger;
  195 + designModeService.setInDesignMode(true);
  196 +
  197 +
  198 + expect(bodyElJq.addClass).toHaveBeenCalledWith(BodyStateClassesService.DESIGN_MODE_ON_CLASSNAME);
  199 + });
171 200 });
... ...
src/app/layout/services/body-state-classes.service.ts
... ... @@ -3,6 +3,7 @@ import {AuthEvents} from &quot;../../login/auth-events&quot;;
3 3 import {AuthService} from "./../../login/auth.service";
4 4 import {HtmlUtils} from "../html-utils";
5 5 import {INgForwardJQuery} from 'ng-forward/cjs/util/jqlite-extensions';
  6 +import {DesignModeService} from './../../admin/layout-edit/designMode.service';
6 7  
7 8 export interface StartParams {
8 9 skin?: string;
... ... @@ -22,12 +23,13 @@ export interface StartParams {
22 23 * - full-content
23 24 */
24 25 @Injectable()
25   -@Inject("$rootScope", "$document", "$state", AuthService)
  26 +@Inject("$rootScope", "$document", "$state", AuthService, DesignModeService)
26 27 export class BodyStateClassesService {
27 28  
28 29 private started: boolean = false;
29 30 private skin: string;
30 31  
  32 + public static get DESIGN_MODE_ON_CLASSNAME(): string { return "noosfero-design-on"; }
31 33 public static get USER_LOGGED_CLASSNAME(): string { return "noosfero-user-logged"; }
32 34 public static get ROUTE_STATE_CLASSNAME_PREFIX(): string { return "noosfero-route-"; }
33 35 public static get CONTENT_WRAPPER_FULL(): string { return "full-content"; }
... ... @@ -38,16 +40,16 @@ export class BodyStateClassesService {
38 40 private $rootScope: ng.IRootScopeService,
39 41 private $document: ng.IDocumentService,
40 42 private $state: ng.ui.IStateService,
41   - private authService: AuthService
  43 + private authService: AuthService,
  44 + private designModeService: DesignModeService
42 45 ) {
43   -
44 46 }
45 47  
46 48 start(config?: StartParams) {
47 49 if (!this.started) {
48 50 this.setupUserLoggedClassToggle();
49 51 this.setupStateClassToggle();
50   -
  52 + this.setupDesignModeClassToggle();
51 53 if (config) {
52 54 this.setThemeSkin(config.skin);
53 55 }
... ... @@ -87,9 +89,19 @@ export class BodyStateClassesService {
87 89 }
88 90  
89 91 /**
90   - * Setup the initial class name on body element indicating the current route
91   - * and adds event handler to swith this class when the current page/state changes
  92 + * setup the listeners to the desigModeService to add class on the Body Element
  93 + * indicating the user activated the designMode
92 94 */
  95 + private setupDesignModeClassToggle() {
  96 + this.designModeService.onToggle.subscribe((designOn: boolean) => {
  97 + if (designOn) {
  98 + this.getBodyElement().addClass(BodyStateClassesService.DESIGN_MODE_ON_CLASSNAME);
  99 + } else {
  100 + this.getBodyElement().removeClass(BodyStateClassesService.DESIGN_MODE_ON_CLASSNAME);
  101 + }
  102 + });
  103 + }
  104 +
93 105 private setupStateClassToggle() {
94 106 let bodyElement = this.getBodyElement();
95 107 bodyElement.addClass(BodyStateClassesService.ROUTE_STATE_CLASSNAME_PREFIX + this.$state.current.name);
... ...
src/app/login/auth.service.spec.ts
... ... @@ -17,7 +17,7 @@ describe(&quot;Services&quot;, () =&gt; {
17 17 let $rootScope: ng.IRootScopeService;
18 18 let user: noosfero.User;
19 19  
20   - beforeEach(angular.mock.module("noosferoApp", ($translateProvider: angular.translate.ITranslateProvider) => {
  20 + beforeEach(angular.mock.module("main", ($translateProvider: angular.translate.ITranslateProvider) => {
21 21 $translateProvider.translations('en', {});
22 22 }));
23 23  
... ...
src/app/main/main.component.spec.ts 0 → 100644
... ... @@ -0,0 +1,86 @@
  1 +import { provide, Component, componentStore, bundleStore } from "ng-forward";
  2 +import {MainComponent} from "./main.component";
  3 +import {TestComponentBuilder, ComponentFixture} from "ng-forward/cjs/testing/test-component-builder";
  4 +
  5 +import {quickCreateComponent} from "../../spec/helpers";
  6 +import {getAngularServiceFactory} from "../../spec/helpers";
  7 +
  8 +describe("MainComponent", function() {
  9 +
  10 + let localFixture: ComponentFixture;
  11 + let $state: angular.ui.IStateService;
  12 + let $q: ng.IQService;
  13 + let $httpBackend: ng.IHttpBackendService;
  14 + let authService: any = jasmine.createSpyObj("authService", ["loginFromCookie", "isAuthenticated", "subscribe"]);
  15 + let environmentService: any = jasmine.createSpyObj("environmentService", ["get", "getCurrentEnvironment"]);
  16 +
  17 + beforeEach(angular.mock.module("ui.router"));
  18 + beforeEach(angular.mock.module("templates"));
  19 + beforeEach(angular.mock.module("pascalprecht.translate", ($translateProvider: angular.translate.ITranslateProvider) => {
  20 + $translateProvider.translations('en', {});
  21 + }));
  22 +
  23 + @Component({
  24 + selector: "parent",
  25 + template: "<main></main>",
  26 + directives: [MainComponent],
  27 + providers: [
  28 + provide("AuthService",
  29 + {
  30 + useValue: authService
  31 + }),
  32 +
  33 + provide("EnvironmentService",
  34 + {
  35 + useValue: environmentService
  36 + }),
  37 + ]
  38 + })
  39 + class MainComponentParent {
  40 + constructor() {
  41 +
  42 + }
  43 + }
  44 +
  45 + beforeEach(() => {
  46 + authService.loginFromCookie = jasmine.createSpy("loginFromCookie").and.returnValue({ id: 1, name: "user1" });
  47 + environmentService.get = jasmine.createSpy("get").and.returnValue({ id: 1, name: "Noosfero Default Environment" });
  48 + });
  49 +
  50 + it("renders the main component only when the login and environment were resolved", (done) => {
  51 + quickCreateComponent({ directives: [MainComponentParent], template: "<parent></parent>" })
  52 + .then((fixture) => {
  53 + fixture.debugElement.getLocal("$httpBackend").expectGET("/api/v1/environments/1/boxes").respond(200, {});
  54 + localFixture = fixture;
  55 + // get the $state service to navigate between routes
  56 + $state = fixture.debugElement.getLocal("$state");
  57 + // navigates to the environment home
  58 + $state.go("main.environment.home");
  59 + localFixture.detectChanges();
  60 + // after changes were detected it checks the current $state route
  61 + expect($state.current.name).toEqual("main.environment.home");
  62 + done();
  63 + });
  64 +
  65 + });
  66 +
  67 + it("does not render the main component when get error loading the environment", (done) => {
  68 + quickCreateComponent({ directives: [MainComponentParent], template: "<parent></parent>" })
  69 + .then((fixture) => {
  70 + localFixture = fixture;
  71 + // get the $state service to navigate between routes
  72 + $state = fixture.debugElement.getLocal("$state");
  73 + // get the $q service to create a rejected promise
  74 + $q = fixture.debugElement.getLocal("$q");
  75 + // mock the environmentService to force a rejected promise
  76 + environmentService.get = jasmine.createSpy("get").and.returnValue($q.reject("Error simulated"));
  77 + // tries to navigate to the environment home
  78 + $state.go("main.environment.home");
  79 + localFixture.detectChanges();
  80 + // after the changes were detected the state remains '' because the environment could not be loaded
  81 + expect($state.current.name).toEqual("");
  82 + done();
  83 + });
  84 +
  85 + });
  86 +});
... ...
src/app/main/main.component.ts
... ... @@ -28,7 +28,7 @@ import {DateFormat} from &quot;../shared/pipes/date-format.filter&quot;;
28 28  
29 29 import {AuthService} from "../login/auth.service";
30 30 import {SessionService} from "../login/session.service";
31   -
  31 +import {EnvironmentService} from "./../../lib/ng-noosfero-api/http/environment.service";
32 32 import {NotificationService} from "../shared/services/notification.service";
33 33  
34 34 import {BodyStateClassesService} from "./../layout/services/body-state-classes.service";
... ... @@ -39,7 +39,9 @@ import {SidebarComponent} from &quot;../layout/sidebar/sidebar.component&quot;;
39 39  
40 40 import {MainBlockComponent} from "../layout/blocks/main/main-block.component";
41 41 import {HtmlEditorComponent} from "../shared/components/html-editor/html-editor.component";
42   -
  42 +import {PermissionDirective} from "../shared/components/permission/permission.directive";
  43 +import {SearchComponent} from "../search/search.component";
  44 +import {SearchFormComponent} from "../search/search-form/search-form.component";
43 45  
44 46 /**
45 47 * @ngdoc controller
... ... @@ -93,17 +95,23 @@ export class EnvironmentContent {
93 95 */
94 96 @Component({
95 97 selector: 'main',
96   - template: '<div ng-view></div>',
  98 + template: '<ui-view></ui-view>',
97 99 directives: [
98 100 ArticleBlogComponent, ArticleViewComponent, BoxesComponent, BlockComponent,
99 101 EnvironmentComponent, PeopleBlockComponent, DisplayContentBlockComponent,
100   - LinkListBlockComponent, CommunitiesBlockComponent, HtmlEditorComponent,
  102 + LinkListBlockComponent, CommunitiesBlockComponent, HtmlEditorComponent, ProfileComponent,
101 103 MainBlockComponent, RecentDocumentsBlockComponent, Navbar, SidebarComponent, ProfileImageBlockComponent,
102 104 MembersBlockComponent, NoosferoTemplate, DateFormat, RawHTMLBlockComponent, StatisticsBlockComponent,
103   - LoginBlockComponent, CustomContentComponent
  105 + LoginBlockComponent, CustomContentComponent, PermissionDirective, SearchFormComponent, SearchComponent
104 106 ].concat(plugins.mainComponents).concat(plugins.hotspots),
105   -
106   - providers: [AuthService, SessionService, NotificationService, BodyStateClassesService]
  107 + providers: [AuthService, SessionService, NotificationService, BodyStateClassesService,
  108 + "ngAnimate", "ngCookies", "ngStorage", "ngTouch",
  109 + "ngSanitize", "ngMessages", "ngAria", "restangular",
  110 + "ui.router", "ui.bootstrap", "toastr", "ngCkeditor",
  111 + "angular-bind-html-compile", "angularMoment", "angular.filter", "akoenig.deckgrid",
  112 + "angular-timeline", "duScroll", "oitozero.ngSweetAlert",
  113 + "pascalprecht.translate", "tmh.dynamicLocale", "angularLoad",
  114 + "angular-click-outside", "toggle-switch", "noosfero.init"]
107 115 })
108 116 @StateConfig([
109 117 {
... ... @@ -114,6 +122,9 @@ export class EnvironmentContent {
114 122 resolve: {
115 123 currentUser: function(AuthService: AuthService) {
116 124 return AuthService.loginFromCookie();
  125 + },
  126 + currentEnvironment: function(EnvironmentService: EnvironmentService) {
  127 + return EnvironmentService.get();
117 128 }
118 129 }
119 130 },
... ... @@ -144,5 +155,4 @@ export class EnvironmentContent {
144 155 }
145 156 }
146 157 ])
147   -export class MainComponent {
148   -}
  158 +export class MainComponent { }
... ...
src/app/profile/custom-content/custom-content.component.spec.ts
... ... @@ -73,5 +73,16 @@ describe(&quot;Components&quot;, () =&gt; {
73 73 helper.component.save();
74 74 expect(helper.component['notificationService'].success).toHaveBeenCalled();
75 75 });
  76 +
  77 + it("hide button to edit content when user doesn't have the permission", () => {
  78 + helper.detectChanges();
  79 + expect(helper.find(".actions").attr('style')).toEqual('display: none; ');
  80 + });
  81 +
  82 + it("show button to edit content when user has the permission", () => {
  83 + (<any>helper.component['profile'])['permissions'] = ['allow_edit'];
  84 + helper.detectChanges();
  85 + expect(helper.find(".actions").attr('style')).toEqual('');
  86 + });
76 87 });
77 88 });
... ...
src/app/profile/custom-content/custom-content.component.ts
1 1 import {Component, Input, Inject} from 'ng-forward';
2 2 import {ProfileService} from '../../../lib/ng-noosfero-api/http/profile.service';
3 3 import {NotificationService} from '../../shared/services/notification.service';
  4 +import {PermissionDirective} from '../../shared/components/permission/permission.directive';
4 5  
5 6 @Component({
6 7 selector: 'custom-content',
7 8 templateUrl: "app/profile/custom-content/custom-content.html",
  9 + directives: [PermissionDirective]
8 10 })
9 11 @Inject("$uibModal", "$scope", ProfileService, NotificationService)
10 12 export class CustomContentComponent {
... ...
src/app/profile/custom-content/custom-content.html
1 1 <div class="custom-content">
2   - <div class="actions">
  2 + <div class="actions" permission="ctrl.profile.permissions" permission-action="allow_edit">
3 3 <button type="submit" class="btn btn-xs btn-default" ng-click="ctrl.openEdit()"><i class="fa fa-edit fa-fw"></i> {{ctrl.label | translate}}</button>
4 4 </div>
5 5 <div class="content" ng-bind-html="ctrl.content"></div>
... ...
src/app/profile/profile-toolbar.component.ts 0 → 100644
... ... @@ -0,0 +1,21 @@
  1 +import {Component, Inject, provide} from "ng-forward";
  2 +import {ProfileService} from "../../lib/ng-noosfero-api/http/profile.service";
  3 +
  4 +@Component({
  5 + selector: "profile-toolbar",
  6 + templateUrl: "app/profile/toolbar.html",
  7 + providers: [
  8 + provide('profileService', { useClass: ProfileService })
  9 + ]
  10 +})
  11 +@Inject(ProfileService)
  12 +export class ProfileToolbarComponent {
  13 + profile: noosfero.Profile;
  14 + parentId: number;
  15 +
  16 + constructor(profileService: ProfileService) {
  17 + profileService.getCurrentProfile().then((profile: noosfero.Profile) => {
  18 + this.profile = profile;
  19 + });
  20 + }
  21 +}
... ...
src/app/profile/profile.component.ts
... ... @@ -10,7 +10,7 @@ import {ProfileService} from &quot;../../lib/ng-noosfero-api/http/profile.service&quot;;
10 10 import {NotificationService} from "../shared/services/notification.service";
11 11 import {MyProfileComponent} from "./myprofile.component";
12 12 import {ProfileActionsComponent} from "./profile-actions.component";
13   -
  13 +import {ProfileToolbarComponent} from "./profile-toolbar.component";
14 14 /**
15 15 * @ngdoc controller
16 16 * @name profile.Profile
... ... @@ -42,6 +42,11 @@ import {ProfileActionsComponent} from &quot;./profile-actions.component&quot;;
42 42 templateUrl: "app/profile/navbar-actions.html",
43 43 controller: ProfileActionsComponent,
44 44 controllerAs: "vm"
  45 + },
  46 + "toolbar@main": {
  47 + templateUrl: "app/profile/toolbar.html",
  48 + controller: ProfileToolbarComponent,
  49 + controllerAs: "vm"
45 50 }
46 51 }
47 52 },
... ... @@ -54,6 +59,11 @@ import {ProfileActionsComponent} from &quot;./profile-actions.component&quot;;
54 59 templateUrl: "app/profile/navbar-actions.html",
55 60 controller: ProfileActionsComponent,
56 61 controllerAs: "vm"
  62 + },
  63 + "toolbar@main": {
  64 + templateUrl: "app/profile/toolbar.html",
  65 + controller: ProfileToolbarComponent,
  66 + controllerAs: "vm"
57 67 }
58 68 }
59 69 },
... ... @@ -106,6 +116,11 @@ import {ProfileActionsComponent} from &quot;./profile-actions.component&quot;;
106 116 templateUrl: "app/article/content-viewer/navbar-actions.html",
107 117 controller: ContentViewerActionsComponent,
108 118 controllerAs: "vm"
  119 + },
  120 + "toolbar@main": {
  121 + templateUrl: "app/profile/toolbar.html",
  122 + controller: ProfileToolbarComponent,
  123 + controllerAs: "vm"
109 124 }
110 125 }
111 126 }
... ...
src/app/profile/toolbar.html 0 → 100644
... ... @@ -0,0 +1,4 @@
  1 +<div class="noosfero-main-toolbar" permission="vm.profile.permissions" permission-action="allow_edit">
  2 + <noosfero-design-toggler class="pull-right"></noosfero-design-toggler>
  3 +
  4 +</div>
0 5 \ No newline at end of file
... ...
src/app/search/search-form/search-form.component.spec.ts 0 → 100644
... ... @@ -0,0 +1,34 @@
  1 +import {ComponentTestHelper, createClass} from "../../../spec/component-test-helper";
  2 +import {SearchFormComponent} from "./search-form.component";
  3 +import * as helpers from "../../../spec/helpers";
  4 +
  5 +const htmlTemplate: string = '<search-form></search-form>';
  6 +
  7 +describe("Components", () => {
  8 + describe("Search Form Component", () => {
  9 +
  10 + let helper: ComponentTestHelper<SearchFormComponent>;
  11 + let stateMock = jasmine.createSpyObj("$state", ["go"]);
  12 +
  13 + beforeEach(angular.mock.module("templates"));
  14 +
  15 + beforeEach((done) => {
  16 + let cls = createClass({
  17 + template: htmlTemplate,
  18 + directives: [SearchFormComponent],
  19 + providers: [helpers.createProviderToValue("$state", stateMock)]
  20 + });
  21 + helper = new ComponentTestHelper<SearchFormComponent>(cls, done);
  22 + });
  23 +
  24 + it("render a input for search query", () => {
  25 + expect(helper.find(".search-input").length).toEqual(1);
  26 + });
  27 +
  28 + it("go to search page when click on search button", () => {
  29 + helper.component.query = 'query';
  30 + helper.component.search();
  31 + expect(stateMock.go).toHaveBeenCalledWith('main.environment.search', { query: 'query' });
  32 + });
  33 + });
  34 +});
... ...
src/app/search/search-form/search-form.component.ts 0 → 100644
... ... @@ -0,0 +1,26 @@
  1 +import {Component, Inject} from "ng-forward";
  2 +
  3 +@Component({
  4 + selector: 'search-form',
  5 + templateUrl: 'app/search/search-form/search-form.html'
  6 +})
  7 +@Inject("$state")
  8 +export class SearchFormComponent {
  9 +
  10 + query: string;
  11 +
  12 + constructor(private $state: ng.ui.IStateService) {
  13 + }
  14 +
  15 + ngOnInit() {
  16 + this.query = this.$state.params['query'];
  17 + }
  18 +
  19 + search() {
  20 + this.$state.go('main.environment.search', { query: this.query });
  21 + }
  22 +
  23 + isSearchPage() {
  24 + return "main.environment.search" === this.$state.current.name;
  25 + }
  26 +}
... ...
src/app/search/search-form/search-form.html 0 → 100644
... ... @@ -0,0 +1,8 @@
  1 +<form class="navbar-form search-form" role="search" ng-if="!ctrl.isSearchPage()">
  2 + <div class="input-group">
  3 + <input type="text" class="search-input form-control" placeholder="Search" name="q" ng-model="ctrl.query">
  4 + <div class="input-group-btn">
  5 + <button class="btn btn-default" type="submit" (click)="ctrl.search()"><i class="fa fa-search fa-fw"></i></button>
  6 + </div>
  7 + </div>
  8 +</form>
... ...
src/app/search/search.component.spec.ts 0 → 100644
... ... @@ -0,0 +1,38 @@
  1 +import {ComponentTestHelper, createClass} from "../../spec/component-test-helper";
  2 +import {SearchComponent} from "./search.component";
  3 +import * as helpers from "../../spec/helpers";
  4 +
  5 +const htmlTemplate: string = '<search></search>';
  6 +
  7 +describe("Components", () => {
  8 + describe("Search Component", () => {
  9 +
  10 + let helper: ComponentTestHelper<SearchComponent>;
  11 + let stateParams = { query: 'query' };
  12 + let articleService = jasmine.createSpyObj("ArticleService", ["search"]);
  13 + let result = Promise.resolve({ data: [{ id: 1 }], headers: (param: string) => { return 1; } });
  14 + articleService.search = jasmine.createSpy("search").and.returnValue(result);
  15 +
  16 + beforeEach(angular.mock.module("templates"));
  17 +
  18 + beforeEach((done) => {
  19 + let cls = createClass({
  20 + template: htmlTemplate,
  21 + directives: [SearchComponent],
  22 + providers: [
  23 + helpers.createProviderToValue("$stateParams", stateParams),
  24 + helpers.createProviderToValue("ArticleService", articleService)
  25 + ].concat(helpers.provideFilters("truncateFilter", "stripTagsFilter"))
  26 + });
  27 + helper = new ComponentTestHelper<SearchComponent>(cls, done);
  28 + });
  29 +
  30 + it("load first page with search results", () => {
  31 + expect(articleService.search).toHaveBeenCalledWith({ query: 'query', per_page: 10, page: 0 });
  32 + });
  33 +
  34 + it("display search results", () => {
  35 + expect(helper.all(".result").length).toEqual(1);
  36 + });
  37 + });
  38 +});
... ...
src/app/search/search.component.ts 0 → 100644
... ... @@ -0,0 +1,40 @@
  1 +import {Component, Inject} from "ng-forward";
  2 +import {ArticleService} from "./../../lib/ng-noosfero-api/http/article.service";
  3 +
  4 +import {SearchFormComponent} from "./search-form/search-form.component";
  5 +
  6 +@Component({
  7 + selector: 'search',
  8 + templateUrl: 'app/search/search.html',
  9 + directives: [SearchFormComponent]
  10 +})
  11 +@Inject(ArticleService, "$stateParams", "$state")
  12 +export class SearchComponent {
  13 +
  14 + articles: noosfero.Article[];
  15 + query: string;
  16 + totalResults = 0;
  17 + perPage = 10;
  18 + currentPage: number = 0;
  19 +
  20 + constructor(private articleService: ArticleService, private $stateParams: ng.ui.IStateParamsService, private $state: ng.ui.IStateService) {
  21 + this.query = this.$stateParams['query'];
  22 + this.loadPage();
  23 + }
  24 +
  25 + search() {
  26 + this.$state.go('main.environment.search', { query: this.query });
  27 + }
  28 +
  29 + loadPage() {
  30 + let filters = {
  31 + query: this.query,
  32 + per_page: this.perPage,
  33 + page: this.currentPage
  34 + };
  35 + this.articleService.search(filters).then((result: noosfero.RestResult<noosfero.Article[]>) => {
  36 + this.totalResults = <number>result.headers("total");
  37 + this.articles = result.data;
  38 + });
  39 + }
  40 +}
... ...
src/app/search/search.html 0 → 100644
... ... @@ -0,0 +1,28 @@
  1 +<form ng-submit="ctrl.search()">
  2 +<label for="query" ng-bind-html="'search.results.query.label' | translate"></label>
  3 +<input id="query" placeholder="{{'search.results.query.placeholder' | translate}}" type="search" class="search-box-title" ng-model="ctrl.query">
  4 +</form>
  5 +<div class="search-results">
  6 + <div class="summary">
  7 + {{"search.results.summary" | translate:{results: ctrl.totalResults}:"messageformat"}}
  8 + </div>
  9 + <div ng-repeat="article in ctrl.articles | orderBy: 'created_at':true" class="result">
  10 + <a class="title" ui-sref="main.profile.page({profile: article.profile.identifier, page: article.path})">
  11 + <h4 ng-bind="article.title"></h4>
  12 + </a>
  13 + <div class="info">
  14 + <a class="profile" ui-sref="main.profile.home({profile: article.profile.identifier})">
  15 + {{article.profile.name}}
  16 + </a>
  17 + <span class="bullet-separator">•</span>
  18 + <span class="time">
  19 + <span am-time-ago="article.created_at | dateFormat"></span>
  20 + </span>
  21 + </div>
  22 + <div class="post-lead" ng-bind-html="article.body | stripTags | truncate: 250: '...': true"></div>
  23 + </div>
  24 + <uib-pagination ng-model="ctrl.currentPage" total-items="ctrl.totalResults" class="pagination-sm center-block"
  25 + boundary-links="true" items-per-page="ctrl.perPage" ng-change="ctrl.loadPage()"
  26 + first-text="«" last-text="»" previous-text="‹" next-text="›">
  27 + </uib-pagination>
  28 +</div>
... ...
src/app/search/search.scss 0 → 100644
... ... @@ -0,0 +1,48 @@
  1 +.search-results {
  2 + .summary {
  3 + color: #bbbbbb;
  4 + font-size: 13px;
  5 + border-top: 1px solid #ececec;
  6 + }
  7 + .result {
  8 + margin: 25px 0;
  9 +
  10 + .title {
  11 + h4 {
  12 + margin: 0;
  13 + }
  14 + }
  15 + .info {
  16 + .profile {
  17 + color: #6e9e7b;
  18 + }
  19 + .time {
  20 + color: #6e9e7b;
  21 + font-size: 12px;
  22 + }
  23 + .bullet-separator {
  24 + margin: 0 2px;
  25 + font-size: 10px;
  26 + color: #afd6ba;
  27 + }
  28 + }
  29 + }
  30 +}
  31 +
  32 +.search-box-title {
  33 + border: 0;
  34 + border-bottom: 1px solid rgba(0,0,0,.15);
  35 + border-radius: 0;
  36 + font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Open Sans","Helvetica Neue",sans-serif;
  37 + letter-spacing: 0;
  38 + font-weight: 300;
  39 + font-style: normal;
  40 + font-size: 50px;
  41 + height: 80px;
  42 + padding: 0;
  43 + width: 100%;
  44 +}
  45 +
  46 +.search-box-title:focus {
  47 + outline: none;
  48 +}
0 49 \ No newline at end of file
... ...
src/app/shared/components/bootstrap-switcher/bootstrap-switcher.component.ts 0 → 100644
... ... @@ -0,0 +1,51 @@
  1 +import {Component, Input, Output, EventEmitter} from 'ng-forward';
  2 +
  3 +
  4 +export interface BootstrapSwitcherItem {
  5 + value: any;
  6 + label: string;
  7 +}
  8 +@Component({
  9 + selector: 'noosfero-bootstrap-switcher',
  10 + template: `
  11 + <span class="switcher-label" ng-bind="ctrl.label | translate"></span>
  12 + <div class="btn-group switcher">
  13 + <button ng-repeat="option in ctrl.options track by $index"
  14 + (click)="ctrl.switcherClick(option)"
  15 + ng-class="ctrl.getCssClassForItem(option)"
  16 + class="btn btn-xs" ng-bind="option.label | translate">
  17 + </button>
  18 + </div>
  19 + `,
  20 + inputs: ['activeClass', 'defaultClass', 'label', 'options', 'defaultOption'],
  21 + outputs: ['onSwitch']
  22 +})
  23 +export class BootstrapSwitcherComponent {
  24 + @Input() activeClass: string = 'active btn-danger';
  25 + @Input() defaultClass: string = 'btn-default';
  26 + @Input() label: string;
  27 + @Input() options: BootstrapSwitcherItem[];
  28 + @Input() defaultOption: BootstrapSwitcherItem;
  29 + @Output() onSwitch: EventEmitter<BootstrapSwitcherItem> = new EventEmitter<BootstrapSwitcherItem>();
  30 +
  31 + selectedOption: BootstrapSwitcherItem = null;
  32 +
  33 + constructor() { }
  34 +
  35 + ngOnInit() {
  36 + this.selectedOption = this.defaultOption;
  37 + }
  38 +
  39 + isSelectedOption(value: BootstrapSwitcherItem): boolean {
  40 + return this.selectedOption === value;
  41 + }
  42 +
  43 + getCssClassForItem(value: BootstrapSwitcherItem): string {
  44 + return this.isSelectedOption(value) ? this.activeClass : this.defaultClass;
  45 + }
  46 +
  47 + switcherClick(value: BootstrapSwitcherItem) {
  48 + this.selectedOption = value;
  49 + this.onSwitch.next(value);
  50 + }
  51 +}
0 52 \ No newline at end of file
... ...
src/app/shared/components/interfaces.ts 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +
  2 +
  3 +export interface IComponentWithPermissions {
  4 + permissions: () => string[];
  5 +}
0 6 \ No newline at end of file
... ...
src/app/shared/components/permission/permission.directive.spec.ts 0 → 100644
... ... @@ -0,0 +1,27 @@
  1 +import {Input, provide, Component} from 'ng-forward';
  2 +import {PermissionDirective} from "./permission.directive";
  3 +import * as helpers from "../../../../spec/helpers";
  4 +
  5 +const htmlTemplate: string = '<div permission="ctrl.permissions" permission-action="action"></div>';
  6 +
  7 +describe("Permission directive", () => {
  8 +
  9 + let element = jasmine.createSpyObj("$element", ["css"]);
  10 + let scope = jasmine.createSpyObj("$scope", ["$watch", "$eval"]);
  11 + scope.$watch = (param: string, f: Function) => { f(); };
  12 + scope.$eval = (param: any) => { return param; };
  13 +
  14 + it("hide element when there is no permission action in the permissions array", (done: Function) => {
  15 + let attrs: any = { permission: [], permissionAction: 'action' };
  16 + let directive = new PermissionDirective(<any>attrs, scope, element);
  17 + expect(element.css).toHaveBeenCalledWith('display', 'none');
  18 + done();
  19 + });
  20 +
  21 + it("show element when the permission action exists in the permissions array", (done: Function) => {
  22 + let attrs = { permission: ['action'], permissionAction: 'action' };
  23 + let directive = new PermissionDirective(<any>attrs, scope, element);
  24 + expect(element.css).toHaveBeenCalledWith('display', '');
  25 + done();
  26 + });
  27 +});
... ...
src/app/shared/components/permission/permission.directive.ts 0 → 100644
... ... @@ -0,0 +1,20 @@
  1 +import {Directive, Inject, Input} from "ng-forward";
  2 +
  3 +@Directive({
  4 + selector: '[permission]'
  5 +})
  6 +@Inject('$attrs', '$scope', '$element')
  7 +export class PermissionDirective {
  8 +
  9 + constructor($attrs: ng.IAttributes, $scope: ng.IScope, $element: ng.IAugmentedJQuery) {
  10 + $scope.$watch($attrs['permission'], () => {
  11 + let permissions = $scope.$eval($attrs['permission']);
  12 + let permissionAction = $attrs['permissionAction'];
  13 + if (!permissions || permissions.indexOf(permissionAction) < 0) {
  14 + $element.css("display", "none");
  15 + } else {
  16 + $element.css("display", "");
  17 + }
  18 + });
  19 + }
  20 +}
... ...
src/index.html
1 1 <!doctype html>
2   -<html ng-app="noosferoApp">
  2 +<html>
3 3 <head>
4 4 <base href="/">
5 5 <meta charset="utf-8">
... ... @@ -25,11 +25,12 @@
25 25 <p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
26 26 <![endif]-->
27 27  
28   - <div ui-view></div>
  28 + <main></main>
29 29  
30 30 <script>
31 31 CKEDITOR_BASEPATH='/bower_components/ng-ckeditor/libs/ckeditor/';
32 32 </script>
  33 + <script src="/bower_components/ng-ckeditor/libs/ckeditor/ckeditor.js"></script>
33 34  
34 35 <!-- build:js(src) scripts/vendor.js -->
35 36 <!-- bower:js -->
... ... @@ -38,15 +39,12 @@
38 39 <!-- endbuild -->
39 40  
40 41 <!-- build:js({.tmp/serve,.tmp/partials,src}) scripts/app.js -->
41   - <script src="commons.js"></script>
42   - <script src="vendor.bundle.js"></script>
43   - <script src="noosfero.js"></script>
44 42 <!-- inject:partials -->
45 43 <!-- angular templates will be automatically converted in js and inserted here -->
46 44 <!-- endinject -->
  45 + <script src="commons.js"></script>
  46 + <script src="vendor.bundle.js"></script>
  47 + <script src="noosfero.js"></script>
47 48 <!-- endbuild -->
48   -
49   - <script src="/bower_components/ng-ckeditor/libs/ckeditor/ckeditor.js"></script>
50   - <script src="/bower_components/ng-ckeditor/ng-ckeditor.min.js"></script>
51 49 </body>
52 50 </html>
... ...
src/languages/en.json
... ... @@ -27,7 +27,6 @@
27 27 "navbar.content_viewer_actions.new_item": "New Item",
28 28 "navbar.profile_actions.new_item": "New Item",
29 29 "navbar.content_viewer_actions.new_post": "New Post",
30   - "//TODO": "Create a way to load plugin translatios - Move plugins translations to the plugins translations files",
31 30 "navbar.content_viewer_actions.new_discussion": "New Discussion",
32 31 "navbar.profile_actions.new_discussion": "New Discussion",
33 32 "notification.error.default.message": "Something went wrong!",
... ... @@ -75,5 +74,11 @@
75 74 "profile.content.success.message": "Profile saved!",
76 75 "custom_content.title": "Edit content",
77 76 "profile.custom_header.label": "Header",
78   - "profile.custom_footer.label": "Footer"
  77 + "profile.custom_footer.label": "Footer",
  78 + "designMode.label": "In Design",
  79 + "designMode.toggle.ON": "ON",
  80 + "designMode.toggle.OFF": "OFF",
  81 + "search.results.summary": "{results, plural, one{result} other{# results}}",
  82 + "search.results.query.label": "Search for:",
  83 + "search.results.query.placeholder": "Search"
79 84 }
... ...
src/languages/pt.json
... ... @@ -25,8 +25,10 @@
25 25 "auth.form.password": "Senha",
26 26 "auth.form.login_button": "Login",
27 27 "navbar.content_viewer_actions.new_item": "Novo Item",
  28 + "navbar.profile_actions.new_item": "Novo Item",
28 29 "navbar.content_viewer_actions.new_post": "Novo Artigo",
29 30 "navbar.content_viewer_actions.new_discussion": "Nova Discussão",
  31 + "navbar.profile_actions.new_discussion": "Nova Discussão",
30 32 "notification.error.default.message": "Algo deu errado!",
31 33 "notification.error.default.title": "Oops...",
32 34 "notification.profile.not_found": "Página não encontrada",
... ... @@ -56,7 +58,7 @@
56 58 "article.remove.success.title": "Bom trabalho!",
57 59 "article.remove.success.message": "Artigo removido!",
58 60 "article.remove.confirmation.title": "Tem certeza?",
59   - "article.remove.confirmation.message": "Não será possível recuperar este artigo!",
  61 + "article.remove.confirmation.message": "Não será possível recuperar este artigo!",
60 62 "article.basic_editor.visibility": "Visibilidade",
61 63 "article.basic_editor.visibility.public": "Público",
62 64 "article.basic_editor.visibility.private": "Privado",
... ... @@ -72,5 +74,11 @@
72 74 "profile.content.success.message": "Perfil salvo!",
73 75 "custom_content.title": "Editar conteúdo",
74 76 "profile.custom_header.label": "Cabeçalho",
75   - "profile.custom_footer.label": "Rodapé"
  77 + "profile.custom_footer.label": "Rodapé",
  78 + "designMode.label": "Modo de Edição",
  79 + "designMode.toggle.ON": "Ligado",
  80 + "designMode.toggle.OFF": "Desligado",
  81 + "search.results.summary": "{results, plural, one{# resultado} other{# resultados}}",
  82 + "search.results.query.label": "Buscar:",
  83 + "search.results.query.placeholder": "Informe aqui sua busca"
76 84 }
... ...
src/lib/ng-noosfero-api/http/article.service.spec.ts
... ... @@ -8,7 +8,7 @@ describe(&quot;Services&quot;, () =&gt; {
8 8 let $httpBackend: ng.IHttpBackendService;
9 9 let articleService: ArticleService;
10 10  
11   - beforeEach(angular.mock.module("noosferoApp", ($translateProvider: angular.translate.ITranslateProvider) => {
  11 + beforeEach(angular.mock.module("main", ($translateProvider: angular.translate.ITranslateProvider) => {
12 12 $translateProvider.translations('en', {});
13 13 }));
14 14  
... ... @@ -23,7 +23,7 @@ describe(&quot;Services&quot;, () =&gt; {
23 23 it("should remove article", (done) => {
24 24 let articleId = 1;
25 25 $httpBackend.expectDELETE(`/api/v1/articles/${articleId}`).respond(200, { success: "true" });
26   - articleService.remove(<noosfero.Article>{id: articleId});
  26 + articleService.remove(<noosfero.Article>{ id: articleId });
27 27 $httpBackend.flush();
28 28 $httpBackend.verifyNoOutstandingExpectation();
29 29 done();
... ... @@ -32,7 +32,7 @@ describe(&quot;Services&quot;, () =&gt; {
32 32 it("should return article children", (done) => {
33 33 let articleId = 1;
34 34 $httpBackend.expectGET(`/api/v1/articles/${articleId}/children`).respond(200, { articles: [{ name: "article1" }] });
35   - articleService.getChildren(<noosfero.Article>{id: articleId}).then((result: noosfero.RestResult<noosfero.Article[]>) => {
  35 + articleService.getChildren(<noosfero.Article>{ id: articleId }).then((result: noosfero.RestResult<noosfero.Article[]>) => {
36 36 expect(result.data).toEqual([{ name: "article1" }]);
37 37 done();
38 38 });
... ... @@ -42,7 +42,7 @@ describe(&quot;Services&quot;, () =&gt; {
42 42 it("should get articles by profile", (done) => {
43 43 let profileId = 1;
44 44 $httpBackend.expectGET(`/api/v1/profiles/${profileId}/articles`).respond(200, { articles: [{ name: "article1" }] });
45   - articleService.getByProfile(<noosfero.Profile>{id: profileId}).then((result: noosfero.RestResult<noosfero.Article[]>) => {
  45 + articleService.getByProfile(<noosfero.Profile>{ id: profileId }).then((result: noosfero.RestResult<noosfero.Article[]>) => {
46 46 expect(result.data).toEqual([{ name: "article1" }]);
47 47 done();
48 48 });
... ... @@ -52,7 +52,7 @@ describe(&quot;Services&quot;, () =&gt; {
52 52 it("should get articles by profile with additional filters", (done) => {
53 53 let profileId = 1;
54 54 $httpBackend.expectGET(`/api/v1/profiles/${profileId}/articles?path=test`).respond(200, { articles: [{ name: "article1" }] });
55   - articleService.getByProfile(<noosfero.Profile>{id: profileId}, { path: 'test' }).then((result: noosfero.RestResult<noosfero.Article[]>) => {
  55 + articleService.getByProfile(<noosfero.Profile>{ id: profileId }, { path: 'test' }).then((result: noosfero.RestResult<noosfero.Article[]>) => {
56 56 expect(result.data).toEqual([{ name: "article1" }]);
57 57 done();
58 58 });
... ... @@ -62,7 +62,7 @@ describe(&quot;Services&quot;, () =&gt; {
62 62 it("should get article children with additional filters", (done) => {
63 63 let articleId = 1;
64 64 $httpBackend.expectGET(`/api/v1/articles/${articleId}/children?path=test`).respond(200, { articles: [{ name: "article1" }] });
65   - articleService.getChildren(<noosfero.Article>{id: articleId}, { path: 'test' }).then((result: noosfero.RestResult<noosfero.Article[]>) => {
  65 + articleService.getChildren(<noosfero.Article>{ id: articleId }, { path: 'test' }).then((result: noosfero.RestResult<noosfero.Article[]>) => {
66 66 expect(result.data).toEqual([{ name: "article1" }]);
67 67 done();
68 68 });
... ... @@ -71,14 +71,25 @@ describe(&quot;Services&quot;, () =&gt; {
71 71  
72 72 it("should create an article in a profile", (done) => {
73 73 let profileId = 1;
74   - let article: noosfero.Article = <any>{ id: null};
75   - $httpBackend.expectPOST(`/api/v1/profiles/${profileId}/articles`, { article: article }).respond(200, {article: { id: 2 }});
76   - articleService.createInProfile(<noosfero.Profile>{id: profileId}, article).then((result: noosfero.RestResult<noosfero.Article>) => {
  74 + let article: noosfero.Article = <any>{ id: null };
  75 + $httpBackend.expectPOST(`/api/v1/profiles/${profileId}/articles`, { article: article }).respond(200, { article: { id: 2 } });
  76 + articleService.createInProfile(<noosfero.Profile>{ id: profileId }, article).then((result: noosfero.RestResult<noosfero.Article>) => {
77 77 expect(result.data).toEqual({ id: 2 });
78 78 done();
79 79 });
80 80 $httpBackend.flush();
81 81 });
  82 +
  83 + it("should search for articles in environment", (done) => {
  84 + let profileId = 1;
  85 + let article: noosfero.Article = <any>{ id: null };
  86 + $httpBackend.expectGET(`/api/v1/search/article?query=query`).respond(200, { articles: [{ id: 2 }] });
  87 + articleService.search({ query: 'query' }).then((result: noosfero.RestResult<noosfero.Article[]>) => {
  88 + expect(result.data).toEqual([{ id: 2 }]);
  89 + done();
  90 + });
  91 + $httpBackend.flush();
  92 + });
82 93 });
83 94  
84 95  
... ...
src/lib/ng-noosfero-api/http/article.service.ts
... ... @@ -118,6 +118,11 @@ export class ArticleService extends RestangularService&lt;noosfero.Article&gt; {
118 118 return this.listSubElements(<noosfero.Article>articleElement, "children", params);
119 119 }
120 120  
  121 + search(params: any): ng.IPromise<noosfero.RestResult<noosfero.Article[]>> {
  122 + let deferred = this.$q.defer<noosfero.RestResult<noosfero.Article[]>>();
  123 + let restRequest = this.restangularService.all("search").customGET('article', params);
  124 + restRequest.then(this.getHandleSuccessFunction(deferred)).catch(this.getHandleErrorFunction(deferred));
  125 + return deferred.promise;
  126 + }
121 127  
122 128 }
123   -
... ...
src/lib/ng-noosfero-api/http/block.service.spec.ts
... ... @@ -8,7 +8,7 @@ describe(&quot;Services&quot;, () =&gt; {
8 8 let $httpBackend: ng.IHttpBackendService;
9 9 let blockService: BlockService;
10 10  
11   - beforeEach(angular.mock.module("noosferoApp", ($translateProvider: angular.translate.ITranslateProvider) => {
  11 + beforeEach(angular.mock.module("main", ($translateProvider: angular.translate.ITranslateProvider) => {
12 12 $translateProvider.translations('en', {});
13 13 }));
14 14  
... ...
src/lib/ng-noosfero-api/http/comment.service.spec.ts
... ... @@ -8,7 +8,7 @@ describe(&quot;Services&quot;, () =&gt; {
8 8 let $httpBackend: ng.IHttpBackendService;
9 9 let commentService: CommentService;
10 10  
11   - beforeEach(angular.mock.module("noosferoApp", ($translateProvider: angular.translate.ITranslateProvider) => {
  11 + beforeEach(angular.mock.module("main", ($translateProvider: angular.translate.ITranslateProvider) => {
12 12 $translateProvider.translations('en', {});
13 13 }));
14 14  
... ...
src/lib/ng-noosfero-api/http/community.service.spec.ts
... ... @@ -8,7 +8,7 @@ describe(&quot;Services&quot;, () =&gt; {
8 8 let $httpBackend: ng.IHttpBackendService;
9 9 let communityService: CommunityService;
10 10  
11   - beforeEach(angular.mock.module("noosferoApp", ($translateProvider: angular.translate.ITranslateProvider) => {
  11 + beforeEach(angular.mock.module("main", ($translateProvider: angular.translate.ITranslateProvider) => {
12 12 $translateProvider.translations('en', {});
13 13 }));
14 14  
... ...
src/lib/ng-noosfero-api/http/environment.service.ts
... ... @@ -4,12 +4,17 @@ import { Injectable, Inject } from &quot;ng-forward&quot;;
4 4 @Inject("Restangular", "$q")
5 5 export class EnvironmentService {
6 6  
7   - private _currentEnvironmentPromise: ng.IDeferred<noosfero.Environment>;
8 7  
  8 + private currentEnvironment: noosfero.Environment = null;
9 9 constructor(private restangular: restangular.IService, private $q: ng.IQService) {
10 10  
11 11 }
12 12  
  13 + getCurrentEnvironment(): noosfero.Environment {
  14 + return this.currentEnvironment;
  15 + }
  16 +
  17 +
13 18 getEnvironmentPeople(params: any): ng.IPromise<noosfero.Person[]> {
14 19 let p = this.restangular.one('people').get(params);
15 20 let deferred = this.$q.defer<noosfero.Person[]>();
... ... @@ -18,10 +23,19 @@ export class EnvironmentService {
18 23 return deferred.promise;
19 24 }
20 25  
21   - getByIdentifier(identifier: string): ng.IPromise<noosfero.Environment> {
  26 + get(identifier: string = 'default'): ng.IPromise<noosfero.Environment> {
22 27 let p = this.restangular.one('environment').customGET(identifier);
23 28 let deferred = this.$q.defer<noosfero.Environment>();
24   - p.then(this.getHandleSuccessFunction<noosfero.Environment>(deferred));
  29 + if (identifier === 'default') {
  30 + p.then((response) => {
  31 + let data = this.restangular.stripRestangular(response.data);
  32 + this.currentEnvironment = data;
  33 + this.getHandleSuccessFunction<noosfero.Environment>(deferred).bind(this)(response);
  34 + });
  35 + } else {
  36 + p.then(this.getHandleSuccessFunction<noosfero.Environment>(deferred));
  37 + }
  38 +
25 39 p.catch(this.getHandleErrorFunction<noosfero.Environment>(deferred));
26 40 return deferred.promise;
27 41 }
... ... @@ -36,8 +50,8 @@ export class EnvironmentService {
36 50  
37 51 /** TODO - Please, use the base class RestangularService
38 52 * (description)
39   - *
40   - * @template T
  53 + *
  54 + * @template T_currentEnvironmentPromise
41 55 * @param {ng.IDeferred<T>} deferred (description)
42 56 * @returns {(response: restangular.IResponse) => void} (description)
43 57 */
... ... @@ -45,7 +59,7 @@ export class EnvironmentService {
45 59 let self = this;
46 60 /**
47 61 * (description)
48   - *
  62 + *
49 63 * @param {restangular.IResponse} response (description)
50 64 */
51 65 let errorFunction = (response: restangular.IResponse): void => {
... ... @@ -62,7 +76,7 @@ export class EnvironmentService {
62 76  
63 77 /**
64 78 * (description)
65   - *
  79 + *
66 80 * @param {restangular.IResponse} response (description)
67 81 */
68 82 let successFunction = (response: restangular.IResponse): void => {
... ... @@ -80,7 +94,7 @@ export class EnvironmentService {
80 94  
81 95 /**
82 96 * (description)
83   - *
  97 + *
84 98 * @param {restangular.IResponse} response (description)
85 99 */
86 100 let successFunction = (response: restangular.IResponse): void => {
... ...
src/lib/ng-noosfero-api/http/profile.service.spec.ts
... ... @@ -8,7 +8,7 @@ describe(&quot;Services&quot;, () =&gt; {
8 8 let profileService: ProfileService;
9 9 let $rootScope: ng.IRootScopeService;
10 10  
11   - beforeEach(angular.mock.module("noosferoApp", ($translateProvider: angular.translate.ITranslateProvider) => {
  11 + beforeEach(angular.mock.module("main", ($translateProvider: angular.translate.ITranslateProvider) => {
12 12 $translateProvider.translations('en', {});
13 13 }));
14 14  
... ...
src/lib/ng-noosfero-api/http/profile.service.ts
... ... @@ -22,7 +22,7 @@ export class ProfileService {
22 22 this._currentProfilePromise.resolve(profile);
23 23 }
24 24  
25   - setCurrentProfileByIdentifier(identifier: string) {
  25 + setCurrentProfileByIdentifier(identifier: string): ng.IPromise<noosfero.Profile> {
26 26 this.resetCurrentProfile();
27 27 return this.getByIdentifier(identifier).then((profile: noosfero.Profile) => {
28 28 this.setCurrentProfile(profile);
... ...
src/lib/ng-noosfero-api/http/restangular_service.spec.ts
... ... @@ -40,7 +40,7 @@ describe(&quot;Restangular Service - base Class&quot;, () =&gt; {
40 40 let objectRestService: ObjectRestService;
41 41 let rootObjectRestService: RootObjectRestService;
42 42  
43   - beforeEach(angular.mock.module("noosferoApp", ($translateProvider: angular.translate.ITranslateProvider) => {
  43 + beforeEach(angular.mock.module("main", ($translateProvider: angular.translate.ITranslateProvider) => {
44 44 $translateProvider.translations('en', {});
45 45 }));
46 46  
... ...
src/lib/ng-noosfero-api/http/restangular_service.ts
... ... @@ -223,7 +223,7 @@ export abstract class RestangularService&lt;T extends noosfero.RestModel&gt; {
223 223 restRequest = restangularObj.remove(queryParams, headers);
224 224  
225 225 restRequest
226   - .then(this.getHandleSuccessFunction(deferred, this.modelRemovedEventEmitter))
  226 + .then(this.getHandleSuccessFunction(deferred, this.modelRemovedEventEmitter, obj))
227 227 .catch(this.getHandleErrorFunction(deferred));
228 228  
229 229 return deferred.promise;
... ... @@ -311,7 +311,7 @@ export abstract class RestangularService&lt;T extends noosfero.RestModel&gt; {
311 311 }
312 312  
313 313 /** HANDLERS */
314   - protected getHandleSuccessFunction<C>(deferred: ng.IDeferred<noosfero.RestResult<C | T | any>>, successEmitter: EventEmitter<T> = null): (response: restangular.IResponse) => void {
  314 + protected getHandleSuccessFunction<C>(deferred: ng.IDeferred<noosfero.RestResult<C | T | any>>, successEmitter: EventEmitter<T> = null, currentModel: T = null): (response: restangular.IResponse) => void {
315 315 let self = this;
316 316  
317 317 /**
... ... @@ -328,7 +328,11 @@ export abstract class RestangularService&lt;T extends noosfero.RestModel&gt; {
328 328 deferred.resolve(resultModel);
329 329 // emits the event if a successEmiter was provided in the successEmitter parameter
330 330 if (successEmitter !== null) {
331   - successEmitter.next(resultModel);
  331 + if (successEmitter !== this.modelRemovedEventEmitter) {
  332 + successEmitter.next(resultModel.data);
  333 + } else {
  334 + successEmitter.next(currentModel !== null ? currentModel : resultModel.data);
  335 + }
332 336 }
333 337 };
334 338 return successFunction;
... ...
src/lib/ng-noosfero-api/interfaces/article.ts
... ... @@ -14,5 +14,7 @@ namespace noosfero {
14 14 start_date: string;
15 15 end_date: string;
16 16 accept_comments: boolean;
  17 +
  18 + permissions: string[];
17 19 }
18 20 }
... ...
src/lib/ng-noosfero-api/interfaces/comment.ts
1 1 namespace noosfero {
2 2 export interface Comment extends RestModel {
  3 + source_id: number;
3 4 reply_of_id: number;
4 5 reply_of: Comment;
5 6 replies: Comment[];
... ...
src/lib/ng-noosfero-api/interfaces/modelWithPermissions.ts 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +namespace noosfero {
  2 + export interface ModelWithPermissions {
  3 + permissions: string[];
  4 + }
  5 +}
0 6 \ No newline at end of file
... ...
src/lib/ng-noosfero-api/interfaces/profile.ts
... ... @@ -78,5 +78,7 @@ namespace noosfero {
78 78 * @returns {string} The Profile footer
79 79 */
80 80 custom_footer: string;
  81 +
  82 + permissions: string[];
81 83 }
82 84 }
... ...
src/plugins/comment_paragraph/allow-comment/allow-comment.component.spec.ts
... ... @@ -3,7 +3,6 @@ import {ComponentTestHelper, createClass} from &#39;../../../spec/component-test-hel
3 3 import * as helpers from "../../../spec/helpers";
4 4 import {Provider} from 'ng-forward';
5 5 import {ComponentFixture} from 'ng-forward/cjs/testing/test-component-builder';
6   -
7 6 let htmlTemplate = '<comment-paragraph-plugin-allow-comment [content]="ctrl.content" [paragraph-uuid]="ctrl.paragraphUuid" [article]="ctrl.article"></comment-paragraph-plugin-allow-comment>';
8 7  
9 8 describe("Components", () => {
... ... @@ -23,6 +22,7 @@ describe(&quot;Components&quot;, () =&gt; {
23 22 };
24 23  
25 24 let providers = [
  25 + new Provider('CommentService', { useValue: helpers.mocks.commentService } ),
26 26 new Provider('CommentParagraphService', { useValue: serviceMock }),
27 27 new Provider('CommentParagraphEventService', { useValue: eventServiceMock })
28 28 ];
... ...
src/plugins/comment_paragraph/allow-comment/allow-comment.component.ts
... ... @@ -2,24 +2,27 @@ import {Component, Input, Inject} from &quot;ng-forward&quot;;
2 2 import {SideCommentsComponent} from "../side-comments/side-comments.component";
3 3 import {CommentParagraphEventService} from "../events/comment-paragraph-event.service";
4 4 import {CommentParagraphService} from "../http/comment-paragraph.service";
  5 +import {CommentService} from "./../../../lib/ng-noosfero-api/http/comment.service";
5 6  
6 7 @Component({
7 8 selector: "comment-paragraph-plugin-allow-comment",
8 9 templateUrl: "plugins/comment_paragraph/allow-comment/allow-comment.html",
9 10 directives: [SideCommentsComponent]
10 11 })
11   -@Inject("$scope", CommentParagraphEventService, CommentParagraphService)
  12 +@Inject("$scope", CommentParagraphEventService, CommentParagraphService, CommentService)
12 13 export class AllowCommentComponent {
13 14  
14 15 @Input() content: string;
15 16 @Input() paragraphUuid: string;
16 17 @Input() article: noosfero.Article;
17   - commentsCount: number;
  18 + commentsCount: number = 0;
18 19 display = false;
19 20  
20 21 constructor(private $scope: ng.IScope,
21 22 private commentParagraphEventService: CommentParagraphEventService,
22   - private commentParagraphService: CommentParagraphService) { }
  23 + private commentParagraphService: CommentParagraphService,
  24 + private commentService: CommentService
  25 + ) { }
23 26  
24 27 ngOnInit() {
25 28 this.commentParagraphEventService.subscribeToggleCommentParagraph((article: noosfero.Article) => {
... ... @@ -27,7 +30,19 @@ export class AllowCommentComponent {
27 30 this.$scope.$apply();
28 31 });
29 32 this.commentParagraphService.commentParagraphCount(this.article, this.paragraphUuid).then((count: number) => {
30   - this.commentsCount = count;
  33 + this.commentsCount = count ? count : 0;
  34 + });
  35 +
  36 + this.commentService.subscribeToModelAdded((comment: noosfero.CommentParagraph) => {
  37 + if (comment.paragraph_uuid === this.paragraphUuid) {
  38 + this.commentsCount += 1;
  39 + };
  40 + });
  41 +
  42 + this.commentService.subscribeToModelRemoved((comment: noosfero.CommentParagraph) => {
  43 + if (comment.paragraph_uuid === this.paragraphUuid) {
  44 + this.commentsCount -= (comment.replies) ? 1 + comment.replies.length : 1;
  45 + };
31 46 });
32 47 }
33 48  
... ...
src/plugins/comment_paragraph/hotspot/export-comment-button.component.spec.ts 0 → 100644
... ... @@ -0,0 +1,58 @@
  1 +import {ExportCommentButtonHotspotComponent} from "./export-comment-button.component";
  2 +import {ComponentTestHelper, createClass} from '../../../spec/component-test-helper';
  3 +import * as helpers from "../../../spec/helpers";
  4 +import {Provider} from 'ng-forward';
  5 +import {ComponentFixture} from 'ng-forward/cjs/testing/test-component-builder';
  6 +import {PermissionDirective} from '../../../app/shared/components/permission/permission.directive';
  7 +
  8 +let htmlTemplate = '<export-comment-button-hotspot [article]="ctrl.article"></export-comment-button-hotspot>';
  9 +
  10 +describe("Components", () => {
  11 + describe("Export Comment Button Hotspot Component", () => {
  12 +
  13 + let serviceMock = jasmine.createSpyObj("CommentParagraphService", ["getArticle"]);
  14 +
  15 + let providers = [new Provider('CommentParagraphService', { useValue: serviceMock })]
  16 + .concat(helpers.provideFilters('translateFilter'));
  17 + let helper: ComponentTestHelper<ExportCommentButtonHotspotComponent>;
  18 +
  19 + beforeEach(angular.mock.module("templates"));
  20 +
  21 + beforeEach((done) => {
  22 + let cls = createClass({
  23 + template: htmlTemplate,
  24 + directives: [ExportCommentButtonHotspotComponent, PermissionDirective],
  25 + providers: providers,
  26 + properties: {
  27 + article: {}
  28 + }
  29 + });
  30 + helper = new ComponentTestHelper<ExportCommentButtonHotspotComponent>(cls, done);
  31 + });
  32 +
  33 + it('return true when comment paragraph is active', () => {
  34 + helper.component.article = <noosfero.Article>{ setting: { comment_paragraph_plugin_activate: true } };
  35 + helper.detectChanges();
  36 + expect(helper.component.isActivated()).toBeTruthy();
  37 + expect(helper.all('.export-comment-button').length).toEqual(1);
  38 + });
  39 +
  40 + it('return false when comment paragraph is not active', () => {
  41 + expect(helper.component.isActivated()).toBeFalsy();
  42 + expect(helper.all('.export-comment-button').length).toEqual(0);
  43 + });
  44 +
  45 + it('return false when article has no setting attribute', () => {
  46 + helper.component.article = <noosfero.Article>{};
  47 + helper.detectChanges();
  48 + expect(helper.component.isActivated()).toBeFalsy();
  49 + expect(helper.all('.export-comment-button').length).toEqual(0);
  50 + });
  51 +
  52 + it('not display export comment button when user does not have permission', () => {
  53 + helper.component.article = <noosfero.Article>{ setting: { comment_paragraph_plugin_activate: true } };
  54 + helper.detectChanges();
  55 + expect(helper.find('.export-comment-button').attr('style')).toEqual("display: none; ");
  56 + });
  57 + });
  58 +});
... ...
src/plugins/comment_paragraph/hotspot/export-comment-button.component.ts 0 → 100644
... ... @@ -0,0 +1,23 @@
  1 +import { Input, Inject, Component } from "ng-forward";
  2 +import {Hotspot} from "../../../app/hotspot/hotspot.decorator";
  3 +import {CommentParagraphService} from "../http/comment-paragraph.service";
  4 +
  5 +@Component({
  6 + selector: "export-comment-button-hotspot",
  7 + templateUrl: "plugins/comment_paragraph/hotspot/export-comment-button.html",
  8 +})
  9 +@Inject(CommentParagraphService)
  10 +@Hotspot("article_extra_toolbar_buttons")
  11 +export class ExportCommentButtonHotspotComponent {
  12 +
  13 + @Input() article: noosfero.Article;
  14 + exportCommentPath: any;
  15 +
  16 + constructor(private commentParagraphService: CommentParagraphService) { }
  17 +
  18 + isActivated() {
  19 + this.exportCommentPath = ["/api/v1/articles/", this.article.id, "/comment_paragraph_plugin/export"].join("");
  20 + return this.article && this.article.setting && this.article.setting.comment_paragraph_plugin_activate;
  21 + }
  22 +
  23 +}
... ...
src/plugins/comment_paragraph/hotspot/export-comment-button.html 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +<a href='{{ctrl.exportCommentPath}}' target="_self"
  2 + class="btn btn-default btn-xs export-comment-button" ng-if="ctrl.isActivated()"
  3 + permission="ctrl.article.permissions" permission-action="allow_edit">
  4 + <i class="fa fa-fw fa-download"></i> {{"comment-paragraph-plugin.export" | translate}}
  5 +</a>
... ...
src/plugins/comment_paragraph/http/comment-paragraph.service.spec.ts
... ... @@ -8,7 +8,7 @@ describe(&quot;Services&quot;, () =&gt; {
8 8 let $httpBackend: ng.IHttpBackendService;
9 9 let commentParagraphService: CommentParagraphService;
10 10  
11   - beforeEach(angular.mock.module("noosferoApp", ($translateProvider: angular.translate.ITranslateProvider) => {
  11 + beforeEach(angular.mock.module("main", ($translateProvider: angular.translate.ITranslateProvider) => {
12 12 $translateProvider.translations('en', {});
13 13 }));
14 14  
... ...
src/plugins/comment_paragraph/index.ts
1 1 import {AllowCommentComponent} from "./allow-comment/allow-comment.component";
  2 +import {ExportCommentButtonHotspotComponent} from "./hotspot/export-comment-button.component";
2 3 import {CommentParagraphFormHotspotComponent} from "./hotspot/comment-paragraph-form.component";
3 4 import {DiscussionEditorComponent} from "./article/cms/discussion-editor/discussion-editor.component";
4 5 import {CommentParagraphArticleContentHotspotComponent} from "./hotspot/article-content/article-content.component";
5 6 import {DiscussionBlockComponent} from "./block/discussion/discussion-block.component";
6 7  
7 8 export let mainComponents: any = [AllowCommentComponent, DiscussionEditorComponent, DiscussionBlockComponent];
8   -export let hotspots: any = [CommentParagraphFormHotspotComponent, CommentParagraphArticleContentHotspotComponent];
  9 +export let hotspots: any = [ExportCommentButtonHotspotComponent, CommentParagraphFormHotspotComponent, CommentParagraphArticleContentHotspotComponent];
... ...
src/plugins/comment_paragraph/languages/en.json
1 1 {
2 2 "comment-paragraph-plugin.title": "Paragraph Comments",
  3 + "comment-paragraph-plugin.export": "Export Comments",
3 4 "comment-paragraph-plugin.discussion.editor.start_date.label": "From",
4 5 "comment-paragraph-plugin.discussion.editor.end_date.label": "To",
5 6 "comment-paragraph-plugin.discussion.header": "Open for comments",
... ...
src/plugins/comment_paragraph/languages/pt.json
1 1 {
2 2 "comment-paragraph-plugin.title": "Comentários por Parágrafo",
  3 + "comment-paragraph-plugin.export": "Exportar Comentários",
3 4 "comment-paragraph-plugin.discussion.editor.start_date.label": "De",
4 5 "comment-paragraph-plugin.discussion.editor.end_date.label": "Até",
5 6 "comment-paragraph-plugin.discussion.header": "Aberto para comentários",
... ...
src/plugins/comment_paragraph/models/comment_paragraph.ts 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +namespace noosfero {
  2 + export interface CommentParagraph extends noosfero.Comment {
  3 + paragraph_uuid?: string;
  4 + }
  5 +}
0 6 \ No newline at end of file
... ...
src/spec/mocks.ts
... ... @@ -85,24 +85,24 @@ export var mocks: any = {
85 85 mocks.articleService.articleAddedFn = fn;
86 86 },
87 87 modelRemovedEventEmitter:
88   - {
89   - subscribe: (fn: Function) => {
90   - mocks.articleService.articleRemovedFn = fn;
91   - },
92   - next: (param: any) => {
93   - mocks.articleService.articleRemovedFn(param);
94   - }
  88 + {
  89 + subscribe: (fn: Function) => {
  90 + mocks.articleService.articleRemovedFn = fn;
  91 + },
  92 + next: (param: any) => {
  93 + mocks.articleService.articleRemovedFn(param);
95 94 }
  95 + }
96 96 ,
97 97 modelAddedEventEmitter:
98   - {
99   - subscribe: (fn: Function) => {
100   - mocks.articleService.articleAddedFn = fn;
101   - },
102   - next: (param: any) => {
103   - mocks.articleService.articleAddedFn(param);
104   - }
  98 + {
  99 + subscribe: (fn: Function) => {
  100 + mocks.articleService.articleAddedFn = fn;
  101 + },
  102 + next: (param: any) => {
  103 + mocks.articleService.articleAddedFn(param);
105 104 }
  105 + }
106 106 ,
107 107 remove: (article: noosfero.Article) => {
108 108 return {
... ... @@ -156,6 +156,34 @@ export var mocks: any = {
156 156 instant: () => { }
157 157 },
158 158 commentService: {
  159 + commentRemovedFn: null,
  160 + commentAddedFn: null,
  161 + subscribeToModelRemoved: (fn: Function) => {
  162 + mocks.commentService.commentRemovedFn = fn;
  163 + },
  164 + subscribeToModelAdded: (fn: Function) => {
  165 + mocks.commentService.commentAddedFn = fn;
  166 + },
  167 + modelRemovedEventEmitter:
  168 + {
  169 + subscribe: (fn: Function) => {
  170 + mocks.commentService.commentRemovedFn = fn;
  171 + },
  172 + next: (param: any) => {
  173 + mocks.commentService.commentRemovedFn(param);
  174 + }
  175 + }
  176 + ,
  177 + modelAddedEventEmitter:
  178 + {
  179 + subscribe: (fn: Function) => {
  180 + mocks.articleService.commentAddedFn = fn;
  181 + },
  182 + next: (param: any) => {
  183 + mocks.articleService.commentAddedFn(param);
  184 + }
  185 + }
  186 + ,
159 187 getByArticle: (article: noosfero.Article) => {
160 188 return Promise.resolve({ data: {} });
161 189 }
... ...
themes/angular-participa-consulta/app/blocks.scss
  1 +%panel-head-theme {
  2 + border: none;
  3 + border-radius: 3px 3px 0 0;
  4 + font-family: "Ubuntu Medium";
  5 + font-size: 15px;
  6 + font-variant: normal;
  7 + font-weight: normal;
  8 + line-height: 30px;
  9 + padding: 15px 15px 15px 15px;
  10 + text-transform: capitalize;
  11 + background-size: 30px 30px;
  12 + background: #DAE1C4 !important;
  13 + color: #4F9CAC;
  14 +}
  15 +
1 16 .block-head-with-icon {
2 17 padding: 15px 15px 15px 55px;
3 18 }
4 19  
  20 +.content-wrapper .block .panel-heading,
  21 +.content-wrapper .side-options .panel-heading {
  22 + @extend %panel-head-theme;
  23 +}
  24 +
5 25 .content-wrapper .block {
6   - .panel-heading {
7   - border: none;
8   - border-radius: 3px 3px 0 0;
9   - font-family: "Ubuntu Medium";
10   - font-size: 15px;
11   - font-variant: normal;
12   - font-weight: normal;
13   - line-height: 30px;
14   - padding: 15px 15px 15px 15px;
15   - text-transform: capitalize;
16   - background-size: 30px 30px;
17   - background: #DAE1C4;
18   - color: #4F9CAC;
19   - }
20 26  
21 27 &.membersblock {
22 28 .panel-heading {
... ...
themes/angular-participa-consulta/app/navbar.scss
  1 +.skin-yellow {
  2 + .navbar-inverse .navbar-toggle {
  3 + border-color: #D49F18;
  4 + }
  5 +
  6 + .container-fluid .navbar-header .navbar-toggle {
  7 + &:hover, &:focus {
  8 + background-color: #f5b025;
  9 + }
  10 + }
  11 +}
  12 +
1 13 .navbar {
2 14 min-height: 123px;
3 15 background-color: #f9c404;
... ...
themes/angular-participa-consulta/app/participa-consulta.scss
  1 +$selected-color: #f6c445;
  2 +
1 3 @font-face {
2 4 font-family: 'Ubuntu';
3 5 font-weight: 300;
... ... @@ -19,9 +21,51 @@
19 21 src: url('../assets/fonts/participa-consulta/Ubuntu-RI.ttf');
20 22 }
21 23  
22   -.skin-whbl .notifications-list .item-footer {
23   - background: #DAE1C4;
24   - color: #4F9CAC;
  24 +.skin-yellow {
  25 + @extend %skin-base;
  26 +
  27 + .notifications-list .item-footer {
  28 + background: #DAE1C4;
  29 + color: #4F9CAC;
  30 + }
  31 +
  32 + .navbar-nav .open > a,
  33 + .navbar-nav .open > a:hover,
  34 + .navbar-nav .open > a:focus {
  35 + background-color: $selected-color;
  36 + color: #000;
  37 + }
  38 +
  39 + .nav .open > a,
  40 + .nav .open > a:hover,
  41 + .nav .open > a:focus {
  42 + border: none;
  43 + }
  44 +
  45 + .dropdown-menu {
  46 + li > a:hover {
  47 + color: #555;
  48 + background-color: #F7F7F7;
  49 + }
  50 +
  51 + .active > a,
  52 + .active > a:hover,
  53 + .active > a:focus {
  54 + color: #000;
  55 + background-color: #CCC;
  56 + }
  57 + }
  58 +
  59 + .nav .caret {
  60 + border-bottom-color: #fff !important;
  61 + border-top-color: #fff !important;
  62 + }
  63 +
  64 + .nav .open .caret {
  65 + border-bottom-color: #000 !important;
  66 + border-top-color: #000 !important;
  67 + }
  68 +
25 69 }
26 70  
27 71 .profile-header, .profile-footer{
... ... @@ -33,3 +77,8 @@
33 77 background-color: #7E7E7E;
34 78 }
35 79 }
  80 +
  81 +.ats-switch {
  82 + border: 0px;
  83 + border-color: transparent;
  84 +}
... ...