Sfoglia il codice sorgente

Merge branch 'master' into imprv/separate-ses-settings

itizawa 5 anni fa
parent
commit
8d82e05d1e
26 ha cambiato i file con 125 aggiunte e 329 eliminazioni
  1. 5 0
      CHANGES.md
  2. 1 1
      resource/locales/en_US/admin/admin.json
  3. 4 18
      resource/locales/en_US/translation.json
  4. 1 1
      resource/locales/ja_JP/admin/admin.json
  5. 4 18
      resource/locales/ja_JP/translation.json
  6. 2 2
      resource/locales/zh_CN/admin/admin.json
  7. 5 19
      resource/locales/zh_CN/translation.json
  8. 4 17
      src/client/js/components/Admin/App/AppSettingsPage.jsx
  9. 4 17
      src/client/js/components/Admin/Customize/Customize.jsx
  10. 1 1
      src/client/js/components/Admin/ImportData/GrowiArchive/ErrorViewer.jsx
  11. 4 17
      src/client/js/components/Admin/ImportDataPage.jsx
  12. 4 17
      src/client/js/components/Admin/MarkdownSetting/MarkDownSetting.jsx
  13. 4 17
      src/client/js/components/Admin/Notification/NotificationSetting.jsx
  14. 4 17
      src/client/js/components/Admin/Security/BasicSecuritySetting.jsx
  15. 4 17
      src/client/js/components/Admin/Security/GitHubSecuritySetting.jsx
  16. 4 17
      src/client/js/components/Admin/Security/GoogleSecuritySetting.jsx
  17. 4 18
      src/client/js/components/Admin/Security/LdapSecuritySetting.jsx
  18. 4 17
      src/client/js/components/Admin/Security/LocalSecuritySetting.jsx
  19. 4 17
      src/client/js/components/Admin/Security/OidcSecuritySetting.jsx
  20. 4 17
      src/client/js/components/Admin/Security/SamlSecuritySetting.jsx
  21. 5 17
      src/client/js/components/Admin/Security/SecurityManagement.jsx
  22. 4 17
      src/client/js/components/Admin/Security/TwitterSecuritySetting.jsx
  23. 3 3
      src/server/routes/apiv3/app-settings.js
  24. 16 8
      src/server/service/export.js
  25. 26 18
      src/server/service/import.js
  26. 0 1
      src/server/service/search-delegator/elasticsearch.js

+ 5 - 0
CHANGES.md

@@ -4,8 +4,13 @@
 
 
 * Feature: Create/edit linker with GUI
 * Feature: Create/edit linker with GUI
 * Improvement: Paging page histories
 * Improvement: Paging page histories
+* Improvement: Avoid using `cursor.snapshot()` in preparation for MongoDB version upgrade
+* Improvement: Allow to save "From e-mail address" only in App Settings
+* Improvement: Allow to empty "From e-mail address" in App Settings
+* Improvement: Export/Import archive data serially so as not to waste memory
 * Fix: To be able to delete attachment metadata even when the actual data does not exist
 * Fix: To be able to delete attachment metadata even when the actual data does not exist
 * Fix: Limit the attrubutes of user data for `/_api/v3/users`
 * Fix: Limit the attrubutes of user data for `/_api/v3/users`
+* Fix: Prevent XSS with SVG
 * Upgrade libs
 * Upgrade libs
     * optimize-css-assets-webpack-plugin
     * optimize-css-assets-webpack-plugin
     * terser-webpack-plugin
     * terser-webpack-plugin

+ 1 - 1
resource/locales/en_US/admin/admin.json

@@ -210,7 +210,7 @@
     "export_collections": "Export Collections",
     "export_collections": "Export Collections",
     "check_all": "Check All",
     "check_all": "Check All",
     "uncheck_all": "Uncheck All",
     "uncheck_all": "Uncheck All",
-    "desc_password_seed": "DO NOT FORGET to set current <code>PASSWORD_SEED</code> to your new GROWI system when restoring user data, or users will NOT be able to login with their password.<br><br><strong>HINT:</strong><br>The current <code>PASSWORD_SEED</code> will be stored in <code>meta.json</code> in exported ZIP.",
+    "desc_password_seed": "<p>DO NOT FORGET to set current <code>PASSWORD_SEED</code> to your new GROWI system when restoring user data, or users will NOT be able to login with their password.<br><br><strong>HINT:</strong><br>The current <code>PASSWORD_SEED</code> will be stored in <code>meta.json</code> in exported ZIP.</p>",
     "create_new_archive_data": "Create New Archive Data",
     "create_new_archive_data": "Create New Archive Data",
     "export": "Export",
     "export": "Export",
     "cancel": "Cancel",
     "cancel": "Cancel",

+ 4 - 18
resource/locales/en_US/translation.json

@@ -711,24 +711,6 @@
     "rebuild_description_1": "Click the button to rebuild index and add all page datas.",
     "rebuild_description_1": "Click the button to rebuild index and add all page datas.",
     "rebuild_description_2": "This may take a while."
     "rebuild_description_2": "This may take a while."
   },
   },
