Compare View
Commits (27)
-
Search ui - Sass variable and PerPage $stateParam See merge request !52
-
Also change from ng-click to (click) when toggle role selection.
-
Add components that display tasks and allow accept/reject See merge request !55
-
Upload profile photo See merge request !58
-
Translate - Set browser lang like default Change the default language to `navigation.language` defined browser. This was set in `angular.config()` flow. See merge request !60
-
After clicking on "Forgot your password?", the user receives an email with a link to create a new password.
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
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("Components", () => { |
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("Components", () => { |
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("Components", () => { |
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("Components", () => { |
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 "ng-forward"; |
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 | } | ... | ... |
... | ... | @@ -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
... | ... | @@ -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 | +}); | ... | ... |
... | ... | @@ -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 | +} | ... | ... |
... | ... | @@ -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
src/app/main/main.component.ts
... | ... | @@ -4,6 +4,7 @@ import { ArticleBlogComponent } from "./../article/types/blog/blog.component"; |
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 "../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 "ng-forward"; |
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 | +}); | ... | ... |
... | ... | @@ -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 | +} | ... | ... |
... | ... | @@ -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> | ... | ... |
... | ... | @@ -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 "./config-bar.component"; |
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("Components", () => { |
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("Components", () => { |
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({ | ... | ... |
... | ... | @@ -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> | ... | ... |
... | ... | @@ -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> | ... | ... |
... | ... | @@ -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 | +}); | ... | ... |
... | ... | @@ -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 | +} | ... | ... |
... | ... | @@ -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 | +}); | ... | ... |
... | ... | @@ -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 | +} | ... | ... |
... | ... | @@ -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> | ... | ... |
... | ... | @@ -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 |