diff --git a/app/assets/stylesheets/avatars/_openproject_avatars.sass b/app/assets/stylesheets/avatars/_openproject_avatars.sass
index 67cb14f4e40a..5ebea84e003f 100644
--- a/app/assets/stylesheets/avatars/_openproject_avatars.sass
+++ b/app/assets/stylesheets/avatars/_openproject_avatars.sass
@@ -7,7 +7,8 @@
width: 64px
.avatars--local-avatar-preview
- width: 64px
+ width: 128px
+ height: 128px
border-radius: 50%
.avatars--current-gravatar
diff --git a/app/controllers/avatars/base_controller.rb b/app/controllers/avatars/base_controller.rb
index c60aa0bd614b..d549b93684ce 100644
--- a/app/controllers/avatars/base_controller.rb
+++ b/app/controllers/avatars/base_controller.rb
@@ -45,7 +45,7 @@ def service_request(type:)
service = ::Avatars::UpdateService.new @user
if type == :update
- service.replace params[:avatar]
+ service.replace params[:file]
elsif type == :destroy
service.destroy
end
diff --git a/app/views/avatars/users/_local_avatars.html.erb b/app/views/avatars/users/_local_avatars.html.erb
index 66a24e0a6abe..9146354df94c 100644
--- a/app/views/avatars/users/_local_avatars.html.erb
+++ b/app/views/avatars/users/_local_avatars.html.erb
@@ -30,6 +30,5 @@
-
-
+
diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml
index 7f0509d96715..b0d575636e42 100644
--- a/config/locales/js-en.yml
+++ b/config/locales/js-en.yml
@@ -5,6 +5,7 @@ en:
button_update: 'Update'
avatars:
label_choose_avatar: "Choose Avatar from file"
+ uploading_avatar: "Uploading your avatar."
text_upload_instructions: |
Upload your own custom avatar of 128 by 128 pixels. Larger files will be resized and cropped to match.
A preview of your avatar will be shown before uploading, once you selected an image.
diff --git a/frontend/app/components/avatars/avatar-upload-form.component.ts b/frontend/app/components/avatars/avatar-upload-form.component.ts
deleted file mode 100644
index eb8626db42a4..000000000000
--- a/frontend/app/components/avatars/avatar-upload-form.component.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-// -- copyright
-// OpenProject is a project management system.
-// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
-//
-// This program is free software; you can redistribute it and/or
-// modify it under the terms of the GNU General Public License version 3.
-//
-// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
-// Copyright (C) 2006-2013 Jean-Philippe Lang
-// Copyright (C) 2010-2013 the ChiliProject Team
-//
-// This program is free software; you can redistribute it and/or
-// modify it under the terms of the GNU General Public License
-// as published by the Free Software Foundation; either version 2
-// of the License, or (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program; if not, write to the Free Software
-// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-//
-// See doc/COPYRIGHT.rdoc for more details.
-// ++
-
-export class AvatarUploadFormController {
- // Form targets
- public form:any;
- public target: string;
- public method: string;
-
- // File
- public avatarFile: any;
- public busy:boolean = false;
-
- // Errors
- public errorFile: any;
-
- // Text
- public text:any;
-
- public constructor(public $scope:any,
- public Upload: any,
- public $timeout:any,
- public NotificationsService:any,
- public $window:any,
- public I18n:op.I18n) {
- this.text = {
- label_choose_avatar: I18n.t('js.avatars.label_choose_avatar'),
- upload_instructions: I18n.t('js.avatars.text_upload_instructions'),
- error_too_large: I18n.t('js.avatars.error_image_too_large'),
- wrong_file_format: I18n.t('js.avatars.wrong_file_format'),
- button_update: I18n.t('js.button_update'),
- preview: I18n.t('js.label_preview')
- }
- }
-
- public $onInit() {
-
- }
-
- public get isInvalid() {
- if (this.formFile.$pristine) {
- return false;
- }
-
- return this.formFile.$invalid;
- }
-
- public get formFile() {
- return this.form.avatar;
- }
-
- public uploadAvatar(evt:any) {
- evt.preventDefault();
- this.busy = true;
- this.Upload.upload({
- url: this.target,
- method: this.method,
- data: {avatar: this.avatarFile},
- }).then(() => {
- this.$timeout(() => {
- this.$window.location.reload();
- });
- }, (response:any) => {
- if (response.status > 0) {
- this.NotificationsService.addError(response.data);
- this.busy = false;
- }
- }, (evt:ProgressEvent) => {
- // Math.min is to fix IE which reports 200% sometimes
- this.avatarFile.progress = 100.0 * evt.loaded / evt.total;
- });
-
- return false;
- }
-}
-
-angular.module('openproject').component('avatarUploadForm', {
- templateUrl: '/templates/plugin-avatars/avatar-upload-form.html',
- controller: AvatarUploadFormController,
- require : {
- form : '^'
- },
- bindings: {
- target: '@',
- method: '@'
- }
-});
diff --git a/frontend/app/templates/plugin-avatars/avatar-upload-form.html b/frontend/app/templates/plugin-avatars/avatar-upload-form.html
deleted file mode 100644
index 82e7a4af8af5..000000000000
--- a/frontend/app/templates/plugin-avatars/avatar-upload-form.html
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
diff --git a/frontend/module/avatar-upload-form/avatar-upload-form.component.ts b/frontend/module/avatar-upload-form/avatar-upload-form.component.ts
new file mode 100644
index 000000000000..0d158a6402c7
--- /dev/null
+++ b/frontend/module/avatar-upload-form/avatar-upload-form.component.ts
@@ -0,0 +1,125 @@
+// -- copyright
+// OpenProject is a project management system.
+// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License version 3.
+//
+// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+// Copyright (C) 2006-2013 Jean-Philippe Lang
+// Copyright (C) 2010-2013 the ChiliProject Team
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See doc/COPYRIGHT.rdoc for more details.
+// ++
+import {Component, ElementRef, OnInit, ViewChild} from "@angular/core";
+import {I18nService} from "core-app/modules/common/i18n/i18n.service";
+import {OpenProjectFileUploadService, UploadBlob} from "core-components/api/op-file-upload/op-file-upload.service";
+import {NotificationsService} from "core-app/modules/common/notifications/notifications.service";
+import {UploadFile} from "core-components/api/op-file-upload/op-file-upload.service";
+import {ImageHelpers} from "core-app/helpers/images/resizer";
+
+@Component({
+ selector: 'avatar-upload-form',
+ templateUrl: './avatar-upload-form.html'
+})
+export class AvatarUploadFormComponent implements OnInit {
+ // Form targets
+ public form:any;
+ public target:string;
+ public method:string;
+
+ // File
+ public avatarFile:any;
+ public avatarPreviewUrl:any;
+ public busy:boolean = false;
+ public fileInvalid = false;
+
+ @ViewChild('avatarFilePicker') public avatarFilePicker:ElementRef;
+
+ // Text
+ public text = {
+ label_choose_avatar: this.I18n.t('js.avatars.label_choose_avatar'),
+ upload_instructions: this.I18n.t('js.avatars.text_upload_instructions'),
+ error_too_large: this.I18n.t('js.avatars.error_image_too_large'),
+ wrong_file_format: this.I18n.t('js.avatars.wrong_file_format'),
+ button_update: this.I18n.t('js.button_update'),
+ uploading: this.I18n.t('js.avatars.uploading_avatar'),
+ preview: this.I18n.t('js.label_preview')
+ };
+
+ public constructor(protected I18n:I18nService,
+ protected elementRef:ElementRef,
+ protected notificationsService:NotificationsService,
+ protected opFileUpload:OpenProjectFileUploadService) {
+ }
+
+ public ngOnInit() {
+ const element = this.elementRef.nativeElement;
+ this.target = element.getAttribute('target');
+ this.method = element.getAttribute('method');
+ }
+
+ public onFilePickerChanged(_evt:Event) {
+ const files:UploadFile[] = Array.from(this.avatarFilePicker.nativeElement.files);
+ if (files.length === 0) {
+ return;
+ }
+
+ const file = files[0];
+ if (['image/jpeg', 'image/png', 'image/gif'].indexOf(file.type) === -1) {
+ this.fileInvalid = true;
+ return;
+ }
+
+ ImageHelpers.resizeFile(128, file).then(([dataURL, blob]) => {
+ // Create resized file
+ blob.name = file.name;
+ this.avatarFile = blob;
+ this.avatarPreviewUrl = dataURL;
+ });
+ }
+
+ public uploadAvatar(evt:Event) {
+ evt.preventDefault();
+ this.busy = true;
+ const upload = this.opFileUpload.uploadSingle(this.target, this.avatarFile, this.method, 'text');
+ this.notificationsService.addWorkPackageUpload(this.text.uploading, [upload]);
+
+ upload[1].subscribe(
+ (evt:any) => {
+ switch (evt.type) {
+ case 0: // Sent
+ return;
+
+ case 4:
+ this.avatarFile.progress = 100;
+ this.busy = false;
+ window.location.reload();
+ return;
+
+ default:
+ // Sent or unknown event
+ return;
+ }
+ },
+ (error:any) => {
+ this.notificationsService.addError(error.error);
+ this.busy = false;
+ }
+ );
+ }
+}
diff --git a/frontend/module/avatar-upload-form/avatar-upload-form.html b/frontend/module/avatar-upload-form/avatar-upload-form.html
new file mode 100644
index 000000000000..2306b12ca4f0
--- /dev/null
+++ b/frontend/module/avatar-upload-form/avatar-upload-form.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/app/openproject-avatars-app.js b/frontend/module/main.ts
similarity index 52%
rename from frontend/app/openproject-avatars-app.js
rename to frontend/module/main.ts
index 2100d3c2db5a..4b1dd2ef7b21 100644
--- a/frontend/app/openproject-avatars-app.js
+++ b/frontend/module/main.ts
@@ -1,6 +1,6 @@
-//-- copyright
+// -- copyright
// OpenProject is a project management system.
-// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
+// Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
@@ -15,7 +15,6 @@
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
@@ -24,12 +23,40 @@
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
-//++
-var openprojectApp = angular.module('openproject');
+import {APP_INITIALIZER, Injector, NgModule} from '@angular/core';
+import {AvatarUploadFormComponent} from "./avatar-upload-form/avatar-upload-form.component";
+import {HookService} from "../../hook-service";
+import {BrowserModule} from "@angular/platform-browser";
+
+
+export function initializeAvatarsPlugin(injector:Injector) {
+ return () => {
+ const hookService = injector.get(HookService);
+ hookService.register('openProjectAngularBootstrap', () => {
+ return [
+ { tagName: 'avatar-upload-form', cls: AvatarUploadFormComponent }
+ ];
+ });
+ }
+}
+
+@NgModule({
+ imports: [
+ BrowserModule,
+ ],
+ providers: [
+ { provide: APP_INITIALIZER, useFactory: initializeAvatarsPlugin, deps: [Injector], multi: true },
+ ],
+ declarations: [
+ AvatarUploadFormComponent
+ ],
+ entryComponents: [
+ AvatarUploadFormComponent
+ ]
+})
+export class PluginModule {
+}
+
-var requireComponent = require.context('./components/', true, /^((?!\.(test|spec)).)*\.(js|ts)$/);
-requireComponent.keys().forEach(requireComponent);
-var requireTemplates = require.context('./templates/', true, /\.html$/);
-requireTemplates.keys().forEach(requireTemplates);
diff --git a/spec/features/shared_avatar_examples.rb b/spec/features/shared_avatar_examples.rb
index 9bdd91ce930e..22b8c982a3b5 100644
--- a/spec/features/shared_avatar_examples.rb
+++ b/spec/features/shared_avatar_examples.rb
@@ -47,7 +47,7 @@
expect(page).to have_no_selector('.form--fieldset-legend', text: 'GRAVATAR')
# Attach a new invalid image
- find('#avatar_file_input').set File.join(image_base_path, 'invalid.jpg')
+ find('#avatar_file_input').set File.join(image_base_path, 'invalid.txt')
# Expect error
expect(page).to have_selector('.form--label.-error')
diff --git a/spec/fixtures/invalid.jpg b/spec/fixtures/invalid.txt
similarity index 100%
rename from spec/fixtures/invalid.jpg
rename to spec/fixtures/invalid.txt