-  "export_management": {
-    "exporting_collection_list": "Exporting Collection List",
-    "exported_data_list": "Exported Archive Data List",
-    "export_collections": "Export Collections",
-    "check_all": "Check All",
-    "uncheck_all": "Uncheck All",
-    "desc_password_seed": "<p>DO NOT FORGET to set current <code>PASSWORD_SEED</code> to your new GROWI system when restoring user data, or users will NOT be able to login with their password.</p><strong>HINT:</strong><p>The current <code>PASSWORD_SEED</code> will be stored in <code>meta.json</code> in exported ZIP.</p>",
-    "create_new_archive_data": "Create New Archive Data",
-    "export": "Export",
-    "cancel": "Cancel",
-    "file": "File",
-    "growi_version": "Growi Version",
-    "collections": "Collections",
-    "exported_at": "Exported At",
-    "export_menu": "Export Menu",
-    "download": "Download",
-    "delete": "Delete"
-  },
   "login": {
   "login": {
     "Sign in error": "Login error",
     "Sign in error": "Login error",
     "Registration successful": "Registration successful",
     "Registration successful": "Registration successful",
@@ -762,5 +744,9 @@
     "complete_to_install1":"Complete to Install GROWI ! Please login as admin account.",
     "complete_to_install1":"Complete to Install GROWI ! Please login as admin account.",
     "complete_to_install2":"Complete to Install GROWI ! Please check each settings on this page first.",
     "complete_to_install2":"Complete to Install GROWI ! Please check each settings on this page first.",
     "failed_to_create_admin_user":"Failed to create admin user. {{errMessage}}"
     "failed_to_create_admin_user":"Failed to create admin user. {{errMessage}}"
+  },
+  "validation":{
+    "aws_region": "For the region, enter the AWS region name. ex):us-east-1",
+    "aws_custom_endpoint":"For the custom endpoint, specify the URL that starts with http(s)://. Also, the trailing slash is not required."
   }
   }
 }
 }

+ 1 - 1
resource/locales/ja_JP/admin/admin.json

@@ -147,7 +147,7 @@
     "export_collections": "コレクションのエクスポート",
     "export_collections": "コレクションのエクスポート",
     "check_all": "全てにチェックを付ける",
     "check_all": "全てにチェックを付ける",
     "uncheck_all": "全てからチェックを外す",
     "uncheck_all": "全てからチェックを外す",
-    "desc_password_seed": "ユーザーデータをバックアップ/リストアする場合、現在の <code>PASSWORD_SEED</code> を新しい GROWI システムにセットすることを忘れないでください。さもなくば、ユーザーがパスワードでログインできなくなります。<br><br><strong>ヒント:</strong><br>現在の <code>PASSWORD_SEED</code> は、エクスポートされる ZIP 中の <code>meta.json</code> に保存されます。",
+    "desc_password_seed": "<p>ユーザーデータをバックアップ/リストアする場合、現在の <code>PASSWORD_SEED</code> を新しい GROWI システムにセットすることを忘れないでください。さもなくば、ユーザーがパスワードでログインできなくなります。<br><br><strong>ヒント:</strong><br>現在の <code>PASSWORD_SEED</code> は、エクスポートされる ZIP 中の <code>meta.json</code> に保存されます。</p>",
     "create_new_archive_data": "アーカイブデータの新規作成",
     "create_new_archive_data": "アーカイブデータの新規作成",
     "export": "エクスポート",
     "export": "エクスポート",
     "cancel": "キャンセル",
     "cancel": "キャンセル",

+ 4 - 18
resource/locales/ja_JP/translation.json

@@ -704,24 +704,6 @@
     "rebuild_description_1": "全てのページのインデックスを削除し、作り直します。",
     "rebuild_description_1": "全てのページのインデックスを削除し、作り直します。",
     "rebuild_description_2": "この作業には数秒かかります。"
     "rebuild_description_2": "この作業には数秒かかります。"
   },
   },
-  "export_management": {
-    "exporting_collection_list": "エクスポート中のコレクション",
-    "exported_data_list": "エクスポートされたアーカイブリスト",
-    "export_collections": "コレクションのエクスポート",
-    "check_all": "全てにチェックを付ける",
-    "uncheck_all": "全てからチェックを外す",
-    "desc_password_seed": "<p>ユーザーデータをバックアップ/リストアする場合、現在の <code>PASSWORD_SEED</code> を新しい GROWI システムにセットすることを忘れないでください。さもなくば、ユーザーがパスワードでログインできなくなります。</p><strong>ヒント:</strong><p>現在の <code>PASSWORD_SEED</code> は、エクスポートされる ZIP 中の <code>meta.json</code> に保存されます。</p>",
-    "create_new_archive_data": "アーカイブデータの新規作成",
-    "export": "エクスポート",
-    "cancel": "キャンセル",
-    "file": "ファイル名",
-    "growi_version": "Growi バージョン",
-    "collections": "コレクション",
-    "exported_at": "エクスポートされた時間",
-    "export_menu": "エクスポートメニュー",
-    "download": "ダウンロード",
-    "delete": "削除"
-  },
   "login": {
   "login": {
     "Sign in error": "ログインエラー",
     "Sign in error": "ログインエラー",
     "Registration successful": "登録完了",
     "Registration successful": "登録完了",
@@ -755,5 +737,9 @@
     "complete_to_install1":"GROWI のインストールが完了しました!管理者アカウントでログインしてください。",
     "complete_to_install1":"GROWI のインストールが完了しました!管理者アカウントでログインしてください。",
     "complete_to_install2":"GROWI のインストールが完了しました!はじめに、このページで各種設定を確認してください。",
     "complete_to_install2":"GROWI のインストールが完了しました!はじめに、このページで各種設定を確認してください。",
     "failed_to_create_admin_user":"管理ユーザーの作成に失敗しました。{{errMessage}}"
     "failed_to_create_admin_user":"管理ユーザーの作成に失敗しました。{{errMessage}}"
+  },
+  "validation":{
+    "aws_region": "リージョンには、AWSリージョン名を入力してください。例: ap-northeast-1",
+    "aws_custom_endpoint": "カスタムエンドポイントは、http(s)://で始まるURLを指定してください。また、末尾の/は不要です。"
   }
   }
 }
 }

