소스 검색

change logo on top navigation bar fix

https://youtrack.weseek.co.jp/issue/GW-7759
- Add and apply translation to CustomizeLogoSetting
- Convert ImageCropModal to tsx
- Move ImageCropModal to common folder
- Update implementation of ImageCropModal in ProfileImageSetting and CustomizeLogoSetting
- Improve null check in attachment route
I Komang Mudana 4 년 전
부모
커밋
fa64eca0f6

+ 6 - 1
packages/app/resource/locales/en_US/admin/admin.json

@@ -201,7 +201,12 @@
     "custom_script": "Custom script",
     "custom_script": "Custom script",
     "write_java": "You can write Javascript that is applied to whole system.",
     "write_java": "You can write Javascript that is applied to whole system.",
     "reflect_change": "You need to reload the page to reflect the change.",
     "reflect_change": "You need to reload the page to reflect the change.",
-    "custom_logo" : "Custom Logo"
+    "custom_logo" : "Custom Logo",
+    "default_logo": "Default Logo",
+    "upload_logo": "Upload Logo",
+    "current_logo": "Current Logo",
+    "upload_new_logo": "Upload New Logo",
+    "delete_logo": "Delete Logo"
   },
   },
   "importer_management": {
   "importer_management": {
     "beta_warning": "This function is Beta.",
     "beta_warning": "This function is Beta.",

+ 6 - 0
packages/app/resource/locales/en_US/translation.json

@@ -1025,5 +1025,11 @@
     "select_group": "Select group",
     "select_group": "Select group",
     "belonging_to_no_group": "Could not find the groups you belong to.",
     "belonging_to_no_group": "Could not find the groups you belong to.",
     "manage_user_groups": "Manage user groups"
     "manage_user_groups": "Manage user groups"
+  },
+  "crop_image_modal": {
+    "image_crop": "Image Crop",
+    "crop": "Crop",
+    "reset": "Reset",
+    "cancel": "Cancel"
   }
   }
 }
 }

+ 6 - 1
packages/app/resource/locales/ja_JP/admin/admin.json

@@ -201,7 +201,12 @@
     "custom_script": "カスタムスクリプト",
     "custom_script": "カスタムスクリプト",
     "write_java": "システム全体に適用されるJavaScriptを記述できます。",
     "write_java": "システム全体に適用されるJavaScriptを記述できます。",
     "reflect_change": "変更の反映はページの更新が必要です。",
     "reflect_change": "変更の反映はページの更新が必要です。",
-    "custom_logo": "カスタムロゴ"
+    "custom_logo": "カスタムロゴ",
+    "default_logo": "デフォルトのロゴ",
+    "upload_logo": "ロゴをアップロード",
+    "current_logo": "現在のロゴ",
+    "upload_new_logo": "新しいロゴをアップロードする",
+    "delete_logo": "ロゴを削除"
   },
   },
   "export_management": {
   "export_management": {
     "exporting_collection_list": "エクスポート中のコレクション",
     "exporting_collection_list": "エクスポート中のコレクション",

+ 6 - 0
packages/app/resource/locales/ja_JP/translation.json

@@ -1017,5 +1017,11 @@
     "select_group": "グループを選ぶ",
     "select_group": "グループを選ぶ",
     "belonging_to_no_group": "所属しているグループが見つかりませんでした。",
     "belonging_to_no_group": "所属しているグループが見つかりませんでした。",
     "manage_user_groups": "グループ管理"
     "manage_user_groups": "グループ管理"
+  },
+  "crop_image_modal": {
+    "image_crop": "画像の切り抜き",
+    "crop": "トリム",
+    "reset": "リセット",
+    "cancel": "キャンセル"
   }
   }
 }
 }

+ 6 - 1
packages/app/resource/locales/zh_CN/admin/admin.json

@@ -211,7 +211,12 @@
     "custom_script": "定制纸条",
     "custom_script": "定制纸条",
     "write_java": "您可以编写应用于整个系统的Javascript。",
     "write_java": "您可以编写应用于整个系统的Javascript。",
     "reflect_change": "您需要重新加载页面以反映更改。",
     "reflect_change": "您需要重新加载页面以反映更改。",
