Compare View
Commits (5)
-
Upload profile photo See merge request !58
Showing
18 changed files
Show diff stats
bower.json
@@ -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", |
gulp/watch.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/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("Components", () => { | @@ -14,6 +14,7 @@ describe("Components", () => { | ||
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("Components", () => { | @@ -24,6 +25,8 @@ describe("Components", () => { | ||
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")) |
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"> |
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/main/main.component.ts
@@ -129,7 +129,7 @@ export class EnvironmentContent { | @@ -129,7 +129,7 @@ export class EnvironmentContent { | ||
129 | "angular-bind-html-compile", "angularMoment", "angular.filter", "akoenig.deckgrid", | 129 | "angular-bind-html-compile", "angularMoment", "angular.filter", "akoenig.deckgrid", |
130 | "angular-timeline", "duScroll", "oitozero.ngSweetAlert", | 130 | "angular-timeline", "duScroll", "oitozero.ngSweetAlert", |
131 | "pascalprecht.translate", "tmh.dynamicLocale", "angularLoad", | 131 | "pascalprecht.translate", "tmh.dynamicLocale", "angularLoad", |
132 | - "angular-click-outside", "ngTagCloud", "noosfero.init", "uiSwitch"] | 132 | + "angular-click-outside", "ngTagCloud", "noosfero.init", "uiSwitch", "ngFileUpload", "ngImgCrop"] |
133 | }) | 133 | }) |
134 | @StateConfig([ | 134 | @StateConfig([ |
135 | { | 135 | { |
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 "ng-forward"; | @@ -10,14 +11,16 @@ import {Inject, Input, Component} from "ng-forward"; | ||
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,54 @@ export class ProfileImageComponent { | @@ -30,12 +33,54 @@ 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 | + console.log("File selected: ", file); | ||
49 | + if (file) { | ||
50 | + this.picFile = file; | ||
51 | + this.modalInstance = this.$uibModal.open({ | ||
52 | + templateUrl: 'app/profile/image/profile-image-editor.html', | ||
53 | + controller: ProfileImageEditorComponent, | ||
54 | + controllerAs: 'ctrl', | ||
55 | + scope: this.$scope, | ||
56 | + bindToController: true, | ||
57 | + backdrop: 'static', | ||
58 | + resolve: { | ||
59 | + picFile: this.picFile, | ||
60 | + profile: this.profile, | ||
61 | + personService: this.personService | ||
62 | + } | ||
63 | + }); | ||
64 | + } | ||
65 | + } | ||
66 | + | ||
67 | + private _showCamera: boolean = false; | ||
68 | + | ||
69 | + showChange(show: boolean) { | ||
70 | + this._showCamera = show; | ||
71 | + } | ||
72 | + | ||
73 | + showCamera() { | ||
74 | + return this._showCamera; | ||
75 | + } | ||
76 | + | ||
77 | + | ||
33 | /** | 78 | /** |
34 | * @ngdoc method | 79 | * @ngdoc method |
35 | * @name ngOnInit | 80 | * @name ngOnInit |
36 | * @methodOf components.noosfero.profile-image.ProfileImage | 81 | * @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 | 82 | + * @description |
83 | + * Initializes the icon names to their corresponding values depending on the profile type passed to the controller | ||
39 | */ | 84 | */ |
40 | ngOnInit() { | 85 | ngOnInit() { |
41 | this.defaultIcon = 'fa-users'; | 86 | this.defaultIcon = 'fa-users'; |
@@ -43,5 +88,5 @@ export class ProfileImageComponent { | @@ -43,5 +88,5 @@ export class ProfileImageComponent { | ||
43 | this.defaultIcon = 'fa-user'; | 88 | this.defaultIcon = 'fa-user'; |
44 | } | 89 | } |
45 | } | 90 | } |
46 | -} | ||
47 | 91 | ||
92 | +} |
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,62 @@ | @@ -0,0 +1,62 @@ | ||
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 | + console.log("PersonServiceMock:", personServiceMock); | ||
51 | + let deferredUploadImage = $q.defer(); | ||
52 | + personServiceMock.uploadImage = jasmine.createSpy('uploadImage').and.returnValue(deferredUploadImage.promise); | ||
53 | + comp.personService = personServiceMock; | ||
54 | + comp.uploadImage(testDataUrl, imageName); | ||
55 | + deferredUploadImage.resolve(); | ||
56 | + $rootScope.$apply(); | ||
57 | + expect(comp.modalInstance.close).toHaveBeenCalled(); | ||
58 | + done(); | ||
59 | + }); | ||
60 | + | ||
61 | + }); | ||
62 | +}); |
@@ -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 | +} |
@@ -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> |
@@ -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/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", |
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", |
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<noosfero.Person> { | @@ -28,4 +28,19 @@ export class PersonService extends RestangularService<noosfero.Person> { | ||
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 | } |