Commit 5822b6f016576c75aa0ac968a11e07b242b138b9

Authored by Victor Costa
2 parents e5598799 8aa6d299
Exists in staging

Merge branch 'master' into staging

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