Compare View

switch
from
...
to
 
Commits (27)
Showing 74 changed files   Show diff stats
@@ -40,12 +40,19 @@ @@ -40,12 +40,19 @@
40 "ng-ckeditor": "^0.2.1", 40 "ng-ckeditor": "^0.2.1",
41 "angular-tag-cloud": "^0.3.0", 41 "angular-tag-cloud": "^0.3.0",
42 "angular-ui-switch": "^0.1.1", 42 "angular-ui-switch": "^0.1.1",
43 - "angular-password": "^1.0.1" 43 + "angular-password": "^1.0.1",
  44 + "ng-file-upload": "^12.0.4",
  45 + "ng-img-crop": "^0.3.2"
44 }, 46 },
45 "devDependencies": { 47 "devDependencies": {
46 "angular-mocks": "~1.5.0" 48 "angular-mocks": "~1.5.0"
47 }, 49 },
48 "overrides": { 50 "overrides": {
  51 + "ng-file-upload": {
  52 + "main": [
  53 + "ng-file-upload-all.js"
  54 + ]
  55 + },
49 "ng-ckeditor": { 56 "ng-ckeditor": {
50 "main": [ 57 "main": [
51 "ng-ckeditor.js", 58 "ng-ckeditor.js",
@@ -46,6 +46,7 @@ gulp.task('watch', ['inject'], function () { @@ -46,6 +46,7 @@ gulp.task('watch', ['inject'], function () {
46 watchPaths.push(path.join(src, '/app/**/*.html')); 46 watchPaths.push(path.join(src, '/app/**/*.html'));
47 watchPaths.push(path.join(src, conf.paths.plugins, '/**/*.html')); 47 watchPaths.push(path.join(src, conf.paths.plugins, '/**/*.html'));
48 }); 48 });
  49 + watchPaths.push(stylePaths);
49 gulp.watch(watchPaths, function(event) { 50 gulp.watch(watchPaths, function(event) {
50 browserSync.reload(event.path); 51 browserSync.reload(event.path);
51 }); 52 });
src/app/environment/environment.component.ts
@@ -32,7 +32,7 @@ import {SearchComponent} from "../search/search.component"; @@ -32,7 +32,7 @@ import {SearchComponent} from "../search/search.component";
32 } 32 }
33 }, 33 },
34 { 34 {
35 - url: '^/search?query', 35 + url: '^/search?query&per_page',
36 component: SearchComponent, 36 component: SearchComponent,
37 name: 'main.environment.search', 37 name: 'main.environment.search',
38 views: { 38 views: {
src/app/index.config.ts
@@ -25,13 +25,16 @@ export function noosferoModuleConfig($logProvider: ng.ILogProvider, @@ -25,13 +25,16 @@ export function noosferoModuleConfig($logProvider: ng.ILogProvider,
25 } 25 }
26 26
27 function configTranslation($translateProvider: angular.translate.ITranslateProvider, tmhDynamicLocaleProvider: any) { 27 function configTranslation($translateProvider: angular.translate.ITranslateProvider, tmhDynamicLocaleProvider: any) {
  28 + let defaultLanguage = (<any>navigator)['languages'] ? (<any>navigator)['languages'][0] : (navigator.language || (<any>navigator)['userLanguage']);
  29 + defaultLanguage = defaultLanguage ? defaultLanguage.replace(/-br|-us/i, '') : 'en';
  30 +
28 $translateProvider.useStaticFilesLoader({ 31 $translateProvider.useStaticFilesLoader({
29 prefix: '/languages/', 32 prefix: '/languages/',
30 suffix: '.json' 33 suffix: '.json'
31 }); 34 });
32 $translateProvider.addInterpolation('$translateMessageFormatInterpolation'); 35 $translateProvider.addInterpolation('$translateMessageFormatInterpolation');
33 $translateProvider.useMissingTranslationHandlerLog(); 36 $translateProvider.useMissingTranslationHandlerLog();
34 - $translateProvider.preferredLanguage('en'); 37 + $translateProvider.preferredLanguage(defaultLanguage);
35 $translateProvider.useSanitizeValueStrategy('escape'); 38 $translateProvider.useSanitizeValueStrategy('escape');
36 tmhDynamicLocaleProvider.localeLocationPattern('bower_components/angular-i18n/angular-locale_{{locale}}.js'); 39 tmhDynamicLocaleProvider.localeLocationPattern('bower_components/angular-i18n/angular-locale_{{locale}}.js');
37 tmhDynamicLocaleProvider.useCookieStorage(); 40 tmhDynamicLocaleProvider.useCookieStorage();
src/app/known-events.ts
1 import { EventsHubKnownEventNames } from './shared/services/events-hub.service'; 1 import { EventsHubKnownEventNames } from './shared/services/events-hub.service';
2 2
3 -export class NoosferoKnownEvents implements EventsHubKnownEventNames { 3 +export class NoosferoKnownEvents implements EventsHubKnownEventNames {
4 IMAGE_PROFILE_UPDATED: string = 'IMAGE_PROFILE_UPDATED'; 4 IMAGE_PROFILE_UPDATED: string = 'IMAGE_PROFILE_UPDATED';
5 PROFILE_INFO_UPDATED: string = 'PROFILE_INFO_UPDATED'; 5 PROFILE_INFO_UPDATED: string = 'PROFILE_INFO_UPDATED';
6 ARTICLE_UPDATED: string = 'ARTICLE_UPDATED'; 6 ARTICLE_UPDATED: string = 'ARTICLE_UPDATED';
  7 + TASK_CLOSED: string = 'TASK_CLOSED';
7 8
8 constructor() { 9 constructor() {
9 } 10 }
@@ -11,4 +12,4 @@ export class NoosferoKnownEvents implements EventsHubKnownEventNames { @@ -11,4 +12,4 @@ export class NoosferoKnownEvents implements EventsHubKnownEventNames {
11 getNames() { 12 getNames() {
12 return Object.getOwnPropertyNames(this); 13 return Object.getOwnPropertyNames(this);
13 } 14 }
14 -}  
15 \ No newline at end of file 15 \ No newline at end of file
  16 +}
src/app/layout/blocks/profile-image/profile-image-block.component.spec.ts
1 -import {TestComponentBuilder, ComponentFixture} from 'ng-forward/cjs/testing/test-component-builder';  
2 -import {Pipe, Input, provide, Component} from 'ng-forward'; 1 +import { TestComponentBuilder, ComponentFixture } from 'ng-forward/cjs/testing/test-component-builder';
  2 +import { Pipe, Input, provide, Component } from 'ng-forward';
3 3
4 -import {ProfileImageBlockComponent} from './profile-image-block.component'; 4 +import { ProfileImageBlockComponent } from './profile-image-block.component';
5 5
6 import * as helpers from "./../../../../spec/helpers"; 6 import * as helpers from "./../../../../spec/helpers";
7 7
@@ -14,6 +14,7 @@ describe(&quot;Components&quot;, () =&gt; { @@ -14,6 +14,7 @@ describe(&quot;Components&quot;, () =&gt; {
14 describe("Profile Image Block Component", () => { 14 describe("Profile Image Block Component", () => {
15 15
16 beforeEach(angular.mock.module("templates")); 16 beforeEach(angular.mock.module("templates"));
  17 + let personService = jasmine.createSpyObj("personService", ["upload"]);
17 18
18 let profileService = jasmine.createSpyObj("ProfileService", ["isMember", "addMember", "removeMember"]); 19 let profileService = jasmine.createSpyObj("ProfileService", ["isMember", "addMember", "removeMember"]);
19 profileService.isMember = jasmine.createSpy("isMember").and.returnValue(Promise.resolve(false)); 20 profileService.isMember = jasmine.createSpy("isMember").and.returnValue(Promise.resolve(false));
@@ -24,6 +25,8 @@ describe(&quot;Components&quot;, () =&gt; { @@ -24,6 +25,8 @@ describe(&quot;Components&quot;, () =&gt; {
24 directives: [ProfileImageBlockComponent], 25 directives: [ProfileImageBlockComponent],
25 providers: [ 26 providers: [
26 helpers.createProviderToValue('SessionService', helpers.mocks.sessionWithCurrentUser({})), 27 helpers.createProviderToValue('SessionService', helpers.mocks.sessionWithCurrentUser({})),
  28 + helpers.createProviderToValue("PersonService", personService),
  29 + helpers.createProviderToValue("$uibModal", helpers.mocks.$modal),
27 helpers.createProviderToValue('ProfileService', profileService), 30 helpers.createProviderToValue('ProfileService', profileService),
28 helpers.createProviderToValue('NotificationService', helpers.mocks.notificationService) 31 helpers.createProviderToValue('NotificationService', helpers.mocks.notificationService)
29 ].concat(helpers.provideFilters("translateFilter")) 32 ].concat(helpers.provideFilters("translateFilter"))
@@ -31,8 +34,7 @@ describe(&quot;Components&quot;, () =&gt; { @@ -31,8 +34,7 @@ describe(&quot;Components&quot;, () =&gt; {
31 class BlockContainerComponent { 34 class BlockContainerComponent {
32 block = { type: 'Block' }; 35 block = { type: 'Block' };
33 owner = { name: 'profile-name' }; 36 owner = { name: 'profile-name' };
34 - constructor() {  
35 - } 37 + constructor() { }
36 } 38 }
37 39
38 it("show image if present", () => { 40 it("show image if present", () => {
@@ -52,7 +54,19 @@ describe(&quot;Components&quot;, () =&gt; { @@ -52,7 +54,19 @@ describe(&quot;Components&quot;, () =&gt; {
52 it("display button to join community", (done: Function) => { 54 it("display button to join community", (done: Function) => {
53 helpers.tcb.createAsync(BlockContainerComponent).then(fixture => { 55 helpers.tcb.createAsync(BlockContainerComponent).then(fixture => {
54 let elProfile = fixture.debugElement.componentViewChildren[0]; 56 let elProfile = fixture.debugElement.componentViewChildren[0];
55 - expect(elProfile.query('.actions .join').length).toEqual(1); 57 + expect(elProfile.componentInstance.displayOrganizationActions()).toBeTruthy();
  58 + expect(elProfile.query('.actions .organization-actions .join').length).toEqual(1);
  59 + done();
  60 + });
  61 + });
  62 +
  63 + it("not display button to join community for person", (done: Function) => {
  64 + helpers.tcb.createAsync(BlockContainerComponent).then(fixture => {
  65 + let elProfile = fixture.debugElement.componentViewChildren[0];
  66 + elProfile.componentInstance.owner = { name: 'person-name', type: 'Person' };
  67 + fixture.detectChanges();
  68 + expect(elProfile.componentInstance.displayOrganizationActions()).toBeFalsy();
  69 + expect(elProfile.queryAll('.actions .organization-actions .join').length).toEqual(0);
56 done(); 70 done();
57 }); 71 });
58 }); 72 });
src/app/layout/blocks/profile-image/profile-image-block.component.ts
1 -import {Inject, Input, Component} from "ng-forward";  
2 -import {ProfileImageComponent} from "./../../../profile/image/image.component";  
3 -import {ProfileService} from "../../../../lib/ng-noosfero-api/http/profile.service";  
4 -import {SessionService} from "./../../../login";  
5 -import {NotificationService} from "../../../shared/services/notification.service"; 1 +import { Inject, Input, Component } from "ng-forward";
  2 +import { ProfileImageComponent } from "./../../../profile/image/image.component";
  3 +import { ProfileService } from "../../../../lib/ng-noosfero-api/http/profile.service";
  4 +import { SessionService } from "./../../../login";
  5 +import { NotificationService } from "../../../shared/services/notification.service";
6 6
7 @Component({ 7 @Component({
8 selector: "noosfero-profile-image-block", 8 selector: "noosfero-profile-image-block",
@@ -48,4 +48,8 @@ export class ProfileImageBlockComponent { @@ -48,4 +48,8 @@ export class ProfileImageBlockComponent {
48 this.loadMembership(); 48 this.loadMembership();
49 }); 49 });
50 } 50 }
  51 +
  52 + displayOrganizationActions() {
  53 + return this.owner.type !== 'Person';
  54 + }
51 } 55 }
src/app/layout/blocks/profile-image/profile-image-block.html
1 <div class="center-block text-center profile-image-block"> 1 <div class="center-block text-center profile-image-block">
2 <a ui-sref="main.profile.info({profile: ctrl.owner.identifier})"> 2 <a ui-sref="main.profile.info({profile: ctrl.owner.identifier})">
3 - <noosfero-profile-image [profile]="ctrl.owner"></noosfero-profile-image> 3 + <noosfero-profile-image [profile]="ctrl.owner" [editable]="true" [edit-class]="'profile-image-block-editable'"></noosfero-profile-image>
4 </a> 4 </a>
5 <a class="settings-link" target="_self" ui-sref="main.profile.settings({profile: ctrl.owner.identifier})">{{"blocks.profile_image.control_panel" | translate}}</a> 5 <a class="settings-link" target="_self" ui-sref="main.profile.settings({profile: ctrl.owner.identifier})">{{"blocks.profile_image.control_panel" | translate}}</a>
6 <div class="actions" ng-show="ctrl.isMember!=null"> 6 <div class="actions" ng-show="ctrl.isMember!=null">
7 - <a ng-if="!ctrl.isMember" ng-click="ctrl.join()" class="btn btn-primary btn-sm join" href="#">{{"blocks.profile_image.join" | translate}}</a>  
8 - <a ng-if="ctrl.isMember" ng-click="ctrl.leave()" class="btn btn-warning btn-sm leave" href="#">{{"blocks.profile_image.leave" | translate}}</a> 7 + <div class="organization-actions" ng-if="ctrl.displayOrganizationActions()">
  8 + <a ng-if="!ctrl.isMember" ng-click="ctrl.join()" class="btn btn-primary btn-sm join" href="#">{{"blocks.profile_image.join" | translate}}</a>
  9 + <a ng-if="ctrl.isMember" ng-click="ctrl.leave()" class="btn btn-warning btn-sm leave" href="#">{{"blocks.profile_image.leave" | translate}}</a>
  10 + </div>
9 </div> 11 </div>
10 </div> 12 </div>
src/app/layout/blocks/profile-image/profile-image-block.scss
@@ -2,4 +2,22 @@ @@ -2,4 +2,22 @@
2 .settings-link { 2 .settings-link {
3 display: block; 3 display: block;
4 } 4 }
  5 + .upload-camera-container {
  6 + top: 77%;
  7 + left: 6%;
  8 + }
  9 +}
  10 +
  11 +.profile-image-block-editable {
  12 + top: 68%;
  13 + width: 284px;
  14 + font-weight: 700;
  15 + height: 43px;
  16 + padding-left: 30px;
  17 + padding-top: 13px;
5 } 18 }
  19 +
  20 +
  21 +
  22 +
  23 +
src/app/layout/navbar/navbar.html
@@ -45,10 +45,12 @@ @@ -45,10 +45,12 @@
45 </ul> 45 </ul>
46 </li> 46 </li>
47 </ul> 47 </ul>
48 -  
49 <ul class="nav navbar-nav navbar-right"> 48 <ul class="nav navbar-nav navbar-right">
50 <language-selector class="nav navbar-nav navbar-right"></language-selector> 49 <language-selector class="nav navbar-nav navbar-right"></language-selector>
51 </ul> 50 </ul>
  51 + <ul class="nav navbar-nav navbar-right">
  52 + <tasks-menu class="nav navbar-nav navbar-right"></tasks-menu>
  53 + </ul>
52 <div ui-view="actions"></div> 54 <div ui-view="actions"></div>
53 <div class="nav navbar-nav search navbar-right"> 55 <div class="nav navbar-nav search navbar-right">
54 <search-form></search-form> 56 <search-form></search-form>
src/app/login/auth.controller.spec.ts
1 import {AuthController} from "./auth.controller"; 1 import {AuthController} from "./auth.controller";
2 import {AuthService} from "./auth.service"; 2 import {AuthService} from "./auth.service";
  3 +import * as helpers from "./../../spec/helpers";
3 4
4 describe("Controllers", () => { 5 describe("Controllers", () => {
5 6
6 7
7 describe("AuthController", () => { 8 describe("AuthController", () => {
8 9
9 - it("calls authenticate on AuthService when login called", () => { 10 + let $modal: any;
  11 + let notificationService = helpers.mocks.notificationService;
  12 + notificationService.success = jasmine.createSpy('info');
  13 + notificationService.error = jasmine.createSpy('error');
10 14
11 - // creating a Mock AuthService  
12 - let AuthServiceMock: AuthService = jasmine.createSpyObj("AuthService", ["login"]); 15 + let authController: any;
  16 + let AuthServiceMock: AuthService;
  17 + let username: string;
13 18
14 - // pass AuthServiceMock into the constructor  
15 - let authController = new AuthController(null, null, AuthServiceMock); 19 + beforeEach(() => {
  20 + $modal = helpers.mocks.$modal;
  21 + AuthServiceMock = jasmine.createSpyObj("AuthService", ["login",
  22 +"forgotPassword"]);
  23 + authController = new AuthController(null, null, AuthServiceMock, $modal, notificationService);
  24 + });
16 25
17 - // setup of authController -> set the credentials instance property  
18 - let credentials = { username: "username", password: "password" };  
19 26
  27 + it("calls authenticate on AuthService when login called", () => {
  28 + let credentials = { username: "username", password: "password" };
20 authController.credentials = credentials; 29 authController.credentials = credentials;
21 -  
22 - // calls the authController login method  
23 authController.login(); 30 authController.login();
24 -  
25 - // checks if the method login of the injected AuthService has been called  
26 expect(AuthServiceMock.login).toHaveBeenCalledWith(credentials); 31 expect(AuthServiceMock.login).toHaveBeenCalledWith(credentials);
27 -  
28 }); 32 });
29 33
  34 + it('should open forgot password on click', (done: Function) => {
  35 + spyOn($modal, "open");
  36 + authController.openForgotPassword();
  37 + expect($modal.open).toHaveBeenCalled();
  38 + expect($modal.open).toHaveBeenCalledWith({
  39 + templateUrl: 'app/login/forgot-password.html',
  40 + controller: AuthController,
  41 + controllerAs: 'vm',
  42 + bindToController: true
  43 + });
  44 + done();
  45 + });
30 46
  47 + it("calls forgotPassword on AuthService when sendPasswdInfo called", done => {
  48 + authController.username = "john";
  49 + AuthServiceMock.forgotPassword = jasmine.createSpy("forgotPassword").and.returnValue(Promise.resolve());
  50 + authController.sendPasswdInfo();
  51 + expect(AuthServiceMock.forgotPassword).toHaveBeenCalledWith("john");
  52 + done();
  53 + });
31 54
32 }); 55 });
33 }); 56 });
src/app/login/auth.controller.ts
1 -import {AuthService} from "./auth.service"; 1 +import { AuthService } from "./auth.service";
  2 +import { NotificationService } from "./../shared/services/notification.service";
2 3
3 export class AuthController { 4 export class AuthController {
4 5
5 - static $inject = ["$log", "$stateParams", "AuthService"]; 6 + static $inject = ["$log", "$stateParams", "AuthService", "$uibModal", "NotificationService"];
  7 + modalInstance: ng.ui.bootstrap.IModalServiceInstance;
6 8
7 constructor( 9 constructor(
8 private $log: ng.ILogService, 10 private $log: ng.ILogService,
9 private $stateParams: any, 11 private $stateParams: any,
10 - private AuthService: AuthService 12 + private AuthService: AuthService,
  13 + private $uibModal: ng.ui.bootstrap.IModalService,
  14 + private notificationService: NotificationService
11 ) { 15 ) {
12 16
13 } 17 }
14 18
15 credentials: noosfero.Credentials; 19 credentials: noosfero.Credentials;
  20 + username: string;
16 21
17 login() { 22 login() {
18 this.AuthService.login(this.credentials); 23 this.AuthService.login(this.credentials);
19 } 24 }
  25 +
  26 + openForgotPassword() {
  27 + this.modalInstance = this.$uibModal.open({
  28 + templateUrl: 'app/login/forgot-password.html',
  29 + controller: AuthController,
  30 + controllerAs: 'vm',
  31 + bindToController: true,
  32 + });
  33 + }
  34 +
  35 + sendPasswdInfo() {
  36 + this.AuthService.forgotPassword(this.username).then((response) => {
  37 + this.notificationService.info({ title: "forgotPasswd.email_sent.title.info", message: "forgotPasswd.email_sent.message.info" });
  38 + }).catch((response) => {
  39 + this.notificationService.error({ title: "forgotPasswd.not_found.title.error", message: "forgotPasswd.not_found.message.error" });
  40 + this.openForgotPassword();
  41 + });
  42 + }
20 } 43 }
src/app/login/auth.service.ts
@@ -2,11 +2,12 @@ import {Injectable, Inject, EventEmitter} from &quot;ng-forward&quot;; @@ -2,11 +2,12 @@ import {Injectable, Inject, EventEmitter} from &quot;ng-forward&quot;;
2 2
3 import {NoosferoRootScope, UserResponse} from "./../shared/models/interfaces"; 3 import {NoosferoRootScope, UserResponse} from "./../shared/models/interfaces";
4 import {SessionService} from "./session.service"; 4 import {SessionService} from "./session.service";
  5 +import { RestangularService } from "./../../lib/ng-noosfero-api/http/restangular_service";
5 6
6 import {AuthEvents} from "./auth-events"; 7 import {AuthEvents} from "./auth-events";
7 8
8 @Injectable() 9 @Injectable()
9 -@Inject("$http", SessionService, "$log") 10 +@Inject("$http", SessionService, "$log", "Restangular")
10 export class AuthService { 11 export class AuthService {
11 12
12 public loginSuccess: EventEmitter<noosfero.User> = new EventEmitter<noosfero.User>(); 13 public loginSuccess: EventEmitter<noosfero.User> = new EventEmitter<noosfero.User>();
@@ -15,7 +16,10 @@ export class AuthService { @@ -15,7 +16,10 @@ export class AuthService {
15 16
16 constructor(private $http: ng.IHttpService, 17 constructor(private $http: ng.IHttpService,
17 private sessionService: SessionService, 18 private sessionService: SessionService,
18 - private $log: ng.ILogService) { 19 + private $log: ng.ILogService,
  20 + private Restangular: restangular.IService
  21 + ) {
  22 + this.Restangular = Restangular;
19 } 23 }
20 24
21 loginFromCookie() { 25 loginFromCookie() {
@@ -76,4 +80,9 @@ export class AuthService { @@ -76,4 +80,9 @@ export class AuthService {
76 throw new Error(`The event: ${eventName} not exists`); 80 throw new Error(`The event: ${eventName} not exists`);
77 } 81 }
78 } 82 }
  83 +
  84 + forgotPassword(value: string): ng.IPromise<noosfero.RestResult<any>> {
  85 + return this.Restangular.all("").customPOST("", "forgot_password", {value: value});
  86 + }
  87 +
79 } 88 }
src/app/login/forgot-password.html 0 → 100644
@@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
  1 +<div class="modal-header">
  2 + <h3 class="modal-title">{{"auth.form.forgot_passwd" | translate}}</h3>
  3 +</div>
  4 +<div class="modal-body">
  5 + <form name="forgotPasswdForm">
  6 + <div class="form-group" ng-class="{'has-error': forgotPasswdForm.username.$error.required }">
  7 + <label for="forgotPasswdLogin">{{"auth.form.login" | translate}}</label>
  8 + <input type="text" name="username" class="form-control" id="forgotPasswdLogin" placeholder="{{'auth.form.login' | translate}}" ng-model="vm.username" required>
  9 + <div class="help-block" ng-messages="forgotPasswdForm.username.$error">
  10 + <div ng-if="forgotPasswdForm.username.$touched">
  11 + <ul class="list-unstyled">
  12 + <li ng-messages-include="app/shared/messages/form-errors.html"></li>
  13 + </ul>
  14 + </div>
  15 + </div>
  16 + </div>
  17 + <button type="submit" class="btn btn-default btn-block" ng-click="vm.sendPasswdInfo(); $close()" ng-disabled="forgotPasswdForm.$invalid">{{"forgotPasswd.send_instructions_button" | translate}}</button>
  18 + </form>
  19 + <p>{{"forgotPasswd.info" | translate}}</p>
  20 +</div>
src/app/login/login.html
@@ -18,6 +18,9 @@ @@ -18,6 +18,9 @@
18 {{"auth.form.keepLoggedIn" | translate}} 18 {{"auth.form.keepLoggedIn" | translate}}
19 </label> 19 </label>
20 </div> 20 </div>
  21 + <div class="pull-right">
  22 + <a ng-click="vm.openForgotPassword()" href="">{{"auth.form.forgot_passwd" | translate}}</a>
  23 + </div>
21 </div> 24 </div>
22 <button type="submit" class="btn btn-default btn-block" ng-click="vm.login()">{{"auth.form.login_button" | translate}}</button> 25 <button type="submit" class="btn btn-default btn-block" ng-click="vm.login()">{{"auth.form.login_button" | translate}}</button>
23 </form> 26 </form>
src/app/login/login.scss
@@ -10,6 +10,12 @@ @@ -10,6 +10,12 @@
10 margin-top: 20px; 10 margin-top: 20px;
11 text-transform: uppercase; 11 text-transform: uppercase;
12 } 12 }
  13 + .form-inline {
  14 + display: inline;
  15 + div {
  16 + display: inline-block;
  17 + }
  18 + }
13 } 19 }
14 } 20 }
15 21
src/app/login/new-password.component.spec.ts 0 → 100644
@@ -0,0 +1,87 @@ @@ -0,0 +1,87 @@
  1 +import { ComponentTestHelper, createClass } from "../../spec/component-test-helper";
  2 +import * as helpers from "../../spec/helpers";
  3 +import { PasswordComponent } from "./new-password.component";
  4 +
  5 +
  6 +describe("Password Component", () => {
  7 + const htmlTemplate: string = '<new-password></new-password>';
  8 +
  9 + let helper: ComponentTestHelper<PasswordComponent>;
  10 + let passwordService = helpers.mocks.passwordService;
  11 + let stateService = jasmine.createSpyObj("$state", ["transitionTo"]);
  12 + let stateParams = jasmine.createSpyObj("$stateParams", ["code"]);
  13 + let notificationService = helpers.mocks.notificationService;
  14 + notificationService.success = jasmine.createSpy('success');
  15 + notificationService.error = jasmine.createSpy('error');
  16 +
  17 +
  18 + let data: any;
  19 +
  20 + beforeEach(() => {
  21 + angular.mock.module('templates');
  22 + angular.mock.module('ngSanitize');
  23 + angular.mock.module('ngMessages');
  24 + angular.mock.module('ngPassword');
  25 + });
  26 +
  27 + beforeEach((done) => {
  28 + let cls = createClass({
  29 + template: htmlTemplate,
  30 + directives: [PasswordComponent],
  31 + providers: [
  32 + helpers.createProviderToValue('$state', stateService),
  33 + helpers.createProviderToValue('$stateParams', stateParams),
  34 + helpers.createProviderToValue('PasswordService', passwordService),
  35 + helpers.createProviderToValue('NotificationService', notificationService),
  36 + ]
  37 + });
  38 + helper = new ComponentTestHelper<PasswordComponent>(cls, done);
  39 + });
  40 +
  41 + it('new password page was rendered', () => {
  42 + expect(helper.debugElement.query('div.new-password-page').length).toEqual(1);
  43 + });
  44 +
  45 + it("changes the user password", done => {
  46 + data = {
  47 + code: '1234567890',
  48 + password: 'test',
  49 + passwordConfirmation: 'test'
  50 + };
  51 +
  52 + helper.component.code = data.code;
  53 + helper.component.password = data.password;
  54 + helper.component.passwordConfirmation = data.passwordConfirmation;
  55 +
  56 + passwordService.new_password = jasmine.createSpy("new_password").and.returnValue(Promise.resolve());
  57 +
  58 + helper.component.sendNewPassword();
  59 + expect(passwordService.new_password).toHaveBeenCalledWith('1234567890', 'test', 'test');
  60 +
  61 + expect(notificationService.success).toHaveBeenCalled();
  62 +
  63 + done();
  64 + });
  65 +
  66 + it("fails when try to change the user password", done => {
  67 + data = {
  68 + code: '1234567890',
  69 + password: 'test',
  70 + passwordConfirmation: 'test-invalid'
  71 + };
  72 +
  73 + helper.component.code = data.code;
  74 + helper.component.password = data.password;
  75 + helper.component.passwordConfirmation = data.passwordConfirmation;
  76 +
  77 + passwordService.new_password = jasmine.createSpy("new_password").and.returnValue(Promise.reject({data: {message: 'Error'}}));
  78 +
  79 + helper.component.sendNewPassword();
  80 + expect(passwordService.new_password).toHaveBeenCalledWith('1234567890', 'test', 'test-invalid');
  81 +
  82 + expect(notificationService.error).toHaveBeenCalled();
  83 +
  84 + done();
  85 + });
  86 +
  87 +});
src/app/login/new-password.component.ts 0 → 100644
@@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
  1 +import { StateConfig, Component, Inject, provide } from 'ng-forward';
  2 +
  3 +import { PasswordService } from "../../lib/ng-noosfero-api/http/password.service";
  4 +import { NotificationService } from "./../shared/services/notification.service";
  5 +import { AuthController } from "./auth.controller";
  6 +
  7 +@Component({
  8 + selector: 'new-password',
  9 + templateUrl: 'app/login/new-password.html',
  10 + providers: [provide('passwordService', { useClass: PasswordService })]
  11 +})
  12 +@Inject(PasswordService, "$state", "$stateParams", NotificationService)
  13 +export class PasswordComponent {
  14 +
  15 + code: string;
  16 + password: string;
  17 + passwordConfirmation: string;
  18 +
  19 + constructor(
  20 + public passwordService: PasswordService,
  21 + private $state: ng.ui.IStateService,
  22 + private $stateParams: ng.ui.IStateParamsService,
  23 + private notificationService: NotificationService) {
  24 +
  25 + this.code = this.$stateParams['code'];
  26 + }
  27 +
  28 + sendNewPassword() {
  29 + this.passwordService.new_password(this.code, this.password, this.passwordConfirmation).then((response) => {
  30 + this.notificationService.success({ title: "newPasswd.success.title", message: "newPasswd.success.message", timer: 5000 });
  31 + this.$state.transitionTo('main.environment');
  32 + }).catch((response) => {
  33 + this.notificationService.error({ title: "newPasswd.failed.title", message: "newPasswd.failed.message" });
  34 + });
  35 + }
  36 +}
src/app/login/new-password.html 0 → 100644
@@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
  1 +<div class="new-password-page col-xs-4 col-xs-offset-4">
  2 + <h1>{{"new_password.welcomeMessageTitle" | translate }}</h1>
  3 +
  4 + <form name="newPasswdForm">
  5 + <div class="form-group" ng-class="{'has-error': newPasswdForm.password.$invalid && newPasswdForm.password.$touched }">
  6 + <div class="input-group">
  7 + <span class="input-group-addon"><i class="fa fa-lock"></i></span>
  8 + <input type="password" class="form-control" id="password" name="password" placeholder="{{'account.register.passwordLabel' | translate }}" ng-model="vm.password" required minlength="4">
  9 + </div>
  10 + <div class="help-block" ng-show="newPasswdForm.password.$touched" ng-messages="newPasswdForm.password.$error">
  11 + <ul class="list-unstyled">
  12 + <li ng-messages-include="app/shared/messages/form-errors.html"></li>
  13 + </ul>
  14 + </div>
  15 + </div>
  16 +
  17 + <div class="form-group" ng-class="{'has-error': newPasswdForm.passwordConfirm.$invalid && newPasswdForm.passwordConfirm.$touched }">
  18 + <div class="input-group">
  19 + <span class="input-group-addon"><i class="fa fa-unlock-alt"></i></span>
  20 + <input type="password" class="form-control" id="passwordConfirm" name="passwordConfirm" match-password="password" placeholder="{{'account.register.passwordConfirmationLabel' | translate}}" ng-model="vm.passwordConfirmation" required>
  21 + </div>
  22 + <div class="help-block" ng-show="newPasswdForm.passwordConfirm.$touched" ng-messages="newPasswdForm.passwordConfirm.$error">
  23 + <ul class="list-unstyled">
  24 + <li ng-messages-include="app/shared/messages/form-errors.html"></li>
  25 + <li ng-message="passwordMatch" translate="messages.invalid.passwordMatch"></li>
  26 + </ul>
  27 + </div>
  28 + </div>
  29 + <button type="submit" class="btn btn-default btn-block" ng-click="vm.sendNewPassword()" ng-disabled="newPasswdForm.$invalid">{{"newPasswdForm.submit" | translate}}</button>
  30 + </form>
  31 +</div>
src/app/main/main.component.spec.ts
@@ -40,7 +40,8 @@ describe(&quot;MainComponent&quot;, function () { @@ -40,7 +40,8 @@ describe(&quot;MainComponent&quot;, function () {
40 useValue: [ 40 useValue: [
41 'IMAGE_PROFILE_UPDATED', 41 'IMAGE_PROFILE_UPDATED',
42 'PROFILE_INFO_UPDATED', 42 'PROFILE_INFO_UPDATED',
43 - 'ARTICLE_UPDATED' 43 + 'ARTICLE_UPDATED',
  44 + 'TASK_CLOSED'
44 ] 45 ]
45 }), 46 }),
46 ] 47 ]
src/app/main/main.component.ts
@@ -4,6 +4,7 @@ import { ArticleBlogComponent } from &quot;./../article/types/blog/blog.component&quot;; @@ -4,6 +4,7 @@ import { ArticleBlogComponent } from &quot;./../article/types/blog/blog.component&quot;;
4 4
5 import { ArticleViewComponent } from "./../article/article-default-view.component"; 5 import { ArticleViewComponent } from "./../article/article-default-view.component";
6 6
  7 +import { PasswordComponent } from "../login/new-password.component";
7 import { ProfileComponent } from "../profile/profile.component"; 8 import { ProfileComponent } from "../profile/profile.component";
8 import { BoxesComponent } from "../layout/boxes/boxes.component"; 9 import { BoxesComponent } from "../layout/boxes/boxes.component";
9 import { BlockContentComponent } from "../layout/blocks/block-content.component"; 10 import { BlockContentComponent } from "../layout/blocks/block-content.component";
@@ -49,9 +50,11 @@ import { HtmlEditorComponent } from &quot;../shared/components/html-editor/html-edito @@ -49,9 +50,11 @@ import { HtmlEditorComponent } from &quot;../shared/components/html-editor/html-edito
49 import { PermissionDirective } from "../shared/components/permission/permission.directive"; 50 import { PermissionDirective } from "../shared/components/permission/permission.directive";
50 import { SearchComponent } from "../search/search.component"; 51 import { SearchComponent } from "../search/search.component";
51 import { SearchFormComponent } from "../search/search-form/search-form.component"; 52 import { SearchFormComponent } from "../search/search-form/search-form.component";
52 -  
53 import { EVENTS_HUB_KNOW_EVENT_NAMES, EventsHubService } from "../shared/services/events-hub.service"; 53 import { EVENTS_HUB_KNOW_EVENT_NAMES, EventsHubService } from "../shared/services/events-hub.service";
54 import { NoosferoKnownEvents } from "../known-events"; 54 import { NoosferoKnownEvents } from "../known-events";
  55 +import { TasksMenuComponent } from "../task/tasks-menu/tasks-menu.component";
  56 +import { TaskListComponent } from "../task/task-list/task-list.component";
  57 +
55 /** 58 /**
56 * @ngdoc controller 59 * @ngdoc controller
57 * @name main.MainContentComponent 60 * @name main.MainContentComponent
@@ -118,16 +121,17 @@ export class EnvironmentContent { @@ -118,16 +121,17 @@ export class EnvironmentContent {
118 MembersBlockComponent, NoosferoTemplate, DateFormat, RawHTMLBlockComponent, StatisticsBlockComponent, 121 MembersBlockComponent, NoosferoTemplate, DateFormat, RawHTMLBlockComponent, StatisticsBlockComponent,
119 LoginBlockComponent, CustomContentComponent, PermissionDirective, SearchFormComponent, SearchComponent, 122 LoginBlockComponent, CustomContentComponent, PermissionDirective, SearchFormComponent, SearchComponent,
120 PersonTagsPluginInterestsBlockComponent, TagsBlockComponent, RecentActivitiesPluginActivitiesBlockComponent, 123 PersonTagsPluginInterestsBlockComponent, TagsBlockComponent, RecentActivitiesPluginActivitiesBlockComponent,
121 - ProfileImagesPluginProfileImagesBlockComponent, BlockComponent, RegisterComponent 124 + ProfileImagesPluginProfileImagesBlockComponent, BlockComponent, RegisterComponent, TasksMenuComponent, TaskListComponent,
  125 + PasswordComponent
122 ].concat(plugins.mainComponents).concat(plugins.hotspots), 126 ].concat(plugins.mainComponents).concat(plugins.hotspots),
123 - providers: [AuthService, SessionService, NotificationService, BodyStateClassesService, RegisterService, 127 + providers: [AuthService, SessionService, NotificationService, BodyStateClassesService,
124 "ngAnimate", "ngCookies", "ngStorage", "ngTouch", 128 "ngAnimate", "ngCookies", "ngStorage", "ngTouch",
125 "ngSanitize", "ngMessages", "ngAria", "restangular", 129 "ngSanitize", "ngMessages", "ngAria", "restangular",
126 "ui.router", "ui.bootstrap", "toastr", "ngCkeditor", 130 "ui.router", "ui.bootstrap", "toastr", "ngCkeditor",
127 "angular-bind-html-compile", "angularMoment", "angular.filter", "akoenig.deckgrid", 131 "angular-bind-html-compile", "angularMoment", "angular.filter", "akoenig.deckgrid",
128 "angular-timeline", "duScroll", "oitozero.ngSweetAlert", 132 "angular-timeline", "duScroll", "oitozero.ngSweetAlert",
129 "pascalprecht.translate", "tmh.dynamicLocale", "angularLoad", 133 "pascalprecht.translate", "tmh.dynamicLocale", "angularLoad",
130 - "angular-click-outside", "ngTagCloud", "noosfero.init", "uiSwitch", "ngPassword"] 134 + "angular-click-outside", "ngTagCloud", "noosfero.init", "uiSwitch", "ngFileUpload", "ngImgCrop"]
131 }) 135 })
132 @StateConfig([ 136 @StateConfig([
133 { 137 {
@@ -148,6 +152,7 @@ export class EnvironmentContent { @@ -148,6 +152,7 @@ export class EnvironmentContent {
148 url: '/', 152 url: '/',
149 component: EnvironmentComponent, 153 component: EnvironmentComponent,
150 name: 'main.environment', 154 name: 'main.environment',
  155 + abstract: true,
151 views: { 156 views: {
152 "content": { 157 "content": {
153 templateUrl: "app/environment/environment.html", 158 templateUrl: "app/environment/environment.html",
@@ -169,6 +174,18 @@ export class EnvironmentContent { @@ -169,6 +174,18 @@ export class EnvironmentContent {
169 } 174 }
170 }, 175 },
171 { 176 {
  177 + url: "/account/new_password/:code",
  178 + component: PasswordComponent,
  179 + name: 'main.newPasswd',
  180 + views: {
  181 + "content": {
  182 + templateUrl: "app/login/new-password.html",
  183 + controller: PasswordComponent,
  184 + controllerAs: "vm"
  185 + }
  186 + }
  187 + },
  188 + {
172 url: "^/:profile", 189 url: "^/:profile",
173 abstract: true, 190 abstract: true,
174 component: ProfileComponent, 191 component: ProfileComponent,
src/app/profile/image/image.component.spec.ts
@@ -4,48 +4,72 @@ @@ -4,48 +4,72 @@
4 * @description 4 * @description
5 * This file contains the tests for the {@link components.noosfero.profile-image.ProfileImage} component. 5 * This file contains the tests for the {@link components.noosfero.profile-image.ProfileImage} component.
6 */ 6 */
7 -  
8 -import {TestComponentBuilder, ComponentFixture} from 'ng-forward/cjs/testing/test-component-builder';  
9 -import {Pipe, Input, provide, Component} from 'ng-forward'; 7 +import { ComponentTestHelper, createClass } from '../../../spec/component-test-helper';
  8 +import { TestComponentBuilder, ComponentFixture } from 'ng-forward/cjs/testing/test-component-builder';
  9 +import { Pipe, Input, provide, Component } from 'ng-forward';
  10 +import { PersonService } from "../../../lib/ng-noosfero-api/http/person.service";
10 11
11 import * as helpers from "../../../spec/helpers"; 12 import * as helpers from "../../../spec/helpers";
12 13
13 -import {ProfileImageComponent} from "./image.component"; 14 +import { ProfileImageComponent } from "./image.component";
14 15
15 -const tcb = new TestComponentBuilder(); 16 +const htmlTemplate: string = '<noosfero-profile-image [editable]="true" [edit-class]="editable-class" [profile]="ctrl.profile"></noosfero-profile-image>';
16 17
17 describe("Components", () => { 18 describe("Components", () => {
18 19
19 describe("Profile Image Component", () => { 20 describe("Profile Image Component", () => {
20 21
  22 + let helper: ComponentTestHelper<ProfileImageComponent>;
  23 +
21 beforeEach(angular.mock.module("templates")); 24 beforeEach(angular.mock.module("templates"));
22 25
23 - it("show community users image if profile is not Person", done => {  
24 - helpers.tcb.createAsync(ProfileImageComponent).then(fixture => {  
25 - let profileImageComponent: ProfileImageComponent = fixture.componentInstance;  
26 - let profile = <noosfero.Profile>{ id: 1, identifier: "myprofile", type: "Community" };  
27 - profileImageComponent.profile = profile;  
28 - profileImageComponent.ngOnInit();  
29 -  
30 - // Check the attribute  
31 - expect(profileImageComponent.defaultIcon).toBe("fa-users", "The default icon should be community users");  
32 - // var elProfile = fixture.debugElement.componentViewChildren[0];  
33 - // expect(elProfile.query('div.profile-image-block').length).toEqual(1);  
34 - done(); 26 + beforeEach((done) => {
  27 + let scope = helpers.mocks.scopeWithEvents;
  28 + let personService = jasmine.createSpyObj("personService", ["upload"]);
  29 + let properties = { profile: { custom_footer: "footer" } };
  30 + let cls = createClass({
  31 + template: htmlTemplate,
  32 + directives: [ProfileImageComponent],
  33 + properties: properties,
  34 + providers: [
  35 + helpers.createProviderToValue("PersonService", personService),
  36 + helpers.createProviderToValue("$uibModal", helpers.mocks.$modal),
  37 + helpers.createProviderToValue("$scope", scope)
  38 + ]
35 }); 39 });
  40 + helper = new ComponentTestHelper<ProfileImageComponent>(cls, done);
36 }); 41 });
37 42
38 - it("show Person image if profile is Person", done => {  
39 - tcb.createAsync(ProfileImageComponent).then(fixture => {  
40 - let profileImageComponent: ProfileImageComponent = fixture.componentInstance;  
41 - let profile = <noosfero.Profile>{ id: 1, identifier: "myprofile", type: "Person" };  
42 - profileImageComponent.profile = profile;  
43 - profileImageComponent.ngOnInit();  
44 - // Check the attribute  
45 - expect(profileImageComponent.defaultIcon).toEqual("fa-user", "The default icon should be person user");  
46 - done();  
47 - }); 43 + it("set modal instance when select files modal", () => {
  44 + helper.component['$uibModal'].open = jasmine.createSpy("open");
  45 + helper.component.fileSelected("file", []);
  46 + expect(helper.component['$uibModal'].open).toHaveBeenCalled();
  47 + });
  48 +
  49 +
  50 + it("show community users image if profile is not Person", (done) => {
  51 +
  52 + let profile = <noosfero.Profile>{ id: 1, identifier: "myprofile", type: "Community" };
  53 + helper.component.profile = profile;
  54 + helper.component.ngOnInit();
  55 +
  56 + // Check the attribute
  57 + expect(helper.component.defaultIcon).toBe("fa-users", "The default icon should be community users");
  58 + // var elProfile = fixture.debugElement.componentViewChildren[0];
  59 + // expect(elProfile.query('div.profile-image-block').length).toEqual(1);
  60 + done();
  61 +
  62 + });
  63 +
  64 + it("show Person image if profile is Person", (done) => {
  65 +
  66 + let profile = <noosfero.Profile>{ id: 1, identifier: "myprofile", type: "Person" };
  67 + helper.component.profile = profile;
  68 + helper.component.ngOnInit();
  69 + // Check the attribute
  70 + expect(helper.component.defaultIcon).toEqual("fa-user", "The default icon should be person user");
  71 + done();
48 }); 72 });
49 73
50 }); 74 });
51 -});  
52 \ No newline at end of file 75 \ No newline at end of file
  76 +});
src/app/profile/image/image.component.ts
1 -import {Inject, Input, Component} from "ng-forward";  
2 - 1 +import { Inject, Input, Component, provide } from "ng-forward";
  2 +import { PersonService } from "../../../lib/ng-noosfero-api/http/person.service";
  3 +import { ProfileImageEditorComponent } from "./profile-image-editor.component";
3 4
4 /** 5 /**
5 * @ngdoc controller 6 * @ngdoc controller
@@ -10,14 +11,16 @@ import {Inject, Input, Component} from &quot;ng-forward&quot;; @@ -10,14 +11,16 @@ import {Inject, Input, Component} from &quot;ng-forward&quot;;
10 @Component({ 11 @Component({
11 selector: "noosfero-profile-image", 12 selector: "noosfero-profile-image",
12 templateUrl: 'app/profile/image/image.html', 13 templateUrl: 'app/profile/image/image.html',
  14 + providers: [provide('personService', { useClass: PersonService })]
13 }) 15 })
  16 +@Inject(PersonService, "$uibModal", "$scope")
14 export class ProfileImageComponent { 17 export class ProfileImageComponent {
15 18
16 /** 19 /**
17 * @ngdoc property 20 * @ngdoc property
18 * @name profile 21 * @name profile
19 * @propertyOf components.noosfero.profile-image.ProfileImage 22 * @propertyOf components.noosfero.profile-image.ProfileImage
20 - * @description 23 + * @description
21 * The Noosfero {@link models.Profile} holding the image. 24 * The Noosfero {@link models.Profile} holding the image.
22 */ 25 */
23 @Input() profile: noosfero.Profile; 26 @Input() profile: noosfero.Profile;
@@ -30,12 +33,53 @@ export class ProfileImageComponent { @@ -30,12 +33,53 @@ export class ProfileImageComponent {
30 */ 33 */
31 defaultIcon: string; 34 defaultIcon: string;
32 35
  36 + @Input() editable: boolean;
  37 +
  38 + @Input() editClass: string;
  39 +
  40 + picFile: any;
  41 + croppedDataUrl: any;
  42 + modalInstance: any;
  43 +
  44 + constructor(private personService: PersonService, private $uibModal: ng.ui.bootstrap.IModalService, private $scope: ng.IScope) {
  45 + }
  46 +
  47 + fileSelected(file: any, errFiles: any) {
  48 + if (file) {
  49 + this.picFile = file;
  50 + this.modalInstance = this.$uibModal.open({
  51 + templateUrl: 'app/profile/image/profile-image-editor.html',
  52 + controller: ProfileImageEditorComponent,
  53 + controllerAs: 'ctrl',
  54 + scope: this.$scope,
  55 + bindToController: true,
  56 + backdrop: 'static',
  57 + resolve: {
  58 + picFile: this.picFile,
  59 + profile: this.profile,
  60 + personService: this.personService
  61 + }
  62 + });
  63 + }
  64 + }
  65 +
  66 + private _showCamera: boolean = false;
  67 +
  68 + showChange(show: boolean) {
  69 + this._showCamera = show;
  70 + }
  71 +
  72 + showCamera() {
  73 + return this._showCamera;
  74 + }
  75 +
  76 +
33 /** 77 /**
34 * @ngdoc method 78 * @ngdoc method
35 * @name ngOnInit 79 * @name ngOnInit
36 * @methodOf components.noosfero.profile-image.ProfileImage 80 * @methodOf components.noosfero.profile-image.ProfileImage
37 - * @description  
38 - * Initializes the icon names to their corresponding values depending on the profile type passed to the controller 81 + * @description
  82 + * Initializes the icon names to their corresponding values depending on the profile type passed to the controller
39 */ 83 */
40 ngOnInit() { 84 ngOnInit() {
41 this.defaultIcon = 'fa-users'; 85 this.defaultIcon = 'fa-users';
@@ -43,5 +87,5 @@ export class ProfileImageComponent { @@ -43,5 +87,5 @@ export class ProfileImageComponent {
43 this.defaultIcon = 'fa-user'; 87 this.defaultIcon = 'fa-user';
44 } 88 }
45 } 89 }
46 -}  
47 90
  91 +}
src/app/profile/image/image.html
1 -<span class="profile-image-wrap" title="{{ctrl.profile.name}}">  
2 - <img ng-if="ctrl.profile.image" ng-src="{{ctrl.profile.image.url}}" class="img-responsive profile-image">  
3 - <i ng-if="!ctrl.profile.image" class="fa {{ctrl.defaultIcon}} fa-5x profile-image"></i>  
4 -</span> 1 +<div id="profile-image-container" style="">
  2 + <div class="profile-image-wrap" title="{{ctrl.profile.name}}" ng-mouseenter="ctrl.showChange(true)" ng-mouseleave="ctrl.showChange(false)">
  3 + <img ng-if="ctrl.profile.image" ng-src="{{ctrl.profile.image.url}}" class="img-responsive profile-image">
  4 + <i ng-if="!ctrl.profile.image" class="fa {{ctrl.defaultIcon}} fa-5x profile-image"></i>
  5 + <div ng-if="ctrl.editable" class="upload-camera-container">
  6 + <i id="camera" class="fa fa-camera upload-camera" aria-hidden="true"></i>
  7 + </div>
  8 + <div ng-if="ctrl.editable" id="select-photo-container" name="select-photo-container" class="select-photo-container container" ng-class="ctrl.editClass">
  9 + <a id="upload-container" class="upload-container" href="#" rel="dialog" role="button">
  10 + <!-- The upload button hidden behind the camera -->
  11 + <div class="upload-button" ngf-select="ctrl.fileSelected($file)"
  12 + ngf-pattern="'image/*'" ngf-accept="'image/*'"
  13 + ngf-max-size="20MB" ngf-resize="{width: 100, height: 100}"
  14 + data-toggle="modal" data-target=".crop-dialog">
  15 + {{"profile.image.upload" | translate}}
  16 + </div>
  17 + </a>
  18 + </div>
  19 +
  20 + </div>
  21 +</div>
src/app/profile/image/image.scss
@@ -5,3 +5,91 @@ i.profile-image { @@ -5,3 +5,91 @@ i.profile-image {
5 background-clip: padding-box; 5 background-clip: padding-box;
6 margin-bottom: 15px; 6 margin-bottom: 15px;
7 } 7 }
  8 +
  9 +.profile-image-wrap {
  10 + display: inline;
  11 +}
  12 +
  13 +#profile-image-container {
  14 + display: inline;
  15 +}
  16 +
  17 +#profile-image-container:hover {
  18 + .select-photo-container {
  19 + z-index: 1;
  20 + }
  21 + .upload-camera-container {
  22 + transform: scale(.75);
  23 + }
  24 +}
  25 +
  26 +.upload-camera-container {
  27 + text-align: left;
  28 + position: absolute;
  29 + z-index: 5;
  30 +}
  31 +
  32 +.upload-camera {
  33 + color: white;
  34 + position: absolute;
  35 + transition: all .3s cubic-bezier(.175, .885, .32, 1.275);
  36 + opacity: 1;
  37 +}
  38 +
  39 +.select-photo-container {
  40 + position: absolute;
  41 + z-index: -1;
  42 + background: #000;
  43 + background: rgba(0, 0, 0, .6);
  44 + background: linear-gradient(transparent, rgba(0, 0, 0, .6) 70%, rgba(0, 0, 0, .6) 100%);
  45 + transition: top .13s ease-out;
  46 +}
  47 +
  48 +#upload-container {
  49 + position: relative;
  50 + text-decoration: none;
  51 +}
  52 +
  53 +.upload-container a:hover {
  54 + text-decoration: none;
  55 +}
  56 +
  57 +.upload-button {
  58 + -webkit-font-smoothing: antialiased;
  59 + color: #fff;
  60 +}
  61 +
  62 +.upload-container {
  63 + color:#fff;
  64 + display: block;
  65 + overflow: hidden;
  66 + position: relative;
  67 + text-align: left;
  68 + min-width: 89px;
  69 +}
  70 +
  71 +.cropArea {
  72 + background: #E4E4E4;
  73 + overflow: hidden;
  74 + width:300px;
  75 + height:150px;
  76 +}
  77 +
  78 +.crop-area {
  79 + display: none;
  80 +}
  81 +
  82 +form .progress {
  83 + line-height: 15px;
  84 +}
  85 +
  86 +.progress {
  87 + display: inline-block;
  88 + width: 100px;
  89 + border: 3px groove #CCC;
  90 +}
  91 +.progress div {
  92 + font-size: smaller;
  93 + background: orange;
  94 + width: 0;
  95 +}
src/app/profile/image/profile-image-editor.component.spec.ts 0 → 100644
@@ -0,0 +1,61 @@ @@ -0,0 +1,61 @@
  1 +import { Pipe, Input, provide, Component } from 'ng-forward';
  2 +import { ComponentTestHelper, createClass } from '../../../spec/component-test-helper';
  3 +import * as helpers from "../../../spec/helpers";
  4 +
  5 +import { ProfileImageEditorComponent } from "./profile-image-editor.component";
  6 +
  7 +describe("Components", () => {
  8 +
  9 + describe("Profile Image Editor Component", () => {
  10 +
  11 + beforeEach(angular.mock.module("templates"));
  12 +
  13 + let expectedData = "iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAgAElEQ…Cm2OLHvfdNPte3zrH709Q0esN1LPQ0t7DL696ERpu+9/8BVPLIpElf7VYAAAAASUVORK5CYII=";
  14 + let testDataUrl = "data:image/png;base64," + expectedData;
  15 +
  16 + let profile = <noosfero.Profile>{ name: "profile_name", id: 1, identifier: "test" };
  17 + let modal = helpers.mocks.$modal;
  18 + let modalInstance = jasmine.createSpyObj("$uibModalInstance", ["close"]);
  19 + let picFile = { type: "png" };
  20 + let $q: ng.IQService;
  21 + let personServiceMock: any;
  22 + let $rootScope: ng.IRootScopeService;
  23 +
  24 + beforeEach(inject((_$q_: ng.IQService, _$rootScope_: ng.IRootScopeService) => {
  25 + $q = _$q_;
  26 + $rootScope = _$rootScope_;
  27 + }));
  28 +
  29 + let comp = new ProfileImageEditorComponent(picFile, this.profile, personServiceMock, modalInstance);
  30 +
  31 + it("get data", done => {
  32 +
  33 + let result = comp.getData(testDataUrl);
  34 + expect(result).toBe(expectedData);
  35 + done();
  36 + });
  37 +
  38 + it("get image name", done => {
  39 + let imageName = "image1";
  40 + let expectedName = "profile_name_" + imageName;
  41 + comp['profile'] = profile;
  42 + let result = comp.getImageName(imageName);
  43 + expect(result).toBe(expectedName);
  44 + done();
  45 + });
  46 +
  47 + it("upload image", done => {
  48 + let imageName = "image1";
  49 + personServiceMock = jasmine.createSpyObj("personServiceMock", ["uploadImage"]);
  50 + let deferredUploadImage = $q.defer();
  51 + personServiceMock.uploadImage = jasmine.createSpy('uploadImage').and.returnValue(deferredUploadImage.promise);
  52 + comp.personService = personServiceMock;
  53 + comp.uploadImage(testDataUrl, imageName);
  54 + deferredUploadImage.resolve();
  55 + $rootScope.$apply();
  56 + expect(comp.modalInstance.close).toHaveBeenCalled();
  57 + done();
  58 + });
  59 +
  60 + });
  61 +});
src/app/profile/image/profile-image-editor.component.ts 0 → 100644
@@ -0,0 +1,43 @@ @@ -0,0 +1,43 @@
  1 +import { StateConfig, Component, Input, Output, Inject, provide } from 'ng-forward';
  2 +import { TranslateProfile } from "../../shared/pipes/translate-profile.filter";
  3 +import { PersonService } from "../../../lib/ng-noosfero-api/http/person.service";
  4 +
  5 +export class ProfileImageEditorComponent {
  6 +
  7 + activities: any;
  8 + croppedDataUrl: string;
  9 + static $inject = ["picFile", "profile", "personService", "$uibModalInstance"];
  10 +
  11 + constructor(public picFile: any, public profile: noosfero.Profile, public personService: PersonService,
  12 + public modalInstance: ng.ui.bootstrap.IModalServiceInstance) {
  13 + }
  14 +
  15 + uploadImage(dataUrl: any, name: any) {
  16 + let base64ImageJson = this.getBase64ImageJson(dataUrl, name);
  17 + this.personService.uploadImage(this.profile, base64ImageJson).then((result: any) => {
  18 + this.modalInstance.close(name);
  19 + });
  20 + }
  21 +
  22 + getBase64ImageJson(dataUrl: any, name: any): any {
  23 + let data = this.getData(dataUrl);
  24 + let image_name = this.getImageName(name);
  25 + return {
  26 + tempfile: data,
  27 + filename: image_name,
  28 + type: this.picFile.type
  29 + };
  30 + }
  31 +
  32 + getImageName(name: any): string {
  33 + return this.profile.name + "_" + name;
  34 + }
  35 +
  36 + getData(dataUrl: any): string {
  37 + return dataUrl.substring(dataUrl.indexOf('base64,') + 7);
  38 + }
  39 +
  40 + cancel() {
  41 + this.modalInstance.close();
  42 + }
  43 +}
src/app/profile/image/profile-image-editor.html 0 → 100644
@@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
  1 +<div class="modal-header">
  2 + <h3>{{"profile.image.edit" | translate}}</h3>
  3 +</div>
  4 +<div class="modal-body">
  5 + <form class="">
  6 + <div ngf-drop ng-model="ctrl.picFile" ngf-pattern="image/*" class="cropArea">
  7 + <img-crop image="ctrl.picFile | ngfDataUrl" area-type="square"
  8 + result-image="ctrl.croppedDataUrl" ng-init="ctrl.croppedDataUrl=''">
  9 + </img-crop>
  10 + </div>
  11 + <div>
  12 + <img ng-src="{{ctrl.croppedDataUrl}}" />
  13 + </div>
  14 + <span class="progress" ng-show="progress >= 0">
  15 + <div style="width: {{progress" ng-bind="progress + '%'"></div>
  16 + </span> <span ng-show="ctrl.result">Upload Successful</span> <span class="err"
  17 + ng-show="ctrl.errorMsg">{{errorMsg}}</span>
  18 + </form>
  19 +
  20 + <div class="actions">
  21 + <button type="submit" class="btn btn-default" (click)="ctrl.uploadImage(ctrl.croppedDataUrl, ctrl.picFile.name)">Upload</button>
  22 + <button type="submit" class="btn btn-danger" (click)="ctrl.cancel()">Cancel</button>
  23 + </div>
  24 +</div>
src/app/profile/info/profile-info.html
@@ -6,10 +6,12 @@ @@ -6,10 +6,12 @@
6 <h2>{{vm.profile.name}}</h2> 6 <h2>{{vm.profile.name}}</h2>
7 </header> 7 </header>
8 <div id="profile-left" class="main-box-body clearfix"> 8 <div id="profile-left" class="main-box-body clearfix">
9 - <noosfero-profile-image [profile]="vm.profile" class="img-responsive center-block"></noosfero-profile-image>  
10 - <span class="label" ng-class="{'label-danger': vm.profile.type == 'Community', 'label-info': vm.profile.type == 'Person'}">{{vm.profile | translateProfile}}</span>  
11 - <div class="profile-since">  
12 - {{"profile.member_since" | translate}}: {{vm.profile.created_at | amDateFormat:'MMMM YYYY'}} 9 + <noosfero-profile-image [profile]="vm.profile" [editable]="true" [edit-class]="'profile-info-editable'" class="img-responsive center-block profile-info"></noosfero-profile-image>
  10 + <div id="profile-info-extrainfo" class="profile-info-extrainfo">
  11 + <span class="label" ng-class="{'label-danger': vm.profile.type == 'Community', 'label-info': vm.profile.type == 'Person'}">{{vm.profile | translateProfile}}</span>
  12 + <div class="profile-since">
  13 + {{"profile.member_since" | translate}}: {{vm.profile.created_at | amDateFormat:'MMMM YYYY'}}
  14 + </div>
13 </div> 15 </div>
14 </div> 16 </div>
15 </div> 17 </div>
src/app/profile/info/profile-info.scss 0 → 100644
@@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
  1 +.profile-info {
  2 + .upload-camera-container {
  3 + top: 55%;
  4 + left: 39px;
  5 + }
  6 +}
  7 +
  8 +.profile-info-editable {
  9 + top: 51%;
  10 + width: 103px;
  11 + height: 28px;
  12 +}
  13 +
  14 +.profile-info-editable {
  15 + .upload-button {
  16 + font-size: 0.8em;
  17 + padding-top: 9px;
  18 + padding-left: 6px;
  19 + font-weight: bold;
  20 + }
  21 +}
  22 +
  23 +.profile-info-extrainfo {
  24 + margin-top: 10px;
  25 +}
src/app/profile/profile.component.ts
1 -import {StateConfig, Component, Inject, provide} from 'ng-forward';  
2 -import {ProfileInfoComponent} from './info/profile-info.component';  
3 -import {ProfileHomeComponent} from './profile-home.component';  
4 -import {BasicEditorComponent} from '../article/cms/basic-editor/basic-editor.component';  
5 -import {CmsComponent} from '../article/cms/cms.component';  
6 -import {ContentViewerComponent} from "../article/content-viewer/content-viewer.component";  
7 -import {ContentViewerActionsComponent} from "../article/content-viewer/content-viewer-actions.component";  
8 -import {ActivitiesComponent} from "./activities/activities.component";  
9 -import {ProfileService} from "../../lib/ng-noosfero-api/http/profile.service";  
10 -import {NotificationService} from "../shared/services/notification.service";  
11 -import {MyProfileComponent} from "./myprofile.component";  
12 -import {ProfileActionsComponent} from "./profile-actions.component";  
13 -import {ConfigBarComponent} from "./config-bar.component"; 1 +import { StateConfig, Component, Inject, provide } from 'ng-forward';
  2 +import { ProfileInfoComponent } from './info/profile-info.component';
  3 +import { ProfileHomeComponent } from './profile-home.component';
  4 +import { BasicEditorComponent } from '../article/cms/basic-editor/basic-editor.component';
  5 +import { CmsComponent } from '../article/cms/cms.component';
  6 +import { ContentViewerComponent } from "../article/content-viewer/content-viewer.component";
  7 +import { ContentViewerActionsComponent } from "../article/content-viewer/content-viewer-actions.component";
  8 +import { ActivitiesComponent } from "./activities/activities.component";
  9 +import { ProfileService } from "../../lib/ng-noosfero-api/http/profile.service";
  10 +import { NotificationService } from "../shared/services/notification.service";
  11 +import { MyProfileComponent } from "./myprofile.component";
  12 +import { ProfileActionsComponent } from "./profile-actions.component";
  13 +import { ConfigBarComponent } from "./config-bar.component";
  14 +import { TasksComponent } from "../task/tasks/tasks.component";
  15 +
14 /** 16 /**
15 * @ngdoc controller 17 * @ngdoc controller
16 * @name profile.Profile 18 * @name profile.Profile
@@ -92,6 +94,18 @@ import {ConfigBarComponent} from &quot;./config-bar.component&quot;; @@ -92,6 +94,18 @@ import {ConfigBarComponent} from &quot;./config-bar.component&quot;;
92 } 94 }
93 }, 95 },
94 { 96 {
  97 + name: 'main.profile.tasks',
  98 + url: "^/myprofile/:profile/tasks",
  99 + component: TasksComponent,
  100 + views: {
  101 + "mainBlockContent": {
  102 + templateUrl: "app/task/tasks/tasks.html",
  103 + controller: TasksComponent,
  104 + controllerAs: "vm"
  105 + }
  106 + }
  107 + },
  108 + {
95 name: 'main.profile.home', 109 name: 'main.profile.home',
96 url: "", 110 url: "",
97 component: ProfileHomeComponent, 111 component: ProfileHomeComponent,
src/app/search/search.component.spec.ts
@@ -8,7 +8,7 @@ describe(&quot;Components&quot;, () =&gt; { @@ -8,7 +8,7 @@ describe(&quot;Components&quot;, () =&gt; {
8 describe("Search Component", () => { 8 describe("Search Component", () => {
9 9
10 let helper: ComponentTestHelper<SearchComponent>; 10 let helper: ComponentTestHelper<SearchComponent>;
11 - let stateParams = { query: 'query' }; 11 + let stateParams = { query: 'query', per_page: 20 };
12 let articleService = jasmine.createSpyObj("ArticleService", ["search"]); 12 let articleService = jasmine.createSpyObj("ArticleService", ["search"]);
13 let result = Promise.resolve({ data: [{ id: 1 }], headers: (param: string) => { return 1; } }); 13 let result = Promise.resolve({ data: [{ id: 1 }], headers: (param: string) => { return 1; } });
14 articleService.search = jasmine.createSpy("search").and.returnValue(result); 14 articleService.search = jasmine.createSpy("search").and.returnValue(result);
@@ -30,7 +30,7 @@ describe(&quot;Components&quot;, () =&gt; { @@ -30,7 +30,7 @@ describe(&quot;Components&quot;, () =&gt; {
30 }); 30 });
31 31
32 it("load first page with search results", () => { 32 it("load first page with search results", () => {
33 - expect(articleService.search).toHaveBeenCalledWith({ query: 'query', per_page: 10, page: 0 }); 33 + expect(articleService.search).toHaveBeenCalledWith({ query: 'query', per_page: 20, page: 0 });
34 }); 34 });
35 35
36 it("display search results", () => { 36 it("display search results", () => {
src/app/search/search.component.ts
@@ -19,6 +19,7 @@ export class SearchComponent { @@ -19,6 +19,7 @@ export class SearchComponent {
19 19
20 constructor(private articleService: ArticleService, private $stateParams: ng.ui.IStateParamsService, private $state: ng.ui.IStateService) { 20 constructor(private articleService: ArticleService, private $stateParams: ng.ui.IStateParamsService, private $state: ng.ui.IStateService) {
21 this.query = this.$stateParams['query']; 21 this.query = this.$stateParams['query'];
  22 + this.perPage = this.$stateParams['per_page'] || this.perPage;
22 this.loadPage(); 23 this.loadPage();
23 } 24 }
24 25
src/app/search/search.scss
  1 +$green-color: #6e9e7b;
  2 +
1 .search-results { 3 .search-results {
2 .summary { 4 .summary {
3 color: #bbbbbb; 5 color: #bbbbbb;
@@ -13,10 +15,10 @@ @@ -13,10 +15,10 @@
13 } 15 }
14 .info { 16 .info {
15 .profile { 17 .profile {
16 - color: #6e9e7b; 18 + color: $green-color;
17 } 19 }
18 .time { 20 .time {
19 - color: #6e9e7b; 21 + color: $green-color;
20 font-size: 12px; 22 font-size: 12px;
21 } 23 }
22 .bullet-separator { 24 .bullet-separator {
src/app/shared/services/events-hub.service.ts
@@ -31,13 +31,13 @@ export class EventsHubService { @@ -31,13 +31,13 @@ export class EventsHubService {
31 emitEvent(eventType: string, payload?: any) { 31 emitEvent(eventType: string, payload?: any) {
32 this.checkKnownEvent(eventType); 32 this.checkKnownEvent(eventType);
33 let event = this.emitters.get(eventType); 33 let event = this.emitters.get(eventType);
34 - if ( event ) this.emitters.get(eventType).next(payload); 34 + if (event) this.emitters.get(eventType).next(payload);
35 } 35 }
36 36
37 subscribeToEvent<T>(eventType: string, generatorOrNext?: ((p?: T) => void), error?: any, complete?: any) { 37 subscribeToEvent<T>(eventType: string, generatorOrNext?: ((p?: T) => void), error?: any, complete?: any) {
38 this.checkKnownEvent(eventType); 38 this.checkKnownEvent(eventType);
39 let event = this.emitters.get(eventType); 39 let event = this.emitters.get(eventType);
40 - if (event) event.subscribe(generatorOrNext, error, complete); 40 + if (event) event.subscribe(generatorOrNext, error, complete);
41 } 41 }
42 42
43 private setupEmitters() { 43 private setupEmitters() {
@@ -53,4 +53,4 @@ export class EventsHubService { @@ -53,4 +53,4 @@ export class EventsHubService {
53 } 53 }
54 54
55 55
56 -}  
57 \ No newline at end of file 56 \ No newline at end of file
  57 +}
src/app/shared/services/notification.service.ts
@@ -14,6 +14,8 @@ export class NotificationService { @@ -14,6 +14,8 @@ export class NotificationService {
14 public static DEFAULT_ERROR_TITLE = "notification.error.default.title"; 14 public static DEFAULT_ERROR_TITLE = "notification.error.default.title";
15 public static DEFAULT_ERROR_MESSAGE = "notification.error.default.message"; 15 public static DEFAULT_ERROR_MESSAGE = "notification.error.default.message";
16 public static DEFAULT_SUCCESS_TIMER = 1000; 16 public static DEFAULT_SUCCESS_TIMER = 1000;
  17 + public static DEFAULT_INFO_TITLE = "notification.info.default.title";
  18 + public static DEFAULT_INFO_MESSAGE = "notification.info.default.message";
17 19
18 error({ 20 error({
19 message = NotificationService.DEFAULT_ERROR_MESSAGE, 21 message = NotificationService.DEFAULT_ERROR_MESSAGE,
@@ -40,6 +42,14 @@ export class NotificationService { @@ -40,6 +42,14 @@ export class NotificationService {
40 this.showMessage({ title: title, text: message, showCancelButton: showCancelButton, type: type, closeOnConfirm: false }, confirmationFunction); 42 this.showMessage({ title: title, text: message, showCancelButton: showCancelButton, type: type, closeOnConfirm: false }, confirmationFunction);
41 } 43 }
42 44
  45 + info({
  46 + message = NotificationService.DEFAULT_INFO_MESSAGE,
  47 + title = NotificationService.DEFAULT_INFO_TITLE,
  48 + showConfirmButton = true
  49 + } = {}) {
  50 + this.showMessage({ title: title, text: message, showConfirmButton: showConfirmButton, type: "info" });
  51 + }
  52 +
43 private showMessage({title, text, type = "success", timer = null, showConfirmButton = true, showCancelButton = false, closeOnConfirm = true}, confirmationFunction: Function = null) { 53 private showMessage({title, text, type = "success", timer = null, showConfirmButton = true, showCancelButton = false, closeOnConfirm = true}, confirmationFunction: Function = null) {
44 this.$log.debug("Notification message:", title, text, type, this.translatorService.currentLanguage()); 54 this.$log.debug("Notification message:", title, text, type, this.translatorService.currentLanguage());
45 this.SweetAlert.swal({ 55 this.SweetAlert.swal({
src/app/task/task-list/accept.html 0 → 100644
@@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
  1 +<div class="task-accept task-confirmation">
  2 + <div class="accept-title confirmation-title">{{"tasks.actions.accept.confirmation.title" | translate}}</div>
  3 + <div class="accept-fields confirmation-details">
  4 + <task-accept ng-if="ctrl.currentTask.accept_details" [task]="ctrl.currentTask" [confirmation-task]="ctrl.confirmationTask"></task-accept>
  5 + </div>
  6 + <div class="actions">
  7 + <button type="submit" class="btn btn-default" ng-click="ctrl.callAccept()">{{"tasks.actions.confirmation.yes" | translate}}</button>
  8 + <button type="button" class="btn btn-warning" ng-click="ctrl.cancel()">{{"tasks.actions.confirmation.cancel" | translate}}</button>
  9 + </div>
  10 +</div>
src/app/task/task-list/reject.html 0 → 100644
@@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
  1 +<div class="task-reject task-confirmation">
  2 + <div class="reject-title confirmation-title">{{"tasks.actions.reject.confirmation.title" | translate}}</div>
  3 + <div class="reject-fields confirmation-details">
  4 + <input ng-model="ctrl.confirmationTask.reject_explanation" id="rejectionExplanationInput" type="text" placeholder="{{'tasks.actions.reject.explanation.label' | translate}}" class="rejection-explanation form-control">
  5 + </div>
  6 + <div class="actions">
  7 + <button type="submit" class="btn btn-default" ng-click="ctrl.callReject()">{{"tasks.actions.confirmation.yes" | translate}}</button>
  8 + <button type="button" class="btn btn-warning" ng-click="ctrl.cancel()">{{"tasks.actions.confirmation.cancel" | translate}}</button>
  9 + </div>
  10 +</div>
src/app/task/task-list/task-accept.component.spec.ts 0 → 100644
@@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
  1 +import { Provider, provide, Component } from 'ng-forward';
  2 +import * as helpers from "../../../spec/helpers";
  3 +import { TaskAcceptComponent } from './task-accept.component';
  4 +
  5 +const htmlTemplate: string = '<task-accept [task]="ctrl.task"></task-accept>';
  6 +
  7 +describe("Components", () => {
  8 + describe("Task Accept Component", () => {
  9 +
  10 + let task = { id: 1, type: "AddMember" };
  11 + let roleService = jasmine.createSpyObj("roleService", ["getByProfile"]);
  12 +
  13 + beforeEach(angular.mock.module("templates"));
  14 +
  15 + function createComponent() {
  16 + return helpers.quickCreateComponent({
  17 + template: htmlTemplate,
  18 + directives: [TaskAcceptComponent],
  19 + properties: { task: task },
  20 + providers: [
  21 + helpers.createProviderToValue("RoleService", roleService)
  22 + ].concat(helpers.provideFilters("translateFilter"))
  23 + });
  24 + }
  25 +
  26 + it("replace element with the specific task accept component", (done: Function) => {
  27 + createComponent().then(fixture => {
  28 + expect(fixture.debugElement.queryAll("add-member-task-accept").length).toBe(1);
  29 + done();
  30 + });
  31 + });
  32 + });
  33 +});
src/app/task/task-list/task-accept.component.ts 0 → 100644
@@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
  1 +import { Input, Inject, Component } from 'ng-forward';
  2 +import { AddMemberTaskAcceptComponent } from "../types/add-member/add-member-task-accept.component";
  3 +
  4 +@Component({
  5 + selector: 'task-accept',
  6 + template: '<div></div>',
  7 + directives: [AddMemberTaskAcceptComponent]
  8 +})
  9 +@Inject("$element", "$scope", "$injector", "$compile")
  10 +export class TaskAcceptComponent {
  11 +
  12 + @Input() task: noosfero.Task;
  13 + @Input() confirmationTask: noosfero.Task;
  14 +
  15 + ngOnInit() {
  16 + let componentName = this.task.type.replace(/::/, '').replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
  17 + componentName += "-task-accept";
  18 + this.$element.replaceWith(this.$compile(`<${componentName} [task]="ctrl.task" [confirmation-task]="ctrl.confirmationTask"></${componentName}>`)(this.$scope));
  19 + }
  20 +
  21 + constructor(private $element: any, private $scope: ng.IScope, private $injector: ng.auto.IInjectorService, private $compile: ng.ICompileService) { }
  22 +}
src/app/task/task-list/task-list.component.spec.ts 0 → 100644
@@ -0,0 +1,106 @@ @@ -0,0 +1,106 @@
  1 +import { Provider, provide, Component } from 'ng-forward';
  2 +import * as helpers from "../../../spec/helpers";
  3 +import { ComponentTestHelper, createClass } from '../../../spec/component-test-helper';
  4 +import { TaskListComponent } from './task-list.component';
  5 +
  6 +const htmlTemplate: string = '<task-list [task]="ctrl.task"></task-list>';
  7 +
  8 +describe("Components", () => {
  9 + describe("Task List Component", () => {
  10 +
  11 + let helper: ComponentTestHelper<TaskListComponent>;
  12 + let taskService = jasmine.createSpyObj("taskService", ["getAllPending"]);
  13 + let tasks = [{ id: 1 }, { id: 2 }];
  14 + let modal = helpers.mocks.$modal;
  15 + let eventsHubService = jasmine.createSpyObj("eventsHubService", ["subscribeToEvent", "emitEvent"]);
  16 + taskService.getAllPending = jasmine.createSpy("getAllPending").and.returnValue(Promise.resolve({ headers: () => { }, data: tasks }));
  17 +
  18 + beforeEach(angular.mock.module("templates"));
  19 +
  20 + beforeEach((done) => {
  21 + let cls = createClass({
  22 + template: htmlTemplate,
  23 + directives: [TaskListComponent],
  24 + providers: [
  25 + helpers.createProviderToValue("TaskService", taskService),
  26 + helpers.createProviderToValue("EventsHubService", eventsHubService),
  27 + helpers.createProviderToValue('NotificationService', helpers.mocks.notificationService),
  28 + helpers.createProviderToValue('$uibModal', modal),
  29 + ].concat(helpers.provideFilters("groupByFilter")),
  30 + properties: { tasks: tasks }
  31 + });
  32 + helper = new ComponentTestHelper<TaskListComponent>(cls, done);
  33 + });
  34 +
  35 + it("return specific template for a task", () => {
  36 + let task = { type: "AddMember" };
  37 + expect(helper.component.getTaskTemplate(<any>task)).toEqual("app/task/types/add-member/add-member.html");
  38 + });
  39 +
  40 + it("return the default template for a task", () => {
  41 + let task = { type: "" };
  42 + expect(helper.component.getTaskTemplate(<any>task)).toEqual("app/task/types/default.html");
  43 + });
  44 +
  45 + it("open confirmation modal when it has details to accept a task", () => {
  46 + let task = { accept_details: true };
  47 + helper.component.accept(<any>task);
  48 + expect(modal.open).toHaveBeenCalled();
  49 + });
  50 +
  51 + it("open confirmation modal when it has details to reject a task", () => {
  52 + let task = { reject_details: true };
  53 + helper.component.reject(<any>task);
  54 + expect(modal.open).toHaveBeenCalled();
  55 + });
  56 +
  57 + it("call api directly when it has no details to accept a task", () => {
  58 + let task = { accept_details: false };
  59 + helper.component.callAccept = jasmine.createSpy("callAccept");
  60 + helper.component.accept(<any>task);
  61 + expect(helper.component.callAccept).toHaveBeenCalled();
  62 + });
  63 +
  64 + it("call api directly when it has no details to reject a task", () => {
  65 + let task = { accept_details: false };
  66 + helper.component.callReject = jasmine.createSpy("callReject");
  67 + helper.component.reject(<any>task);
  68 + expect(helper.component.callReject).toHaveBeenCalled();
  69 + });
  70 +
  71 + it("call cancel and emit event when accept was called successfully", () => {
  72 + helper.component.currentTask = <any>{ id: 1 };
  73 + let result = helpers.mocks.promiseResultTemplate({ data: { id: 1 } });
  74 + taskService.closeTask = jasmine.createSpy("closeTask").and.returnValue(result);
  75 + helper.component.cancel = jasmine.createSpy("cancel");
  76 + helper.component.callAccept();
  77 + expect(helper.component.cancel).toHaveBeenCalled();
  78 + expect((<any>helper.component)['eventsHubService'].emitEvent).toHaveBeenCalled();
  79 + });
  80 +
  81 + it("call cancel and emit event when reject was called successfully", () => {
  82 + helper.component.currentTask = <any>{ id: 1 };
  83 + let result = helpers.mocks.promiseResultTemplate({ data: { id: 1 } });
  84 + taskService.closeTask = jasmine.createSpy("closeTask").and.returnValue(result);
  85 + helper.component.cancel = jasmine.createSpy("cancel");
  86 + helper.component.callReject();
  87 + expect(helper.component.cancel).toHaveBeenCalled();
  88 + expect((<any>helper.component)['eventsHubService'].emitEvent).toHaveBeenCalled();
  89 + });
  90 +
  91 + it("reset currentTask and close modal when call cancel", () => {
  92 + let modalInstance = jasmine.createSpyObj("modalInstance", ["close"]);
  93 + helper.component["modalInstance"] = modalInstance;
  94 + helper.component.currentTask = <any>{ id: 1 };
  95 + helper.component.cancel();
  96 + expect(modalInstance.close).toHaveBeenCalled();
  97 + expect(helper.component.currentTask).toBeNull();
  98 + });
  99 +
  100 + it("not fail when call cancel with no modalInstance", () => {
  101 + helper.component["modalInstance"] = null;
  102 + helper.component.currentTask = null;
  103 + helper.component.cancel();
  104 + });
  105 + });
  106 +});
src/app/task/task-list/task-list.component.ts 0 → 100644
@@ -0,0 +1,107 @@ @@ -0,0 +1,107 @@
  1 +import { Component, Input, Inject, provide } from "ng-forward";
  2 +import { NotificationService } from "../../shared/services/notification.service";
  3 +import { TaskService } from "../../../lib/ng-noosfero-api/http/task.service";
  4 +import { TaskAcceptComponent } from "./task-accept.component";
  5 +import { Arrays } from "../../../lib/util/arrays";
  6 +import { EventsHubService } from "../../shared/services/events-hub.service";
  7 +import { NoosferoKnownEvents } from "../../known-events";
  8 +
  9 +@Component({
  10 + selector: "task-list",
  11 + templateUrl: "app/task/task-list/task-list.html",
  12 + directives: [TaskAcceptComponent],
  13 + providers: [
  14 + provide('eventsHubService', { useClass: EventsHubService })
  15 + ]
  16 +})
  17 +@Inject(NotificationService, "$scope", "$uibModal", TaskService, EventsHubService)
  18 +export class TaskListComponent {
  19 +
  20 + @Input() tasks: noosfero.Task[];
  21 +
  22 + private taskTemplates = ["AddFriend", "AddMember", "CreateCommunity", "SuggestArticle", "AbuseComplaint"];
  23 +
  24 + currentTask: noosfero.Task;
  25 + confirmationTask: noosfero.Task;
  26 + eventsNames: NoosferoKnownEvents;
  27 + private modalInstance: any = null;
  28 +
  29 + constructor(private notificationService: NotificationService,
  30 + private $scope: ng.IScope,
  31 + private $uibModal: any,
  32 + private taskService: TaskService,
  33 + private eventsHubService: EventsHubService) {
  34 +
  35 + this.eventsNames = new NoosferoKnownEvents();
  36 + }
  37 +
  38 + ngOnInit() {
  39 + this.eventsHubService.subscribeToEvent(this.eventsNames.TASK_CLOSED, (task: noosfero.Task) => {
  40 + Arrays.remove(this.tasks, task);
  41 + });
  42 + }
  43 +
  44 + getTaskTemplate(task: noosfero.Task) {
  45 + if (this.taskTemplates.indexOf(task.type) >= 0) {
  46 + let templateName = this.getTemplateName(task);
  47 + return `app/task/types/${templateName}/${templateName}.html`;
  48 + } else {
  49 + return 'app/task/types/default.html';
  50 + }
  51 + }
  52 +
  53 + accept(task: noosfero.Task) {
  54 + this.closeTask(task, task.accept_details, "app/task/task-list/accept.html", () => { this.callAccept(); });
  55 + }
  56 +
  57 + reject(task: noosfero.Task) {
  58 + this.closeTask(task, task.reject_details, "app/task/task-list/reject.html", () => { this.callReject(); });
  59 + }
  60 +
  61 + private closeTask(task: noosfero.Task, hasDetails: boolean, templateUrl: string, confirmationFunction: Function) {
  62 + this.currentTask = task;
  63 + this.confirmationTask = <any>{ id: task.id };
  64 + if (hasDetails) {
  65 + this.modalInstance = this.$uibModal.open({
  66 + templateUrl: templateUrl,
  67 + controller: TaskListComponent,
  68 + controllerAs: 'modal',
  69 + bindToController: true,
  70 + scope: this.$scope
  71 + });
  72 + } else {
  73 + confirmationFunction();
  74 + }
  75 + }
  76 +
  77 + callAccept() {
  78 + this.callCloseTask("finish", "tasks.actions.accept.title", "tasks.actions.accept.message");
  79 + }
  80 +
  81 + callReject() {
  82 + this.callCloseTask("cancel", "tasks.actions.reject.title", "tasks.actions.reject.message");
  83 + }
  84 +
  85 + private callCloseTask(action: string, title: string, message: string) {
  86 + this.taskService.closeTask(this.confirmationTask, action).then(() => {
  87 + this.eventsHubService.emitEvent(this.eventsNames.TASK_CLOSED, this.currentTask);
  88 + this.notificationService.success({ title: title, message: message });
  89 + }).finally(() => {
  90 + this.cancel();
  91 + });
  92 + }
  93 +
  94 + cancel() {
  95 + if (this.modalInstance) {
  96 + this.modalInstance.close();
  97 + this.modalInstance = null;
  98 + }
  99 + this.currentTask = null;
  100 + this.confirmationTask = null;
  101 + }
  102 +
  103 + private getTemplateName(task: noosfero.Task) {
  104 + return task.type.replace(/::/, '').replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
  105 + }
  106 +
  107 +}
src/app/task/task-list/task-list.html 0 → 100644
@@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
  1 +<ul class="task-list">
  2 + <li class="task-group" ng-repeat="(target, tasks) in ctrl.tasks | groupBy: 'target.name'">
  3 + <div class="task-target">
  4 + <noosfero-profile-image ng-if="tasks[0].target.type" [profile]="tasks[0].target"></noosfero-profile-image>
  5 + <div class="target-name">{{target}}</div>
  6 + </div>
  7 + <div class="task-body" ng-repeat="task in tasks | orderBy: 'created_at':true">
  8 + <div class="task">
  9 + <ng-include src="ctrl.getTaskTemplate(task)"></ng-include>
  10 + </div>
  11 + <div class="actions">
  12 + <a href="#" ng-if="!task.accept_disabled" ng-click="ctrl.accept(task)" class="accept" uib-tooltip="{{'tasks.actions.accept' | translate}}">
  13 + <i class="fa fa-check"></i>
  14 + </a>
  15 + <a href="#" ng-if="!task.reject_disabled" ng-click="ctrl.reject(task)" class="reject" uib-tooltip="{{'tasks.actions.reject' | translate}}">
  16 + <i class="fa fa-close"></i>
  17 + </a>
  18 + </div>
  19 + <span class="time">
  20 + <span class="bullet-separator">•</span> <span am-time-ago="task.created_at | dateFormat"></span>
  21 + </span>
  22 + </div>
  23 + </li>
  24 +</ul>
src/app/task/task-list/task-list.scss 0 → 100644
@@ -0,0 +1,86 @@ @@ -0,0 +1,86 @@
  1 +$task-action-accept-color: #77c123;
  2 +$task-action-reject-color: #d64e18;
  3 +
  4 +.task-list {
  5 + width: 100%;
  6 + padding: 0;
  7 + list-style-type: none;
  8 + .task-group {
  9 + border-top: 1px solid #f3f3f3;
  10 + padding: 4px 16px 8px 16px;
  11 + }
  12 + .task-target {
  13 + margin-top: 12px;
  14 + .profile-image {
  15 + color: #2c3e50;
  16 + font-size: 25px;
  17 + width: 25px;
  18 + display: inline-block;
  19 + @extend .img-rounded;
  20 + }
  21 + .target-name {
  22 + display: inline-block;
  23 + margin-left: 10px;
  24 + font-size: 18px;
  25 + font-weight: bold;
  26 + }
  27 + }
  28 + .task-body {
  29 + margin-left: 35px;
  30 + padding: 2px;
  31 + .task {
  32 + display: inline-block;
  33 + color: #949494;
  34 + .task-icon {
  35 + font-size: 18px;
  36 + color: #e84e40;
  37 + }
  38 + .requestor, .target {
  39 + font-style: italic;
  40 + color: #676767;
  41 + }
  42 + }
  43 + .actions {
  44 + display: inline-block;
  45 + font-size: 19px;
  46 + a {
  47 + text-decoration: none;
  48 + }
  49 + .accept {
  50 + color: $task-action-accept-color;
  51 + &:hover {
  52 + color: darken($task-action-accept-color, 10%);
  53 + }
  54 + }
  55 + .reject {
  56 + color: $task-action-reject-color;
  57 + &:hover {
  58 + color: darken($task-action-reject-color, 10%);
  59 + }
  60 + }
  61 + }
  62 + .time {
  63 + color: #c1c1c1;
  64 + font-size: 12px;
  65 + .bullet-separator {
  66 + font-size: 10px;
  67 + color: #d1d1d1;
  68 + }
  69 + }
  70 + }
  71 +}
  72 +.task-confirmation {
  73 + @extend .form-group;
  74 + .confirmation-details {
  75 + margin-bottom: 20px;
  76 + }
  77 + .confirmation-title {
  78 + text-align: center;
  79 + font-size: 30px;
  80 + font-weight: bold;
  81 + margin-bottom: 20px;
  82 + }
  83 + .actions {
  84 + text-align: center;
  85 + }
  86 +}
src/app/task/tasks-menu/tasks-menu.component.spec.ts 0 → 100644
@@ -0,0 +1,44 @@ @@ -0,0 +1,44 @@
  1 +import { Provider, provide, Component } from 'ng-forward';
  2 +import * as helpers from "../../../spec/helpers";
  3 +import { ComponentTestHelper, createClass } from '../../../spec/component-test-helper';
  4 +import { TasksMenuComponent } from './tasks-menu.component';
  5 +import { AuthEvents } from "./../../login";
  6 +
  7 +const htmlTemplate: string = '<tasks-menu></tasks-menu>';
  8 +
  9 +describe("Components", () => {
  10 + describe("Task Menu Component", () => {
  11 +
  12 + let helper: ComponentTestHelper<TasksMenuComponent>;
  13 + let taskService = jasmine.createSpyObj("taskService", ["getAllPending"]);
  14 + let tasks = [{ id: 1 }, { id: 2 }];
  15 + let eventsHubService = jasmine.createSpyObj("eventsHubService", ["subscribeToEvent", "emitEvent"]);
  16 + taskService.getAllPending = jasmine.createSpy("getAllPending").and.returnValue(Promise.resolve({ headers: () => { }, data: tasks }));
  17 +
  18 + beforeEach(angular.mock.module("templates"));
  19 +
  20 + beforeEach((done) => {
  21 + let cls = createClass({
  22 + template: htmlTemplate,
  23 + directives: [TasksMenuComponent],
  24 + providers: [
  25 + helpers.createProviderToValue("TaskService", taskService),
  26 + helpers.createProviderToValue("EventsHubService", eventsHubService),
  27 + helpers.createProviderToValue('SessionService', helpers.mocks.sessionWithCurrentUser({}))
  28 + ]
  29 + });
  30 + helper = new ComponentTestHelper<TasksMenuComponent>(cls, done);
  31 + });
  32 +
  33 + it("load person tasks", () => {
  34 + expect(taskService.getAllPending).toHaveBeenCalled();
  35 + });
  36 +
  37 + it("load person tasks when receive a login event", () => {
  38 + helper.component.loadTasks = jasmine.createSpy("loadTasks");
  39 + helper.component.ngOnInit();
  40 + (<any>helper.component['authService'])[AuthEvents[AuthEvents.loginSuccess]].next({});
  41 + expect(helper.component.loadTasks).toHaveBeenCalled();
  42 + });
  43 + });
  44 +});
src/app/task/tasks-menu/tasks-menu.component.ts 0 → 100644
@@ -0,0 +1,46 @@ @@ -0,0 +1,46 @@
  1 +import { Component, Inject } from "ng-forward";
  2 +import { TaskService } from "../../../lib/ng-noosfero-api/http/task.service";
  3 +import { AuthService, SessionService, AuthEvents } from "./../../login";
  4 +import { EventsHubService } from "../../shared/services/events-hub.service";
  5 +import { NoosferoKnownEvents } from "../../known-events";
  6 +
  7 +@Component({
  8 + selector: "tasks-menu",
  9 + templateUrl: "app/task/tasks-menu/tasks-menu.html"
  10 +})
  11 +@Inject(TaskService, SessionService, AuthService, EventsHubService)
  12 +export class TasksMenuComponent {
  13 +
  14 + tasks: noosfero.Task[];
  15 + total: number;
  16 + perPage = 5;
  17 + person: noosfero.Person;
  18 + eventsNames: NoosferoKnownEvents;
  19 +
  20 + constructor(private taskService: TaskService,
  21 + private session: SessionService,
  22 + private authService: AuthService,
  23 + private eventsHubService: EventsHubService) {
  24 +
  25 + this.eventsNames = new NoosferoKnownEvents();
  26 + }
  27 +
  28 + ngOnInit() {
  29 + this.eventsHubService.subscribeToEvent(this.eventsNames.TASK_CLOSED, (task: noosfero.Task) => {
  30 + this.total--;
  31 + });
  32 + this.authService.subscribe(AuthEvents[AuthEvents.loginSuccess], () => {
  33 + this.loadTasks();
  34 + });
  35 + this.loadTasks();
  36 + }
  37 +
  38 + loadTasks() {
  39 + if (!this.session.currentUser()) return;
  40 + this.person = this.session.currentUser().person;
  41 + this.taskService.getAllPending({ per_page: this.perPage }).then((result: noosfero.RestResult<noosfero.Task[]>) => {
  42 + this.total = result.headers('total');
  43 + this.tasks = result.data;
  44 + });
  45 + }
  46 +}
src/app/task/tasks-menu/tasks-menu.html 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  1 +<li class="btn-nav tasks-menu" uib-dropdown ng-show="ctrl.total > 0" ng-if="ctrl.total">
  2 + <a href="#" uib-dropdown-toggle>
  3 + <i class="fa fa-bell-o fa-fw fa-2x"></i>
  4 + <span class="badge">{{ctrl.total}}</span>
  5 + </a>
  6 + <div class="dropdown-menu task-panel" uib-dropdown-menu>
  7 + <div class="task-menu-header">{{"tasks.menu.header" | translate:{tasks: ctrl.total}:"messageformat"}}</div>
  8 + <task-list [tasks]="ctrl.tasks"></task-list>
  9 + <a ui-sref="main.profile.tasks({profile: ctrl.person.identifier})" class="all-tasks btn btn-default btn-xs">{{"tasks.menu.all" | translate}}</a>
  10 + </div>
  11 +</li>
src/app/task/tasks-menu/tasks-menu.scss 0 → 100644
@@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
  1 +tasks-menu {
  2 + .tasks-menu {
  3 + position: relative;
  4 + margin-right: 0;
  5 + .badge {
  6 + position: absolute;
  7 + top: 1px;
  8 + right: 8px;
  9 + border-radius: 6px;
  10 + background-color: #e84e40;
  11 + color: #fff;
  12 + }
  13 + .all-tasks {
  14 + text-align: center;
  15 + width: 100%;
  16 + display: block;
  17 + line-height: 25px;
  18 + }
  19 + .task-panel {
  20 + width: 550px;
  21 + padding: 0;
  22 + }
  23 + .task-menu-header {
  24 + text-align: center;
  25 + padding: 7px;
  26 + font-weight: bold;
  27 + }
  28 + }
  29 +}
src/app/task/tasks/tasks.component.spec.ts 0 → 100644
@@ -0,0 +1,34 @@ @@ -0,0 +1,34 @@
  1 +import { Provider, provide, Component } from 'ng-forward';
  2 +import * as helpers from "../../../spec/helpers";
  3 +import { ComponentTestHelper, createClass } from '../../../spec/component-test-helper';
  4 +import { TasksComponent } from './tasks.component';
  5 +import { AuthEvents } from "./../../login";
  6 +
  7 +const htmlTemplate: string = '<tasks></tasks>';
  8 +
  9 +describe("Components", () => {
  10 + describe("Task Menu Component", () => {
  11 +
  12 + let helper: ComponentTestHelper<TasksComponent>;
  13 + let taskService = jasmine.createSpyObj("taskService", ["getAllPending"]);
  14 + let tasks = [{ id: 1 }, { id: 2 }];
  15 + taskService.getAllPending = jasmine.createSpy("getAllPending").and.returnValue(Promise.resolve({ headers: () => { }, data: tasks }));
  16 +
  17 + beforeEach(angular.mock.module("templates"));
  18 +
  19 + beforeEach((done) => {
  20 + let cls = createClass({
  21 + template: htmlTemplate,
  22 + directives: [TasksComponent],
  23 + providers: [
  24 + helpers.createProviderToValue("TaskService", taskService)
  25 + ]
  26 + });
  27 + helper = new ComponentTestHelper<TasksComponent>(cls, done);
  28 + });
  29 +
  30 + it("load person tasks", () => {
  31 + expect(taskService.getAllPending).toHaveBeenCalled();
  32 + });
  33 + });
  34 +});
src/app/task/tasks/tasks.component.ts 0 → 100644
@@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
  1 +import { Component, Inject, provide } from "ng-forward";
  2 +import { TaskService } from "../../../lib/ng-noosfero-api/http/task.service";
  3 +
  4 +@Component({
  5 + selector: "tasks",
  6 + templateUrl: "app/task/tasks/tasks.html",
  7 + providers: [
  8 + provide('taskService', { useClass: TaskService })
  9 + ]
  10 +})
  11 +@Inject(TaskService)
  12 +export class TasksComponent {
  13 +
  14 + tasks: noosfero.Task[];
  15 + total: number;
  16 + currentPage: number;
  17 + perPage = 5;
  18 +
  19 + constructor(private taskService: TaskService) {
  20 + this.loadPage();
  21 + }
  22 +
  23 + loadPage() {
  24 + this.taskService.getAllPending({ page: this.currentPage, per_page: this.perPage }).then((result: noosfero.RestResult<noosfero.Task[]>) => {
  25 + this.total = result.headers('total');
  26 + this.tasks = result.data;
  27 + });
  28 + }
  29 +
  30 +}
src/app/task/tasks/tasks.html 0 → 100644
@@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
  1 +<h3>{{"tasks.header" | translate}}</h3>
  2 +
  3 +<task-list [tasks]="vm.tasks"></task-list>
  4 +
  5 +<uib-pagination ng-model="vm.currentPage" total-items="vm.total" class="pagination-sm center-block"
  6 + boundary-links="true" items-per-page="vm.perPage" ng-change="vm.loadPage()"
  7 + first-text="«" last-text="»" previous-text="‹" next-text="›">
  8 +</uib-pagination>
src/app/task/types/abuse-complaint/abuse-complaint.html 0 → 100644
@@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
  1 +<i class="task-icon fa fa-fw fa-user-times"></i>
  2 +<span class="requestor">{{task.requestor.name}}</span> {{"tasks.abuse_complaint.message" | translate}}
src/app/task/types/add-friend/add-friend.html 0 → 100644
@@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
  1 +<i class="task-icon fa fa-fw fa-user-plus"></i>
  2 +<span class="requestor">{{task.requestor.name}}</span> {{"tasks.add_friend.message" | translate}} <span class="target">{{task.target.name}}</span>
src/app/task/types/add-member/add-member-accept.html 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  1 +<div class="add-member-details">
  2 + <label>{{"tasks.add_member.accept.select_role" | translate}}</label>
  3 + <div class="form-group roles">
  4 + <div class="checkbox-nice" ng-repeat="role in ctrl.roles">
  5 + <input type="checkbox" id="role_{{role.name}}" (click)="ctrl.toggleSelection(role)">
  6 + <label for="role_{{role.name}}">
  7 + {{role.name}}
  8 + </label>
  9 + </div>
  10 + </div>
  11 +</div>
src/app/task/types/add-member/add-member-task-accept-component.spec.ts 0 → 100644
@@ -0,0 +1,44 @@ @@ -0,0 +1,44 @@
  1 +import { Provider, provide, Component } from 'ng-forward';
  2 +import * as helpers from "../../../../spec/helpers";
  3 +import { ComponentTestHelper, createClass } from '../../../../spec/component-test-helper';
  4 +import { AddMemberTaskAcceptComponent } from './add-member-task-accept.component';
  5 +
  6 +const htmlTemplate: string = '<add-member-task-accept [task]="ctrl.task" [confirmation-task]="ctrl.confirmationTask"></add-member-task-accept>';
  7 +
  8 +describe("Components", () => {
  9 + describe("Add Member Task Accept Component", () => {
  10 +
  11 + let helper: ComponentTestHelper<AddMemberTaskAcceptComponent>;
  12 + let roleService = jasmine.createSpyObj("roleService", ["getByProfile"]);
  13 + let roles = [{ id: 1 }, { id: 2 }];
  14 + let task = <any>{ target: { id: 5 } };
  15 + roleService.getByProfile = jasmine.createSpy("getByProfile").and.returnValue(Promise.resolve({ headers: () => { }, data: roles }));
  16 +
  17 + beforeEach(angular.mock.module("templates"));
  18 +
  19 + beforeEach((done) => {
  20 + let cls = createClass({
  21 + template: htmlTemplate,
  22 + directives: [AddMemberTaskAcceptComponent],
  23 + providers: [
  24 + helpers.createProviderToValue("RoleService", roleService)
  25 + ].concat(helpers.provideFilters("translateFilter")),
  26 + properties: { task: task, confirmationTask: task }
  27 + });
  28 + helper = new ComponentTestHelper<AddMemberTaskAcceptComponent>(cls, done);
  29 + });
  30 +
  31 + it("insert role id in roles list when toggle selection", () => {
  32 + let role = { id: 1 };
  33 + helper.component.toggleSelection(<any>role);
  34 + expect(helper.component.confirmationTask.roles).toEqual([role.id]);
  35 + });
  36 +
  37 + it("remove role id from roles list when toggle selection", () => {
  38 + let role = { id: 1 };
  39 + helper.component.confirmationTask.roles = [role.id];
  40 + helper.component.toggleSelection(<any>role);
  41 + expect(helper.component.confirmationTask.roles).toEqual([]);
  42 + });
  43 + });
  44 +});
src/app/task/types/add-member/add-member-task-accept.component.ts 0 → 100644
@@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
  1 +import { Component, Input, Inject } from "ng-forward";
  2 +import { RoleService } from "../../../../lib/ng-noosfero-api/http/role.service";
  3 +
  4 +@Component({
  5 + selector: "add-member-task-accept",
  6 + templateUrl: "app/task/types/add-member/add-member-accept.html",
  7 +})
  8 +@Inject(RoleService)
  9 +export class AddMemberTaskAcceptComponent {
  10 +
  11 + @Input() task: noosfero.Task;
  12 + @Input() confirmationTask: noosfero.AddMemberTask;
  13 + roles: noosfero.Role[];
  14 +
  15 + constructor(private roleService: RoleService) { }
  16 +
  17 + ngOnInit() {
  18 + if (!this.task.target) return;
  19 + this.confirmationTask.roles = [];
  20 + this.roleService.getByProfile(this.task.target.id).then((result: noosfero.RestResult<noosfero.Role[]>) => {
  21 + this.roles = result.data;
  22 + });
  23 + }
  24 +
  25 + toggleSelection(role: noosfero.Role) {
  26 + let index = this.confirmationTask.roles.indexOf(role.id);
  27 + if (index >= 0) {
  28 + this.confirmationTask.roles.splice(index, 1);
  29 + } else {
  30 + this.confirmationTask.roles.push(role.id);
  31 + }
  32 + }
  33 +}
src/app/task/types/add-member/add-member.html 0 → 100644
@@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
  1 +<i class="task-icon fa fa-fw fa-user-plus"></i>
  2 +<span class="requestor">{{task.requestor.name}}</span> {{"tasks.add_member.message" | translate}} <span class="target">{{task.target.name}}</span>
src/app/task/types/add-member/add-member.scss 0 → 100644
@@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
  1 +.add-member-details {
  2 + .roles {
  3 + margin-left: 40px;
  4 + }
  5 +}
src/app/task/types/create-community/create-community.html 0 → 100644
@@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
  1 +<i class="task-icon fa fa-fw fa-users"></i>
  2 +<span class="requestor">{{task.requestor.name}}</span> {{"tasks.create_community.message" | translate}} <span class="target">{{task.data.name}}</span>
src/app/task/types/default.html 0 → 100644
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 +<!-- not implemented yet -->
src/app/task/types/suggest-article/suggest-article.html 0 → 100644
@@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
  1 +<i class="task-icon fa fa-fw fa-file-text"></i>
  2 +<span class="requestor">{{task.requestor.name}}</span> {{"tasks.suggest_article.message" | translate}} <span class="target">{{task.data.article.name}}</span>
src/languages/en.json
@@ -23,6 +23,8 @@ @@ -23,6 +23,8 @@
23 "profile.others_info": "Others", 23 "profile.others_info": "Others",
24 "profile.community.title": "Community", 24 "profile.community.title": "Community",
25 "profile.person.title": "Person", 25 "profile.person.title": "Person",
  26 + "profile.image.edit": "Crop photo",
  27 + "profile.image.upload": "Upload Photo",
26 "activities.title": "Activities", 28 "activities.title": "Activities",
27 "activities.create_article.description": "has published on", 29 "activities.create_article.description": "has published on",
28 "activities.scrap.description": "wrote in its timeline", 30 "activities.scrap.description": "wrote in its timeline",
@@ -32,8 +34,15 @@ @@ -32,8 +34,15 @@
32 "auth.form.login": "Username or Email address", 34 "auth.form.login": "Username or Email address",
33 "auth.form.password": "Password", 35 "auth.form.password": "Password",
34 "auth.form.keepLoggedIn": "Keep me logged in", 36 "auth.form.keepLoggedIn": "Keep me logged in",
  37 + "auth.form.forgot_passwd": "Forgot your password?",
35 "auth.form.login_button": "Login", 38 "auth.form.login_button": "Login",
36 "auth.createAccount": "Create account", 39 "auth.createAccount": "Create account",
  40 + "forgotPasswd.send_instructions_button": "Send instructions",
  41 + "forgotPasswd.info": "Upon clicking on the button above, you'll receive an email with instructions on how to create a new password.",
  42 + "forgotPasswd.email_sent.title.info": "Change your password",
  43 + "forgotPasswd.email_sent.message.info": "Follow the instructions sent by email to change your password",
  44 + "forgotPasswd.not_found.title.error": "User not found",
  45 + "forgotPasswd.not_found.message.error": "Could not send password recovery for the user",
37 "navbar.content_viewer_actions.new_item": "New Item", 46 "navbar.content_viewer_actions.new_item": "New Item",
38 "navbar.profile_actions.new_item": "New Item", 47 "navbar.profile_actions.new_item": "New Item",
39 "navbar.content_viewer_actions.new_post": "New Post", 48 "navbar.content_viewer_actions.new_post": "New Post",
@@ -41,6 +50,8 @@ @@ -41,6 +50,8 @@
41 "navbar.profile_actions.new_discussion": "New Discussion", 50 "navbar.profile_actions.new_discussion": "New Discussion",
42 "notification.error.default.message": "Something went wrong!", 51 "notification.error.default.message": "Something went wrong!",
43 "notification.error.default.title": "Oops...", 52 "notification.error.default.title": "Oops...",
  53 + "notification.info.default.message": "",
  54 + "notification.info.default.title": "Information",
44 "notification.profile.not_found": "Page not found", 55 "notification.profile.not_found": "Page not found",
45 "notification.http_error.401.message": "Unauthorized", 56 "notification.http_error.401.message": "Unauthorized",
46 "notification.http_error.500.message": "Server error", 57 "notification.http_error.500.message": "Server error",
@@ -124,5 +135,26 @@ @@ -124,5 +135,26 @@
124 "messages.invalid.maxlength": "This field is too long", 135 "messages.invalid.maxlength": "This field is too long",
125 "messages.invalid.minlength": "This field is too short", 136 "messages.invalid.minlength": "This field is too short",
126 "messages.invalid.email": "This needs to be a valid email", 137 "messages.invalid.email": "This needs to be a valid email",
127 - "messages.invalid.passwordMatch": "Your passwords did not match" 138 + "messages.invalid.passwordMatch": "Your passwords did not match",
  139 + "tasks.menu.all": "All tasks",
  140 + "tasks.menu.all": "See all tasks",
  141 + "tasks.menu.header": "You have {tasks, plural, one{one pending task} other{# pending tasks}}",
  142 + "tasks.actions.accept": "Accept",
  143 + "tasks.actions.reject": "Reject",
  144 + "tasks.header": "Tasks",
  145 + "tasks.actions.accept.confirmation.title": "Confirm task acceptance?",
  146 + "tasks.actions.reject.confirmation.title": "Confirm task rejection?",
  147 + "tasks.actions.confirmation.yes": "Yes",
  148 + "tasks.actions.confirmation.cancel": "Cancel",
  149 + "tasks.actions.accept.title": "Good job!",
  150 + "tasks.actions.reject.title": "Good job!",
  151 + "tasks.actions.accept.message": "Task Accepted",
  152 + "tasks.actions.reject.message": "Task Rejected",
  153 + "tasks.actions.reject.explanation.label": "Rejection explanation",
  154 + "tasks.add_member.accept.select_role": "Select Roles:",
  155 + "tasks.abuse_complaint.message": "was reported due to inappropriate behavior",
  156 + "tasks.add_friend.message": "wants to be friend of",
  157 + "tasks.add_member.message": "wants to join",
  158 + "tasks.create_community.message": "wants to create a new community:",
  159 + "tasks.suggest_article.message": "suggested a new article:"
128 } 160 }
src/languages/pt.json
@@ -23,6 +23,8 @@ @@ -23,6 +23,8 @@
23 "profile.others_info": "Outras informações", 23 "profile.others_info": "Outras informações",
24 "profile.community.title": "Comunidade", 24 "profile.community.title": "Comunidade",
25 "profile.person.title": "Pessoa", 25 "profile.person.title": "Pessoa",
  26 + "profile.image.edit": "Recortar photo",
  27 + "profile.image.upload": "Enviar photo",
26 "activities.title": "Atividades", 28 "activities.title": "Atividades",
27 "activities.create_article.description": "publicou em", 29 "activities.create_article.description": "publicou em",
28 "activities.scrap.description": "escreveu em sua linha do tempo", 30 "activities.scrap.description": "escreveu em sua linha do tempo",
@@ -32,8 +34,15 @@ @@ -32,8 +34,15 @@
32 "auth.form.login": "Nome de usuário ou Email", 34 "auth.form.login": "Nome de usuário ou Email",
33 "auth.form.password": "Senha", 35 "auth.form.password": "Senha",
34 "auth.form.keepLoggedIn": "Continuar logado", 36 "auth.form.keepLoggedIn": "Continuar logado",
  37 + "auth.form.forgot_passwd": "Esqueceu sua senha?",
35 "auth.form.login_button": "Login", 38 "auth.form.login_button": "Login",
36 "auth.createAccount": "Criar conta", 39 "auth.createAccount": "Criar conta",
  40 + "forgotPasswd.send_instructions_button": "Send instructions",
  41 + "forgotPasswd.info": "Ao clicar no botão acima, você receberá um email com instruções para criar uma nova senha.",
  42 + "forgotPasswd.email_sent.title.info": "Altere sua senha",
  43 + "forgotPasswd.email_sent.message.info": "Siga as instruções enviadas por e-mail para alterar sua senha",
  44 + "forgotPasswd.not_found.title.error": "Usuário não encontrado",
  45 + "forgotPasswd.not_found.message.error": "Não foi possível recuperar a senha deste usuário",
37 "navbar.content_viewer_actions.new_item": "Novo Item", 46 "navbar.content_viewer_actions.new_item": "Novo Item",
38 "navbar.profile_actions.new_item": "Novo Item", 47 "navbar.profile_actions.new_item": "Novo Item",
39 "navbar.content_viewer_actions.new_post": "Novo Post", 48 "navbar.content_viewer_actions.new_post": "Novo Post",
@@ -41,6 +50,8 @@ @@ -41,6 +50,8 @@
41 "navbar.profile_actions.new_discussion": "Nova Discussão", 50 "navbar.profile_actions.new_discussion": "Nova Discussão",
42 "notification.error.default.message": "Algo deu errado!", 51 "notification.error.default.message": "Algo deu errado!",
43 "notification.error.default.title": "Oops...", 52 "notification.error.default.title": "Oops...",
  53 + "notification.info.default.message": "",
  54 + "notification.info.default.title": "Informação",
44 "notification.profile.not_found": "Página não encontrada", 55 "notification.profile.not_found": "Página não encontrada",
45 "notification.http_error.401.message": "Não autorizado", 56 "notification.http_error.401.message": "Não autorizado",
46 "notification.http_error.500.message": "Erro no servidor", 57 "notification.http_error.500.message": "Erro no servidor",
@@ -127,5 +138,26 @@ @@ -127,5 +138,26 @@
127 "messages.invalid.maxlength": "O valor é muito longo", 138 "messages.invalid.maxlength": "O valor é muito longo",
128 "messages.invalid.minlength": "O valor é muito curto", 139 "messages.invalid.minlength": "O valor é muito curto",
129 "messages.invalid.email": "Informe um email válido", 140 "messages.invalid.email": "Informe um email válido",
130 - "messages.invalid.passwordMatch": "As senhas não coincidem" 141 + "messages.invalid.passwordMatch": "As senhas não coincidem",
  142 + "tasks.menu.all": "Todas as tarefas",
  143 + "tasks.menu.all": "Veja todas as tarefas",
  144 + "tasks.menu.header": "Você tem {tasks, plural, one{uma tarefa pendente} other{# tarefas pendentes}}",
  145 + "tasks.actions.accept": "Aceitar",
  146 + "tasks.actions.reject": "Rejeitar",
  147 + "tasks.header": "Tarefas",
  148 + "tasks.actions.accept.confirmation.title": "Confirmar aprovação da tarefa?",
  149 + "tasks.actions.reject.confirmation.title": "Confirmar reprovação da tarefa?",
  150 + "tasks.actions.confirmation.yes": "Sim",
  151 + "tasks.actions.confirmation.cancel": "Cancelar",
  152 + "tasks.actions.accept.title": "Bom trabalho!",
  153 + "tasks.actions.reject.title": "Bom trabalho!",
  154 + "tasks.actions.accept.message": "Tarefa Aceita",
  155 + "tasks.actions.reject.message": "Tarefa Rejeitada",
  156 + "tasks.actions.reject.explanation.label": "Motivo da rejeição",
  157 + "tasks.add_member.accept.select_role": "Selecionar papéis:",
  158 + "tasks.abuse_complaint.message": "foi notificado por comportamento inadequado",
  159 + "tasks.add_friend.message": "quer ser amigo de",
  160 + "tasks.add_member.message": "quer entrar em",
  161 + "tasks.create_community.message": "quer criar uma nova comunidade:",
  162 + "tasks.suggest_article.message": "sugeriu um novo artigo:"
131 } 163 }
src/lib/ng-noosfero-api/http/password.service.spec.ts 0 → 100644
@@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
  1 +import { PasswordService } from "./password.service";
  2 +
  3 +describe("Services", () => {
  4 +
  5 + describe("Password Service", () => {
  6 +
  7 + let $httpBackend: ng.IHttpBackendService;
  8 + let passwordService: PasswordService;
  9 + let $rootScope: ng.IRootScopeService;
  10 + let data: any;
  11 +
  12 + beforeEach(angular.mock.module("main", ($translateProvider: angular.translate.ITranslateProvider) => {
  13 + $translateProvider.translations('en', {});
  14 + }));
  15 +
  16 + beforeEach(inject((_$httpBackend_: ng.IHttpBackendService,
  17 +_PasswordService_: PasswordService, _$rootScope_: ng.IRootScopeService) => {
  18 + $httpBackend = _$httpBackend_;
  19 + passwordService = _PasswordService_;
  20 + $rootScope = _$rootScope_;
  21 + }));
  22 +
  23 + describe("Succesfull request", () => {
  24 +
  25 + it("should change user password", (done) => {
  26 + data = {
  27 + code: '1234567890',
  28 + password: 'test',
  29 + password_confirmation: 'test'
  30 + };
  31 +
  32 + $httpBackend.expectPATCH(`/api/v1/new_password?code=${data.code}&password=${data.password}&password_confirmation=${data.password_confirmation}`).respond(201, [{ login: "test" }]);
  33 + passwordService.new_password('1234567890', 'test', 'test').then((response: restangular.IResponse) => {
  34 + expect(response.data[0].login).toEqual("test");
  35 + done();
  36 + });
  37 + $httpBackend.flush();
  38 + });
  39 + });
  40 + });
  41 +});
src/lib/ng-noosfero-api/http/password.service.ts 0 → 100644
@@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
  1 +import { Injectable, Inject } from "ng-forward";
  2 +import { RestangularService } from "./restangular_service";
  3 +
  4 +@Injectable()
  5 +@Inject("Restangular")
  6 +export class PasswordService {
  7 + constructor(private Restangular: restangular.IService) {
  8 + this.Restangular = Restangular;
  9 + }
  10 +
  11 + new_password(code: string, password: string, password_confirmation: string): ng.IPromise<noosfero.RestResult<noosfero.User>> {
  12 + return this.Restangular.all("").customOperation("patch", "new_password", {code: code, password: password, password_confirmation: password_confirmation });
  13 + }
  14 +}
src/lib/ng-noosfero-api/http/person.service.ts
1 import { Injectable, Inject } from "ng-forward"; 1 import { Injectable, Inject } from "ng-forward";
2 -import {RestangularService} from "./restangular_service";  
3 -import {ProfileService} from "./profile.service"; 2 +import { RestangularService } from "./restangular_service";
  3 +import { ProfileService } from "./profile.service";
4 4
5 @Injectable() 5 @Injectable()
6 @Inject("Restangular", "$q", "$log", ProfileService) 6 @Inject("Restangular", "$q", "$log", ProfileService)
@@ -28,4 +28,19 @@ export class PersonService extends RestangularService&lt;noosfero.Person&gt; { @@ -28,4 +28,19 @@ export class PersonService extends RestangularService&lt;noosfero.Person&gt; {
28 p.catch(this.getHandleErrorFunction<noosfero.RestResult<any>>(deferred)); 28 p.catch(this.getHandleErrorFunction<noosfero.RestResult<any>>(deferred));
29 return deferred.promise; 29 return deferred.promise;
30 } 30 }
  31 +
  32 + uploadImage(profile: noosfero.Profile, base64ImageJson: any) {
  33 + let headers = { 'Content-Type': 'application/json' };
  34 + let deferred = this.$q.defer<noosfero.RestResult<noosfero.Profile>>();
  35 + // TODO dynamically copy the selected attributes to update
  36 + let attributesToUpdate: any = {
  37 + person: { image_builder: base64ImageJson }
  38 + };
  39 + let restRequest: ng.IPromise<noosfero.RestResult<any>> =
  40 + this.getElement(profile.id).customPOST(attributesToUpdate, null, null, headers);
  41 + restRequest.then(this.getHandleSuccessFunction(deferred))
  42 + .catch(this.getHandleErrorFunction(deferred));
  43 + return deferred.promise;
  44 + }
  45 +
31 } 46 }
src/lib/ng-noosfero-api/http/role.service.spec.ts 0 → 100644
@@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
  1 +import { RoleService } from "./role.service";
  2 +
  3 +describe("Services", () => {
  4 +
  5 + describe("Role Service", () => {
  6 +
  7 + let $httpBackend: ng.IHttpBackendService;
  8 + let roleService: RoleService;
  9 +
  10 + beforeEach(angular.mock.module("main", ($translateProvider: angular.translate.ITranslateProvider) => {
  11 + $translateProvider.translations('en', {});
  12 + }));
  13 +
  14 + beforeEach(inject((_$httpBackend_: ng.IHttpBackendService, _RoleService_: RoleService) => {
  15 + $httpBackend = _$httpBackend_;
  16 + roleService = _RoleService_;
  17 + }));
  18 +
  19 +
  20 + describe("Succesfull requests", () => {
  21 +
  22 + it("list organization roles", (done) => {
  23 + $httpBackend.expectGET(`/api/v1/profiles/1/roles`).respond(200, { roles: [{ id: 1 }] });
  24 + roleService.getByProfile(1).then((result: noosfero.RestResult<noosfero.Role[]>) => {
  25 + expect(result.data).toEqual([{ id: 1 }]);
  26 + done();
  27 + });
  28 + $httpBackend.flush();
  29 + });
  30 + });
  31 +
  32 + });
  33 +});
src/lib/ng-noosfero-api/http/role.service.ts 0 → 100644
@@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
  1 +import { Injectable, Inject } from "ng-forward";
  2 +import { RestangularService } from "./restangular_service";
  3 +
  4 +@Injectable()
  5 +@Inject("Restangular", "$q", "$log")
  6 +export class RoleService extends RestangularService<noosfero.Role> {
  7 +
  8 + constructor(Restangular: restangular.IService, $q: ng.IQService, $log: ng.ILogService) {
  9 + super(Restangular, $q, $log);
  10 + }
  11 +
  12 + getResourcePath() {
  13 + return "roles";
  14 + }
  15 +
  16 + getDataKeys() {
  17 + return {
  18 + singular: 'role',
  19 + plural: 'roles'
  20 + };
  21 + }
  22 +
  23 + getByProfile(profileId: number, params: any = {}) {
  24 + return this.list(this.restangularService.one("profiles", profileId), params);
  25 + }
  26 +
  27 +}
src/lib/ng-noosfero-api/http/task.service.spec.ts 0 → 100644
@@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
  1 +import { TaskService } from "./task.service";
  2 +
  3 +
  4 +describe("Services", () => {
  5 +
  6 + describe("Task Service", () => {
  7 +
  8 + let $httpBackend: ng.IHttpBackendService;
  9 + let taskService: TaskService;
  10 +
  11 + beforeEach(angular.mock.module("main", ($translateProvider: angular.translate.ITranslateProvider) => {
  12 + $translateProvider.translations('en', {});
  13 + }));
  14 +
  15 + beforeEach(inject((_$httpBackend_: ng.IHttpBackendService, _TaskService_: TaskService) => {
  16 + $httpBackend = _$httpBackend_;
  17 + taskService = _TaskService_;
  18 + }));
  19 +
  20 +
  21 + describe("Succesfull requests", () => {
  22 +
  23 + it("list pending tasks", (done) => {
  24 + $httpBackend.expectGET(`/api/v1/tasks?all_pending=true&status=1`).respond(200, { tasks: [{ id: 1 }] });
  25 + taskService.getAllPending().then((result: noosfero.RestResult<noosfero.Task[]>) => {
  26 + expect(result.data).toEqual([{ id: 1 }]);
  27 + done();
  28 + });
  29 + $httpBackend.flush();
  30 + });
  31 +
  32 + it("finish a task", (done) => {
  33 + let taskId = 1;
  34 + let task: noosfero.Task = <any>{ id: taskId };
  35 + $httpBackend.expectPUT(`/api/v1/tasks/${taskId}/finish`).respond(200, { task: { id: taskId } });
  36 + taskService.finishTask(task).then((result: noosfero.RestResult<noosfero.Task>) => {
  37 + expect(result.data).toEqual({ id: 1 });
  38 + done();
  39 + });
  40 + $httpBackend.flush();
  41 + });
  42 +
  43 + it("cancel a task", (done) => {
  44 + let taskId = 1;
  45 + let task: noosfero.Task = <any>{ id: taskId };
  46 + $httpBackend.expectPUT(`/api/v1/tasks/${taskId}/cancel`).respond(200, { task: { id: taskId } });
  47 + taskService.cancelTask(task).then((result: noosfero.RestResult<noosfero.Task>) => {
  48 + expect(result.data).toEqual({ id: 1 });
  49 + done();
  50 + });
  51 + $httpBackend.flush();
  52 + });
  53 + });
  54 +
  55 + });
  56 +});
src/lib/ng-noosfero-api/http/task.service.ts 0 → 100644
@@ -0,0 +1,46 @@ @@ -0,0 +1,46 @@
  1 +import { Injectable, Inject } from "ng-forward";
  2 +import { RestangularService } from "./restangular_service";
  3 +
  4 +@Injectable()
  5 +@Inject("Restangular", "$q", "$log")
  6 +export class TaskService extends RestangularService<noosfero.Task> {
  7 +
  8 + constructor(Restangular: restangular.IService, $q: ng.IQService, $log: ng.ILogService) {
  9 + super(Restangular, $q, $log);
  10 + }
  11 +
  12 + getResourcePath() {
  13 + return "tasks";
  14 + }
  15 +
  16 + getDataKeys() {
  17 + return {
  18 + singular: 'task',
  19 + plural: 'tasks'
  20 + };
  21 + }
  22 +
  23 + getAllPending(params: any = {}) {
  24 + params['all_pending'] = true;
  25 + params['status'] = 1;
  26 + return this.list(null, params);
  27 + }
  28 +
  29 + finishTask(task: noosfero.Task) {
  30 + return this.closeTask(task, "finish");
  31 + }
  32 +
  33 + cancelTask(task: noosfero.Task) {
  34 + return this.closeTask(task, "cancel");
  35 + }
  36 +
  37 + closeTask(task: noosfero.Task, action: string) {
  38 + let element = this.getElement(task.id);
  39 + delete task.id;
  40 + let put = element.customPUT({ task: task }, action);
  41 + let deferred = this.$q.defer<noosfero.RestResult<noosfero.Task>>();
  42 + put.then(this.getHandleSuccessFunction<noosfero.RestResult<noosfero.Task>>(deferred));
  43 + put.catch(this.getHandleErrorFunction<noosfero.RestResult<noosfero.Task>>(deferred));
  44 + return deferred.promise;
  45 + }
  46 +}
src/lib/ng-noosfero-api/interfaces/add_member_task.ts 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  1 +namespace noosfero {
  2 + /**
  3 + * @ngdoc interface
  4 + * @name noosfero.AddMemberTask
  5 + * @description
  6 + * A representation of an AddMemberTask in Noosfero.
  7 + */
  8 + export interface AddMemberTask extends Task {
  9 + roles: number[];
  10 + }
  11 +}
src/lib/ng-noosfero-api/interfaces/role.ts 0 → 100644
@@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
  1 +namespace noosfero {
  2 + /**
  3 + * @ngdoc interface
  4 + * @name noosfero.Role
  5 + * @description
  6 + * A representation of a Role in Noosfero.
  7 + */
  8 + export interface Role extends RestModel {
  9 + name: string;
  10 + key: string;
  11 + }
  12 +}
src/lib/ng-noosfero-api/interfaces/task.ts 0 → 100644
@@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
  1 +namespace noosfero {
  2 + /**
  3 + * @ngdoc interface
  4 + * @name noosfero.Task
  5 + * @description
  6 + * A representation of a Task in Noosfero.
  7 + */
  8 + export interface Task extends RestModel {
  9 + type: string;
  10 + accept_details: boolean;
  11 + reject_details: boolean;
  12 + target: noosfero.Profile | noosfero.Environment;
  13 + }
  14 +}
src/spec/mocks.ts
@@ -237,11 +237,16 @@ export var mocks: any = { @@ -237,11 +237,16 @@ export var mocks: any = {
237 } 237 }
238 }, 238 },
239 promiseResultTemplate: (response?: {}) => { 239 promiseResultTemplate: (response?: {}) => {
240 -  
241 - return {  
242 - then: (func?: (response: any) => void) => {  
243 - if (func) { return func(response); } 240 + let callback = (func?: (response: any) => any) => {
  241 + if (func) {
  242 + let ret = func(response);
  243 + if (ret && typeof ret.then === "function") return ret;
244 } 244 }
  245 + return mocks.promiseResultTemplate(response);
  246 + };
  247 + return {
  248 + then: callback,
  249 + finally: callback
245 }; 250 };
246 }, 251 },
247 $log: { 252 $log: {
@@ -252,6 +257,11 @@ export var mocks: any = { @@ -252,6 +257,11 @@ export var mocks: any = {
252 changeLanguage: (lang: string) => { }, 257 changeLanguage: (lang: string) => { },
253 translate: (text: string) => { return text; } 258 translate: (text: string) => { return text; }
254 }, 259 },
  260 + passwordService: {
  261 + new_password: (param: any) => {
  262 + return Promise.resolve({ status: 201 });
  263 + }
  264 + },
255 notificationService: { 265 notificationService: {
256 success: () => { }, 266 success: () => { },
257 confirmation: () => { }, 267 confirmation: () => { },