Merge Request #39

Merged
noosfero-themes/angular-theme!39
Created by Ábner Oliveira

Design mode - Added 'inDesign' Toggler

Added 'inDesign' Toggler to allow users with permission to edit to profile to change the profile layout dispositions and customizations

issue #91

Assignee: Michel Felipe
Milestone: 2016.06

Merged by Ábner Oliveira

Source branch has been removed
Commits (7)
2 participants
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"
... ...
src/app/admin/layout-edit/designMode.service.spec.ts 0 → 100644
... ... @@ -0,0 +1,31 @@
  1 +import {DesignModeService} from './designMode.service';
  2 +
  3 +describe('DesignMode Service', () => {
  4 + let service: DesignModeService;
  5 +
  6 + beforeEach(() => {
  7 + service = new DesignModeService();
  8 + });
  9 +
  10 + it('has the designModeOn equals false as default', () => {
  11 + expect(service.isInDesignMode()).toBeFalsy();
  12 + });
  13 +
  14 + it('allows set the designMode value', () => {
  15 + spyOn(service.onToggle, 'next').and.stub();
  16 + service.setInDesignMode(true);
  17 + expect(service.isInDesignMode).toBeTruthy();
  18 + });
  19 +
  20 + it('emits the onToggle event when changing the designModeOn property', () => {
  21 + spyOn(service.onToggle, 'next').and.stub();
  22 + service.setInDesignMode(true);
  23 + expect(service.onToggle.next).toHaveBeenCalled();
  24 + });
  25 +
  26 + it('does not emit onToggle event when there is no change on designModeOn property', () => {
  27 + spyOn(service.onToggle, 'next').and.stub();
  28 + service.setInDesignMode(false);
  29 + expect(service.onToggle.next).not.toHaveBeenCalled();
  30 + });
  31 +});
0 32 \ No newline at end of file
... ...
src/app/admin/layout-edit/designMode.service.ts 0 → 100644
... ... @@ -0,0 +1,23 @@
  1 +import {Component, Injectable, Output, EventEmitter} from 'ng-forward';
  2 +import {BodyStateClassesService} from '../../layout/services/body-state-classes.service';
  3 +
  4 +@Injectable()
  5 +export class DesignModeService {
  6 + @Output() onToggle: EventEmitter<boolean> = new EventEmitter<boolean>();
  7 +
  8 + private designModeOn: boolean = false;
  9 +
  10 + isInDesignMode(): boolean {
  11 + return this.designModeOn;
  12 + }
  13 +
  14 + setInDesignMode(value: boolean) {
  15 + if (this.designModeOn !== value) {
  16 + this.designModeOn = value;
  17 + this.onToggle.next(this.designModeOn);
  18 + }
  19 + }
  20 +
  21 + constructor() {
  22 + }
  23 +}