-    "custom_logo": "自定义徽标"
+    "custom_logo": "自定义徽标",
+    "default_logo": "默认徽标",
+    "upload_logo": "上传徽标",
+    "current_logo": "当前标志",
+    "upload_new_logo": "上传新徽标",
+    "delete_logo": "删除徽标"
   },
   },
   "importer_management": {
   "importer_management": {
     "beta_warning": "这个函数是Beta。",
     "beta_warning": "这个函数是Beta。",

+ 6 - 0
packages/app/resource/locales/zh_CN/translation.json

@@ -1027,5 +1027,11 @@
     "select_group": "选择组别",
     "select_group": "选择组别",
     "belonging_to_no_group": "无法找到你所属的团体。",
     "belonging_to_no_group": "无法找到你所属的团体。",
     "manage_user_groups": "管理用户组"
     "manage_user_groups": "管理用户组"
+  },
+  "crop_image_modal": {
+    "image_crop": "图像裁剪",
+    "crop": "修剪",
+    "reset": "重启",
+    "cancel": "取消"
   }
   }
 }
 }

+ 21 - 13
packages/app/src/components/Admin/Customize/CustomizeLogoSetting.tsx

@@ -1,14 +1,18 @@
 import React, { FC, useState } from 'react';
 import React, { FC, useState } from 'react';