+ 2 - 2
resource/locales/zh_CN/admin/admin.json

@@ -225,7 +225,7 @@
 		"export_collections": "导出集合",
 		"export_collections": "导出集合",
 		"check_all": "全部检查",
 		"check_all": "全部检查",
 		"uncheck_all": "全部取消选中",
 		"uncheck_all": "全部取消选中",
-		"desc_password_seed": "DO NOT FORGET to set current <code>PASSWORD_SEED</code> to your new GROWI system when restoring user data, or users will NOT be able to login with their password.<br><br><strong>HINT:</strong><br>The current <code>PASSWORD_SEED</code> will be stored in <code>meta.json</code> in exported ZIP.",
+		"desc_password_seed": "<p>还原用户数据时,不要忘记将当前的<code>密码种子设置到新的GROWI系统,否则用户将无法使用其密码登录。<br><br><strong>提示:</strong><br>当前的<code>密码种子将存储在<code>meta.json格式</code>在导出的zip压缩包中。</p>",
 		"create_new_archive_data": "创建新的存档数据",
 		"create_new_archive_data": "创建新的存档数据",
 		"export": "导出",
 		"export": "导出",
 		"cancel": "取消",
 		"cancel": "取消",
@@ -236,7 +236,7 @@
 		"export_menu": "导出菜单",
 		"export_menu": "导出菜单",
 		"download": "下载",
 		"download": "下载",
 		"delete": "删除"
 		"delete": "删除"
-	},
+  },
 	"user_management": {
 	"user_management": {
 		"invite_users": "邀请新用户",
 		"invite_users": "邀请新用户",
 		"click_twice_same_checkbox": "您应该至少选中一个复选框。",
 		"click_twice_same_checkbox": "您应该至少选中一个复选框。",

+ 5 - 19
resource/locales/zh_CN/translation.json

@@ -693,24 +693,6 @@
 		"rebuild_description_1": "单击按钮以重新生成索引并添加所有页面数据。",
 		"rebuild_description_1": "单击按钮以重新生成索引并添加所有页面数据。",
 		"rebuild_description_2": "这可能需要一段时间。"
 		"rebuild_description_2": "这可能需要一段时间。"
 	},
 	},
-	"export_management": {
-		"exporting_collection_list": "正在导出集合列表",
-		"exported_data_list": "导出的存档数据列表",
-		"export_collections": "导出集合",
-		"check_all": "全部选中",
-		"uncheck_all": "全部取消选中",
-		"desc_password_seed": "还原用户数据时,不要忘记将当前的<code>密码种子设置到新的GROWI系统,否则用户将无法使用其密码登录。<br><br><strong>提示:</strong><br>当前的<code>密码种子将存储在<code>meta.json格式</code>在导出的zip压缩包中。",
-		"create_new_archive_data": "创建新的存档数据",
-		"export": "导出",
-		"cancel": "取消",
-		"file": "文件",
-		"growi_version": "Growi 版本号",
-		"collections": "收藏",
-		"exported_at": "导出在",
-		"export_menu": "导出菜单",
-		"download": "下载",
-		"delete": "删除"
-	},
 	"personal_dropdown": {
 	"personal_dropdown": {
 		"home": "家",
 		"home": "家",
 		"settings": "设置",
 		"settings": "设置",
@@ -756,5 +738,9 @@
 		"complete_to_install1": "完成安装GROWI!请以管理员帐户登录。",
 		"complete_to_install1": "完成安装GROWI!请以管理员帐户登录。",
 		"complete_to_install2": "完成安装GROWI!请先检查此页上的每个设置。",
 		"complete_to_install2": "完成安装GROWI!请先检查此页上的每个设置。",
 		"failed_to_create_admin_user": "无法创建管理用户。{{errMessage}"
 		"failed_to_create_admin_user": "无法创建管理用户。{{errMessage}"
-	}
+  },
+  "validation":{
+    "aws_region": "关于地区,请输入AWS地区名,例如:ap-east-1",
+    "aws_custom_endpoint": "关于自定义端点,请指定以http(s)://开头的URL,链接末尾不需要添加“/”"
+  }
 }
 }

+ 4 - 17
src/client/js/components/Admin/App/AppSettingsPage.jsx

@@ -1,10 +1,11 @@
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import loggerFactory from '@alias/logger';
 import loggerFactory from '@alias/logger';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 
 import AdminAppContainer from '../../../services/AdminAppContainer';
 import AdminAppContainer from '../../../services/AdminAppContainer';
 
 
@@ -12,20 +13,6 @@ import AppSettingsPageContents from './AppSettingsPageContents';
 
 
 const logger = loggerFactory('growi:appSettings');
 const logger = loggerFactory('growi:appSettings');
 
 
