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 @@ -
- -
- -
-
- -
- - {{ ::$ctrl.text.error_too_large }} - {{errorFile.size / 1000000|number:1}}MB: max 2MB - -
- - {{ ::$ctrl.text.wrong_file_format }} - -
-
-
-
-
- - -
- - 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