0 24 \ No newline at end of file
... ...
src/app/admin/layout-edit/designModeToggler.component.spec.ts 0 → 100644
... ... @@ -0,0 +1,59 @@
  1 +import {ComponentTestHelper, createClass} from '../../../spec/component-test-helper';
  2 +import * as helpers from '../../../spec/helpers';
  3 +import {DesignModeTogglerComponent} from './designModeToggler.component';
  4 +import {DesignModeService} from './designMode.service';
  5 +
  6 +describe('DesignModeToggler Component', () => {
  7 + const htmlTemplate: string = '<noosfero-design-toggler></noosfero-design-toggler>';
  8 +
  9 + let helper: ComponentTestHelper<DesignModeTogglerComponent>;
  10 + beforeEach(() => {
  11 + angular.mock.module('templates');
  12 + angular.mock.module('ngSanitize');
  13 + angular.mock.module('toggle-switch');
  14 + });
  15 +
  16 + let designModeService: DesignModeService;
  17 + beforeEach((done) => {
  18 + designModeService = new DesignModeService();
2
  • Me
    Michel Felipe @mfdeveloper (Edited )

    Pq não utilizar um mock deste serviço nesse teste? Algum motivo em especial? Em linhas gerais, temos mocado todas as dependências nos outros testes, correto?

    Talvez seja interessante padronizar uma abordagem geral, para que todos nós saibamos quando (ou não) utilizar mocks para as dependências. Principalmente nesse caso q este serviço utiliza um EventEmitter internamente :)

    Choose File ...   File name...
    Cancel
  • 0deafa1501ec8dd687ee70f90488d592?s=40&d=identicon
    Ábner Oliveira @abner

    a idéia é fazer mock das dependências e não do próprio componente "under test". Nesse caso como ele não tem dependências não precisei criar mocks. Apenas utilizei "stubs" onde precisei

    Choose File ...   File name...
    Cancel
  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;";
2
  • Me
    Michel Felipe @mfdeveloper

    Não seria mais interessante passar somente as classes css aq neste atributo e mover o HTML para o template? Me pareceu meio "poluído" este HTML dentro do componente.

    Choose File ...   File name...
    Cancel
  • 0deafa1501ec8dd687ee70f90488d592?s=40&d=identicon
    Ábner Oliveira @abner

    OK, Feito

    Choose File ...   File name...
    Cancel
  11 +
  12 + constructor(private designModeService: DesignModeService) {
  13 + }
  14 +
  15 + private _inDesignMode: boolean = false;
  16 +
  17 + get inDesignMode(): boolean {
2
  • Me
    Michel Felipe @mfdeveloper

    Outros componentes que quiserem utilizar o serviço DesignModeService precisariam criar um get/set dessa forma?

    Choose File ...   File name...
    Cancel
  • 0deafa1501ec8dd687ee70f90488d592?s=40&d=identicon
    Ábner Oliveira @abner

    Não. utilizado apenas para bind com o componente visual

    Choose File ...   File name...
    Cancel
  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/layout/navbar/navbar.html
... ... @@ -26,19 +26,20 @@
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>
3
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>
... ... @@ -49,3 +50,5 @@
49 50 </div>
50 51 </div>
51 52 </nav>
  53 +<div ui-view="toolbar">
  54 +</div>
52 55 \ No newline at end of file
... ...
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,24 @@
35 35 padding: 0 20px 20px 20px;
36 36 }
37 37 }
  38 +
  39 +
  40 +body.noosfero-design-on {
  41 +
  42 + div.content-wrapper {
  43 + opacity: 0.5;
  44 + }
  45 +}
  46 +
  47 +ul.nav > li.toggler-container {
2
  48 + position: relative;
  49 + padding-top: 12px;
  50 +}
  51 +
  52 +.noosfero-main-toolbar {
  53 + padding: 5px;
  54 + @include make-row();
  55 + margin-left: 0px;
  56 + margin-right: 0px;
  57 + background-color: #edecec;
  58 +}
38 59 \ No newline at end of file
... ...
src/app/layout/scss/skins/_whbl.scss
... ... @@ -281,6 +281,31 @@ $whbl-font-color: #16191c;
281 281 .pace .pace-progress {
282 282 background-color: #fff;
283 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 +
284 309 }
285 310 .rtl.skin-whbl #content-wrapper {
286 311 border-left: 0;
... ...
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
... ... @@ -111,7 +111,7 @@ export class EnvironmentContent {
111 111 "angular-bind-html-compile", "angularMoment", "angular.filter", "akoenig.deckgrid",
112 112 "angular-timeline", "duScroll", "oitozero.ngSweetAlert",
113 113 "pascalprecht.translate", "tmh.dynamicLocale", "angularLoad",
114   - "angular-click-outside", "noosfero.init"]
  114 + "angular-click-outside", "toggle-switch", "noosfero.init"]
115 115 })
116 116 @StateConfig([
117 117 {
... ...
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 {
2
  • Me
    Michel Felipe @mfdeveloper

    Essa nova toolbar é especifica do profile ou do contexto geral da aplicação? Ela só será exibida ao acessar profiles (pessoas, comunidades...) ?

    Choose File ...   File name...
    Cancel
  • 0deafa1501ec8dd687ee70f90488d592?s=40&d=identicon
    Ábner Oliveira @abner

    A idéia é que possa apresentar botões relativos aos profiles e de contexto geral. No momento existe apenas um botão (toggler) que é comum mas a idéia é ter opções diferenciadas se for profile ou se for environment

    Segui o que é feito com o navbar-actions.

    Choose File ...   File name...
    Cancel
  13 + profile: noosfero.Profile;
  14 + parentId: number;
  15 +
  16 + constructor(profileService: ProfileService) {
  17 + profileService.getCurrentProfile().then((profile: noosfero.Profile) => {
  18 + this.profile = profile;
  19 + });
  20 + }
  21 +}
... ...
src/app/profile/profile.component.ts
... ... @@ -10,7 +10,7 @@ import {ProfileService} from &quot;../../lib/ng-noosfero-api/http/profile.service&quot;;
10 10 import {NotificationService} from "../shared/services/notification.service";
11 11 import {MyProfileComponent} from "./myprofile.component";
12 12 import {ProfileActionsComponent} from "./profile-actions.component";
13   -
  13 +import {ProfileToolbarComponent} from "./profile-toolbar.component";
14 14 /**
15 15 * @ngdoc controller
16 16 * @name profile.Profile
... ... @@ -42,6 +42,11 @@ import {ProfileActionsComponent} from &quot;./profile-actions.component&quot;;
42 42 templateUrl: "app/profile/navbar-actions.html",
43 43 controller: ProfileActionsComponent,
44 44 controllerAs: "vm"
  45 + },
  46 + "toolbar@main": {
  47 + templateUrl: "app/profile/toolbar.html",
  48 + controller: ProfileToolbarComponent,
  49 + controllerAs: "vm"
45 50 }
46 51 }
47 52 },
... ... @@ -54,6 +59,11 @@ import {ProfileActionsComponent} from &quot;./profile-actions.component&quot;;
54 59 templateUrl: "app/profile/navbar-actions.html",
55 60 controller: ProfileActionsComponent,
56 61 controllerAs: "vm"
  62 + },
  63 + "toolbar@main": {
  64 + templateUrl: "app/profile/toolbar.html",
  65 + controller: ProfileToolbarComponent,
  66 + controllerAs: "vm"
57 67 }
58 68 }
59 69 },
... ... @@ -106,6 +116,11 @@ import {ProfileActionsComponent} from &quot;./profile-actions.component&quot;;
106 116 templateUrl: "app/article/content-viewer/navbar-actions.html",
107 117 controller: ContentViewerActionsComponent,
108 118 controllerAs: "vm"
  119 + },
  120 + "toolbar@main": {
  121 + templateUrl: "app/profile/toolbar.html",
  122 + controller: ProfileToolbarComponent,
  123 + controllerAs: "vm"
109 124 }
110 125 }
111 126 }
... ...
src/app/profile/toolbar.html 0 → 100644
... ... @@ -0,0 +1,4 @@
  1 +<div class="noosfero-main-toolbar" permission="vm.profile.permissions" permission-action="allow_edit">
  2 + <noosfero-design-toggler class="pull-right"></noosfero-design-toggler>
  3 +
  4 +</div>
0 5 \ No newline at end of file
... ...
src/app/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 {
2
  • Me
    Michel Felipe @mfdeveloper

    Quem usa essa interface? Não consegui encontrar o seu uso

    Choose File ...   File name...
    Cancel
  • 0deafa1501ec8dd687ee70f90488d592?s=40&d=identicon
    Ábner Oliveira @abner

    Para uso futuro. A idéia é ter uma interface para componentes com controles de permissões na exibição.

    Choose File ...   File name...
    Cancel
  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
... ... @@ -75,6 +75,9 @@
75 75 "custom_content.title": "Edit content",
76 76 "profile.custom_header.label": "Header",
77 77 "profile.custom_footer.label": "Footer",
  78 + "designMode.label": "In Design",
  79 + "designMode.toggle.ON": "ON",
  80 + "designMode.toggle.OFF": "OFF",
78 81 "search.results.summary": "{results, plural, one{result} other{# results}}",
79 82 "search.results.query.label": "Search for:",
80 83 "search.results.query.placeholder": "Search"
... ...
src/languages/pt.json
... ... @@ -75,6 +75,9 @@
75 75 "custom_content.title": "Editar conteúdo",
76 76 "profile.custom_header.label": "Cabeçalho",
77 77 "profile.custom_footer.label": "Rodapé",
  78 + "designMode.label": "Modo de Edição",
  79 + "designMode.toggle.ON": "Ligado",
  80 + "designMode.toggle.OFF": "Desligado",
78 81 "search.results.summary": "{results, plural, one{# resultado} other{# resultados}}",
79 82 "search.results.query.label": "Buscar:",
80 83 "search.results.query.placeholder": "Informe aqui sua busca"
... ...
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/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/modelWithPermissions.ts 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +namespace noosfero {
  2 + export interface ModelWithPermissions {
  3 + permissions: string[];
  4 + }
  5 +}
0 6 \ No newline at end of file
... ...
src/lib/ng-noosfero-api/interfaces/profile.ts
... ... @@ -78,5 +78,7 @@ namespace noosfero {
78 78 * @returns {string} The Profile footer
79 79 */
80 80 custom_footer: string;
  81 +
  82 + permissions: string[];
81 83 }
82 84 }
... ...
themes/angular-participa-consulta/app/participa-consulta.scss
... ... @@ -77,3 +77,8 @@ $selected-color: #f6c445;
77 77 background-color: #7E7E7E;
78 78 }
79 79 }
  80 +
  81 +.ats-switch {
  82 + border: 0px;
  83 + border-color: transparent;
  84 +}