-function AppSettingsPageWithContainerWithSuspense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-      )}
-    >
-      <AppSettingsPageWithUnstatedContainer />
-    </Suspense>
-  );
-}
-
 let retrieveErrors = null;
 let retrieveErrors = null;
 function AppSettingsPage(props) {
 function AppSettingsPage(props) {
   if (props.adminAppContainer.state.title === props.adminAppContainer.dummyTitle) {
   if (props.adminAppContainer.state.title === props.adminAppContainer.dummyTitle) {
@@ -59,6 +46,6 @@ AppSettingsPage.propTypes = {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const AppSettingsPageWithUnstatedContainer = withUnstatedContainers(AppSettingsPage, [AdminAppContainer]);
+const AppSettingsPageWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(AppSettingsPage), [AdminAppContainer]);
 
 
-export default AppSettingsPageWithContainerWithSuspense;
+export default AppSettingsPageWithUnstatedContainer;

+ 4 - 17
src/client/js/components/Admin/Customize/Customize.jsx

@@ -1,5 +1,5 @@
 
 
-import React, { Fragment, Suspense } from 'react';
+import React, { Fragment } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import loggerFactory from '@alias/logger';
 import loggerFactory from '@alias/logger';
@@ -8,6 +8,7 @@ import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 
 import CustomizeLayoutSetting from './CustomizeLayoutSetting';
 import CustomizeLayoutSetting from './CustomizeLayoutSetting';
 import CustomizeFunctionSetting from './CustomizeFunctionSetting';
 import CustomizeFunctionSetting from './CustomizeFunctionSetting';
@@ -19,20 +20,6 @@ import CustomizeTitle from './CustomizeTitle';
 
 
 const logger = loggerFactory('growi:services:AdminCustomizePage');
 const logger = loggerFactory('growi:services:AdminCustomizePage');
 
 
-function CustomizePageWithContainerWithSusupense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-      )}
-    >
-      <CustomizePageWithUnstatedContainer />
-    </Suspense>
-  );
-}
-
 let retrieveErrors = null;
 let retrieveErrors = null;
 function Customize(props) {
 function Customize(props) {
   const { adminCustomizeContainer } = props;
   const { adminCustomizeContainer } = props;
@@ -83,10 +70,10 @@ function Customize(props) {
   );
   );
 }
 }
 
 
-const CustomizePageWithUnstatedContainer = withUnstatedContainers(Customize, [AdminCustomizeContainer]);
+const CustomizePageWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(Customize), [AdminCustomizeContainer]);
 
 
 Customize.propTypes = {
 Customize.propTypes = {
   adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
   adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
 };
 };
 
 
-export default CustomizePageWithContainerWithSusupense;
+export default CustomizePageWithUnstatedContainer;

+ 1 - 1
src/client/js/components/Admin/ImportData/GrowiArchive/ErrorViewer.jsx

@@ -20,7 +20,7 @@ class ErrorViewer extends React.Component {
     }
     }
 
 
     return (
     return (
-      <Modal isOpen={this.props.isOpen} toggle={this.props.onClose}>
+      <Modal isOpen={this.props.isOpen} toggle={this.props.onClose} size="lg">
         <ModalHeader tag="h4" toggle={this.props.onClose} className="bg-danger text-light">
         <ModalHeader tag="h4" toggle={this.props.onClose} className="bg-danger text-light">
           Errors
           Errors
         </ModalHeader>
         </ModalHeader>

+ 4 - 17
src/client/js/components/Admin/ImportDataPage.jsx

@@ -1,9 +1,10 @@
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import loggerFactory from '@alias/logger';
 import loggerFactory from '@alias/logger';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import toArrayIfNot from '../../../../lib/util/toArrayIfNot';
 import toArrayIfNot from '../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../SuspenseUtils';
 
 
 import AdminImportContainer from '../../services/AdminImportContainer';
 import AdminImportContainer from '../../services/AdminImportContainer';
 import { toastError } from '../../util/apiNotification';
 import { toastError } from '../../util/apiNotification';
@@ -12,20 +13,6 @@ import ImportDataPageContents from './ImportData/ImportDataPageContents';
 
 
 const logger = loggerFactory('growi:importer');
 const logger = loggerFactory('growi:importer');
 
 
-function ImportDataPageWithContainerWithSuspense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-      )}
-    >
-      <ImportDataPageWithUnstatedContainer />
-    </Suspense>
-  );
-}
-
 let retrieveErrors = null;
 let retrieveErrors = null;
 function ImportDataPage(props) {
 function ImportDataPage(props) {
   const { adminImportContainer } = props;
   const { adminImportContainer } = props;
@@ -60,6 +47,6 @@ ImportDataPage.propTypes = {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const ImportDataPageWithUnstatedContainer = withUnstatedContainers(ImportDataPage, [AdminImportContainer]);
+const ImportDataPageWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(ImportDataPage), [AdminImportContainer]);
 
 
-export default ImportDataPageWithContainerWithSuspense;
+export default ImportDataPageWithUnstatedContainer;

+ 4 - 17
src/client/js/components/Admin/MarkdownSetting/MarkDownSetting.jsx

@@ -1,4 +1,4 @@
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import loggerFactory from '@alias/logger';
 import loggerFactory from '@alias/logger';
@@ -6,26 +6,13 @@ import loggerFactory from '@alias/logger';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 
 import MarkDownSettingContents from './MarkDownSettingContents';
 import MarkDownSettingContents from './MarkDownSettingContents';
 import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
 import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
 
 
 const logger = loggerFactory('growi:MarkDown');
 const logger = loggerFactory('growi:MarkDown');
 
 
-function MarkdownSettingWithContainerWithSuspense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-      )}
-    >
-      <MarkdownSettingWithUnstatedContainer />
-    </Suspense>
-  );
-}
-
 let retrieveErrors = null;
 let retrieveErrors = null;
 function MarkdownSetting(props) {
 function MarkdownSetting(props) {
   const { adminMarkDownContainer } = props;
   const { adminMarkDownContainer } = props;
@@ -52,10 +39,10 @@ function MarkdownSetting(props) {
   return <MarkDownSettingContents />;
   return <MarkDownSettingContents />;
 }
 }
 
 
-const MarkdownSettingWithUnstatedContainer = withUnstatedContainers(MarkdownSetting, [AdminMarkDownContainer]);
+const MarkdownSettingWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(MarkdownSetting), [AdminMarkDownContainer]);
 
 
 MarkdownSetting.propTypes = {
 MarkdownSetting.propTypes = {
   adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
   adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 };
 };
 
 
