Compare View

switch
from
...
to
 
Commits (38)
Showing 89 changed files   Show diff stats
.gitignore
... ... @@ -14,3 +14,4 @@ typings
14 14 npm-debug.log
15 15 src/vendor.bundle.js*
16 16 .vagrant/
  17 +*.sw*
... ...
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"
... ...
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,22 @@
  1 +import {Injectable, Output, EventEmitter} from 'ng-forward';
  2 +
  3 +@Injectable()
  4 +export class DesignModeService {
  5 + @Output() onToggle: EventEmitter<boolean> = new EventEmitter<boolean>();
  6 +
  7 + private designModeOn: boolean = false;
  8 +
  9 + isInDesignMode(): boolean {
  10 + return this.designModeOn;
  11 + }
  12 +
  13 + setInDesignMode(value: boolean) {
  14 + if (this.designModeOn !== value) {
  15 + this.designModeOn = value;
  16 + this.onToggle.next(this.designModeOn);
  17 + }
  18 + }
  19 +
  20 + constructor() {
  21 + }
  22 +}
... ...
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/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/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,6 +30,18 @@ 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 47 @Inject(EnvironmentService, "$state", "currentEnvironment")
... ...
src/app/environment/environment.html
1 1 <div class="environment-container">
2 2 <div class="row">
3   - <noosfero-boxes ng-if="vm.boxes" [boxes]="vm.boxes" [owner]="vm.environment"></noosfero-boxes>
  3 + <noosfero-boxes ng-if="vm.boxes"
  4 + [layout]="vm.environment.layout_template"
  5 + [boxes]="vm.boxes"
  6 + [owner]="vm.environment">
  7 + </noosfero-boxes>
4 8 </div>
5 9 </div>
... ...
src/app/index.ts
... ... @@ -6,7 +6,7 @@ import {AuthEvents} from &quot;./login/auth-events&quot;;
6 6  
7 7 declare var moment: any;
8 8  
9   -//FIXME see a better way to declare template modules for dev mode
  9 +// FIXME see a better way to declare template modules for dev mode
