Просмотр исходного кода

Merge pull request #1688 from weseek/reactify/upload-image-file

Reactify/upload image file
itizawa 6 лет назад
Родитель
Сommit
97a6b72ee6

+ 1 - 0
package.json

@@ -75,6 +75,7 @@
     "archiver": "^3.1.1",
     "array.prototype.flatmap": "^1.2.2",
     "async": "^3.0.1",
+    "async-canvas-to-blob": "^1.0.3",
     "aws-sdk": "^2.88.0",
     "axios": "^0.19.0",
     "body-parser": "^1.18.2",

+ 16 - 14
src/client/js/components/Me/ImageCropModal.jsx

@@ -1,5 +1,8 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+import loggerFactory from '@alias/logger';
+import canvasToBlob from 'async-canvas-to-blob';
+
 import Modal from 'react-bootstrap/es/Modal';
 import Button from 'react-bootstrap/es/Button';
 import { withTranslation } from 'react-i18next';
@@ -7,6 +10,9 @@ import ReactCrop from 'react-image-crop';
 import AppContainer from '../../services/AppContainer';
 import { createSubscribedElement } from '../UnstatedUtils';
 import 'react-image-crop/dist/ReactCrop.css';
+import { toastError } from '../../util/apiNotification';
+
+const logger = loggerFactory('growi:ImageCropModal');
 
 class ImageCropModal extends React.Component {
 
@@ -42,25 +48,21 @@ class ImageCropModal extends React.Component {
     canvas.height = crop.height;
     const ctx = canvas.getContext('2d');
     ctx.drawImage(image, crop.x * scaleX, crop.y * scaleY, crop.width * scaleX, crop.height * scaleY, 0, 0, crop.width, crop.height);
-    return new Promise((resolve, reject) => {
-      canvas.toBlob((blob) => {
-        if (!blob) {
-          reject(new Error('Canvas is empty'));
-          return;
-        }
-        blob.name = fileName;
-        window.URL.revokeObjectURL(this.fileUrl);
-        this.fileUrl = window.URL.createObjectURL(blob);
-        resolve(this.fileUrl);
-      }, 'image/jpeg');
-    });
+    try {
+      const blob = await canvasToBlob(canvas);
+      return blob;
+    }
+    catch (err) {
+      logger.error(err);
+      toastError(new Error('Failed to draw image'));
+    }
   }
 
   async crop() {
     // crop immages
     if (this.state.imageRef && this.state.crop.width && this.state.crop.height) {
-      const croppedImageUrl = await this.getCroppedImg(this.state.imageRef, this.state.crop, '/images/icons/user');
-      this.props.onCropCompleted(croppedImageUrl);
+      const croppedImage = await this.getCroppedImg(this.state.imageRef, this.state.crop, '/images/icons/user');
+      this.props.onCropCompleted(croppedImage);
     }
   }
 

+ 20 - 7
src/client/js/components/Me/ProfileImageSettings.jsx

@@ -23,6 +23,7 @@ class ProfileImageSettings extends React.Component {
 
     this.imageRef = null;
     this.onSelectFile = this.onSelectFile.bind(this);
+    this.onClickDeleteBtn = this.onClickDeleteBtn.bind(this);
     this.hideModal = this.hideModal.bind(this);
     this.cancelModal = this.cancelModal.bind(this);
     this.onCropCompleted = this.onCropCompleted.bind(this);
@@ -56,12 +57,14 @@ class ProfileImageSettings extends React.Component {
     }
   }
 
-  async onCropCompleted(croppedImageUrl) {
+  /**
+   * @param {object} croppedImage cropped profile image for upload
+   */
+  async onCropCompleted(croppedImage) {
     const { t, personalContainer } = this.props;
-    personalContainer.setState({ croppedImageUrl });
     try {
-      await personalContainer.uploadAttachment(croppedImageUrl);
-      toastSuccess(t('toaster.update_successed', { target: t('Upload Image') }));
+      await personalContainer.uploadAttachment(croppedImage);
+      toastSuccess(t('toaster.update_successed', { target: t('Current Image') }));
     }
     catch (err) {
       toastError(err);
@@ -69,6 +72,17 @@ class ProfileImageSettings extends React.Component {
     this.hideModal();
   }
 
+  async onClickDeleteBtn() {
+    const { t, personalContainer } = this.props;
+    try {
+      await personalContainer.deleteProfileImage();
+      toastSuccess(t('toaster.update_successed', { target: t('Current Image') }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
   showModal() {
     this.setState({ show: true });
   }
@@ -83,7 +97,7 @@ class ProfileImageSettings extends React.Component {
 
   render() {
     const { t, personalContainer } = this.props;
-    const { uploadedPictureSrc, isGravatarEnabled } = personalContainer.state;
+    const { uploadedPictureSrc, isGravatarEnabled, isUploadedPicture } = personalContainer.state;
 
     return (
       <React.Fragment>
@@ -133,8 +147,7 @@ class ProfileImageSettings extends React.Component {
               </label>
               <div className="col-sm-8">
                 {uploadedPictureSrc && (<p><img src={uploadedPictureSrc} className="picture picture-lg img-circle" id="settingUserPicture" /></p>)}
-                {/* TODO GW-1218 create apiV3 for delete image */}
-                <button type="button" className="btn btn-danger">{ t('Delete Image') }</button>
+                {isUploadedPicture && <button type="button" className="btn btn-danger" onClick={this.onClickDeleteBtn}>{ t('Delete Image') }</button>}
               </div>
             </div>
             <div className="row">

+ 27 - 3
src/client/js/services/PersonalContainer.js

@@ -5,6 +5,8 @@ import loggerFactory from '@alias/logger';
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:services:PersonalContainer');
 
+const DEFAULT_IMAGE = '/images/icons/user.svg';
+
 /**
  * Service container for personal settings page (PersonalSettings.jsx)
  * @extends {Container} unstated Container
@@ -24,6 +26,7 @@ export default class PersonalContainer extends Container {
       isEmailPublished: false,
       lang: 'en-US',
       isGravatarEnabled: false,
+      isUploadedPicture: false,
       uploadedPictureSrc: this.getUploadedPictureSrc(this.appContainer.currentUser),
       externalAccounts: [],
       isPasswordSet: false,
@@ -68,13 +71,15 @@ export default class PersonalContainer extends Container {
    */
   getUploadedPictureSrc(user) {
     if (user.image) {
+      this.setState({ isUploadedPicture: true });
       return user.image;
     }
     if (user.imageAttachment != null) {
+      this.setState({ isUploadedPicture: true });
       return user.imageAttachment.filePathProxied;
     }
 
-    return '/images/icons/user.svg';
+    return DEFAULT_IMAGE;
   }
 
   /**
@@ -182,9 +187,13 @@ export default class PersonalContainer extends Container {
   /**
    * Upload image
    */
-  async uploadAttachment() {
+  async uploadAttachment(file) {
     try {
-      // TODO GW-1218 create api
+      const formData = new FormData();
+      formData.append('file', file);
+      formData.append('_csrf', this.appContainer.csrfToken);
+      const response = await this.appContainer.apiPost('/attachments.uploadProfileImage', formData);
+      this.setState({ isUploadedPicture: true, uploadedPictureSrc: response.attachment.filePathProxied });
     }
     catch (err) {
       this.setState({ retrieveError: err });
@@ -193,4 +202,19 @@ export default class PersonalContainer extends Container {
     }
   }
 
+  /**
+   * Delete image
+   */
+  async deleteProfileImage() {
+    try {
+      await this.appContainer.apiPost('/attachments.removeProfileImage', { _csrf: this.appContainer.csrfToken });
+      this.setState({ isUploadedPicture: false, uploadedPictureSrc: DEFAULT_IMAGE });
+    }
+    catch (err) {
+      this.setState({ retrieveError: err });
+      logger.error(err);
+      throw new Error('Failed to delete profile image');
+    }
+  }
+
 }

+ 5 - 0
yarn.lock

@@ -2145,6 +2145,11 @@ astral-regex@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
 
+async-canvas-to-blob@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/async-canvas-to-blob/-/async-canvas-to-blob-1.0.3.tgz#dbea3ecdca99ecdf6d0340d645dc5342b5032be6"
+  integrity sha512-jXuowR9cJC9TzAyGv4sUh6ilOKuGUvjzJ1GAZMwgaa+q0rXO+SFVyo7GUUCp89mJ/OEVYlAT/gIx3Tlv0fChRw==
+
 async-each-series@0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/async-each-series/-/async-each-series-0.1.1.tgz#7617c1917401fd8ca4a28aadce3dbae98afeb432"