-export default MarkdownSettingWithContainerWithSuspense;
+export default MarkdownSettingWithUnstatedContainer;

+ 4 - 17
src/client/js/components/Admin/Notification/NotificationSetting.jsx

@@ -1,4 +1,4 @@
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import loggerFactory from '@alias/logger';
 import loggerFactory from '@alias/logger';
@@ -6,6 +6,7 @@ import loggerFactory from '@alias/logger';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 
 import AdminNotificationContainer from '../../../services/AdminNotificationContainer';
 import AdminNotificationContainer from '../../../services/AdminNotificationContainer';
 
 
@@ -13,20 +14,6 @@ import NotificationSettingContents from './NotificationSettingContents';
 
 
 const logger = loggerFactory('growi:NotificationSetting');
 const logger = loggerFactory('growi:NotificationSetting');
 
 
-function NotificationSettingWithContainerWithSuspense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-      )}
-    >
-      <NotificationSettingWithUnstatedContainer />
-    </Suspense>
-  );
-}
-
 let retrieveErrors = null;
 let retrieveErrors = null;
 function NotificationSetting(props) {
 function NotificationSetting(props) {
   const { adminNotificationContainer } = props;
   const { adminNotificationContainer } = props;
@@ -52,10 +39,10 @@ function NotificationSetting(props) {
   return <NotificationSettingContents />;
   return <NotificationSettingContents />;
 }
 }
 
 
-const NotificationSettingWithUnstatedContainer = withUnstatedContainers(NotificationSetting, [AdminNotificationContainer]);
+const NotificationSettingWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(NotificationSetting), [AdminNotificationContainer]);
 
 
 NotificationSetting.propTypes = {
 NotificationSetting.propTypes = {
   adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
   adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
 };
 };
 
 
-export default NotificationSettingWithContainerWithSuspense;
+export default NotificationSettingWithUnstatedContainer;

+ 4 - 17
src/client/js/components/Admin/Security/BasicSecuritySetting.jsx

@@ -1,10 +1,11 @@
 /* eslint-disable react/no-danger */
 /* eslint-disable react/no-danger */
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 
 import AdminBasicSecurityContainer from '../../../services/AdminBasicSecurityContainer';
 import AdminBasicSecurityContainer from '../../../services/AdminBasicSecurityContainer';
 
 
@@ -43,22 +44,8 @@ BasicSecurityManagement.propTypes = {
   adminBasicSecurityContainer: PropTypes.instanceOf(AdminBasicSecurityContainer).isRequired,
   adminBasicSecurityContainer: PropTypes.instanceOf(AdminBasicSecurityContainer).isRequired,
 };
 };
 
 
-const BasicSecurityManagementWithUnstatedContainer = withUnstatedContainers(BasicSecurityManagement, [
+const BasicSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(BasicSecurityManagement), [
   AdminBasicSecurityContainer,
   AdminBasicSecurityContainer,
 ]);
 ]);
 
 
-function BasicSecurityManagementWithContainerWithSuspense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-      )}
-    >
-      <BasicSecurityManagementWithUnstatedContainer />
-    </Suspense>
-  );
-}
-
-export default BasicSecurityManagementWithContainerWithSuspense;
+export default BasicSecurityManagementWithUnstatedContainer;

+ 4 - 17
src/client/js/components/Admin/Security/GitHubSecuritySetting.jsx

@@ -1,10 +1,11 @@
 /* eslint-disable react/no-danger */
 /* eslint-disable react/no-danger */
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 
 import AdminGitHubSecurityContainer from '../../../services/AdminGitHubSecurityContainer';
 import AdminGitHubSecurityContainer from '../../../services/AdminGitHubSecurityContainer';
 
 
@@ -39,22 +40,8 @@ GitHubSecurityManagement.propTypes = {
   adminGitHubSecurityContainer: PropTypes.instanceOf(AdminGitHubSecurityContainer).isRequired,
   adminGitHubSecurityContainer: PropTypes.instanceOf(AdminGitHubSecurityContainer).isRequired,
 };
 };
 
 
-const GitHubSecurityManagementWithUnstatedContainer = withUnstatedContainers(GitHubSecurityManagement, [
+const GitHubSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(GitHubSecurityManagement), [
   AdminGitHubSecurityContainer,
   AdminGitHubSecurityContainer,
 ]);
 ]);
 
 
-function GitHubSecurityManagementWithContainerWithSuspense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-      )}
-    >
-      <GitHubSecurityManagementWithUnstatedContainer />
-    </Suspense>
-  );
-}
-
-export default GitHubSecurityManagementWithContainerWithSuspense;
+export default GitHubSecurityManagementWithUnstatedContainer;

+ 4 - 17
src/client/js/components/Admin/Security/GoogleSecuritySetting.jsx

@@ -1,10 +1,11 @@
 /* eslint-disable react/no-danger */
 /* eslint-disable react/no-danger */
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 
 import AdminGoogleSecurityContainer from '../../../services/AdminGoogleSecurityContainer';
 import AdminGoogleSecurityContainer from '../../../services/AdminGoogleSecurityContainer';
 import GoogleSecurityManagementContents from './GoogleSecuritySettingContents';
 import GoogleSecurityManagementContents from './GoogleSecuritySettingContents';