10 10 try {
11 11 angular.module('noosfero.templates.app');
12 12 } catch (error) {
... ...
src/app/layout/blocks/block-content.component.spec.ts 0 → 100644
... ... @@ -0,0 +1,91 @@
  1 +import {TestComponentBuilder} from 'ng-forward/cjs/testing/test-component-builder';
  2 +import {Input, provide, Component} from 'ng-forward';
  3 +
  4 +import {BlockContentComponent} from './block-content.component';
  5 +
  6 +const tcb = new TestComponentBuilder();
  7 +
  8 +const htmlTemplate: string = '<noosfero-block-content [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-block-content>';
  9 +
  10 +describe("Components", () => {
  11 + describe("Block Component", () => {
  12 +
  13 + // the karma preprocessor html2js transform the templates html into js files which put
  14 + // the templates to the templateCache into the module templates
  15 + // we need to load the module templates here as the template for the
  16 + // component Block will be load on our tests
  17 + beforeEach(angular.mock.module("templates"));
  18 +
  19 + it("receives the block and the owner as inputs", done => {
  20 +
  21 + // Creating a container component (BlockContainerComponent) to include
  22 + // the component under test (Block)
  23 + @Component({ selector: 'test-container-component', template: htmlTemplate, directives: [BlockContentComponent] })
  24 + class BlockContainerComponent {
  25 + block = { type: 'Block' };
  26 + owner = { name: 'profile-name' };
  27 + constructor() {
  28 + }
  29 + }
  30 +
  31 + // uses the TestComponentBuilder instance to initialize the component
  32 + tcb
  33 + .createAsync(BlockContainerComponent).then(fixture => {
  34 + // and here we can inspect and run the test assertions
  35 + let myComponent: BlockContentComponent = fixture.componentInstance;
  36 +
  37 + // assure the block object inside the Block matches
  38 + // the provided through the parent component
  39 + expect(myComponent.block.type).toEqual("Block");
  40 + expect(myComponent.owner.name).toEqual("profile-name");
  41 + done();
  42 + });
  43 + });
  44 +
  45 +
  46 + it("renders a component which matches to the block type", done => {
  47 + // CustomBlock component created to check if it will be used
  48 + // when a block with type 'CustomBlock' is provided to the noosfero-block (Block)
  49 + // *** Important *** - the selector is what ng-forward uses to define the name of the directive provider
  50 + @Component({ selector: 'noosfero-custom-block', template: "<h1>My Custom Block</h1>" })
  51 + class CustomBlock {
  52 + @Input() block: any;
  53 + @Input() owner: any;
  54 + }
  55 +
  56 + @Component({ selector: 'test-container-component', template: htmlTemplate, directives: [BlockContentComponent, CustomBlock] })
  57 + class CustomBlockType {
  58 + block = { type: 'CustomBlock' };
  59 + owner = { name: 'profile-name' };
  60 + constructor() {
  61 + }
  62 + }
  63 + tcb
  64 + .createAsync(CustomBlockType).then(fixture => {
  65 + let myComponent: CustomBlockType = fixture.componentInstance;
  66 + expect(myComponent.block.type).toEqual("CustomBlock");
  67 + expect(fixture.debugElement.componentViewChildren[0].text()).toEqual("My Custom Block");
  68 + done();
  69 + });
  70 + });
  71 +
  72 +
  73 + it("renders the default block when hasn't defined a block type", done => {
  74 + @Component({ selector: 'test-container-component', template: htmlTemplate, directives: [BlockContentComponent] })
  75 + class CustomBlockType {
  76 + block: any = { type: null };
  77 + owner: any = { name: 'profile-name' };
  78 + constructor() {
  79 + }
  80 + }
  81 + tcb
  82 + .createAsync(CustomBlockType).then(fixture => {
  83 + let myComponent: CustomBlockType = fixture.componentInstance;
  84 + expect(myComponent.block.type).toBeNull();
  85 + expect(!!fixture.debugElement.nativeElement.querySelector("noosfero-default-block")).toBeTruthy();
  86 + done();
  87 + });
  88 + });
  89 +
  90 + });
  91 +});
... ...
src/app/layout/blocks/block-content.component.ts 0 → 100644
... ... @@ -0,0 +1,20 @@
  1 +import { Input, Inject, Component } from 'ng-forward';
  2 +
  3 +@Component({
  4 + selector: 'noosfero-block-content',
  5 + template: '<div></div>'
  6 +})
  7 +@Inject("$element", "$scope", "$injector", "$compile")
  8 +export class BlockContentComponent {
  9 +
  10 + @Input() block: any;
  11 + @Input() owner: any;
  12 +
  13 + ngOnInit() {
  14 + let blockName = (this.block && this.block.type) ? this.block.type.replace(/::/, '').replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() : "default-block";
  15 + this.$element.replaceWith(this.$compile('<noosfero-' + blockName + ' [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-' + blockName + '>')(this.$scope));
  16 + }
  17 +
  18 + constructor(private $element: any, private $scope: ng.IScope, private $injector: ng.auto.IInjectorService, private $compile: ng.ICompileService) {
  19 + }
  20 +}
... ...
src/app/layout/blocks/block-edition/block-edition.component.spec.ts 0 → 100644
... ... @@ -0,0 +1,34 @@
  1 +import {Component} from 'ng-forward';
  2 +import {BlockEditionComponent} from './block-edition.component';
  3 +import * as helpers from "../../../../spec/helpers";
  4 +import {ComponentTestHelper, createClass} from '../../../../spec/component-test-helper';
  5 +
  6 +const htmlTemplate: string = '<noosfero-block-edition></noosfero-block-edition>';
  7 +
  8 +describe("Boxes Component", () => {
  9 +
  10 + let helper: ComponentTestHelper<BlockEditionComponent>;
  11 + let translatorService = {
  12 + availableLanguages: { 'en': 'English', 'pt': 'Portuguese' }
  13 + };
  14 +
  15 + beforeEach(() => {
  16 + angular.mock.module("templates");
  17 + });
  18 +
  19 + beforeEach((done) => {
  20 + let cls = createClass({
  21 + template: htmlTemplate,
  22 + directives: [BlockEditionComponent],
  23 + providers: [
  24 + helpers.createProviderToValue('TranslatorService', translatorService)
  25 + ]
  26 + });
  27 + helper = new ComponentTestHelper<BlockEditionComponent>(cls, done);
  28 + });
  29 +
  30 + it("get available languages from translator service", () => {
  31 + expect(helper.component.languageOptions).toEqual(['all', 'en', 'pt']);
  32 + });
  33 +
  34 +});
... ...
src/app/layout/blocks/block-edition/block-edition.component.ts 0 → 100644
... ... @@ -0,0 +1,22 @@
  1 +import { Input, Inject, Component } from 'ng-forward';
  2 +import { TranslatorService } from "../../../shared/services/translator.service";
  3 +
  4 +@Component({
  5 + selector: 'noosfero-block-edition',
  6 + templateUrl: 'app/layout/blocks/block-edition/block-edition.html'
  7 +})
  8 +@Inject(TranslatorService)
  9 +export class BlockEditionComponent {
  10 +
  11 + static $inject = ["TranslatorService"]; // @Inject doesn't works with uibModal.open
  12 +
  13 + displayOptions: any;
  14 + displayUserOptions: any;
  15 + languageOptions: any;
  16 +
  17 + constructor(private translatorService: TranslatorService) {
  18 + this.displayOptions = ["always", "home_page_only", "except_home_page", "never"];
  19 + this.displayUserOptions = ["all", "logged", "not_logged"];
  20 + this.languageOptions = ["all"].concat(Object.keys(translatorService.availableLanguages));
  21 + }
  22 +}
... ...
src/app/layout/blocks/block-edition/block-edition.html 0 → 100644
... ... @@ -0,0 +1,34 @@
  1 +<div class="edit-block">
  2 + <h3>{{"block.edition.title" | translate}}</h3>
  3 +
  4 + <form class="options">
  5 + <div class="title block-option">
  6 + <label for="titleInput">{{"block.edition.title.label" | translate}}</label>
  7 + <input type="text" id="titleInput" ng-model="ctrl.block.title" class="block-input">
  8 + </div>
  9 + <div class="display block-option">
  10 + <label for="displayInput">{{"block.edition.display.label" | translate}}</label>
  11 + <select id="displayInput" ng-model="ctrl.block.settings.display" class="block-input">
  12 + <option ng-repeat="option in modal.displayOptions" value="{{option}}">{{"block.edition.display." + option | translate}}</option>
  13 + </select>
  14 + </div>
  15 + <div class="displayUser block-option">
  16 + <label for="displayUserInput">{{"block.edition.display_user.label" | translate}}</label>
  17 + <select id="displayUserInput" ng-model="ctrl.block.settings.display_user" class="block-input">
  18 + <option ng-repeat="option in modal.displayUserOptions" value="{{option}}">{{"block.edition.display_user." + option | translate}}</option>
  19 + </select>
  20 + </div>
  21 + <div class="language block-option">
  22 + <label for="languageInput">{{"block.edition.language.label" | translate}}</label>
  23 + <select id="languageInput" ng-model="ctrl.block.settings.language" class="block-input">
  24 + <option ng-repeat="option in modal.languageOptions" value="{{option}}">{{"language." + option | translate}}</option>
  25 + </select>
  26 + </div>
  27 + </form>
  28 +
  29 + <div class="actions">
  30 + <button type="submit" class="btn btn-default" ng-click="ctrl.save()">Save</button>
  31 + <button type="submit" class="btn btn-warning" ng-click="ctrl.preview()">Preview</button>
  32 + <button type="submit" class="btn btn-danger" ng-click="ctrl.cancel()">Cancel</button>
  33 + </div>
  34 +</div>
... ...
src/app/layout/blocks/block-edition/block-edition.scss 0 → 100644
... ... @@ -0,0 +1,14 @@
  1 +.edit-block {
  2 + margin: 20px;
  3 +
  4 + .options {
  5 + margin-bottom: 20px;
  6 +
  7 + .block-option {
  8 + @extend .form-group;
  9 + .block-input {
  10 + @extend .form-control;
  11 + }
  12 + }
  13 + }
  14 +}
... ...
src/app/layout/blocks/block.component.spec.ts
1   -import {TestComponentBuilder} from 'ng-forward/cjs/testing/test-component-builder';
2   -import {Input, provide, Component} from 'ng-forward';
3   -
  1 +import {Component} from 'ng-forward';
4 2 import {BlockComponent} from './block.component';
  3 +import * as helpers from "../../../spec/helpers";
  4 +import {ComponentTestHelper, createClass} from '../../../spec/component-test-helper';
5 5  
6   -const tcb = new TestComponentBuilder();
7   -
8   -const htmlTemplate: string = '<noosfero-block [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-block>';
9   -
10   -describe("Components", () => {
11   - describe("Block Component", () => {
12   -
13   - // the karma preprocessor html2js transform the templates html into js files which put
14   - // the templates to the templateCache into the module templates
15   - // we need to load the module templates here as the template for the
16   - // component Block will be load on our tests
17   - beforeEach(angular.mock.module("templates"));
18   -
19   - it("receives the block and the owner as inputs", done => {
20   -
21   - // Creating a container component (BlockContainerComponent) to include
22   - // the component under test (Block)
23   - @Component({ selector: 'test-container-component', template: htmlTemplate, directives: [BlockComponent] })
24   - class BlockContainerComponent {
25   - block = { type: 'Block' };
26   - owner = { name: 'profile-name' };
27   - constructor() {
28   - }
29   - }
30   -
31   - // uses the TestComponentBuilder instance to initialize the component
32   - tcb
33   - .createAsync(BlockContainerComponent).then(fixture => {
34   - // and here we can inspect and run the test assertions
35   - let myComponent: BlockComponent = fixture.componentInstance;
36   -
37   - // assure the block object inside the Block matches
38   - // the provided through the parent component
39   - expect(myComponent.block.type).toEqual("Block");
40   - expect(myComponent.owner.name).toEqual("profile-name");
41   - done();
42   - });
43   - });
  6 +const htmlTemplate: string = '<noosfero-block [block]="ctrl.block" [owner]="ctrl.profile"></noosfero-block>';
44 7  
  8 +describe("Boxes Component", () => {
45 9  
46   - it("renders a component which matches to the block type", done => {
47   - // CustomBlock component created to check if it will be used
48   - // when a block with type 'CustomBlock' is provided to the noosfero-block (Block)
49   - // *** Important *** - the selector is what ng-forward uses to define the name of the directive provider
50   - @Component({ selector: 'noosfero-custom-block', template: "<h1>My Custom Block</h1>" })
51   - class CustomBlock {
52   - @Input() block: any;
53   - @Input() owner: any;
54   - }
55   -
56   - @Component({ selector: 'test-container-component', template: htmlTemplate, directives: [BlockComponent, CustomBlock] })
57   - class CustomBlockType {
58   - block = { type: 'CustomBlock' };
59   - owner = { name: 'profile-name' };
60   - constructor() {
61   - }
62   - }
63   - tcb
64   - .createAsync(CustomBlockType).then(fixture => {
65   - let myComponent: CustomBlockType = fixture.componentInstance;
66   - expect(myComponent.block.type).toEqual("CustomBlock");
67   - expect(fixture.debugElement.componentViewChildren[0].text()).toEqual("My Custom Block");
68   - done();
69   - });
  10 + let helper: ComponentTestHelper<BlockComponent>;
  11 + beforeEach(() => {
  12 + angular.mock.module("templates");
  13 + });
  14 +
  15 + let properties = {
  16 + block: { id: 1 },
  17 + owner: {
  18 + id: 1,
  19 + identifier: 'profile-name',
  20 + type: 'Person'
  21 + }
  22 + };
  23 + beforeEach((done) => {
  24 + let cls = createClass({
  25 + template: htmlTemplate,
  26 + directives: [BlockComponent],
  27 + properties: properties,
  28 + providers: [
  29 + helpers.createProviderToValue('SessionService', helpers.mocks.sessionWithCurrentUser({})),
  30 + helpers.createProviderToValue('AuthService', helpers.mocks.authService),
  31 + helpers.createProviderToValue('$state', state),
  32 + helpers.createProviderToValue('TranslatorService', translatorService),
  33 + helpers.createProviderToValue('$uibModal', helpers.mocks.$modal),
  34 + helpers.createProviderToValue('BlockService', blockService),
  35 + helpers.createProviderToValue('NotificationService', helpers.mocks.notificationService)
  36 + ]
70 37 });
  38 + helper = new ComponentTestHelper<BlockComponent>(cls, done);
  39 + });
71 40  
  41 + let translatorService = jasmine.createSpyObj("translatorService", ["currentLanguage"]);
  42 + let blockService = jasmine.createSpyObj("blockService", ["update"]);
  43 + let state = jasmine.createSpyObj("state", ["current"]);
  44 + state.current = { name: "" };
72 45  
73   - it("renders the default block when hasn't defined a block type", done => {
74   - @Component({ selector: 'test-container-component', template: htmlTemplate, directives: [BlockComponent] })
75   - class CustomBlockType {
76   - block: any = { type: null };
77   - owner: any = { name: 'profile-name' };
78   - constructor() {
79   - }
80   - }
81   - tcb
82   - .createAsync(CustomBlockType).then(fixture => {
83   - let myComponent: CustomBlockType = fixture.componentInstance;
84   - expect(myComponent.block.type).toBeNull();
85   - expect(!!fixture.debugElement.nativeElement.querySelector("noosfero-default-block")).toBeTruthy();
86   - done();
87   - });
88   - });
  46 + it("set isHomepage as false by default", () => {
  47 + expect(helper.component.isHomepage).toBeFalsy();
  48 + });
  49 +
  50 + it("set isHomepage as true when in profile home page", () => {
  51 + state.current = { name: "main.profile.home" };
  52 + helper.component.ngOnInit();
  53 + expect(helper.component.isHomepage).toBeTruthy();
  54 + });
  55 +
  56 + it("set isHomepage as true when in profile info page", () => {
  57 + state.current = { name: "main.profile.info" };
  58 + helper.component.ngOnInit();
  59 + expect(helper.component.isHomepage).toBeTruthy();
  60 + });
  61 +
  62 + it("set isHomepage as true when in profile page", () => {
  63 + state.current = { name: "main.profile.page" };
  64 + state.params = { page: "/page" };
  65 + (<noosfero.Profile>helper.component.owner).homepage = '/page';
  66 + helper.component.ngOnInit();
  67 + expect(helper.component.isHomepage).toBeTruthy();
  68 + });
  69 +
  70 + it("set isHomepage as true when in environment home page", () => {
  71 + state.current = { name: "main.environment.home" };
  72 + helper.component.owner = <noosfero.Environment>{};
  73 + helper.component.ngOnInit();
  74 + expect(helper.component.isHomepage).toBeTruthy();
  75 + });
  76 +
  77 + it("return true in canDisplay when no display option is setted", () => {
  78 + helper.component.block = <any>{};
  79 + expect(helper.component.canDisplay()).toEqual(true);
  80 + });
  81 +
  82 + it("return false in canDisplay for an invisible block", () => {
  83 + helper.component.block = <any>{ settings: { display: "never" } };
  84 + expect(helper.component.canDisplay()).toEqual(false);
  85 + });
  86 +
  87 + it("return false in canDisplay with except_home_page in homepage", () => {
  88 + helper.component.block = <any>{ settings: { display_user: "except_home_page" } };
  89 + expect(helper.component.canDisplay()).toEqual(false);
  90 + });
  91 +
  92 + it("return false in canDisplay with home_page_only outside homepage", () => {
  93 + helper.component.block = <any>{ settings: { display_user: "home_page_only" } };
  94 + expect(helper.component.canDisplay()).toEqual(false);
  95 + });
  96 +
  97 + it("return true in canDisplay when display_user is all for logged user", () => {
  98 + helper.component.block = <any>{ settings: { display_user: "all" } };
  99 + expect(helper.component.canDisplay()).toEqual(true);
  100 + });
  101 +
  102 + it("return true in canDisplay when display_user is all for not logged user", () => {
  103 + helper.component.currentUser = null;
  104 + helper.component.block = <any>{ settings: { display_user: "all" } };
  105 + expect(helper.component.canDisplay()).toEqual(true);
  106 + });
89 107  
  108 + it("return false in canDisplay when display_user is logged for not logged user", () => {
  109 + helper.component.currentUser = null;
  110 + helper.component.block = <any>{ settings: { display_user: "logged" } };
  111 + expect(helper.component.canDisplay()).toEqual(false);
90 112 });
91   -});
92 113 \ No newline at end of file
  114 +
  115 + it("return false in canDisplay when display_user is not_logged for logged user", () => {
  116 + helper.component.block = <any>{ settings: { display_user: "not_logged" } };
  117 + expect(helper.component.canDisplay()).toEqual(false);
  118 + });
  119 +
  120 + it("return false in canDisplay when current language is not equal to language in block settings", () => {
  121 + helper.component['translatorService'].currentLanguage = jasmine.createSpy("currentLanguage").and.returnValue("pt");
  122 + helper.component.block = <any>{ settings: { language: "en" } };
  123 + expect(helper.component.canDisplay()).toEqual(false);
  124 + });
  125 +
  126 + it("return false in canDisplay when hide is true", () => {
  127 + helper.component.block = <any>{ id: 1, hide: true };
  128 + expect(helper.component.canDisplay()).toEqual(false);
  129 + });
  130 +
  131 + it("return true in canDisplay when hide is not true", () => {
  132 + helper.component.block = <any>{ id: 1, hide: false };
  133 + expect(helper.component.canDisplay()).toEqual(true);
  134 + });
  135 +
  136 +});
... ...
src/app/layout/blocks/block.component.ts
1   -import { Input, Inject, Component } from 'ng-forward';
  1 +import { Input, Component, Inject } from 'ng-forward';
  2 +import { BlockEditionComponent } from './block-edition/block-edition.component';
  3 +import { BlockService } from '../../../lib/ng-noosfero-api/http/block.service';
  4 +import { NotificationService } from '../../shared/services/notification.service';
  5 +import { AuthService, SessionService, AuthEvents } from "../../login";
  6 +import { TranslatorService } from "../../shared/services/translator.service";
  7 +import { DesignModeService } from "../../admin/layout-edit/designMode.service";
2 8  
3 9 @Component({
4 10 selector: 'noosfero-block',
5   - template: '<div></div>'
  11 + templateUrl: 'app/layout/blocks/block.html',
  12 + directives: [BlockEditionComponent]
6 13 })
7   -@Inject("$element", "$scope", "$injector", "$compile")
  14 +@Inject("$uibModal", "$scope", "$state", "$rootScope", BlockService, NotificationService, AuthService, SessionService, TranslatorService, DesignModeService)
8 15 export class BlockComponent {
9 16  
10   - @Input() block: any;
11   - @Input() owner: any;
  17 + @Input() block: noosfero.Block;
  18 + @Input() owner: noosfero.Profile | noosfero.Environment;
  19 +
  20 + private modalInstance: any = null;
  21 + originalBlock: noosfero.Block;
  22 +
  23 + currentUser: noosfero.User;
  24 + isHomepage = true;
  25 + editionMode = false;
  26 + designMode = false;
  27 +
  28 + constructor(private $uibModal: any,
  29 + private $scope: ng.IScope,
  30 + private $state: ng.ui.IStateService,
  31 + private $rootScope: ng.IRootScopeService,
  32 + private blockService: BlockService,
  33 + private notificationService: NotificationService,
  34 + private authService: AuthService,
  35 + private session: SessionService,
  36 + private translatorService: TranslatorService,
  37 + private designModeService: DesignModeService) {
  38 +
  39 + this.currentUser = this.session.currentUser();
  40 + this.authService.subscribe(AuthEvents[AuthEvents.loginSuccess], () => {
  41 + this.currentUser = this.session.currentUser();
  42 + this.verifyHomepage();
  43 + });
  44 + this.authService.subscribe(AuthEvents[AuthEvents.logoutSuccess], () => {
  45 + this.currentUser = this.session.currentUser();
  46 + this.verifyHomepage();
  47 + });
  48 + this.$rootScope.$on("$stateChangeSuccess", (event: ng.IAngularEvent, toState: ng.ui.IState) => {
  49 + this.verifyHomepage();
  50 + });
  51 + this.designModeService.onToggle.subscribe((designModeOn: boolean) => {
  52 + this.editionMode = designModeOn;
  53 + this.designMode = designModeOn;
  54 + this.$scope.$apply();
  55 + });
  56 + }
12 57  
13 58 ngOnInit() {
14   - let blockName = (this.block && this.block.type) ? this.block.type.replace(/::/, '').replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() : "default-block";
15   - this.$element.replaceWith(this.$compile('<noosfero-' + blockName + ' [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-' + blockName + '>')(this.$scope));
  59 + this.verifyHomepage();
  60 + }
  61 +
  62 + openEdit() {
  63 + this.editionMode = true;
  64 + if (!this.originalBlock) this.originalBlock = JSON.parse(JSON.stringify(this.block)); // deep copy of block data
  65 + this.modalInstance = this.$uibModal.open({
  66 + templateUrl: 'app/layout/blocks/block-edition/block-edition.html',
  67 + size: 'lg',
  68 + controller: BlockEditionComponent,
  69 + controllerAs: 'modal',
  70 + bindToController: true,
  71 + scope: this.$scope
  72 + });
  73 + }
  74 +
  75 + save() {
  76 + this.editionMode = false;
  77 + this.blockService.update(this.attributesToUpdate()).then(() => {
  78 + this.closeEdit();
  79 + this.notificationService.success({ title: "block.edition.success.title", message: "block.edition.success.message" });
  80 + });
  81 + }
  82 +
  83 + preview() {
  84 + this.closeEdit();
  85 + }
  86 +
  87 + cancel() {
  88 + this.editionMode = false;
  89 + this.block = this.originalBlock;
  90 + this.closeEdit();
16 91 }
17 92  
18   - constructor(private $element: any, private $scope: ng.IScope, private $injector: ng.auto.IInjectorService, private $compile: ng.ICompileService) {
  93 + canDisplay() {
  94 + return this.visible() && this.displayToUser() &&
  95 + this.displayOnLanguage(this.translatorService.currentLanguage()) &&
  96 + !this.block.hide;
19 97 }
  98 +
  99 + protected visible() {
  100 + let display = this.block.settings ? (<any>this.block.settings)['display'] : null;
  101 + return !display || ((this.isHomepage ? display !== "except_home_page" : display !== "home_page_only") && display !== "never");
  102 + }
  103 +
  104 + protected displayToUser() {
  105 + let displayUser = this.block.settings ? (<any>this.block.settings)['display_user'] : null;
  106 + return !displayUser || displayUser === "all" ||
  107 + (this.currentUser ? displayUser === "logged" : displayUser === "not_logged");
  108 + }
  109 +
  110 + protected displayOnLanguage(language: string) {
  111 + let displayLanguage = this.block.settings ? (<any>this.block.settings)['language'] : null;
  112 + return !displayLanguage || displayLanguage === "all" ||
  113 + language === displayLanguage;
  114 + }
  115 +
  116 + protected attributesToUpdate() {
  117 + return <any>{
  118 + id: this.block.id,
  119 + display: (<any>this.block.settings).display,
  120 + title: this.block.title,
  121 + display_user: (<any>this.block.settings).display_user,
  122 + language: (<any>this.block.settings).language
  123 + };
  124 + }
  125 +
  126 + protected verifyHomepage() {
  127 + if (this.owner && ["Profile", "Community", "Person"].indexOf((<any>this.owner)['type']) >= 0) {
  128 + let profile = <noosfero.Profile>this.owner;
  129 + this.isHomepage = this.$state.current.name === "main.profile.home";
  130 + if (profile.homepage) {
  131 + this.isHomepage = this.isHomepage ||
  132 + (this.$state.current.name === "main.profile.page" && profile.homepage === this.$state.params['page']);
  133 + } else {
  134 + this.isHomepage = this.isHomepage || this.$state.current.name === "main.profile.info";
  135 + }
  136 + } else {
  137 + this.isHomepage = this.$state.current.name === "main.environment.home";
  138 + }
  139 + }
  140 +
  141 + private closeEdit() {
  142 + if (this.modalInstance) {
  143 + this.modalInstance.close();
  144 + this.modalInstance = null;
  145 + }
  146 + }
  147 +
20 148 }
... ...
src/app/layout/blocks/block.html 0 → 100644
... ... @@ -0,0 +1,13 @@
  1 +<div ng-show="ctrl.canDisplay() || ctrl.editionMode || ctrl.designMode" ng-class="{'invisible-block': !ctrl.canDisplay()}" class="noosfero-block" ng-mouseover="displayActions = true" ng-mouseleave="displayActions = false">
  2 + <div ng-show="displayActions" class="actions block-actions" permission="ctrl.block.permissions" permission-action="allow_edit">
  3 + <button type="submit" class="btn btn-xs btn-default" ng-click="ctrl.openEdit()"><i class="fa fa-edit fa-fw"></i></button>
  4 + </div>
  5 + <div class="panel panel-default block {{ctrl.block.type | lowercase}}" >
  6 + <div class="panel-heading" ng-show="ctrl.block.title">
  7 + <h3 class="panel-title">{{ctrl.block.title}}</h3>
  8 + </div>
  9 + <div class="panel-body {{ctrl.block.type | lowercase}}" >
  10 + <noosfero-block-content [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-block-content>
  11 + </div>
  12 + </div>
  13 +</div>
... ...
src/app/layout/blocks/block.scss
1   -.block {
2   - .panel-title {
3   - font-size: 15px;
4   - font-weight: bold;
  1 +.noosfero-block {
  2 + position: relative;
  3 + &.invisible-block {
  4 + .block {
  5 + opacity: 0.4;
  6 + }
5 7 }
6   - .panel-heading {
7   - background-color: transparent;
8   - border: 0;
  8 + .block {
  9 + .panel-title {
  10 + font-size: 15px;
  11 + font-weight: bold;
  12 + }
  13 + .panel-heading {
  14 + background-color: transparent;
  15 + border: 0;
  16 + }
  17 + }
  18 + .block-actions {
  19 + position: absolute;
  20 + z-index: 100;
  21 + right: 1px;
  22 + top: 1px;
9 23 }
10 24 }
... ...
src/app/layout/blocks/index.ts
1 1 /* Module Index Entry - generated using the script npm run generate-index */
2   -export * from "./block.component";
  2 +export * from "./block-content.component";
... ...
src/app/layout/blocks/main/main-block.component.ts
1 1 import {Component, Input} from 'ng-forward';
2   -import {BlockComponent} from '../block.component';
3 2  
4 3 @Component({
5 4 selector: 'noosfero-main-block',
... ...
src/app/layout/blocks/person-tags-plugin-interests/index.ts 0 → 100644
... ... @@ -0,0 +1,2 @@
  1 +/* Module Index Entry - generated using the script npm run generate-index */
  2 +export * from "./person-tags-plugin-interests-block.component";
... ...
src/app/layout/blocks/person-tags-plugin-interests/person-tags-plugin-interests-block.component.spec.ts 0 → 100644
... ... @@ -0,0 +1,56 @@
  1 +import {TestComponentBuilder} from 'ng-forward/cjs/testing/test-component-builder';
  2 +import {Provider, Input, provide, Component} from 'ng-forward';
  3 +import {provideFilters} from '../../../../spec/helpers';
  4 +import {PersonTagsPluginInterestsBlockComponent} from './person-tags-plugin-interests-block.component';
  5 +import * as helpers from "./../../../../spec/helpers";
  6 +
  7 +const htmlTemplate: string = '<noosfero-person-tags-plugin-interests-block [block]="ctrl.block" [owner]="ctrl.owner"></noosfero-person-tags-plugin-interests-block>';
  8 +
  9 +const tcb = new TestComponentBuilder();
  10 +
  11 +describe("Components", () => {
  12 + describe("Person Tags Interests Block Component", () => {
  13 +
  14 + let settingsObj = {};
  15 + let person = <noosfero.Person>{ name: "Person" };
  16 + let mockedService = {
  17 + getTags: (profile: noosfero.Profile): any => {
  18 + return Promise.resolve({ data: ['foo', 'bar'], headers: (name: string) => { return name; } });
  19 + }
  20 + };
  21 + beforeEach(angular.mock.module("templates"));
  22 +
  23 + let state = jasmine.createSpyObj("state", ["go"]);
  24 +
  25 +
  26 + function getProviders() {
  27 + return [
  28 + new Provider('$state', { useValue: state }),
  29 + new Provider('PersonService', {
  30 + useValue: mockedService
  31 + })
  32 + ].concat(provideFilters("truncateFilter", "stripTagsFilter"));
  33 + }
  34 + let componentClass: any = null;
  35 +
  36 + function getComponent() {
  37 + @Component({ selector: 'test-container-component', template: htmlTemplate, directives: [PersonTagsPluginInterestsBlockComponent], providers: getProviders() })
  38 + class BlockContainerComponent {
  39 + block = { type: 'Block', settings: settingsObj };
  40 + owner = person;
  41 + constructor() {
  42 + }
  43 + }
  44 + return BlockContainerComponent;
  45 + }
  46 +
  47 + it("get tags from the person service", done => {
  48 + tcb.createAsync(getComponent()).then(fixture => {
  49 + let PersonTagsPluginInterestsBlock: PersonTagsPluginInterestsBlockComponent = fixture.debugElement.componentViewChildren[0].componentInstance;
  50 + expect(PersonTagsPluginInterestsBlock.tags).toEqual(['foo', 'bar']);
  51 + done();
  52 + });
  53 + });
  54 +
  55 + });
  56 +});
... ...
src/app/layout/blocks/person-tags-plugin-interests/person-tags-plugin-interests-block.component.ts 0 → 100644
... ... @@ -0,0 +1,27 @@
  1 +import {Component, Inject, Input} from "ng-forward";
  2 +import {PersonService} from "./../../../../lib/ng-noosfero-api/http/person.service";
  3 +import {Arrays} from "./../../../../lib/util/arrays";
  4 +
  5 +@Component({
  6 + selector: "noosfero-person-tags-plugin-interests-block",
  7 + templateUrl: 'app/layout/blocks/person-tags-plugin-interests/person-tags-plugin-interests-block.html'
  8 +})
  9 +@Inject(PersonService, "$state")
  10 +export class PersonTagsPluginInterestsBlockComponent {
  11 +
  12 + @Input() block: any;
  13 + @Input() owner: any;
  14 +
  15 + profile: any;
  16 + tags: any;
  17 +
  18 + constructor(private personService: PersonService, private $state: any) { }
  19 +
  20 + ngOnInit() {
  21 + this.profile = this.owner;
  22 + this.tags = [];
  23 + this.personService.getTags(this.owner).then((result: noosfero.RestResult<any>) => {
  24 + this.tags = result.data;
  25 + });
  26 + }
  27 +}
... ...
src/app/layout/blocks/person-tags-plugin-interests/person-tags-plugin-interests-block.html 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +<ul class="person-tags-plugin-interests">
  2 + <li ng-repeat="tag in ctrl.tags">{{tag}}</li>
  3 +</ul>
... ...
src/app/layout/blocks/person-tags-plugin-interests/person-tags-plugin-interests-block.scss 0 → 100644
... ... @@ -0,0 +1,14 @@
  1 +.person-tags-plugin-interests {
  2 + padding: 0;
  3 + margin: 0;
  4 +
  5 + li {
  6 + list-style: none;
  7 + float: left;
  8 + padding: 5px;
  9 + margin: 5px;
  10 + text-transform: lowercase;
  11 + color: #fff;
  12 + background-color: #1E96D0;
  13 + }
  14 +}
... ...
src/app/layout/boxes/box.html
1   -<div ng-class="{'col-md-2-5': box.position!=1, 'col-md-7': box.position==1}">
2   - <div ng-repeat="block in box.blocks | displayBlocks:ctrl.isHomepage:ctrl.currentUser | orderBy: 'position'" class="panel panel-default block {{block.type | lowercase}}" >
  1 +<div ng-class="box.position | setBoxLayout:ctrl.layout">
  2 +
  3 + <div ng-repeat="block in box.blocks | orderBy: 'position'" class="panel panel-default block {{block.type | lowercase}}" >
3 4 <div class="panel-heading" ng-show="block.title">
4 5 <h3 class="panel-title">{{block.title}}</h3>
5 6 </div>
... ...
src/app/layout/boxes/boxes.component.spec.ts
... ... @@ -48,40 +48,4 @@ describe(&quot;Boxes Component&quot;, () =&gt; {
48 48 expect(helper.find('div.col-md-7').length).toEqual(1);
49 49 expect(helper.find('div.col-md-2-5').length).toEqual(1);
50 50 });
51   -
52   - it("check the boxes order", () => {
53   - expect(helper.component.boxesOrder(properties['boxes'][0])).toEqual(1);
54   - expect(helper.component.boxesOrder(properties['boxes'][1])).toEqual(0);
55   - });
56   -
57   - it("set isHomepage as false by default", () => {
58   - expect(helper.component.isHomepage).toBeFalsy();
59   - });
60   -
61   - it("set isHomepage as true when in profile home page", () => {
62   - state.current = { name: "main.profile.home" };
63   - helper.component.ngOnInit();
64   - expect(helper.component.isHomepage).toBeTruthy();
65   - });
66   -
67   - it("set isHomepage as true when in profile info page", () => {
68   - state.current = { name: "main.profile.info" };
69   - helper.component.ngOnInit();
70   - expect(helper.component.isHomepage).toBeTruthy();
71   - });
72   -
73   - it("set isHomepage as true when in profile page", () => {
74   - state.current = { name: "main.profile.page" };
75   - state.params = { page: "/page" };
76   - (<noosfero.Profile>helper.component.owner).homepage = '/page';
77   - helper.component.ngOnInit();
78   - expect(helper.component.isHomepage).toBeTruthy();
79   - });
80   -
81   - it("set isHomepage as true when in environment home page", () => {
82   - state.current = { name: "main.environment.home" };
83   - helper.component.owner = <noosfero.Environment>{};
84   - helper.component.ngOnInit();
85   - expect(helper.component.isHomepage).toBeTruthy();
86   - });
87 51 });
... ...
src/app/layout/boxes/boxes.component.ts
1 1 import {Input, Inject, Component} from 'ng-forward';
2   -import {SessionService, AuthService, AuthEvents} from "../../login";
3   -import {DisplayBlocks} from "./display-blocks.filter";
  2 +import {DisplayBoxes} from "./display-boxes.filter";
  3 +import {SetBoxLayout} from "./set-box-layout.filter";
4 4  
5 5 @Component({
6 6 selector: "noosfero-boxes",
7 7 templateUrl: "app/layout/boxes/boxes.html",
8   - directives: [DisplayBlocks]
  8 + directives: [DisplayBoxes, SetBoxLayout]
9 9 })
10   -@Inject("SessionService", 'AuthService', "$state", "$rootScope")
11 10 export class BoxesComponent {
12 11  
13 12 @Input() boxes: noosfero.Box[];
14 13 @Input() owner: noosfero.Profile | noosfero.Environment;
  14 + @Input() layout: string;
15 15  
16   - currentUser: noosfero.User;
17   - isHomepage = true;
18   -
19   - constructor(private session: SessionService,
20   - private authService: AuthService,
21   - private $state: ng.ui.IStateService,
22   - private $rootScope: ng.IRootScopeService) {
23   -
24   - this.currentUser = this.session.currentUser();
25   - this.authService.subscribe(AuthEvents[AuthEvents.loginSuccess], () => {
26   - this.currentUser = this.session.currentUser();
27   - this.verifyHomepage();
28   - });
29   - this.authService.subscribe(AuthEvents[AuthEvents.logoutSuccess], () => {
30   - this.currentUser = this.session.currentUser();
31   - this.verifyHomepage();
32   - });
33   - this.$rootScope.$on("$stateChangeSuccess", (event: ng.IAngularEvent, toState: ng.ui.IState) => {
34   - this.verifyHomepage();
35   - });
36   - }
37   -
38   - ngOnInit() {
39   - this.verifyHomepage();
40   - }
41   -
42   - boxesOrder(box: noosfero.Box) {
43   - if (box.position === 2) return 0;
44   - return box.position;
45   - }
46   -
47   - private verifyHomepage() {
48   - if (this.owner && ["Profile", "Community", "Person"].indexOf((<any>this.owner)['type']) >= 0) {
49   - let profile = <noosfero.Profile>this.owner;
50   - this.isHomepage = this.$state.current.name === "main.profile.home";
51   - if (profile.homepage) {
52   - this.isHomepage = this.isHomepage ||
53   - (this.$state.current.name === "main.profile.page" && profile.homepage === this.$state.params['page']);
54   - } else {
55   - this.isHomepage = this.isHomepage || this.$state.current.name === "main.profile.info";
56   - }
57   - } else {
58   - this.isHomepage = this.$state.current.name === "main.environment.home";
59   - }
60   - }
61 16 }
... ...
src/app/layout/boxes/boxes.html
1   -<ng-include ng-repeat="box in ctrl.boxes | orderBy: ctrl.boxesOrder" src="'app/layout/boxes/box.html'"></ng-include>
  1 +<ng-include ng-repeat="box in ctrl.boxes | displayBoxes:ctrl.layout" src="'app/layout/boxes/box.html'"></ng-include>
... ...
src/app/layout/boxes/boxes.scss
1 1 .col-md-2-5 {
2 2 @extend .col-md-3;
3   - @media (min-width: 920px) {
  3 + @media (min-width: 992px) {
4 4 width: 20.83%;
5 5 }
6 6 }
... ...
src/app/layout/boxes/display-boxes.filter.ts 0 → 100644
... ... @@ -0,0 +1,39 @@
  1 +import {Pipe, Inject} from "ng-forward";
  2 +import {TranslatorService} from "../../shared/services/translator.service";
  3 +
  4 +@Pipe("displayBoxes")
  5 +@Inject(TranslatorService)
  6 +export class DisplayBoxes {
  7 +
  8 + constructor(private translatorService: TranslatorService) { }
  9 +
  10 + transform(boxes: noosfero.Box[], layout: string) {
  11 + let function_str: string = "visible_on" + layout;
  12 + let valid_boxes: number[] = [];
  13 + let selected: noosfero.Box[] = [];
  14 + boxes = boxes || [];
  15 +
  16 + if(layout == "rightbar") {
  17 + valid_boxes = this.visible_on_right_bar();
  18 + }else {
  19 + valid_boxes = this.visible_on_default();
  20 + }
  21 +
  22 + for (let box of boxes) {
  23 + if(valid_boxes.indexOf(box.position) != -1){
  24 + selected.push(box);
  25 + }
  26 + }
  27 + return selected;
  28 + }
  29 +
  30 + private visible_on_default() {
  31 + return [1,2,3];
  32 + }
  33 +
  34 + private visible_on_right_bar() {
  35 + return [1,3];
  36 + }
  37 +
  38 +}
  39 +
... ...
src/app/layout/boxes/set-box-layout.filter.ts 0 → 100644
... ... @@ -0,0 +1,37 @@
  1 +import {Pipe, Inject} from "ng-forward";
  2 +import {TranslatorService} from "../../shared/services/translator.service";
  3 +
  4 +@Pipe("setBoxLayout")
  5 +@Inject(TranslatorService)
  6 +export class SetBoxLayout {
  7 +
  8 + constructor(private translatorService: TranslatorService) { }
  9 +
  10 + transform(pos: number, layout: string) {
  11 + console.log(layout)
  12 + if(layout == "rightbar") {
  13 + return this.right_bar(pos);
  14 + }else {
  15 + return this.default(pos);
  16 + }
  17 + }
  18 +
  19 + private default(position: number) {
  20 + if(position == 1) {
  21 + return "col-md-6 col-md-push-3";
  22 + }else if(position == 2){
  23 + return "col-md-3 col-md-pull-6";
  24 + }else {
  25 + return "col-md-3";
  26 + }
  27 + }
  28 +
  29 + private right_bar(position: number) {
  30 + if(position == 1) {
  31 + return "col-sm-6 col-sm-push-2";
  32 + }else{
  33 + return "col-sm-3 col-sm-push-2";
  34 + }
  35 + }
  36 +
  37 +}
... ...
src/app/layout/navbar/navbar.html
... ... @@ -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.ts
... ... @@ -4,11 +4,13 @@ import {SessionService, AuthService, AuthController, AuthEvents} from &quot;./../../l
4 4 import {EnvironmentService} from "./../../../lib/ng-noosfero-api/http/environment.service";
5 5 import {SidebarNotificationService} from "../sidebar/sidebar.notification.service";
6 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';
7 9  
8 10 @Component({
9 11 selector: "acme-navbar",
10 12 templateUrl: "app/layout/navbar/navbar.html",
11   - directives: [LanguageSelectorComponent],
  13 + directives: [LanguageSelectorComponent, DesignModeTogglerComponent, BootstrapSwitcherComponent],
12 14 providers: [AuthService, SessionService, SidebarNotificationService, EnvironmentService]
13 15 })
14 16 @Inject("$uibModal", AuthService, "SessionService", "$state", SidebarNotificationService, BodyStateClassesService, EnvironmentService)
... ... @@ -18,7 +20,6 @@ export class Navbar {
18 20 private modalInstance: any = null;
19 21 public showHamburger: boolean = false;
20 22 public currentEnvironment: noosfero.Environment = <any>{ name: '' };
21   -
22 23 /**
23 24 *
24 25 */
... ...
src/app/layout/scss/_layout.scss
... ... @@ -35,3 +35,16 @@
35 35 padding: 0 20px 20px 20px;
36 36 }
37 37 }
  38 +
  39 +ul.nav > li.toggler-container {
  40 + position: relative;
  41 + padding-top: 12px;
  42 +}
  43 +
  44 +.noosfero-main-toolbar {
  45 + padding: 5px;
  46 + @include make-row();
  47 + margin-left: 0px;
  48 + margin-right: 0px;
  49 + background-color: #edecec;
  50 +}
... ...
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/main/main.component.ts
... ... @@ -6,6 +6,7 @@ import {ArticleViewComponent} from &quot;./../article/article-default-view.component&quot;
6 6  
7 7 import {ProfileComponent} from "../profile/profile.component";
8 8 import {BoxesComponent} from "../layout/boxes/boxes.component";
  9 +import {BlockContentComponent} from "../layout/blocks/block-content.component";
9 10 import {BlockComponent} from "../layout/blocks/block.component";
10 11 import {EnvironmentComponent} from "../environment/environment.component";
11 12 import {EnvironmentHomeComponent} from "../environment/environment-home.component";
... ... @@ -16,6 +17,7 @@ import {RecentDocumentsBlockComponent} from &quot;../layout/blocks/recent-documents/r
16 17 import {ProfileImageBlockComponent} from "../layout/blocks/profile-image/profile-image-block.component";
17 18 import {RawHTMLBlockComponent} from "../layout/blocks/raw-html/raw-html-block.component";
18 19 import {StatisticsBlockComponent} from "../layout/blocks/statistics/statistics-block.component";
  20 +import {PersonTagsPluginInterestsBlockComponent} from "../layout/blocks/person-tags-plugin-interests/person-tags-plugin-interests-block.component";
19 21 import {CustomContentComponent} from "../profile/custom-content/custom-content.component";
20 22  
21 23 import {MembersBlockComponent} from "../layout/blocks/members/members-block.component";
... ... @@ -40,6 +42,8 @@ import {SidebarComponent} from &quot;../layout/sidebar/sidebar.component&quot;;
40 42 import {MainBlockComponent} from "../layout/blocks/main/main-block.component";
41 43 import {HtmlEditorComponent} from "../shared/components/html-editor/html-editor.component";
42 44 import {PermissionDirective} from "../shared/components/permission/permission.directive";
  45 +import {SearchComponent} from "../search/search.component";
  46 +import {SearchFormComponent} from "../search/search-form/search-form.component";
43 47  
44 48 /**
45 49 * @ngdoc controller
... ... @@ -82,7 +86,7 @@ export class EnvironmentContent {
82 86 * @name main.Main
83 87 * @requires AuthService, Session, Notification, ArticleBlog, ArticleView, Boxes, Block, LinkListBlock,
84 88 * MainBlock, RecentDocumentsBlock, Navbar, ProfileImageBlock, MembersBlock,
85   - * NoosferoTemplate, DateFormat, RawHTMLBlock
  89 + * NoosferoTemplate, DateFormat, RawHTMLBlock, PersonTagsPluginInterestsBlock
86 90 * @description
87 91 * The Main controller for the Noosfero Angular Theme application.
88 92 *
... ... @@ -95,12 +99,13 @@ export class EnvironmentContent {
95 99 selector: 'main',
96 100 template: '<ui-view></ui-view>',
97 101 directives: [
98   - ArticleBlogComponent, ArticleViewComponent, BoxesComponent, BlockComponent,
  102 + ArticleBlogComponent, ArticleViewComponent, BoxesComponent, BlockContentComponent,
99 103 EnvironmentComponent, PeopleBlockComponent, DisplayContentBlockComponent,
100 104 LinkListBlockComponent, CommunitiesBlockComponent, HtmlEditorComponent, ProfileComponent,
101 105 MainBlockComponent, RecentDocumentsBlockComponent, Navbar, SidebarComponent, ProfileImageBlockComponent,
102 106 MembersBlockComponent, NoosferoTemplate, DateFormat, RawHTMLBlockComponent, StatisticsBlockComponent,
103   - LoginBlockComponent, CustomContentComponent, PermissionDirective
  107 + LoginBlockComponent, CustomContentComponent, PermissionDirective, SearchFormComponent, SearchComponent,
  108 + PersonTagsPluginInterestsBlockComponent, BlockComponent
104 109 ].concat(plugins.mainComponents).concat(plugins.hotspots),
105 110 providers: [AuthService, SessionService, NotificationService, BodyStateClassesService,
106 111 "ngAnimate", "ngCookies", "ngStorage", "ngTouch",
... ... @@ -109,7 +114,7 @@ export class EnvironmentContent {
109 114 "angular-bind-html-compile", "angularMoment", "angular.filter", "akoenig.deckgrid",
110 115 "angular-timeline", "duScroll", "oitozero.ngSweetAlert",
111 116 "pascalprecht.translate", "tmh.dynamicLocale", "angularLoad",
112   - "angular-click-outside", "noosfero.init"]
  117 + "angular-click-outside", "toggle-switch", "noosfero.init"]
113 118 })
114 119 @StateConfig([
115 120 {
... ...
src/app/profile/custom-content/custom-content.component.ts
... ... @@ -2,27 +2,32 @@ import {Component, Input, Inject} from &#39;ng-forward&#39;;
2 2 import {ProfileService} from '../../../lib/ng-noosfero-api/http/profile.service';
3 3 import {NotificationService} from '../../shared/services/notification.service';
4 4 import {PermissionDirective} from '../../shared/components/permission/permission.directive';
  5 +import {DesignModeService} from '../../admin/layout-edit/designMode.service';
5 6  
6 7 @Component({
7 8 selector: 'custom-content',
8 9 templateUrl: "app/profile/custom-content/custom-content.html",
9 10 directives: [PermissionDirective]
10 11 })
11   -@Inject("$uibModal", "$scope", ProfileService, NotificationService)
  12 +@Inject("$uibModal", "$scope", ProfileService, NotificationService, DesignModeService)
12 13 export class CustomContentComponent {
13 14  
  15 + static $inject = ["DesignModeService"]; // @Inject doesn't works with uibModal.open
  16 +
14 17 @Input() attribute: string;
15 18 @Input() profile: noosfero.Profile;
16 19 @Input() label: string;
17 20  
18 21 content: string;
19 22 originalContent: string;
  23 + editionMode = false;
20 24 private modalInstance: any = null;
21 25  
22 26 constructor(private $uibModal: any,
23 27 private $scope: ng.IScope,
24 28 private profileService: ProfileService,
25   - private notificationService: NotificationService) { }
  29 + private notificationService: NotificationService,
  30 + private designModeService: DesignModeService) { }
26 31  
27 32 ngOnInit() {
28 33 this.$scope.$watch(() => {
... ... @@ -30,6 +35,9 @@ export class CustomContentComponent {
30 35 }, () => {
31 36 if (this.profile) this.content = (<any>this.profile)[this.attribute];
32 37 });
  38 + this.designModeService.onToggle.subscribe((designModeOn: boolean) => {
  39 + this.editionMode = designModeOn;
  40 + });
33 41 }
34 42  
35 43 openEdit() {
... ...
src/app/profile/custom-content/custom-content.html
1 1 <div class="custom-content">
2   - <div class="actions" permission="ctrl.profile.permissions" permission-action="allow_edit">
  2 + <div class="actions" permission="ctrl.profile.permissions" permission-action="allow_edit" ng-show="ctrl.editionMode">
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/profile.html
1 1 <div class="profile-container">
2   - <custom-content class="profile-header" [label]="'profile.custom_header.label'" [attribute]="'custom_header'" [profile]="vm.profile"></custom-content>
3   - <div class="row">
4   - <noosfero-boxes ng-if="vm.boxes" [boxes]="vm.boxes" [owner]="vm.profile"></noosfero-boxes>
5   - </div>
  2 + <custom-content class="profile-header"
  3 + [label]="'profile.custom_header.label'"
  4 + [attribute]="'custom_header'"
  5 + [profile]="vm.profile">
  6 + </custom-content>
  7 + <div class="row" ui-view="profile-info"></div>
  8 + <noosfero-boxes ng-if="vm.boxes"
  9 + [layout]="vm.profile.layout_template"
  10 + [boxes]="vm.boxes"
  11 + [owner]="vm.profile" class="row">
  12 + </noosfero-boxes>
6 13 <custom-content class="profile-footer" [label]="'profile.custom_footer.label'" [attribute]="'custom_footer'" [profile]="vm.profile"></custom-content>
7 14 </div>
... ...
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", "params", "current"]);
  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,40 @@
  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 + let stateMock = jasmine.createSpyObj("$state", ["go"]);
  16 +
  17 + beforeEach(angular.mock.module("templates"));
  18 +
  19 + beforeEach((done) => {
  20 + let cls = createClass({
  21 + template: htmlTemplate,
  22 + directives: [SearchComponent],
  23 + providers: [
  24 + helpers.createProviderToValue("$stateParams", stateParams),
  25 + helpers.createProviderToValue("ArticleService", articleService),
  26 + helpers.createProviderToValue("$state", stateMock),
  27 + ].concat(helpers.provideFilters("truncateFilter", "stripTagsFilter"))
  28 + });
  29 + helper = new ComponentTestHelper<SearchComponent>(cls, done);
  30 + });
  31 +
  32 + it("load first page with search results", () => {
  33 + expect(articleService.search).toHaveBeenCalledWith({ query: 'query', per_page: 10, page: 0 });
  34 + });
  35 +
  36 + it("display search results", () => {
  37 + expect(helper.all(".result").length).toEqual(1);
  38 + });
  39 + });
  40 +});
... ...
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.ts
... ... @@ -6,7 +6,7 @@ import {Directive, Inject, Input} from &quot;ng-forward&quot;;
6 6 @Inject('$attrs', '$scope', '$element')
7 7 export class PermissionDirective {
8 8  
9   - constructor($attrs: ng.IAttributes, $scope: ng.IScope, $element: any) {
  9 + constructor($attrs: ng.IAttributes, $scope: ng.IScope, $element: ng.IAugmentedJQuery) {
10 10 $scope.$watch($attrs['permission'], () => {
11 11 let permissions = $scope.$eval($attrs['permission']);
12 12 let permissionAction = $attrs['permissionAction'];
... ...
src/languages/en.json
... ... @@ -6,6 +6,7 @@
6 6 "navbar.logout": "Log Out",
7 7 "navbar.login": "Login",
8 8 "navbar.toggle_menu": "Toggle navigation",
  9 + "language.all": "All languages",
9 10 "language.en": "English",
10 11 "language.pt": "Portuguese",
11 12 "language.selector": "Language",
... ... @@ -74,5 +75,26 @@
74 75 "profile.content.success.message": "Profile saved!",
75 76 "custom_content.title": "Edit content",
76 77 "profile.custom_header.label": "Header",
77   - "profile.custom_footer.label": "Footer"
  78 + "profile.custom_footer.label": "Footer",
  79 + "designMode.label": "In Design",
  80 + "designMode.toggle.ON": "ON",
  81 + "designMode.toggle.OFF": "OFF",
  82 + "search.results.summary": "{results, plural, one{result} other{# results}}",
  83 + "search.results.query.label": "Search for:",
  84 + "search.results.query.placeholder": "Search",
  85 + "block.edit": "Edit",
  86 + "block.edition.title": "Edit Block",
  87 + "block.edition.success.title": "Good job!",
  88 + "block.edition.success.message": "Block saved!",
  89 + "block.edition.display.label": "Display this block:",
  90 + "block.edition.title.label": "Custom title for this block:",
  91 + "block.edition.display.always": "In all pages",
  92 + "block.edition.display.home_page_only": "Only in the homepage",
  93 + "block.edition.display.except_home_page": "In all pages, except in the homepage",
  94 + "block.edition.display.never": "Don't display",
  95 + "block.edition.display_user.label": "Display to users:",
  96 + "block.edition.display_user.all": "All users",
  97 + "block.edition.display_user.logged": "Logged",
  98 + "block.edition.display_user.not_logged": "Not logged",
  99 + "block.edition.language.label": "Show for:"
78 100 }
... ...
src/languages/pt.json
... ... @@ -6,6 +6,7 @@
6 6 "navbar.logout": "Sair",
7 7 "navbar.login": "Login",
8 8 "navbar.toggle_menu": "Abrir Menu",
  9 + "language.all": "Todos os idiomas",
9 10 "language.en": "Inglês",
10 11 "language.pt": "Português",
11 12 "language.selector": "Idioma",
... ... @@ -58,7 +59,7 @@
58 59 "article.remove.success.title": "Bom trabalho!",
59 60 "article.remove.success.message": "Artigo removido!",
60 61 "article.remove.confirmation.title": "Tem certeza?",
61   - "article.remove.confirmation.message": "Não será possível recuperar este artigo!",
  62 + "article.remove.confirmation.message": "Não será possível recuperar este artigo!",
62 63 "article.basic_editor.visibility": "Visibilidade",
63 64 "article.basic_editor.visibility.public": "Público",
64 65 "article.basic_editor.visibility.private": "Privado",
... ... @@ -74,5 +75,26 @@
74 75 "profile.content.success.message": "Perfil salvo!",
75 76 "custom_content.title": "Editar conteúdo",
76 77 "profile.custom_header.label": "Cabeçalho",
77   - "profile.custom_footer.label": "Rodapé"
  78 + "profile.custom_footer.label": "Rodapé",
  79 + "designMode.label": "Modo de Edição",
  80 + "designMode.toggle.ON": "Ligado",
  81 + "designMode.toggle.OFF": "Desligado",
  82 + "search.results.summary": "{results, plural, one{# resultado} other{# resultados}}",
  83 + "search.results.query.label": "Buscar:",
  84 + "search.results.query.placeholder": "Informe aqui sua busca",
  85 + "block.edit": "Editar",
  86 + "block.edition.title": "Editar Bloco",
  87 + "block.edition.success.title": "Bom trabalho!",
  88 + "block.edition.success.message": "Bloco salvo com sucesso!",
  89 + "block.edition.display.label": "Exibir este bloco:",
  90 + "block.edition.title.label": "Título personalizado do bloco:",
  91 + "block.edition.display.always": "Em todas as páginas",
  92 + "block.edition.display.home_page_only": "Apenas na página inicial",
  93 + "block.edition.display.except_home_page": "Em todas as páginas, exceto na inicial",
  94 + "block.edition.display.never": "Não exibir",
  95 + "block.edition.display_user.label": "Exibir para usuários:",
  96 + "block.edition.display_user.all": "Todos os usuários",
  97 + "block.edition.display_user.logged": "Logados",
  98 + "block.edition.display_user.not_logged": "Não logados",
  99 + "block.edition.language.label": "Exibir para:"
78 100 }
... ...
src/lib/ng-noosfero-api/http/article.service.spec.ts
... ... @@ -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
... ... @@ -29,6 +29,16 @@ describe(&quot;Services&quot;, () =&gt; {
29 29 });
30 30 $httpBackend.flush();
31 31 });
  32 +
  33 + it("update block settings", (done) => {
  34 + let blockId = 1;
  35 + $httpBackend.expectPOST(`/api/v1/blocks/${blockId}`).respond(200, { block: { id: blockId } });
  36 + blockService.update(<any>{ id: blockId, display: 'never' }).then((result: noosfero.RestResult<noosfero.Block>) => {
  37 + expect(result.data).toEqual({ id: blockId });
  38 + done();
  39 + });
  40 + $httpBackend.flush();
  41 + });
32 42 });
33 43  
34 44  
... ...
src/lib/ng-noosfero-api/http/block.service.ts
... ... @@ -47,4 +47,12 @@ export class BlockService extends RestangularService&lt;noosfero.Block&gt; {
47 47 return deferred.promise;
48 48 }
49 49  
  50 + update(block: noosfero.Block) {
  51 + let element = this.getElement(block.id);
  52 + let headers = {
  53 + 'Content-Type': 'application/json'
  54 + };
  55 + return this.post(null, element, { block: block }, headers);
  56 + }
  57 +
50 58 }
... ...
src/lib/ng-noosfero-api/http/person.service.ts
1 1 import { Injectable, Inject } from "ng-forward";
2 2 import {RestangularService} from "./restangular_service";
  3 +import {ProfileService} from "./profile.service";
3 4  
4 5 @Injectable()
5   -@Inject("Restangular", "$q", "$log")
  6 +@Inject("Restangular", "$q", "$log", ProfileService)
6 7 export class PersonService extends RestangularService<noosfero.Person> {
7 8  
8   - constructor(Restangular: restangular.IService, $q: ng.IQService, $log: ng.ILogService) {
  9 + constructor(Restangular: restangular.IService, $q: ng.IQService, $log: ng.ILogService, protected profileService: ProfileService) {
9 10 super(Restangular, $q, $log);
10 11 }
11 12  
... ... @@ -20,4 +21,11 @@ export class PersonService extends RestangularService&lt;noosfero.Person&gt; {
20 21 };
21 22 }
22 23  
  24 + getTags(profile: noosfero.Profile): ng.IPromise<noosfero.RestResult<any>> {
  25 + let p = this.getElement(<number>profile.id).customGET('tags');
  26 + let deferred = this.$q.defer<noosfero.RestResult<any>>();
  27 + p.then(this.getHandleSuccessFunction<noosfero.RestResult<any>>(deferred));
  28 + p.catch(this.getHandleErrorFunction<noosfero.RestResult<any>>(deferred));
  29 + return deferred.promise;
  30 + }
23 31 }
... ...
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.ts
... ... @@ -79,7 +79,7 @@ export abstract class RestangularService&lt;T extends noosfero.RestModel&gt; {
79 79 }
80 80 }
81 81 return {
82   - data: response.data[dataKey],
  82 + data: (response.data[dataKey] || response.data),
83 83 headers: response.headers
84 84 };
85 85 };
... ... @@ -289,7 +289,7 @@ export abstract class RestangularService&lt;T extends noosfero.RestModel&gt; {
289 289 let restRequest: ng.IPromise<any>;
290 290  
291 291 if (rootElement) {
292   - restRequest = rootElement.customPOST(data, path, headers);
  292 + restRequest = rootElement.customPOST(data, path, null, headers);
293 293 } else {
294 294 restRequest = this.baseResource.customPOST(data, path, headers);
295 295 }
... ...
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/block.ts
... ... @@ -5,5 +5,6 @@ namespace noosfero {
5 5 limit: number;
6 6 api_content: any;
7 7 hide: boolean;
  8 + title: string;
8 9 }
9 10 }
... ...
src/lib/ng-noosfero-api/interfaces/environment.ts
... ... @@ -15,5 +15,13 @@ namespace noosfero {
15 15 */
16 16 id: number;
17 17 settings: any
  18 +
  19 + /**
  20 + * @ngdoc property
  21 + * @name layout_template
  22 + * @propertyOf noofero.Environment
  23 + * @returns {string} The Environment layout (e.g. default, rightbar)
  24 + */
  25 + layout_template: string;
18 26 }
19   -}
20 27 \ No newline at end of file
  28 +}
... ...
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,15 @@ namespace noosfero {
78 78 * @returns {string} The Profile footer
79 79 */
80 80 custom_footer: string;
  81 +
  82 + permissions: string[];
  83 +
  84 + /**
  85 + * @ngdoc property
  86 + * @name layout_template
  87 + * @propertyOf noofero.Profile
  88 + * @returns {string} The Profile layout template (e.g.: "rightbar", "default")
  89 + */
  90 + layout_template: string;
81 91 }
82 92 }
... ...
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/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",
... ...
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 +
  16 +.panel-title {
  17 + color: #4F9CAC;
  18 +}
  19 +
1 20 .block-head-with-icon {
2 21 padding: 15px 15px 15px 55px;
3 22 }
4 23  
  24 +.content-wrapper .block .panel-heading,
  25 +.content-wrapper .side-options .panel-heading {
  26 + @extend %panel-head-theme;
  27 +}
  28 +
5 29 .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 30  
21 31 &.membersblock {
22 32 .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 +}
... ...