Compare View

switch
from
...
to
 
Commits (27)
Showing 74 changed files   Show diff stats
bower.json
... ... @@ -40,12 +40,19 @@
40 40 "ng-ckeditor": "^0.2.1",
41 41 "angular-tag-cloud": "^0.3.0",
42 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 47 "devDependencies": {
46 48 "angular-mocks": "~1.5.0"
47 49 },
48 50 "overrides": {
  51 + "ng-file-upload": {
  52 + "main": [
  53 + "ng-file-upload-all.js"
  54 + ]
  55 + },
49 56 "ng-ckeditor": {
50 57 "main": [
51 58 "ng-ckeditor.js",
... ...
gulp/watch.js
... ... @@ -46,6 +46,7 @@ gulp.task('watch', ['inject'], function () {
46 46 watchPaths.push(path.join(src, '/app/**/*.html'));
47 47 watchPaths.push(path.join(src, conf.paths.plugins, '/**/*.html'));
48 48 });
  49 + watchPaths.push(stylePaths);
49 50 gulp.watch(watchPaths, function(event) {
50 51 browserSync.reload(event.path);
51 52 });
... ...
src/app/environment/environment.component.ts
... ... @@ -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 36 component: SearchComponent,
37 37 name: 'main.environment.search',
38 38 views: {
... ...
src/app/index.config.ts
... ... @@ -25,13 +25,16 @@ export function noosferoModuleConfig($logProvider: ng.ILogProvider,
25 25 }
26 26  
27 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 31 $translateProvider.useStaticFilesLoader({
29 32 prefix: '/languages/',
30 33 suffix: '.json'
31 34 });
32 35 $translateProvider.addInterpolation('$translateMessageFormatInterpolation');
33 36 $translateProvider.useMissingTranslationHandlerLog();
34   - $translateProvider.preferredLanguage('en');
  37 + $translateProvider.preferredLanguage(defaultLanguage);
35 38 $translateProvider.useSanitizeValueStrategy('escape');
36 39 tmhDynamicLocaleProvider.localeLocationPattern('bower_components/angular-i18n/angular-locale_{{locale}}.js');
37 40 tmhDynamicLocaleProvider.useCookieStorage();
... ...
src/app/known-events.ts
1 1 import { EventsHubKnownEventNames } from './shared/services/events-hub.service';
2 2  
3   -export class NoosferoKnownEvents implements EventsHubKnownEventNames {
  3 +export class NoosferoKnownEvents implements EventsHubKnownEventNames {
4 4 IMAGE_PROFILE_UPDATED: string = 'IMAGE_PROFILE_UPDATED';
5 5 PROFILE_INFO_UPDATED: string = 'PROFILE_INFO_UPDATED';
6 6 ARTICLE_UPDATED: string = 'ARTICLE_UPDATED';
  7 + TASK_CLOSED: string = 'TASK_CLOSED';
7 8  
8 9 constructor() {
9 10 }
... ... @@ -11,4 +12,4 @@ export class NoosferoKnownEvents implements EventsHubKnownEventNames {
11 12 getNames() {
12 13 return Object.getOwnPropertyNames(this);
13 14 }
14   -}
15 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 6 import * as helpers from "./../../../../spec/helpers";
7 7  
... ... @@ -14,6 +14,7 @@ describe(&quot;Components&quot;, () =&gt; {
14 14 describe("Profile Image Block Component", () => {
15 15  
16 16 beforeEach(angular.mock.module("templates"));
  17 + let personService = jasmine.createSpyObj("personService", ["upload"]);
17 18  
18 19 let profileService = jasmine.createSpyObj("ProfileService", ["isMember", "addMember", "removeMember"]);
19 20 profileService.isMember = jasmine.createSpy("isMember").and.returnValue(Promise.resolve(false));
... ... @@ -24,6 +25,8 @@ describe(&quot;Components&quot;, () =&gt; {
24 25 directives: [ProfileImageBlockComponent],
25 26 providers: [
26 27 helpers.createProviderToValue('SessionService', helpers.mocks.sessionWithCurrentUser({})),
  28 + helpers.createProviderToValue("PersonService", personService),
  29 + helpers.createProviderToValue("$uibModal", helpers.mocks.$modal),
27 30 helpers.createProviderToValue('ProfileService', profileService),
28 31 helpers.createProviderToValue('NotificationService', helpers.mocks.notificationService)
29 32 ].concat(helpers.provideFilters("translateFilter"))
... ... @@ -31,8 +34,7 @@ describe(&quot;Components&quot;, () =&gt; {
31 34 class BlockContainerComponent {
32 35 block = { type: 'Block' };
33 36 owner = { name: 'profile-name' };
34   - constructor() {
35   - }
  37 + constructor() { }
36 38 }
37 39  
38 40 it("show image if present", () => {
... ... @@ -52,7 +54,19 @@ describe(&quot;Components&quot;, () =&gt; {
52 54 it("display button to join community", (done: Function) => {
53 55 helpers.tcb.createAsync(BlockContainerComponent).then(fixture => {
54 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 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 7 @Component({
8 8 selector: "noosfero-profile-image-block",
... ... @@ -48,4 +48,8 @@ export class ProfileImageBlockComponent {
48 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 1 <div class="center-block text-center profile-image-block">
2 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 4 </a>
5 5 <a class="settings-link" target="_self" ui-sref="main.profile.settings({profile: ctrl.owner.identifier})">{{"blocks.profile_image.control_panel" | translate}}</a>
6 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 11 </div>
10 12 </div>
... ...
src/app/layout/blocks/profile-image/profile-image-block.scss
... ... @@ -2,4 +2,22 @@
2 2 .settings-link {
3 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 45 </ul>
46 46 </li>
47 47 </ul>
48   -
49 48 <ul class="nav navbar-nav navbar-right">
50 49 <language-selector class="nav navbar-nav navbar-right"></language-selector>
51 50 </ul>
  51 + <ul class="nav navbar-nav navbar-right">
  52 + <tasks-menu class="nav navbar-nav navbar-right"></tasks-menu>
  53 + </ul>
52 54 <div ui-view="actions"></div>
53 55 <div class="nav navbar-nav search navbar-right">
54 56 <search-form></search-form>
... ...
src/app/login/auth.controller.spec.ts
1 1 import {AuthController} from "./auth.controller";
2 2 import {AuthService} from "./auth.service";
  3 +import * as helpers from "./../../spec/helpers";
3 4  
4 5 describe("Controllers", () => {
5 6  
6 7  
7 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 29 authController.credentials = credentials;
21   -
22   - // calls the authController login method
23 30 authController.login();
24   -
25   - // checks if the method login of the injected AuthService has been called
26 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 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 9 constructor(
8 10 private $log: ng.ILogService,
9 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 19 credentials: noosfero.Credentials;
  20 + username: string;
16 21  
17 22 login() {
18 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 2  
3 3 import {NoosferoRootScope, UserResponse} from "./../shared/models/interfaces";
4 4 import {SessionService} from "./session.service";
  5 +import { RestangularService } from "./../../lib/ng-noosfero-api/http/restangular_service";
5 6  
6 7 import {AuthEvents} from "./auth-events";
7 8  
8 9 @Injectable()
9   -@Inject("$http", SessionService, "$log")
  10 +@Inject("$http", SessionService, "$log", "Restangular")
10 11 export class AuthService {
11 12  
12 13 public loginSuccess: EventEmitter<noosfero.User> = new EventEmitter<noosfero.User>();
... ... @@ -15,7 +16,10 @@ export class AuthService {
15 16  
16 17 constructor(private $http: ng.IHttpService,
17 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 25 loginFromCookie() {
... ... @@ -76,4 +80,9 @@ export class AuthService {
76 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 @@
  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 18 {{"auth.form.keepLoggedIn" | translate}}
19 19 </label>
20 20 </div>
  21 + <div class="pull-right">
  22 + <a ng-click="vm.openForgotPassword()" href="">{{"auth.form.forgot_passwd" | translate}}</a>
  23 + </div>
21 24 </div>
22 25 <button type="submit" class="btn btn-default btn-block" ng-click="vm.login()">{{"auth.form.login_button" | translate}}</button>
23 26 </form>
... ...
src/app/login/login.scss
... ... @@ -10,6 +10,12 @@
10 10 margin-top: 20px;
11 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 @@
  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 @@
  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 @@
  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 40 useValue: [
41 41 'IMAGE_PROFILE_UPDATED',
42 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 4  
5 5 import { ArticleViewComponent } from "./../article/article-default-view.component";
6 6  
  7 +import { PasswordComponent } from "../login/new-password.component";
7 8 import { ProfileComponent } from "../profile/profile.component";
8 9 import { BoxesComponent } from "../layout/boxes/boxes.component";
9 10 import { BlockContentComponent } from "../layout/blocks/block-content.component";
... ... @@ -49,9 +50,11 @@ import { HtmlEditorComponent } from &quot;../shared/components/html-editor/html-edito
49 50 import { PermissionDirective } from "../shared/components/permission/permission.directive";
50 51 import { SearchComponent } from "../search/search.component";
51 52 import { SearchFormComponent } from "../search/search-form/search-form.component";
52   -
53 53 import { EVENTS_HUB_KNOW_EVENT_NAMES, EventsHubService } from "../shared/services/events-hub.service";
54 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 59 * @ngdoc controller
57 60 * @name main.MainContentComponent
... ... @@ -118,16 +121,17 @@ export class EnvironmentContent {
118 121 MembersBlockComponent, NoosferoTemplate, DateFormat, RawHTMLBlockComponent, StatisticsBlockComponent,
119 122 LoginBlockComponent, CustomContentComponent, PermissionDirective, SearchFormComponent, SearchComponent,
120 123 PersonTagsPluginInterestsBlockComponent, TagsBlockComponent, RecentActivitiesPluginActivitiesBlockComponent,
121   - ProfileImagesPluginProfileImagesBlockComponent, BlockComponent, RegisterComponent
  124 + ProfileImagesPluginProfileImagesBlockComponent, BlockComponent, RegisterComponent, TasksMenuComponent, TaskListComponent,
  125 + PasswordComponent
122 126 ].concat(plugins.mainComponents).concat(plugins.hotspots),
123   - providers: [AuthService, SessionService, NotificationService, BodyStateClassesService, RegisterService,
  127 + providers: [AuthService, SessionService, NotificationService, BodyStateClassesService,
124 128 "ngAnimate", "ngCookies", "ngStorage", "ngTouch",
125 129 "ngSanitize", "ngMessages", "ngAria", "restangular",
126 130 "ui.router", "ui.bootstrap", "toastr", "ngCkeditor",
127 131 "angular-bind-html-compile", "angularMoment", "angular.filter", "akoenig.deckgrid",
128 132 "angular-timeline", "duScroll", "oitozero.ngSweetAlert",
129 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 136 @StateConfig([
133 137 {
... ... @@ -148,6 +152,7 @@ export class EnvironmentContent {
148 152 url: '/',
149 153 component: EnvironmentComponent,
150 154 name: 'main.environment',
  155 + abstract: true,
151 156 views: {
152 157 "content": {
153 158 templateUrl: "app/environment/environment.html",
... ... @@ -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 189 url: "^/:profile",
173 190 abstract: true,
174 191 component: ProfileComponent,
... ...
src/app/profile/image/image.component.spec.ts
... ... @@ -4,48 +4,72 @@
4 4 * @description
5 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 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 18 describe("Components", () => {
18 19  
19 20 describe("Profile Image Component", () => {
20 21  
  22 + let helper: ComponentTestHelper<ProfileImageComponent>;
  23 +
21 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 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 6 * @ngdoc controller
... ... @@ -10,14 +11,16 @@ import {Inject, Input, Component} from &quot;ng-forward&quot;;
10 11 @Component({
11 12 selector: "noosfero-profile-image",
12 13 templateUrl: 'app/profile/image/image.html',
  14 + providers: [provide('personService', { useClass: PersonService })]
13 15 })
  16 +@Inject(PersonService, "$uibModal", "$scope")
14 17 export class ProfileImageComponent {
15 18  
16 19 /**
17 20 * @ngdoc property
18 21 * @name profile
19 22 * @propertyOf components.noosfero.profile-image.ProfileImage
20   - * @description
  23 + * @description
21 24 * The Noosfero {@link models.Profile} holding the image.
22 25 */
23 26 @Input() profile: noosfero.Profile;
... ... @@ -30,12 +33,53 @@ export class ProfileImageComponent {
30 33 */
31 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 78 * @ngdoc method
35 79 * @name ngOnInit
36 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 84 ngOnInit() {
41 85 this.defaultIcon = 'fa-users';
... ... @@ -43,5 +87,5 @@ export class ProfileImageComponent {
43 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 5 background-clip: padding-box;
6 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 @@
  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 @@
  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 @@
  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 6 <h2>{{vm.profile.name}}</h2>
7 7 </header>
8 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 15 </div>
14 16 </div>
15 17 </div>
... ...
src/app/profile/info/profile-info.scss 0 → 100644
... ... @@ -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 17 * @ngdoc controller
16 18 * @name profile.Profile
... ... @@ -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 109 name: 'main.profile.home',
96 110 url: "",
97 111 component: ProfileHomeComponent,
... ...
src/app/search/search.component.spec.ts
... ... @@ -8,7 +8,7 @@ describe(&quot;Components&quot;, () =&gt; {
8 8 describe("Search Component", () => {
9 9  
10 10 let helper: ComponentTestHelper<SearchComponent>;
11   - let stateParams = { query: 'query' };
  11 + let stateParams = { query: 'query', per_page: 20 };
12 12 let articleService = jasmine.createSpyObj("ArticleService", ["search"]);
13 13 let result = Promise.resolve({ data: [{ id: 1 }], headers: (param: string) => { return 1; } });
14 14 articleService.search = jasmine.createSpy("search").and.returnValue(result);
... ... @@ -30,7 +30,7 @@ describe(&quot;Components&quot;, () =&gt; {
30 30 });
31 31  
32 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 36 it("display search results", () => {
... ...
src/app/search/search.component.ts
... ... @@ -19,6 +19,7 @@ export class SearchComponent {
19 19  
20 20 constructor(private articleService: ArticleService, private $stateParams: ng.ui.IStateParamsService, private $state: ng.ui.IStateService) {
21 21 this.query = this.$stateParams['query'];
  22 + this.perPage = this.$stateParams['per_page'] || this.perPage;
22 23 this.loadPage();
23 24 }
24 25  
... ...
src/app/search/search.scss
  1 +$green-color: #6e9e7b;
  2 +
1 3 .search-results {
2 4 .summary {
3 5 color: #bbbbbb;
... ... @@ -13,10 +15,10 @@
13 15 }
14 16 .info {
15 17 .profile {
16   - color: #6e9e7b;
  18 + color: $green-color;
17 19 }
18 20 .time {
19   - color: #6e9e7b;
  21 + color: $green-color;
20 22 font-size: 12px;
21 23 }
22 24 .bullet-separator {
... ...
src/app/shared/services/events-hub.service.ts
... ... @@ -31,13 +31,13 @@ export class EventsHubService {
31 31 emitEvent(eventType: string, payload?: any) {
32 32 this.checkKnownEvent(eventType);
33 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 37 subscribeToEvent<T>(eventType: string, generatorOrNext?: ((p?: T) => void), error?: any, complete?: any) {
38 38 this.checkKnownEvent(eventType);
39 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 43 private setupEmitters() {
... ... @@ -53,4 +53,4 @@ export class EventsHubService {
53 53 }
54 54  
55 55  
56   -}
57 56 \ No newline at end of file
  57 +}
... ...
src/app/shared/services/notification.service.ts
... ... @@ -14,6 +14,8 @@ export class NotificationService {
14 14 public static DEFAULT_ERROR_TITLE = "notification.error.default.title";
15 15 public static DEFAULT_ERROR_MESSAGE = "notification.error.default.message";
16 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 20 error({
19 21 message = NotificationService.DEFAULT_ERROR_MESSAGE,
... ... @@ -40,6 +42,14 @@ export class NotificationService {
40 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 53 private showMessage({title, text, type = "success", timer = null, showConfirmButton = true, showCancelButton = false, closeOnConfirm = true}, confirmationFunction: Function = null) {
44 54 this.$log.debug("Notification message:", title, text, type, this.translatorService.currentLanguage());
45 55 this.SweetAlert.swal({
... ...
src/app/task/task-list/accept.html 0 → 100644
... ... @@ -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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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 @@
  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