@@ -38,22 +39,8 @@ GoogleSecurityManagement.propTypes = {
   adminGoogleSecurityContainer: PropTypes.instanceOf(AdminGoogleSecurityContainer).isRequired,
   adminGoogleSecurityContainer: PropTypes.instanceOf(AdminGoogleSecurityContainer).isRequired,
 };
 };
 
 
-const GoogleSecurityManagementWithUnstatedContainer = withUnstatedContainers(GoogleSecurityManagement, [
+const GoogleSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(GoogleSecurityManagement), [
   AdminGoogleSecurityContainer,
   AdminGoogleSecurityContainer,
 ]);
 ]);
 
 
-function GoogleSecurityManagementWithContainerWithSuspense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-      )}
-    >
-      <GoogleSecurityManagementWithUnstatedContainer />
-    </Suspense>
-  );
-}
-
-export default GoogleSecurityManagementWithContainerWithSuspense;
+export default GoogleSecurityManagementWithUnstatedContainer;

+ 4 - 18
src/client/js/components/Admin/Security/LdapSecuritySetting.jsx

@@ -1,9 +1,10 @@
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 
 import AdminLdapSecurityContainer from '../../../services/AdminLdapSecurityContainer';
 import AdminLdapSecurityContainer from '../../../services/AdminLdapSecurityContainer';
 
 
@@ -37,23 +38,8 @@ LdapSecuritySetting.propTypes = {
   adminLdapSecurityContainer: PropTypes.instanceOf(AdminLdapSecurityContainer).isRequired,
   adminLdapSecurityContainer: PropTypes.instanceOf(AdminLdapSecurityContainer).isRequired,
 };
 };
 
 
-const LdapSecuritySettingWithUnstatedContainer = withUnstatedContainers(LdapSecuritySetting, [
+const LdapSecuritySettingWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(LdapSecuritySetting), [
   AdminLdapSecurityContainer,
   AdminLdapSecurityContainer,
 ]);
 ]);
 
 
-function LdapSecuritySettingWithContainerWithSuspense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-      )}
-    >
-      <LdapSecuritySettingWithUnstatedContainer />
-    </Suspense>
-  );
-}
-
-
-export default LdapSecuritySettingWithContainerWithSuspense;
+export default LdapSecuritySettingWithUnstatedContainer;

+ 4 - 17
src/client/js/components/Admin/Security/LocalSecuritySetting.jsx

@@ -1,10 +1,11 @@
 /* eslint-disable react/no-danger */
 /* eslint-disable react/no-danger */
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 
 import AdminLocalSecurityContainer from '../../../services/AdminLocalSecurityContainer';
 import AdminLocalSecurityContainer from '../../../services/AdminLocalSecurityContainer';
 
 
@@ -38,22 +39,8 @@ LocalSecuritySetting.propTypes = {
   adminLocalSecurityContainer: PropTypes.instanceOf(AdminLocalSecurityContainer).isRequired,
   adminLocalSecurityContainer: PropTypes.instanceOf(AdminLocalSecurityContainer).isRequired,
 };
 };
 
 
-const LocalSecuritySettingWithUnstatedContainer = withUnstatedContainers(LocalSecuritySetting, [
+const LocalSecuritySettingWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(LocalSecuritySetting), [
   AdminLocalSecurityContainer,
   AdminLocalSecurityContainer,
 ]);
 ]);
 
 
-function LocalSecuritySettingWithContainerWithSuspense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-)}
-    >
-      <LocalSecuritySettingWithUnstatedContainer />
-    </Suspense>
-  );
-}
-
-export default LocalSecuritySettingWithContainerWithSuspense;
+export default LocalSecuritySettingWithUnstatedContainer;

+ 4 - 17
src/client/js/components/Admin/Security/OidcSecuritySetting.jsx

@@ -1,10 +1,11 @@
 /* eslint-disable react/no-danger */
 /* eslint-disable react/no-danger */
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 
 import AdminOidcSecurityContainer from '../../../services/AdminOidcSecurityContainer';
 import AdminOidcSecurityContainer from '../../../services/AdminOidcSecurityContainer';
 
 
@@ -38,22 +39,8 @@ OidcSecurityManagement.propTypes = {
   adminOidcSecurityContainer: PropTypes.instanceOf(AdminOidcSecurityContainer).isRequired,
   adminOidcSecurityContainer: PropTypes.instanceOf(AdminOidcSecurityContainer).isRequired,
 };
 };
 
 
-const OidcSecurityManagementWithUnstatedContainer = withUnstatedContainers(OidcSecurityManagement, [
+const OidcSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(OidcSecurityManagement), [
   AdminOidcSecurityContainer,
   AdminOidcSecurityContainer,
 ]);
 ]);
 
 
-function OidcSecurityManagementWithContainerWithSuspense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-      )}
-    >
-      <OidcSecurityManagementWithUnstatedContainer />
-    </Suspense>
-  );
-}
-
-export default OidcSecurityManagementWithContainerWithSuspense;
+export default OidcSecurityManagementWithUnstatedContainer;

+ 4 - 17
src/client/js/components/Admin/Security/SamlSecuritySetting.jsx

@@ -1,5 +1,5 @@
 /* eslint-disable react/no-danger */
 /* eslint-disable react/no-danger */
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -7,6 +7,7 @@ import { toastError } from '../../../util/apiNotification';
 
 
 import AdminSamlSecurityContainer from '../../../services/AdminSamlSecurityContainer';
 import AdminSamlSecurityContainer from '../../../services/AdminSamlSecurityContainer';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 
 import SamlSecuritySettingContents from './SamlSecuritySettingContents';
 import SamlSecuritySettingContents from './SamlSecuritySettingContents';
 
 