+
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 
+import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
+import {
+  toastError, toastSuccess,
+} from '~/client/util/apiNotification';
+import ImageCropModal from '~/components/Common/ImageCropModal';
 
 
-import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
+import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-import CropLogoModal from './CropLogoModal';
+
 
 
 type Props = {
 type Props = {
   adminCustomizeContainer : AdminCustomizeContainer
   adminCustomizeContainer : AdminCustomizeContainer
@@ -33,7 +37,7 @@ const CustomizeLogoSetting: FC<Props> = (props: Props) => {
   };
   };
 
 
   const onSelectFile = (e) => {
   const onSelectFile = (e) => {
-    if (e.target.files && e.target.files.length > 0) {
+    if (e.target.files != null && e.target.files.length > 0) {
       const reader = new FileReader();
       const reader = new FileReader();
       reader.addEventListener('load', () => setSrc(reader.result));
       reader.addEventListener('load', () => setSrc(reader.result));
       reader.readAsDataURL(e.target.files[0]);
       reader.readAsDataURL(e.target.files[0]);
@@ -54,7 +58,7 @@ const CustomizeLogoSetting: FC<Props> = (props: Props) => {
   const onClickDeleteBtn = async() => {
   const onClickDeleteBtn = async() => {
     try {
     try {
       await adminCustomizeContainer.deleteLogo();
       await adminCustomizeContainer.deleteLogo();
-      toastSuccess(t('toaster.update_successed', { target: t('Current Image') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.current_logo') }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
@@ -64,7 +68,7 @@ const CustomizeLogoSetting: FC<Props> = (props: Props) => {
   const onCropCompleted = async(croppedImage) => {
   const onCropCompleted = async(croppedImage) => {
     try {
     try {
       await adminCustomizeContainer.uploadAttachment(croppedImage);
       await adminCustomizeContainer.uploadAttachment(croppedImage);
-      toastSuccess(t('toaster.update_successed', { target: t('Current Image') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.current_logo') }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
@@ -92,7 +96,7 @@ const CustomizeLogoSetting: FC<Props> = (props: Props) => {
                     onChange={() => { adminCustomizeContainer.switchDefaultLogo() }}
                     onChange={() => { adminCustomizeContainer.switchDefaultLogo() }}
                   />
                   />
                   <label className="custom-control-label" htmlFor="radioDefaultLogo">
                   <label className="custom-control-label" htmlFor="radioDefaultLogo">
-                    Default Logo
+                    {t('admin:customize_setting.default_logo')}
                   </label>
                   </label>
                 </div>
                 </div>
               </h4>
               </h4>
@@ -111,22 +115,26 @@ const CustomizeLogoSetting: FC<Props> = (props: Props) => {
                     onChange={() => { adminCustomizeContainer.switchDefaultLogo() }}
                     onChange={() => { adminCustomizeContainer.switchDefaultLogo() }}
                   />
                   />
                   <label className="custom-control-label" htmlFor="radioUploadLogo">
                   <label className="custom-control-label" htmlFor="radioUploadLogo">
-                    { t('Upload Logo') }
+                    { t('admin:customize_setting.upload_logo') }
                   </label>
                   </label>
                 </div>
                 </div>
               </h4>
               </h4>
               <div className="row mb-3">
               <div className="row mb-3">
                 <label className="col-sm-4 col-12 col-form-label text-left">
                 <label className="col-sm-4 col-12 col-form-label text-left">
-                  { t('Current Logo') }
+                  { t('admin:customize_setting.current_logo') }
                 </label>
                 </label>
                 <div className="col-sm-8 col-12">
                 <div className="col-sm-8 col-12">
                   {uploadedLogoSrc && (<p><img src={uploadedLogoSrc} className="picture picture-lg " id="settingBrandLogo" width="64" /></p>)}
                   {uploadedLogoSrc && (<p><img src={uploadedLogoSrc} className="picture picture-lg " id="settingBrandLogo" width="64" /></p>)}
-                  {isUploadedLogo && <button type="button" className="btn btn-danger" onClick={onClickDeleteBtn}>{ t('Delete Logo') }</button>}
+                  {isUploadedLogo && (
+                    <button type="button" className="btn btn-danger" onClick={onClickDeleteBtn}>
+                      { t('admin:customize_setting.delete_logo') }
+                    </button>
+                  )}
                 </div>
                 </div>
               </div>
               </div>
               <div className="row">
               <div className="row">
                 <label className="col-sm-4 col-12 col-form-label text-left">
                 <label className="col-sm-4 col-12 col-form-label text-left">
-                  {t('Upload new logo')}
+                  { t('admin:customize_setting.upload_new_logo') }
                 </label>
                 </label>
                 <div className="col-sm-8 col-12">
                 <div className="col-sm-8 col-12">
                   <input type="file" onChange={onSelectFile} name="brandLogo" accept="image/*" />
                   <input type="file" onChange={onSelectFile} name="brandLogo" accept="image/*" />
@@ -138,7 +146,7 @@ const CustomizeLogoSetting: FC<Props> = (props: Props) => {
         </div>
         </div>
       </div>
       </div>
 
 
-      <CropLogoModal
+      <ImageCropModal
         show={isShow}
         show={isShow}
         src={src}
         src={src}
         onModalClose={cancelModal}
         onModalClose={cancelModal}

+ 126 - 0
packages/app/src/components/Common/ImageCropModal.tsx

@@ -0,0 +1,126 @@
+import React, {
+  FC, useCallback, useEffect, useState,
+} from 'react';
+
+import canvasToBlob from 'async-canvas-to-blob';
+import { useTranslation } from 'react-i18next';
+import ReactCrop from 'react-image-crop';
+import {
+  Modal,
+  ModalHeader,
+  ModalBody,
+  ModalFooter,
+} from 'reactstrap';
+
+import { toastError } from '~/client/util/apiNotification';
+import loggerFactory from '~/utils/logger';
+import 'react-image-crop/dist/ReactCrop.css';
+
+const logger = loggerFactory('growi:ImageCropModal');
+
+interface ICropOptions {
+  aspect: number
+  unit: string,
+  x: number
+  y: number
+  width: number,
+  height: number,
+}
+
+type CropOptions = ICropOptions | null
+
+type Props = {
+  show: boolean,
+  src: string | ArrayBuffer | null,
+  onModalClose: () => void,
+  onCropCompleted: (res: any) => void
+}
+const ImageCropModal: FC<Props> = (props: Props) => {
+
+  const {
+    show, src, onModalClose, onCropCompleted,
+  } = props;
+
+  const [imageRef, setImageRef] = useState<HTMLImageElement>();
+  const [cropOptions, setCropOtions] = useState<CropOptions>(null);
+  const { t } = useTranslation();
+  const reset = useCallback(() => {
+    if (imageRef) {
+      const size = Math.min(imageRef.width, imageRef.height);
+      setCropOtions({
+        aspect: 1,
+        unit: 'px',
+        x: imageRef.width / 2 - size / 2,
+        y: imageRef.height / 2 - size / 2,
+        width: size,
+        height: size,
+      });
+    }
+  }, [imageRef]);
+
+  useEffect(() => {
+    document.body.style.position = 'static';
+    reset();
+  }, [reset]);
+
+  const onImageLoaded = (image) => {
+    setImageRef(image);
+    reset();
+    return false;
+  };
+
+
+  const onCropChange = (crop) => {
+    setCropOtions(crop);
+  };
+
+  const getCroppedImg = async(image, crop) => {
+    const canvas = document.createElement('canvas');
+    const scaleX = image.naturalWidth / image.width;
+    const scaleY = image.naturalHeight / image.height;
+    canvas.width = crop.width;
+    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);
+    try {
+      const blob = await canvasToBlob(canvas);
+      return blob;
+    }
+    catch (err) {
+      logger.error(err);
+      toastError(new Error('Failed to draw image'));
+    }
+  };
+
+  const crop = async() => {
+    // crop immages
+    if (imageRef && cropOptions?.width && cropOptions.height) {
+      const result = await getCroppedImg(imageRef, cropOptions);
+      onCropCompleted(result);
+    }
+  };
+
+  return (
+    <Modal isOpen={show} toggle={onModalClose}>
+      <ModalHeader tag="h4" toggle={onModalClose} className="bg-info text-light">
+        {t('crop_image_modal.image_crop')}
+      </ModalHeader>
+      <ModalBody className="my-4">
+        <ReactCrop src={src} crop={cropOptions} onImageLoaded={onImageLoaded} onChange={onCropChange} />
+      </ModalBody>
+      <ModalFooter>
+        <button type="button" className="btn btn-outline-danger rounded-pill mr-auto" onClick={reset}>
+          {t('crop_image_modal.reset')}
+        </button>
+        <button type="button" className="btn btn-outline-secondary rounded-pill mr-2" onClick={onModalClose}>
+          {t('crop_image_modal.cancel')}
+        </button>
+        <button type="button" className="btn btn-outline-primary rounded-pill" onClick={crop}>
+          {t('crop_image_modal.crop')}
+        </button>
+      </ModalFooter>
+    </Modal>
+  );
+};
+
+export default ImageCropModal;

+ 0 - 124
packages/app/src/components/Me/ImageCropModal.jsx

@@ -1,124 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import canvasToBlob from 'async-canvas-to-blob';
-
-import {
-  Modal,
-  ModalHeader,
-  ModalBody,
-  ModalFooter,
-} from 'reactstrap';
-import { withTranslation } from 'react-i18next';
-import ReactCrop from 'react-image-crop';
-import loggerFactory from '~/utils/logger';
-import AppContainer from '~/client/services/AppContainer';
-import { withUnstatedContainers } from '../UnstatedUtils';
-import 'react-image-crop/dist/ReactCrop.css';
-import { toastError } from '~/client/util/apiNotification';
-
-const logger = loggerFactory('growi:ImageCropModal');
-
-class ImageCropModal extends React.Component {
-
-  // demo: https://codesandbox.io/s/72py4jlll6
-  constructor(props) {
-    super();
-    this.state = {
-      crop: null,
-      imageRef: null,
-    };
-    this.onImageLoaded = this.onImageLoaded.bind(this);
-    this.onCropChange = this.onCropChange.bind(this);
-    this.getCroppedImg = this.getCroppedImg.bind(this);
-    this.crop = this.crop.bind(this);
-    this.reset = this.reset.bind(this);
-    this.imageRef = null;
-  }
-
-  onImageLoaded(image) {
-    this.setState({ imageRef: image }, () => this.reset());
-    return false; // Return false when setting crop state in here.
-  }
-
-  onCropChange(crop) {
-    this.setState({ crop });
-  }
-
-  async getCroppedImg(image, crop, fileName) {
-    const canvas = document.createElement('canvas');
-    const scaleX = image.naturalWidth / image.width;
-    const scaleY = image.naturalHeight / image.height;
-    canvas.width = crop.width;
-    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);
-    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 croppedImage = await this.getCroppedImg(this.state.imageRef, this.state.crop, '/images/icons/user');
-      this.props.onCropCompleted(croppedImage);
-    }
-  }
-
-  reset() {
-    const size = Math.min(this.state.imageRef.width, this.state.imageRef.height);
-    this.setState({
-      crop: {
-        aspect: 1,
-        unit: 'px',
-        x: this.state.imageRef.width / 2 - size / 2,
-        y: this.state.imageRef.height / 2 - size / 2,
-        width: size,
-        height: size,
-      },
-    });
-  }
-
-  render() {
-    return (
-      <Modal isOpen={this.props.show} toggle={this.props.onModalClose}>
-        <ModalHeader tag="h4" toggle={this.props.onModalClose} className="bg-info text-light">
-          Image Crop
-        </ModalHeader>
-        <ModalBody className="my-4">
-          <ReactCrop circularCrop src={this.props.src} crop={this.state.crop} onImageLoaded={this.onImageLoaded} onChange={this.onCropChange} />
-        </ModalBody>
-        <ModalFooter>
-          <button type="button" className="btn btn-outline-danger rounded-pill mr-auto" onClick={this.reset}>
-            Reset
-          </button>
-          <button type="button" className="btn btn-outline-secondary rounded-pill mr-2" onClick={this.props.onModalClose}>
-            Cancel
-          </button>
-          <button type="button" className="btn btn-outline-primary rounded-pill" onClick={this.crop}>
-            Crop
-          </button>
-        </ModalFooter>
-      </Modal>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const ProfileImageFormWrapper = withUnstatedContainers(ImageCropModal, [AppContainer]);
-ImageCropModal.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  show: PropTypes.bool.isRequired,
-  src: PropTypes.string,
-  onModalClose: PropTypes.func.isRequired,
-  onCropCompleted: PropTypes.func.isRequired,
-};
-export default withTranslation()(ProfileImageFormWrapper);

+ 6 - 5
packages/app/src/components/Me/ProfileImageSettings.jsx

@@ -1,15 +1,16 @@
 import React from 'react';
 import React from 'react';
+
+import md5 from 'md5';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
-import md5 from 'md5';
-
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { withUnstatedContainers } from '../UnstatedUtils';
 
 
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import PersonalContainer from '~/client/services/PersonalContainer';
 import PersonalContainer from '~/client/services/PersonalContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import ImageCropModal from '~/components/Common/ImageCropModal';
+
+import { withUnstatedContainers } from '../UnstatedUtils';
 
 
-import ImageCropModal from './ImageCropModal';
 
 
 class ProfileImageSettings extends React.Component {
 class ProfileImageSettings extends React.Component {
 
 

+ 5 - 5
packages/app/src/server/routes/attachment.js

@@ -1,4 +1,5 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
+
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 /* eslint-disable no-use-before-define */
 /* eslint-disable no-use-before-define */
 
 
@@ -7,7 +8,6 @@ const logger = loggerFactory('growi:routes:attachment');
 
 
 const { serializePageSecurely } = require('../models/serializers/page-serializer');
 const { serializePageSecurely } = require('../models/serializers/page-serializer');
 const { serializeRevisionSecurely } = require('../models/serializers/revision-serializer');
 const { serializeRevisionSecurely } = require('../models/serializers/revision-serializer');
-
 const ApiResponse = require('../util/apiResponse');
 const ApiResponse = require('../util/apiResponse');
 
 
 /**
 /**
@@ -427,7 +427,7 @@ module.exports = function(crowi, app) {
     if (pageId == null && pagePath == null) {
     if (pageId == null && pagePath == null) {
       return res.json(ApiResponse.error('Either page_id or path is required.'));
       return res.json(ApiResponse.error('Either page_id or path is required.'));
     }
     }
-    if (!req.file) {
+    if (req.file == null) {
       return res.json(ApiResponse.error('File error.'));
       return res.json(ApiResponse.error('File error.'));
     }
     }
 
 
@@ -542,7 +542,7 @@ module.exports = function(crowi, app) {
    */
    */
   api.uploadProfileImage = async function(req, res) {
   api.uploadProfileImage = async function(req, res) {
     // check params
     // check params
-    if (!req.file) {
+    if (req.file == null) {
       return res.json(ApiResponse.error('File error.'));
       return res.json(ApiResponse.error('File error.'));
     }
     }
     if (!req.user) {
     if (!req.user) {
@@ -701,10 +701,10 @@ module.exports = function(crowi, app) {
 
 
   api.uploadBrandLogo = async function(req, res) {
   api.uploadBrandLogo = async function(req, res) {
     // check params
     // check params
-    if (!req.file) {
+    if (req.file == null) {
       return res.json(ApiResponse.error('File error.'));
       return res.json(ApiResponse.error('File error.'));
     }
     }
-    if (!req.user) {
+    if (req.user == null) {
       return res.json(ApiResponse.error('param "user" must be set.'));
       return res.json(ApiResponse.error('param "user" must be set.'));
     }
     }