... ...
  • Me
    Michel Felipe @mfdeveloper

    Reassigned to @mfdeveloper

    Choose File ...   File name...
    Cancel
  • Me
    Michel Felipe started a discussion on the diff
    last updated by Ábner Oliveira
    src/app/admin/layout-edit/designModeToggler.component.spec.ts 0 → 100644
      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();
    2
    • Me
      Michel Felipe @mfdeveloper (Edited )

      Pq não utilizar um mock deste serviço nesse teste? Algum motivo em especial? Em linhas gerais, temos mocado todas as dependências nos outros testes, correto?

      Talvez seja interessante padronizar uma abordagem geral, para que todos nós saibamos quando (ou não) utilizar mocks para as dependências. Principalmente nesse caso q este serviço utiliza um EventEmitter internamente :)

      Choose File ...   File name...
      Cancel
    • 0deafa1501ec8dd687ee70f90488d592?s=40&d=identicon
      Ábner Oliveira @abner

      a idéia é fazer mock das dependências e não do próprio componente "under test". Nesse caso como ele não tem dependências não precisei criar mocks. Apenas utilizei "stubs" onde precisei

      Choose File ...   File name...
      Cancel
    Me
    Michel Felipe started a discussion on the diff
    last updated by Ábner Oliveira
    src/app/admin/layout-edit/designModeToggler.component.ts 0 → 100644
      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;";
    2
    • Me
      Michel Felipe @mfdeveloper

      Não seria mais interessante passar somente as classes css aq neste atributo e mover o HTML para o template? Me pareceu meio "poluído" este HTML dentro do componente.

      Choose File ...   File name...
      Cancel
    • 0deafa1501ec8dd687ee70f90488d592?s=40&d=identicon
      Ábner Oliveira @abner

      OK, Feito

      Choose File ...   File name...
      Cancel
    Me
    Michel Felipe started a discussion on the diff
    last updated by Ábner Oliveira
    src/app/admin/layout-edit/designModeToggler.component.ts 0 → 100644
      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 {
    2
    • Me
      Michel Felipe @mfdeveloper

      Outros componentes que quiserem utilizar o serviço DesignModeService precisariam criar um get/set dessa forma?

      Choose File ...   File name...
      Cancel
    • 0deafa1501ec8dd687ee70f90488d592?s=40&d=identicon
      Ábner Oliveira @abner

      Não. utilizado apenas para bind com o componente visual

      Choose File ...   File name...
      Cancel
    Me
    Michel Felipe started a discussion on the diff
    last updated by Ábner Oliveira
    src/app/layout/navbar/navbar.html
    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>
    3
    Me
    Michel Felipe started a discussion on the diff
    last updated by Ábner Oliveira
    src/app/layout/scss/_layout.scss
    35 35 padding: 0 20px 20px 20px;
    36 36 }
    37 37 }
      38 +
      39 +
      40 +body.noosfero-design-on {
      41 +
      42 + div.content-wrapper {
      43 + opacity: 0.5;
      44 + }
      45 +}
      46 +
      47 +ul.nav > li.toggler-container {
    2
    Me
    Michel Felipe started a discussion on the diff
    last updated by Ábner Oliveira
    src/app/layout/scss/skins/_whbl.scss
    289 289 .pace .pace-progress {
    290 290 background-color: #fff;
    291 291 }
      292 +
      293 + noosfero-design-toggler .ats-switch .knob i {
    2
    Me
    Michel Felipe started a discussion on the diff
    last updated by Ábner Oliveira
    src/app/profile/profile-toolbar.component.ts 0 → 100644
      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 {
    2
    • Me
      Michel Felipe @mfdeveloper

      Essa nova toolbar é especifica do profile ou do contexto geral da aplicação? Ela só será exibida ao acessar profiles (pessoas, comunidades...) ?

      Choose File ...   File name...
      Cancel
    • 0deafa1501ec8dd687ee70f90488d592?s=40&d=identicon
      Ábner Oliveira @abner

      A idéia é que possa apresentar botões relativos aos profiles e de contexto geral. No momento existe apenas um botão (toggler) que é comum mas a idéia é ter opções diferenciadas se for profile ou se for environment

      Segui o que é feito com o navbar-actions.

      Choose File ...   File name...
      Cancel
    Me
    Michel Felipe started a discussion on the diff
    last updated by Michel Felipe
    themes/angular-participa-consulta/app/participa-consulta.scss
    33 33 background-color: #7E7E7E;
    34 34 }
    35 35 }
      36 +
      37 +.ats-switch {
    1
    • Me
      Michel Felipe @mfdeveloper

      Subtemas deveriam sobrescrever os arquivos .scss do tema principal n? Não entendi muito bem como está essa estrutura de modificações de sass em subtemas, mas colocar tdo isso em um único sass me parece "quebrar" a organização do tema principal :(

      Choose File ...   File name...
      Cancel
    Me
    Michel Felipe started a discussion on the diff
    last updated by Ábner Oliveira
    src/app/shared/components/interfaces.ts 0 → 100644
      1 +
      2 +
      3 +export interface IComponentWithPermissions {
    2
    • Me
      Michel Felipe @mfdeveloper

      Quem usa essa interface? Não consegui encontrar o seu uso

      Choose File ...   File name...
      Cancel
    • 0deafa1501ec8dd687ee70f90488d592?s=40&d=identicon
      Ábner Oliveira @abner

      Para uso futuro. A idéia é ter uma interface para componentes com controles de permissões na exibição.

      Choose File ...   File name...
      Cancel
    Me
    Michel Felipe started a discussion on the outdated diff
    last updated by Michel Felipe
    src/app/shared/components/bootstrap-switcher/bootstrap-switcher.component.ts 0 → 100644
      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: ['label', 'options', 'defaultOption'],
      21 + outputs: ['onSwitch']
      22 +})
      23 +export class BootstrapSwitcherComponent {
      24 + @Input('activeClass') activeClass: string = 'active btn-danger';
    1
    • Me
      Michel Felipe @mfdeveloper

      Pq está repetido o mesmo nome do atributo na anotação @Input() ? Por padrão ele não utiliza o mesmo nome do atributo?

      Choose File ...   File name...
      Cancel
  • Me
    Michel Felipe @mfdeveloper (Edited )

    Algumas sugestões de alteração visual:

    • O botão de toggle está com uma borda forte quando está em foco (:focus). Talvez igualar o mesmo estilo do :hover já melhore bastante!

    toggle-focus

    • Sugiro remover o gradiente da nova toolbar e ajustar a cor de fundo para um cinza claro e sucinto: #edecec

    background-grey

    Choose File ...   File name...
    Cancel
  • 0deafa1501ec8dd687ee70f90488d592?s=40&d=identicon
    Ábner Oliveira @abner

    Added 14 new commits:

    • a8b16135 - Fix application startup process
    • dc8c43fd - adding KNOWN ISSUES section to README.md
    • 6a66f44b - Merge branch 'master' of softwarepublico.gov.br:noosfero-themes/angular-theme
    • 63a0a102 - Fix application startup process
    • d00c6e8c - Added export comments button on comment paragraph plugin
    • ba519042 - Display button to export comments only to users with permission to edit article
    • 36e4d9d9 - Merge branch 'export-comments' into 'master'
    • f276722d - New npm config 'skin' to allow sass by theme skins
    • 04a99db3 - Merge branch 'theme-skin-yellow'
    • 69ff070c - Component to search for articles in the environment
    • 5bc45a30 - fixed pt translation
    • 575daccf - changed search.component and the search-form.component to fill the query field w…
    • d0fa7823 - Merge branch 'search' into 'master'
    • f4964918 - merge with master. fixed toolbar background-color. small fixes on BootstrapSwitcherItem
    Choose File ...   File name...
    Cancel
  • 0deafa1501ec8dd687ee70f90488d592?s=40&d=identicon
    Ábner Oliveira @abner

    melhorias e ajustes finos serão feitos em nova issue #95

    Choose File ...   File name...
    Cancel
  • 0deafa1501ec8dd687ee70f90488d592?s=40&d=identicon
    Ábner Oliveira @abner

    Status changed to closed

    Choose File ...   File name...
    Cancel
  • 0deafa1501ec8dd687ee70f90488d592?s=40&d=identicon
    Ábner Oliveira @abner

    Status changed to reopened

    Choose File ...   File name...
    Cancel
  • 0deafa1501ec8dd687ee70f90488d592?s=40&d=identicon
    Ábner Oliveira @abner
    Choose File ...   File name...
    Cancel
  • 0deafa1501ec8dd687ee70f90488d592?s=40&d=identicon
    Ábner Oliveira @abner
    Choose File ...   File name...
    Cancel
  • 0deafa1501ec8dd687ee70f90488d592?s=40&d=identicon
    Ábner Oliveira @abner

    mentioned in merge request !42

    Choose File ...   File name...
    Cancel
  • 0deafa1501ec8dd687ee70f90488d592?s=40&d=identicon
    Ábner Oliveira @abner
    Choose File ...   File name...
    Cancel
  • Me
    Michel Felipe @mfdeveloper
    Choose File ...   File name...
    Cancel