@@ -38,22 +39,8 @@ SamlSecurityManagement.propTypes = {
   adminSamlSecurityContainer: PropTypes.instanceOf(AdminSamlSecurityContainer).isRequired,
   adminSamlSecurityContainer: PropTypes.instanceOf(AdminSamlSecurityContainer).isRequired,
 };
 };
 
 
-const SamlSecurityManagementWithUnstatedContainer = withUnstatedContainers(SamlSecurityManagement, [
+const SamlSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(SamlSecurityManagement), [
   AdminSamlSecurityContainer,
   AdminSamlSecurityContainer,
 ]);
 ]);
 
 
-function SamlSecurityManagementWithContainerWithSuspense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-      )}
-    >
-      <SamlSecurityManagementWithUnstatedContainer />
-    </Suspense>
-  );
-}
-
-export default SamlSecurityManagementWithContainerWithSuspense;
+export default SamlSecurityManagementWithUnstatedContainer;

+ 5 - 17
src/client/js/components/Admin/Security/SecurityManagement.jsx

@@ -1,9 +1,11 @@
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { toastError } from '../../../util/apiNotification';
 import { toastError } from '../../../util/apiNotification';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
+
 import AdminGeneralSecurityContainer from '../../../services/AdminGeneralSecurityContainer';
 import AdminGeneralSecurityContainer from '../../../services/AdminGeneralSecurityContainer';
 import SecurityManagementContents from './SecurityManagementContents';
 import SecurityManagementContents from './SecurityManagementContents';
 
 
@@ -38,20 +40,6 @@ SecurityManagement.propTypes = {
   adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
   adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
 };
 };
 
 
-const SecurityManagementWithUnstatedContainer = withUnstatedContainers(SecurityManagement, [AdminGeneralSecurityContainer]);
-
-function SecurityManagementWithContainerWithSuspense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-      )}
-    >
-      <SecurityManagementWithUnstatedContainer {...props} />
-    </Suspense>
-  );
-}
+const SecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(SecurityManagement), [AdminGeneralSecurityContainer]);
 
 
-export default SecurityManagementWithContainerWithSuspense;
+export default SecurityManagementWithUnstatedContainer;

+ 4 - 17
src/client/js/components/Admin/Security/TwitterSecuritySetting.jsx

@@ -1,10 +1,11 @@
 /* eslint-disable react/no-danger */
 /* eslint-disable react/no-danger */
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 
 import AdminTwitterSecurityContainer from '../../../services/AdminTwitterSecurityContainer';
 import AdminTwitterSecurityContainer from '../../../services/AdminTwitterSecurityContainer';
 
 
@@ -40,22 +41,8 @@ TwitterSecurityManagement.propTypes = {
   adminTwitterSecurityContainer: PropTypes.instanceOf(AdminTwitterSecurityContainer).isRequired,
   adminTwitterSecurityContainer: PropTypes.instanceOf(AdminTwitterSecurityContainer).isRequired,
 };
 };
 
 
-const TwitterSecurityManagementWithUnstatedContainer = withUnstatedContainers(TwitterSecurityManagement, [
+const TwitterSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(TwitterSecurityManagement), [
   AdminTwitterSecurityContainer,
   AdminTwitterSecurityContainer,
 ]);
 ]);
 
 
-function TwitterSecurityManagementWithContainerWithSuspense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-      )}
-    >
-      <TwitterSecurityManagementWithUnstatedContainer />
-    </Suspense>
-  );
-}
-
-export default TwitterSecurityManagementWithContainerWithSuspense;
+export default TwitterSecurityManagementWithUnstatedContainer;

+ 3 - 3
src/server/routes/apiv3/app-settings.js

@@ -120,7 +120,7 @@ module.exports = (crowi) => {
       body('siteUrl').trim().matches(/^(https?:\/\/[^/]+|)$/).isURL({ require_tld: false }),
       body('siteUrl').trim().matches(/^(https?:\/\/[^/]+|)$/).isURL({ require_tld: false }),
     ],
     ],
     fromAddress: [
     fromAddress: [
-      body('fromAddress').trim().isEmail(),
+      body('fromAddress').trim().if(value => value !== '').isEmail(),
     ],
     ],
     mailSetting: [
     mailSetting: [
       body('smtpHost').trim(),
       body('smtpHost').trim(),
@@ -129,8 +129,8 @@ module.exports = (crowi) => {
       body('smtpPassword').trim(),
       body('smtpPassword').trim(),
     ],
     ],
     awsSetting: [
     awsSetting: [
-      body('region').trim().matches(/^[a-z]+-[a-z]+-\d+$/).withMessage('リージョンには、AWSリージョン名を入力してください。 例: ap-northeast-1'),
-      body('customEndpoint').trim().matches(/^(https?:\/\/[^/]+|)$/).withMessage('カスタムエンドポイントは、http(s)://で始まるURLを指定してください。また、末尾の/は不要です。'),
+      body('region').trim().matches(/^[a-z]+-[a-z]+-\d+$/).withMessage((value, { req }) => req.t('validation.aws_region')),
+      body('customEndpoint').trim().matches(/^(https?:\/\/[^/]+|)$/).withMessage((value, { req }) => req.t('validation.aws_custom_endpoint')),
       body('bucket').trim(),
       body('bucket').trim(),
       body('accessKeyId').trim().matches(/^[\da-zA-Z]+$/),
       body('accessKeyId').trim().matches(/^[\da-zA-Z]+$/),
       body('secretAccessKey').trim(),
       body('secretAccessKey').trim(),

+ 16 - 8
src/server/service/export.js

@@ -50,11 +50,17 @@ class ExportService {
    * @return {object} info for zip files and whether currentProgressingStatus exists
    * @return {object} info for zip files and whether currentProgressingStatus exists
    */
    */
   async getStatus() {
   async getStatus() {
-    const zipFiles = fs.readdirSync(this.baseDir).filter((file) => { return path.extname(file) === '.zip' });
-    const zipFileStats = await Promise.all(zipFiles.map((file) => {
+    const zipFiles = fs.readdirSync(this.baseDir).filter(file => path.extname(file) === '.zip');
+
+    // process serially so as not to waste memory
+    const zipFileStats = [];
+    const parseZipFilePromises = zipFiles.map((file) => {
       const zipFile = this.getFile(file);
       const zipFile = this.getFile(file);
       return this.growiBridgeService.parseZipFile(zipFile);
       return this.growiBridgeService.parseZipFile(zipFile);
-    }));
+    });
+    for await (const stat of parseZipFilePromises) {
+      zipFileStats.push(stat);
+    }
 
 
     // filter null object (broken zip)
     // filter null object (broken zip)
     const filtered = zipFileStats.filter(element => element != null);
     const filtered = zipFileStats.filter(element => element != null);
@@ -166,9 +172,7 @@ class ExportService {
     const collection = mongoose.connection.collection(collectionName);
     const collection = mongoose.connection.collection(collectionName);
 
 
     const nativeCursor = collection.find();
     const nativeCursor = collection.find();
-    const readStream = nativeCursor
-      .snapshot()
-      .stream({ transform: JSON.stringify });
+    const readStream = nativeCursor.stream({ transform: JSON.stringify });
 
 
     // get TransformStream
     // get TransformStream
     const transformStream = this.generateTransformStream();
     const transformStream = this.generateTransformStream();
@@ -201,8 +205,12 @@ class ExportService {
   async exportCollectionsToZippedJson(collections) {
   async exportCollectionsToZippedJson(collections) {
     const metaJson = await this.createMetaJson();
     const metaJson = await this.createMetaJson();
 
 
-    const promises = collections.map(collectionName => this.exportCollectionToJson(collectionName));
-    const jsonFiles = await Promise.all(promises);
+    // process serially so as not to waste memory
+    const jsonFiles = [];
+    const jsonFilesPromises = collections.map(collectionName => this.exportCollectionToJson(collectionName));
+    for await (const jsonFile of jsonFilesPromises) {
+      jsonFiles.push(jsonFile);
+    }
 
 
     // send terminate event
     // send terminate event
     this.emitStartZippingEvent();
     this.emitStartZippingEvent();

+ 26 - 18
src/server/service/import.js

@@ -128,10 +128,16 @@ class ImportService {
    */
    */
   async getStatus() {
   async getStatus() {
     const zipFiles = fs.readdirSync(this.baseDir).filter(file => path.extname(file) === '.zip');
     const zipFiles = fs.readdirSync(this.baseDir).filter(file => path.extname(file) === '.zip');
-    const zipFileStats = await Promise.all(zipFiles.map((file) => {
+
+    // process serially so as not to waste memory
+    const zipFileStats = [];
+    const parseZipFilePromises = zipFiles.map((file) => {
       const zipFile = this.getFile(file);
       const zipFile = this.getFile(file);
       return this.growiBridgeService.parseZipFile(zipFile);
       return this.growiBridgeService.parseZipFile(zipFile);
-    }));
+    });
+    for await (const stat of parseZipFilePromises) {
+      zipFileStats.push(stat);
+    }
 
 
     // filter null object (broken zip)
     // filter null object (broken zip)
     const filtered = zipFileStats
     const filtered = zipFileStats
@@ -158,23 +164,25 @@ class ImportService {
     // init status object
     // init status object
     this.currentProgressingStatus = new CollectionProgressingStatus(collections);
     this.currentProgressingStatus = new CollectionProgressingStatus(collections);
 
 
-    try {
-      const promises = collections.map((collectionName) => {
-        const importSettings = importSettingsMap[collectionName];
-        return this.importCollection(collectionName, importSettings);
-      });
-      await Promise.all(promises);
-    }
-    // catch ImportingCollectionError
-    catch (err) {
-      const { collectionProgress } = err;
-      logger.error(`failed to import to ${collectionProgress.collectionName}`, err);
-      this.emitProgressEvent(collectionProgress, { message: err.message });
-    }
-    finally {
-      this.currentProgressingStatus = null;
-      this.emitTerminateEvent();
+    // process serially so as not to waste memory
+    const promises = collections.map((collectionName) => {
+      const importSettings = importSettingsMap[collectionName];
+      return this.importCollection(collectionName, importSettings);
+    });
+    for await (const promise of promises) {
+      try {
+        await promise;
+      }
+      // catch ImportingCollectionError
+      catch (err) {
+        const { collectionProgress } = err;
+        logger.error(`failed to import to ${collectionProgress.collectionName}`, err);
+        this.emitProgressEvent(collectionProgress, { message: err.message });
+      }
     }
     }
+
+    this.currentProgressingStatus = null;
+    this.emitTerminateEvent();
   }
   }
 
 
   /**
   /**

+ 0 - 1
src/server/service/search-delegator/elasticsearch.js

@@ -378,7 +378,6 @@ class ElasticsearchDelegator {
         { path: 'creator', model: 'User', select: 'username' },
         { path: 'creator', model: 'User', select: 'username' },
         { path: 'revision', model: 'Revision', select: 'body' },
         { path: 'revision', model: 'Revision', select: 'body' },
       ])
       ])
-      .snapshot()
       .lean()
       .lean()
       .cursor();
       .cursor();