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

Merge branch 'imprv/duplicate-Page-with-child' into imprv/gw3374

# Conflicts:
#	src/client/js/components/PageRenameModal.jsx
白石誠 5 лет назад
Родитель
Сommit
87580677f1
62 измененных файлов с 1162 добавлено и 775 удалено
  1. 9 0
      CHANGES.md
  2. 1 0
      config/webpack.prod.js
  3. 2 2
      package.json
  4. 1 1
      resource/locales/en_US/admin/admin.json
  5. 7 19
      resource/locales/en_US/translation.json
  6. 1 1
      resource/locales/ja_JP/admin/admin.json
  7. 6 18
      resource/locales/ja_JP/translation.json
  8. 2 2
      resource/locales/zh_CN/admin/admin.json
  9. 8 20
      resource/locales/zh_CN/translation.json
  10. 4 17
      src/client/js/components/Admin/App/AppSettingsPage.jsx
  11. 80 59
      src/client/js/components/Admin/App/MailSetting.jsx
  12. 4 17
      src/client/js/components/Admin/Customize/Customize.jsx
  13. 1 1
      src/client/js/components/Admin/ImportData/GrowiArchive/ErrorViewer.jsx
  14. 1 1
      src/client/js/components/Admin/ImportData/ImportDataPageContents.jsx
  15. 4 17
      src/client/js/components/Admin/ImportDataPage.jsx
  16. 4 17
      src/client/js/components/Admin/MarkdownSetting/MarkDownSetting.jsx
  17. 4 17
      src/client/js/components/Admin/Notification/NotificationSetting.jsx
  18. 4 17
      src/client/js/components/Admin/Security/BasicSecuritySetting.jsx
  19. 4 17
      src/client/js/components/Admin/Security/GitHubSecuritySetting.jsx
  20. 4 17
      src/client/js/components/Admin/Security/GoogleSecuritySetting.jsx
  21. 4 18
      src/client/js/components/Admin/Security/LdapSecuritySetting.jsx
  22. 4 17
      src/client/js/components/Admin/Security/LocalSecuritySetting.jsx
  23. 4 17
      src/client/js/components/Admin/Security/OidcSecuritySetting.jsx
  24. 4 17
      src/client/js/components/Admin/Security/SamlSecuritySetting.jsx
  25. 5 17
      src/client/js/components/Admin/Security/SecurityManagement.jsx
  26. 4 17
      src/client/js/components/Admin/Security/TwitterSecuritySetting.jsx
  27. 60 0
      src/client/js/components/ComparePathsTable.jsx
  28. 4 3
      src/client/js/components/Fab.jsx
  29. 26 0
      src/client/js/components/Icons/CreatePageIcon.jsx
  30. 20 0
      src/client/js/components/Icons/MoonIcon.jsx
  31. 20 0
      src/client/js/components/Icons/ReturnTopIcon.jsx
  32. 20 0
      src/client/js/components/Icons/SidebarDockIcon.jsx
  33. 25 0
      src/client/js/components/Icons/SidebarDrawerIcon.jsx
  34. 28 0
      src/client/js/components/Icons/SunIcon.jsx
  35. 25 6
      src/client/js/components/Me/PasswordSettings.jsx
  36. 33 15
      src/client/js/components/Navbar/PersonalDropdown.jsx
  37. 31 37
      src/client/js/components/PageDuplicateModal.jsx
  38. 38 25
      src/client/js/components/PageEditor/EditorIcon.jsx
  39. 1 0
      src/client/js/components/PageRenameModal.jsx
  40. 13 0
      src/client/js/services/AdminAppContainer.js
  41. 2 2
      src/client/js/services/PageHistoryContainer.js
  42. 0 2
      src/client/js/services/PersonalContainer.js
  43. 1 1
      src/client/styles/scss/_editor-navbar.scss
  44. 7 4
      src/client/styles/scss/_layout.scss
  45. 10 2
      src/client/styles/scss/_navbar.scss
  46. 36 1
      src/client/styles/scss/_override-bootstrap-variables.scss
  47. 19 0
      src/client/styles/scss/theme/_apply-colors.scss
  48. 16 0
      src/lib/util/path-utils.js
  49. 2 2
      src/server/middlewares/certify-shared-page.js
  50. 3 28
      src/server/models/user.js
  51. 0 17
      src/server/routes/admin.js
  52. 55 9
      src/server/routes/apiv3/app-settings.js
  53. 1 4
      src/server/routes/apiv3/pages.js
  54. 46 2
      src/server/routes/apiv3/personal-setting.js
  55. 2 1
      src/server/routes/apiv3/users.js
  56. 4 1
      src/server/routes/attachment.js
  57. 0 1
      src/server/routes/index.js
  58. 16 8
      src/server/service/export.js
  59. 26 18
      src/server/service/import.js
  60. 0 1
      src/server/service/search-delegator/elasticsearch.js
  61. 0 7
      src/test/models/user.test.js
  62. 396 215
      yarn.lock

+ 9 - 0
CHANGES.md

@@ -4,7 +4,16 @@
 
 * Feature: Create/edit linker with GUI
 * 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: Limit the attrubutes of user data for `/_api/v3/users`
+* Fix: Prevent XSS with SVG
+* Upgrade libs
+    * optimize-css-assets-webpack-plugin
+    * terser-webpack-plugin
 
 ## v4.1.2
 

+ 1 - 0
config/webpack.prod.js

@@ -67,6 +67,7 @@ module.exports = require('./webpack.common')({
 
   ],
   optimization: {
+    minimize: true,
     minimizer: [
       new TerserPlugin({}),
       new OptimizeCSSAssetsPlugin({}),

+ 2 - 2
package.json

@@ -220,7 +220,7 @@
     "normalize-path": "^3.0.0",
     "null-loader": "^3.0.0",
     "on-headers": "^1.0.1",
-    "optimize-css-assets-webpack-plugin": "^5.0.0",
+    "optimize-css-assets-webpack-plugin": "^5.0.3",
     "penpal": "^4.0.0",
     "plantuml-encoder": "^1.2.5",
     "postcss-loader": "^3.0.0",
@@ -249,7 +249,7 @@
     "stylelint-config-recess-order": "^2.0.1",
     "swagger-jsdoc": "^3.4.0",
     "swagger2openapi": "^5.3.1",
-    "terser-webpack-plugin": "^2.0.1",
+    "terser-webpack-plugin": "^4.1.0",
     "throttle-debounce": "^2.0.0",
     "toastr": "^2.1.2",
     "unstated": "^2.1.1",

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

@@ -210,7 +210,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>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",
     "export": "Export",
     "cancel": "Cancel",

+ 7 - 19
resource/locales/en_US/translation.json

@@ -134,12 +134,14 @@
   "Disassociate": "Disassociate",
   "Recent Created": "Recent Created",
   "Recent Changes": "Recent Changes",
+  "original_path":"Original path",
+  "new_path":"New path",
   "personal_dropdown": {
     "home": "Home",
     "settings": "Settings",
     "color_mode": "Color mode",
     "sidebar_mode": "Sidebar mode",
-    "sidebar_mode_editor": "Sidebar mode on Editor",
+    "sidebar_mode_editor": "Sidebar mode on editor",
     "use_os_settings": "Use OS settings"
   },
   "form_validation": {
@@ -716,24 +718,6 @@
     "rebuild_description_1": "Click the button to rebuild index and add all page datas.",
     "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": {
     "Sign in error": "Login error",
     "Registration successful": "Registration successful",
@@ -767,5 +751,9 @@
     "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.",
     "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": "コレクションのエクスポート",
     "check_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": "アーカイブデータの新規作成",
     "export": "エクスポート",
     "cancel": "キャンセル",

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

@@ -137,6 +137,8 @@
   "Sidebar mode on Editor": "サイドバーモード(編集時)",
   "Recent Created": "最新の作成",
   "Recent Changes": "最新の変更",
+  "original_path":"元のパス",
+  "new_path":"新しいパス",
   "personal_dropdown": {
     "home": "ホーム",
     "settings": "設定",
@@ -709,24 +711,6 @@
     "rebuild_description_1": "全てのページのインデックスを削除し、作り直します。",
     "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": {
     "Sign in error": "ログインエラー",
     "Registration successful": "登録完了",
@@ -760,5 +744,9 @@
     "complete_to_install1":"GROWI のインストールが完了しました!管理者アカウントでログインしてください。",
     "complete_to_install2":"GROWI のインストールが完了しました!はじめに、このページで各種設定を確認してください。",
     "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": "导出集合",
 		"check_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": "创建新的存档数据",
 		"export": "导出",
 		"cancel": "取消",
@@ -236,7 +236,7 @@
 		"export_menu": "导出菜单",
 		"download": "下载",
 		"delete": "删除"
-	},
+  },
 	"user_management": {
 		"invite_users": "邀请新用户",
 		"click_twice_same_checkbox": "您应该至少选中一个复选框。",

+ 8 - 20
resource/locales/zh_CN/translation.json

@@ -136,7 +136,9 @@
 	"Sign out": "退出",
 	"Disassociate": "解除关联",
 	"Recent Created": "最新创建",
-	"Recent Changes": "最新修改",
+  "Recent Changes": "最新修改",
+  "original_path":"Original path",
+  "new_path":"New path",
 	"form_validation": {
 		"error_message": "有些值不正确",
 		"required": "%s 是必需的",
@@ -698,24 +700,6 @@
 		"rebuild_description_1": "单击按钮以重新生成索引并添加所有页面数据。",
 		"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": {
 		"home": "家",
 		"settings": "设置",
@@ -761,5 +745,9 @@
 		"complete_to_install1": "完成安装GROWI!请以管理员帐户登录。",
 		"complete_to_install2": "完成安装GROWI!请先检查此页上的每个设置。",
 		"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 loggerFactory from '@alias/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 import AdminAppContainer from '../../../services/AdminAppContainer';
 
@@ -12,20 +13,6 @@ import AppSettingsPageContents from './AppSettingsPageContents';
 
 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;
 function AppSettingsPage(props) {
   if (props.adminAppContainer.state.title === props.adminAppContainer.dummyTitle) {
@@ -59,6 +46,6 @@ AppSettingsPage.propTypes = {
 /**
  * Wrapper component for using unstated
  */
-const AppSettingsPageWithUnstatedContainer = withUnstatedContainers(AppSettingsPage, [AdminAppContainer]);
+const AppSettingsPageWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(AppSettingsPage), [AdminAppContainer]);
 
-export default AppSettingsPageWithContainerWithSuspense;
+export default AppSettingsPageWithUnstatedContainer;

+ 80 - 59
src/client/js/components/Admin/App/MailSetting.jsx

@@ -29,6 +29,7 @@ class MailSetting extends React.Component {
 
     this.openInitializeValueModal = this.openInitializeValueModal.bind(this);
     this.closeInitializeValueModal = this.closeInitializeValueModal.bind(this);
+    this.submitFromAdressHandler = this.submitFromAdressHandler.bind(this);
     this.submitHandler = this.submitHandler.bind(this);
     this.initialize = this.initialize.bind(this);
   }
@@ -54,14 +55,26 @@ class MailSetting extends React.Component {
     }
   }
 
+  async submitFromAdressHandler() {
+    const { t, adminAppContainer } = this.props;
+
+    try {
+      await adminAppContainer.updateFromAdressHandler();
+      toastSuccess(t('toaster.update_successed', { target: t('admin:app_setting.mail_settings') }));
+    }
+    catch (err) {
+      toastError(err);
+      logger.error(err);
+    }
+  }
+
   async initialize() {
     const { t, adminAppContainer } = this.props;
 
     try {
       const mailSettingParams = await adminAppContainer.initializeMailSettingHandler();
-      toastSuccess(t('toaster.initialize_successed', { target: t('admin:app_setting.mail_settings') }));
+      toastSuccess(t('toaster.initialize_successed', { target: t('admin:app_setting.smtp_settings') }));
       // convert values to '' if value is null for overwriting values of inputs with refs
-      this.emailInput.current.value = mailSettingParams.fromAddress || '';
       this.hostInput.current.value = mailSettingParams.smtpHost || '';
       this.portInput.current.value = mailSettingParams.smtpPort || '';
       this.userInput.current.value = mailSettingParams.smtpUser || '';
@@ -93,70 +106,78 @@ class MailSetting extends React.Component {
             />
           </div>
         </div>
-
-        <div className="row form-group mb-5">
-          <label className="col-md-3 col-form-label text-left">{t('admin:app_setting.smtp_settings')}</label>
-          <div className="col-md-4">
-            <label>{t('admin:app_setting.host')}</label>
-            <input
-              className="form-control"
-              type="text"
-              ref={this.hostInput}
-              defaultValue={adminAppContainer.state.smtpHost || ''}
-              onChange={(e) => { adminAppContainer.changeSmtpHost(e.target.value) }}
-            />
-          </div>
-          <div className="col-md-2">
-            <label>{t('admin:app_setting.port')}</label>
-            <input
-              className="form-control"
-              ref={this.portInput}
-              defaultValue={adminAppContainer.state.smtpPort || ''}
-              onChange={(e) => { adminAppContainer.changeSmtpPort(e.target.value) }}
-            />
+        <div className="row my-3">
+          <div className="mx-auto">
+            <button type="button" className="btn btn-primary" onClick={this.submitFromAdressHandler}>{ t('Update') }</button>
           </div>
         </div>
-
-        <div className="row form-group mb-5">
-          <div className="col-md-3 offset-md-3">
-            <label>{t('admin:app_setting.user')}</label>
-            <input
-              className="form-control"
-              type="text"
-              ref={this.userInput}
-              defaultValue={adminAppContainer.state.smtpUser || ''}
-              onChange={(e) => { adminAppContainer.changeSmtpUser(e.target.value) }}
-            />
-          </div>
-          <div className="col-md-3">
-            <label>{t('Password')}</label>
-            <input
-              className="form-control"
-              type="password"
-              ref={this.passwordInput}
-              defaultValue={adminAppContainer.state.smtpPassword || ''}
-              onChange={(e) => { adminAppContainer.changeSmtpPassword(e.target.value) }}
-            />
+        <div id="mail-smtp" className="tab-pane active mt-5">
+          <div className="row form-group mb-5">
+            <label className="col-md-3 col-form-label text-left">{t('admin:app_setting.smtp_settings')}</label>
+            <div className="col-md-4">
+              <label>{t('admin:app_setting.host')}</label>
+              <input
+                className="form-control"
+                type="text"
+                ref={this.hostInput}
+                defaultValue={adminAppContainer.state.smtpHost || ''}
+                onChange={(e) => { adminAppContainer.changeSmtpHost(e.target.value) }}
+              />
+            </div>
+            <div className="col-md-2">
+              <label>{t('admin:app_setting.port')}</label>
+              <input
+                className="form-control"
+                ref={this.portInput}
+                defaultValue={adminAppContainer.state.smtpPort || ''}
+                onChange={(e) => { adminAppContainer.changeSmtpPort(e.target.value) }}
+              />
+            </div>
           </div>
-        </div>
 
-        <div className="row my-3">
-          <div className="offset-5">
-            <button type="button" className="btn btn-primary" onClick={this.submitHandler} disabled={adminAppContainer.state.retrieveError != null}>
-              { t('Update') }
-            </button>
+          <div className="row form-group mb-5">
+            <div className="col-md-3 offset-md-3">
+              <label>{t('admin:app_setting.user')}</label>
+              <input
+                className="form-control"
+                type="text"
+                ref={this.userInput}
+                defaultValue={adminAppContainer.state.smtpUser || ''}
+                onChange={(e) => { adminAppContainer.changeSmtpUser(e.target.value) }}
+              />
+            </div>
+            <div className="col-md-3">
+              <label>{t('Password')}</label>
+              <input
+                className="form-control"
+                type="password"
+                ref={this.passwordInput}
+                defaultValue={adminAppContainer.state.smtpPassword || ''}
+                onChange={(e) => { adminAppContainer.changeSmtpPassword(e.target.value) }}
+              />
+            </div>
           </div>
-          <div className="offset-1">
-            <button
-              type="button"
-              className="btn btn-secondary"
-              onClick={this.openInitializeValueModal}
-              disabled={adminAppContainer.state.retrieveError != null}
-            >
-              {t('admin:app_setting.initialize_mail_settings')}
-            </button>
+
+          <div className="row my-3">
+            <div className="offset-5">
+              <button type="button" className="btn btn-primary" onClick={this.submitHandler} disabled={adminAppContainer.state.retrieveError != null}>
+                { t('Update') }
+              </button>
+            </div>
+            <div className="offset-1">
+              <button
+                type="button"
+                className="btn btn-secondary"
+                onClick={this.openInitializeValueModal}
+                disabled={adminAppContainer.state.retrieveError != null}
+              >
+                {t('admin:app_setting.initialize_mail_settings')}
+              </button>
+            </div>
           </div>
         </div>
+
+
         <Modal isOpen={this.state.isInitializeValueModalOpen} toggle={this.closeInitializeValueModal} className="initialize-mail-settings">
           <ModalHeader tag="h4" toggle={this.closeInitializeValueModal} className="bg-danger text-light">
             {t('admin:app_setting.initialize_mail_modal_header')}

+ 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 loggerFactory from '@alias/logger';
@@ -8,6 +8,7 @@ import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 import CustomizeLayoutSetting from './CustomizeLayoutSetting';
 import CustomizeFunctionSetting from './CustomizeFunctionSetting';
@@ -19,20 +20,6 @@ import CustomizeTitle from './CustomizeTitle';
 
 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;
 function Customize(props) {
   const { adminCustomizeContainer } = props;
@@ -83,10 +70,10 @@ function Customize(props) {
   );
 }
 
-const CustomizePageWithUnstatedContainer = withUnstatedContainers(Customize, [AdminCustomizeContainer]);
+const CustomizePageWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(Customize), [AdminCustomizeContainer]);
 
 Customize.propTypes = {
   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 (
-      <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">
           Errors
         </ModalHeader>

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

@@ -189,7 +189,7 @@ class ImportDataPageContents extends React.Component {
                   className="form-control"
                   type="password"
                   name="qiitaAccessToken"
-                  value={adminImportContainer.stateqiitaAccessToken}
+                  value={adminImportContainer.state.qiitaAccessToken}
                   onChange={adminImportContainer.handleInputValue}
                 />
               </div>

+ 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 loggerFactory from '@alias/logger';
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import toArrayIfNot from '../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../SuspenseUtils';
 
 import AdminImportContainer from '../../services/AdminImportContainer';
 import { toastError } from '../../util/apiNotification';
@@ -12,20 +13,6 @@ import ImportDataPageContents from './ImportData/ImportDataPageContents';
 
 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;
 function ImportDataPage(props) {
   const { adminImportContainer } = props;
@@ -60,6 +47,6 @@ ImportDataPage.propTypes = {
 /**
  * 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 loggerFactory from '@alias/logger';
@@ -6,26 +6,13 @@ import loggerFactory from '@alias/logger';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 import MarkDownSettingContents from './MarkDownSettingContents';
 import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
 
 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;
 function MarkdownSetting(props) {
   const { adminMarkDownContainer } = props;
@@ -52,10 +39,10 @@ function MarkdownSetting(props) {
   return <MarkDownSettingContents />;
 }
 
-const MarkdownSettingWithUnstatedContainer = withUnstatedContainers(MarkdownSetting, [AdminMarkDownContainer]);
+const MarkdownSettingWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(MarkdownSetting), [AdminMarkDownContainer]);
 
 MarkdownSetting.propTypes = {
   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 loggerFactory from '@alias/logger';
@@ -6,6 +6,7 @@ import loggerFactory from '@alias/logger';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 import AdminNotificationContainer from '../../../services/AdminNotificationContainer';
 
@@ -13,20 +14,6 @@ import NotificationSettingContents from './NotificationSettingContents';
 
 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;
 function NotificationSetting(props) {
   const { adminNotificationContainer } = props;
@@ -52,10 +39,10 @@ function NotificationSetting(props) {
   return <NotificationSettingContents />;
 }
 
-const NotificationSettingWithUnstatedContainer = withUnstatedContainers(NotificationSetting, [AdminNotificationContainer]);
+const NotificationSettingWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(NotificationSetting), [AdminNotificationContainer]);
 
 NotificationSetting.propTypes = {
   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 */
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 import AdminBasicSecurityContainer from '../../../services/AdminBasicSecurityContainer';
 
@@ -43,22 +44,8 @@ BasicSecurityManagement.propTypes = {
   adminBasicSecurityContainer: PropTypes.instanceOf(AdminBasicSecurityContainer).isRequired,
 };
 
-const BasicSecurityManagementWithUnstatedContainer = withUnstatedContainers(BasicSecurityManagement, [
+const BasicSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(BasicSecurityManagement), [
   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 */
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 import AdminGitHubSecurityContainer from '../../../services/AdminGitHubSecurityContainer';
 
@@ -39,22 +40,8 @@ GitHubSecurityManagement.propTypes = {
   adminGitHubSecurityContainer: PropTypes.instanceOf(AdminGitHubSecurityContainer).isRequired,
 };
 
-const GitHubSecurityManagementWithUnstatedContainer = withUnstatedContainers(GitHubSecurityManagement, [
+const GitHubSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(GitHubSecurityManagement), [
   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 */
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 import AdminGoogleSecurityContainer from '../../../services/AdminGoogleSecurityContainer';
 import GoogleSecurityManagementContents from './GoogleSecuritySettingContents';
@@ -38,22 +39,8 @@ GoogleSecurityManagement.propTypes = {
   adminGoogleSecurityContainer: PropTypes.instanceOf(AdminGoogleSecurityContainer).isRequired,
 };
 
-const GoogleSecurityManagementWithUnstatedContainer = withUnstatedContainers(GoogleSecurityManagement, [
+const GoogleSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(GoogleSecurityManagement), [
   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 { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 import AdminLdapSecurityContainer from '../../../services/AdminLdapSecurityContainer';
 
@@ -37,23 +38,8 @@ LdapSecuritySetting.propTypes = {
   adminLdapSecurityContainer: PropTypes.instanceOf(AdminLdapSecurityContainer).isRequired,
 };
 
-const LdapSecuritySettingWithUnstatedContainer = withUnstatedContainers(LdapSecuritySetting, [
+const LdapSecuritySettingWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(LdapSecuritySetting), [
   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 */
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 import AdminLocalSecurityContainer from '../../../services/AdminLocalSecurityContainer';
 
@@ -38,22 +39,8 @@ LocalSecuritySetting.propTypes = {
   adminLocalSecurityContainer: PropTypes.instanceOf(AdminLocalSecurityContainer).isRequired,
 };
 
-const LocalSecuritySettingWithUnstatedContainer = withUnstatedContainers(LocalSecuritySetting, [
+const LocalSecuritySettingWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(LocalSecuritySetting), [
   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 */
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 import AdminOidcSecurityContainer from '../../../services/AdminOidcSecurityContainer';
 
@@ -38,22 +39,8 @@ OidcSecurityManagement.propTypes = {
   adminOidcSecurityContainer: PropTypes.instanceOf(AdminOidcSecurityContainer).isRequired,
 };
 
-const OidcSecurityManagementWithUnstatedContainer = withUnstatedContainers(OidcSecurityManagement, [
+const OidcSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(OidcSecurityManagement), [
   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 */
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -7,6 +7,7 @@ import { toastError } from '../../../util/apiNotification';
 
 import AdminSamlSecurityContainer from '../../../services/AdminSamlSecurityContainer';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 import SamlSecuritySettingContents from './SamlSecuritySettingContents';
 
@@ -38,22 +39,8 @@ SamlSecurityManagement.propTypes = {
   adminSamlSecurityContainer: PropTypes.instanceOf(AdminSamlSecurityContainer).isRequired,
 };
 
-const SamlSecurityManagementWithUnstatedContainer = withUnstatedContainers(SamlSecurityManagement, [
+const SamlSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(SamlSecurityManagement), [
   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 { toastError } from '../../../util/apiNotification';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
+
 import AdminGeneralSecurityContainer from '../../../services/AdminGeneralSecurityContainer';
 import SecurityManagementContents from './SecurityManagementContents';
 
@@ -38,20 +40,6 @@ SecurityManagement.propTypes = {
   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 */
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 import AdminTwitterSecurityContainer from '../../../services/AdminTwitterSecurityContainer';
 
@@ -40,22 +41,8 @@ TwitterSecurityManagement.propTypes = {
   adminTwitterSecurityContainer: PropTypes.instanceOf(AdminTwitterSecurityContainer).isRequired,
 };
 
-const TwitterSecurityManagementWithUnstatedContainer = withUnstatedContainers(TwitterSecurityManagement, [
+const TwitterSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(TwitterSecurityManagement), [
   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;

+ 60 - 0
src/client/js/components/ComparePathsTable.jsx

@@ -0,0 +1,60 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { withTranslation } from 'react-i18next';
+import { withUnstatedContainers } from './UnstatedUtils';
+
+import PageContainer from '../services/PageContainer';
+import { convertToNewAffiliationPath } from '../../../lib/util/path-utils';
+
+function ComparePathsTable(props) {
+  const {
+    subordinatedPages, pageContainer, newPagePath, t,
+  } = props;
+  const { path } = pageContainer.state;
+
+  return (
+    <table className="table table-bordered">
+      <thead>
+        <tr>
+          <th className="w-50">{t('original_path')}</th>
+          <th className="w-50">{t('new_path')}</th>
+        </tr>
+      </thead>
+      <tbody>
+        {subordinatedPages.map((subordinatedPage) => {
+          const convertedPath = convertToNewAffiliationPath(path, newPagePath, subordinatedPage.path);
+          return (
+            <tr key={subordinatedPage._id}>
+              <td className="text-break">
+                <a href={subordinatedPage.path}>
+                  {subordinatedPage.path}
+                </a>
+              </td>
+              <td className="text-break">
+                {convertedPath}
+              </td>
+            </tr>
+          );
+        })}
+      </tbody>
+    </table>
+  );
+}
+
+
+/**
+ * Wrapper component for using unstated
+ */
+const PageDuplicateModallWrapper = withUnstatedContainers(ComparePathsTable, [PageContainer]);
+
+ComparePathsTable.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+  subordinatedPages: PropTypes.array.isRequired,
+  newPagePath: PropTypes.string.isRequired,
+};
+
+
+export default withTranslation()(PageDuplicateModallWrapper);

+ 4 - 3
src/client/js/components/Fab.jsx

@@ -6,7 +6,8 @@ import StickyEvents from 'sticky-events';
 
 import NavigationContainer from '../services/NavigationContainer';
 import { withUnstatedContainers } from './UnstatedUtils';
-
+import CreatePageIcon from './Icons/CreatePageIcon';
+import ReturnTopIcon from './Icons/ReturnTopIcon';
 
 const logger = loggerFactory('growi:cli:Fab');
 
@@ -47,12 +48,12 @@ const Fab = (props) => {
           className="btn btn-lg btn-create-page btn-primary rounded-circle p-0 waves-effect waves-light"
           onClick={navigationContainer.openPageCreateModal}
         >
-          <i className="icon-pencil"></i>
+          <CreatePageIcon />
         </button>
       </div>
       <div className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: 0, right: 0 }}>
         <button type="button" className="btn btn-light btn-scroll-to-top rounded-circle p-0" onClick={() => navigationContainer.smoothScrollIntoView()}>
-          <i className="icon-control-start"></i>
+          <ReturnTopIcon />
         </button>
       </div>
     </div>

+ 26 - 0
src/client/js/components/Icons/CreatePageIcon.jsx

@@ -0,0 +1,26 @@
+import React from 'react';
+
+const CreatePageIcon = () => (
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    viewBox="0 0 27 30"
+  >
+    <path
+      d="M22.81,8.2a4.2,4.2,0,0,0,1.36-2.95,4,4,0,0,0-1.43-2.81,4.53,4.53,0,0,0-1.28-.89,3.26,3.26,0,
+      0,0-1.37-.31,4,4,0,0,0-2.91,1.29q-.42.4-14.83,14.84a.7.7,0,0,0-.26.33c-.07.26-.72,2.46-2,6.58a.73.73,0,
+      0,0,.3,1,.78.78,0,0,0,.7,0c3.3-1.08,5.45-1.76,6.47-2.06A.57.57,0,0,0,7.91,23l8.5-8.42Q22.25,8.81,22.81,8.2ZM1.93,
+      23.44c.16-.44,1.39-4.39,1.5-4.78A4.93,4.93,0,0,1,5.59,20a4.53,4.53,0,0,1,1.12,1.87Zm15-18.52a4.7,4.7,0,0,1,2.16,1.31,5.08,5.08,
+      0,0,1,.72,1,5.3,5.3,0,0,1,.37.8c.05.17.09.34.13.51Q17.19,11.65,8,20.79a6.42,6.42,0,0,0-1.29-1.92,6.67,6.67,0,0,0-2.2-1.48Zm4.64,
+      2.37a6.36,6.36,0,0,0-1.36-2.13,6.61,6.61,0,0,0-2.12-1.43s.29-.28.41-.38A3,3,0,0,1,19.17,3a2,2,0,0,1,.9-.21A1.87,1.87,0,0,1,20.9,3a2.53,2.53,0,0,
+      1,.79.56,3.81,3.81,0,0,1,.71.89,1.87,1.87,0,0,1,.25.87,2.75,2.75,0,0,1-.94,1.83Z"
+    />
+    <path d="M26.41,20.05H22.84V16.48a.72.72,0,0,0-1.43,0v3.57H17.84a.72.72,0,0,0,0,1.43h3.57v3.57a.72.72,0,0,0,
+    1.43.17V21.48h3.57a.72.72,0,1,0,.17-1.43A.48.48,0,0,0,26.41,20.05Z"
+    />
+    <rect fillOpacity="0" width="27" height="27" />
+  </svg>
+
+);
+
+
+export default CreatePageIcon;

+ 20 - 0
src/client/js/components/Icons/MoonIcon.jsx

@@ -0,0 +1,20 @@
+import React from 'react';
+
+const MoonIcon = () => (
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    viewBox="0 0 23 23"
+  >
+    <g transform="translate(-923.5 -688.5)">
+      <rect width="23" height="23" fill="none" transform="translate(923.5 688.5)" />
+      <path d="M934.893,710.532a10.646,10.646,0,0,1-10.378-8.416.7.7,0,0,1,1.138-.686,
+       7.621,7.621,0,0,0,10.721-10.744.7.7,0,0,1,.683-1.14,10.6,10.6,0,0,1-2.164,
+        20.986Zm-8.417-6.9A9.2,9.2,0,1,0,938.583,691.5a9.028,9.028,0,0,1-12.107,12.133Z"
+      />
+    </g>
+  </svg>
+
+);
+
+
+export default MoonIcon;

+ 20 - 0
src/client/js/components/Icons/ReturnTopIcon.jsx

@@ -0,0 +1,20 @@
+import React from 'react';
+
+const ReturnTopIcon = () => (
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    viewBox="0 0 23 23"
+  >
+    <path d="M.41,18.71a.82.82,0,0,0,0,.26.71.71,0,0,0,0,.29.5.5,0,0,0,.16.22.66.66,0,0,0,.51.21.67.67,0,0,0,
+    .51-.21l9.57-9.56,9.43,9.43a.71.71,0,0,0,.51.21.68.68,0,0,0,.51-.21.72.72,
+    0,0,0,0-1l-9.94-10a.78.78,0,0,0-.51-.19.76.76,0,0,0-.5.19L.58,18.46A.85.85,0,0,0,.41,18.71Z"
+    />
+    <path d="M22.35,4.61H.65a.65.65,0,0,1,0-1.3h21.7a.65.65,0,1,1,0,1.3Z" />
+    <rect fillOpacity="0" width="23" height="23" />
+
+  </svg>
+
+);
+
+
+export default ReturnTopIcon;

+ 20 - 0
src/client/js/components/Icons/SidebarDockIcon.jsx

@@ -0,0 +1,20 @@
+import React from 'react';
+
+const SidebarDockIcon = () => (
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    viewBox="0 0 23 23"
+  >
+    <rect width="23" height="23" fillOpacity="0" />
+    <path
+      d="M20.86,3.92a.64.64,0,0,1,.64.63v13.9a.64.64,0,0,1-.64.63H2.14a.64.64,0,0,
+      1-.64-.63V4.55a.64.64,0,0,1,.64-.63H20.86m0-1.5H2.14A2.13,2.13,0,0,0,0,4.55v13.9a2.13,
+      2.13,0,0,0,2.14,2.13H20.86A2.13,2.13,0,0,0,23,18.45V4.55a2.13,2.13,0,0,0-2.14-2.13Z"
+    />
+    <rect x="7.49" y="3.05" width="1.2" height="16.91" />
+  </svg>
+
+);
+
+
+export default SidebarDockIcon;

+ 25 - 0
src/client/js/components/Icons/SidebarDrawerIcon.jsx

@@ -0,0 +1,25 @@
+import React from 'react';
+
+const SidebarDrawerIcon = () => (
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    viewBox="0 0 23 23"
+  >
+    <rect width="23" height="23" fillOpacity="0" />
+    <path d="M20.9,3.9c0.3,0,0.6,0.3,0.6,0.6v13.9c0,0.3-0.3,0.6-0.6,0.6H2.1c-0.3,0-0.6-0.3-0.6-0.6V4.5c0-0.3,0.3-0.6,0.6-0.6H20.9
+      M20.9,2.4H2.1C1,2.4,0,3.4,0,4.5c0,0,0,0,0,0v13.9c0,1.2,1,2.1,2.1,2.1c0,0,0,0,0,0h18.7c1.2,0,2.1-0.9,2.1-2.1c0,0,0,0,0,0V4.5
+      C23,3.4,22,2.4,20.9,2.4C20.9,2.4,20.9,2.4,20.9,2.4z"
+    />
+    <rect x="7.5" y="3.9" width="1.2" height="0.8" />
+    <rect x="7.5" y="15.3" width="1.2" height="1.5" />
+    <rect x="7.5" y="12.3" width="1.2" height="1.5" />
+    <rect x="7.5" y="9.2" width="1.2" height="1.6" />
+    <rect x="7.5" y="6.1" width="1.2" height="1.6" />
+    <rect x="7.5" y="18.4" width="1.2" height="0.8" />
+    <path d="M15.1,14.9c-0.2,0-0.3-0.1-0.4-0.2l-2.8-2.8c-0.2-0.2-0.2-0.6,0-0.8l2.8-2.8c0.2-0.2,0.6-0.2,0.9,0s0.2,0.6,0,0.9l-2.4,2.4
+      l2.4,2.4c0.2,0.2,0.2,0.6,0,0.9C15.4,14.8,15.3,14.9,15.1,14.9z"
+    />
+  </svg>
+);
+
+export default SidebarDrawerIcon;

+ 28 - 0
src/client/js/components/Icons/SunIcon.jsx

@@ -0,0 +1,28 @@
+import React from 'react';
+
+const SunIcon = () => (
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    viewBox="0 0 23 23"
+  >
+    <g transform="translate(-888.497 -688.492)">
+      <rect width="23" height="23" transform="translate(888.503 688.509)" fillOpacity="0" />
+      <path d="M900,695.489a4.5,4.5,0,1,1-4.5,4.5,4.5,4.5,0,0,1,4.5-4.5m0-1.408a5.9,5.9,0,1,0,5.9,5.9,5.91,5.91,0,0,0-5.9-5.9Z" />
+      <path d="M893.968,694.573a.6.6,0,0,1-.426-.176l-1.681-1.681a.6.6,0,0,1,.853-.852l1.681,1.68a.6.6,0,0,1-.427,1.029Z" />
+      <path d="M907.707,708.295a.6.6,0,0,1-.427-.177l-1.681-1.68a.6.6,0,0,1,.854-.853l1.68,1.681a.6.6,0,0,1-.426,1.029Z" />
+
+      <path d="M899.991,692.074a.6.6,0,0,1-.6-.6v-2.377a.6.6,0,0,1,1.206,0v2.377A.6.6,0,0,1,899.991,692.074Z" />
+      <path d="M900,711.491a.6.6,0,0,1-.6-.6v-2.377a.6.6,0,1,1,1.206,0v2.377A.6.6,0,0,1,900,711.491Z" />
+
+      <path d="M906.017,694.564a.6.6,0,0,1-.426-1.029l1.68-1.68a.6.6,0,0,1,.853.854l-1.68,1.68A.6.6,0,0,1,906.017,694.564Z" />
+      <path d="M892.3,708.3a.6.6,0,0,1-.426-1.029l1.68-1.681a.6.6,0,1,1,.853.852l-1.68,1.681A.6.6,0,0,1,892.3,708.3Z" />
+
+      <path d="M910.894,700.587h-2.377a.6.6,0,1,1,0-1.2h2.377a.6.6,0,1,1,0,1.2Z" />
+      <path d="M891.477,700.6H889.1a.6.6,0,1,1,0-1.2h2.377a.6.6,0,1,1,0,1.2Z" />
+    </g>
+  </svg>
+
+);
+
+
+export default SunIcon;

+ 25 - 6
src/client/js/components/Me/PasswordSettings.jsx

@@ -3,7 +3,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-
 import { toastSuccess, toastError } from '../../util/apiNotification';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
@@ -23,6 +22,7 @@ class PasswordSettings extends React.Component {
       oldPassword: '',
       newPassword: '',
       newPasswordConfirm: '',
+      isPasswordSet: false,
     };
 
     this.onClickSubmit = this.onClickSubmit.bind(this);
@@ -30,6 +30,21 @@ class PasswordSettings extends React.Component {
 
   }
 
+  async componentDidMount() {
+    const { appContainer } = this.props;
+
+    try {
+      const res = await appContainer.apiv3Get('/personal-setting/is-password-set');
+      const { isPasswordSet } = res.data;
+      this.setState({ isPasswordSet });
+    }
+    catch (err) {
+      toastError(err);
+      this.setState({ retrieveError: err });
+    }
+
+  }
+
   async onClickSubmit() {
     const { t, appContainer, personalContainer } = this.props;
     const { oldPassword, newPassword, newPasswordConfirm } = this.state;
@@ -61,22 +76,26 @@ class PasswordSettings extends React.Component {
   }
 
   render() {
-    const { t, personalContainer } = this.props;
+    const { t } = this.props;
     const { newPassword, newPasswordConfirm } = this.state;
     const isIncorrectConfirmPassword = (newPassword !== newPasswordConfirm);
 
+    if (this.state.retrieveError != null) {
+      throw new Error(this.state.retrieveError.message);
+    }
+
     return (
       <React.Fragment>
-        { (!personalContainer.state.isPasswordSet) && (
+        { (!this.state.isPasswordSet) && (
           <div className="alert alert-warning">{ t('personal_settings.password_is_not_set') }</div>
         ) }
 
         <div className="container-fluid my-4">
-          {(personalContainer.state.isPasswordSet)
+          {(this.state.isPasswordSet)
             ? <h2 className="border-bottom">{t('personal_settings.update_password')}</h2>
           : <h2 className="border-bottom">{t('personal_settings.set_new_password')}</h2>}
         </div>
-        {(personalContainer.state.isPasswordSet)
+        {(this.state.isPasswordSet)
         && (
           <div className="row mb-3">
             <label htmlFor="oldPassword" className="col-md-3 text-md-right">{ t('personal_settings.current_password') }</label>
@@ -124,7 +143,7 @@ class PasswordSettings extends React.Component {
               type="button"
               className="btn btn-primary"
               onClick={this.onClickSubmit}
-              disabled={this.state.retrieveError != null || isIncorrectConfirmPassword}
+              disabled={isIncorrectConfirmPassword}
             >
               {t('Update')}
             </button>

+ 33 - 15
src/client/js/components/Navbar/PersonalDropdown.jsx

@@ -20,6 +20,12 @@ import {
 
 import UserPicture from '../User/UserPicture';
 
+import SidebarDrawerIcon from '../Icons/SidebarDrawerIcon';
+import SidebarDockIcon from '../Icons/SidebarDockIcon';
+import MoonIcon from '../Icons/MoonIcon';
+import SunIcon from '../Icons/SunIcon';
+
+
 const PersonalDropdown = (props) => {
 
   const { t, appContainer, navigationContainer } = props;
@@ -79,16 +85,12 @@ const PersonalDropdown = (props) => {
   } = navigationContainer.state;
 
   /* eslint-disable react/prop-types */
-  const DrawerIcon = props => (
-    <>
-      <i id={props.id} className="icon-drawer px-2"></i>
-      <UncontrolledTooltip placement="bottom" fade={false} target={props.id}>Drawer</UncontrolledTooltip>
-    </>
-  );
-  const DockIcon = props => (
+  const IconWithTooltip = ({
+    id, label, children, additionalClasses,
+  }) => (
     <>
-      <i id={props.id} className="ti-layout-sidebar-left px-2"></i>
-      <UncontrolledTooltip placement="bottom" fade={false} target={props.id}>Dock</UncontrolledTooltip>
+      <div id={id} className={`px-2 grw-icon-container ${additionalClasses != null ? additionalClasses : ''}`}>{children}</div>
+      <UncontrolledTooltip placement="bottom" fade={false} target={id}>{label}</UncontrolledTooltip>
     </>
   );
   /* eslint-enable react/prop-types */
@@ -129,11 +131,14 @@ const PersonalDropdown = (props) => {
 
         <div className="dropdown-divider"></div>
 
+        {/* Sidebar Mode */}
         <h6 className="dropdown-header">{t('personal_dropdown.sidebar_mode')}</h6>
         <form className="px-4">
           <div className="form-row justify-content-center">
             <div className="form-group col-auto mb-0 d-flex align-items-center">
-              <DrawerIcon id="icon-prefer-drawer" />
+              <IconWithTooltip id="iwt-sidebar-drawer" label="Drawer">
+                <SidebarDrawerIcon />
+              </IconWithTooltip>
               <div className="custom-control custom-switch custom-checkbox-secondary ml-2">
                 <input
                   id="swSidebarMode"
@@ -144,15 +149,21 @@ const PersonalDropdown = (props) => {
                 />
                 <label className="custom-control-label" htmlFor="swSidebarMode"></label>
               </div>
-              <DockIcon id="icon-prefer-dock" />
+              <IconWithTooltip id="iwt-sidebar-dock" label="Dock">
+                <SidebarDockIcon />
+              </IconWithTooltip>
             </div>
           </div>
         </form>
+
+        {/* Sidebar Mode on Editor */}
         <h6 className="dropdown-header">{t('personal_dropdown.sidebar_mode_editor')}</h6>
         <form className="px-4">
           <div className="form-row justify-content-center">
             <div className="form-group col-auto mb-0 d-flex align-items-center">
-              <DrawerIcon id="icon-prefer-drawer-on-edit" />
+              <IconWithTooltip id="iwt-sidebar-editor-drawer" label="Drawer">
+                <SidebarDrawerIcon />
+              </IconWithTooltip>
               <div className="custom-control custom-switch custom-checkbox-secondary ml-2">
                 <input
                   id="swSidebarModeOnEditor"
@@ -163,13 +174,16 @@ const PersonalDropdown = (props) => {
                 />
                 <label className="custom-control-label" htmlFor="swSidebarModeOnEditor"></label>
               </div>
-              <DockIcon id="icon-prefer-dock-on-edit" />
+              <IconWithTooltip id="iwt-sidebar-editor-dock" label="Dock">
+                <SidebarDockIcon />
+              </IconWithTooltip>
             </div>
           </div>
         </form>
 
         <div className="dropdown-divider"></div>
 
+        {/* Color Mode */}
         <h6 className="dropdown-header">{t('personal_dropdown.color_mode')}</h6>
         <form className="px-4">
           <div className="form-row">
@@ -188,7 +202,9 @@ const PersonalDropdown = (props) => {
           </div>
           <div className="form-row justify-content-center">
             <div className="form-group col-auto mb-0 d-flex align-items-center">
-              <span className={useOsSettings ? '' : 'text-muted'}>Light</span>
+              <IconWithTooltip id="iwt-light" label="Light" additionalClasses={useOsSettings ? 'grw-icon-container-muted' : ''}>
+                <SunIcon />
+              </IconWithTooltip>
               <div className="custom-control custom-switch custom-checkbox-secondary ml-2">
                 <input
                   id="swUserPreference"
@@ -200,7 +216,9 @@ const PersonalDropdown = (props) => {
                 />
                 <label className="custom-control-label" htmlFor="swUserPreference"></label>
               </div>
-              <span className={useOsSettings ? '' : 'text-muted'}>Dark</span>
+              <IconWithTooltip id="iwt-dark" label="Dark" additionalClasses={useOsSettings ? 'grw-icon-container-muted' : ''}>
+                <MoonIcon />
+              </IconWithTooltip>
             </div>
           </div>
         </form>

+ 31 - 37
src/client/js/components/PageDuplicateModal.jsx

@@ -7,11 +7,13 @@ import {
 
 import { withTranslation } from 'react-i18next';
 import { withUnstatedContainers } from './UnstatedUtils';
+import { toastError } from '../util/apiNotification';
 
 import AppContainer from '../services/AppContainer';
 import PageContainer from '../services/PageContainer';
 import PagePathAutoComplete from './PagePathAutoComplete';
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
+import ComparePathsTable from './ComparePathsTable';
 
 
 const PageDuplicateModal = (props) => {
@@ -26,15 +28,9 @@ const PageDuplicateModal = (props) => {
 
   const [errs, setErrs] = useState(null);
 
-  // ToDo: subordinatedPaths is not used yet so commented. Will use when all the child of { path } is needed.
-  // const [subordinatedPaths, setSubordinatedPaths] = useState([]);
-  // for now we use the code below.
-  const [setSubordinatedPaths] = useState([]);
-  const [getSubordinatedError, setGetSuborinatedError] = useState(null);
-  const [isDuplicateRecursively, setIsDuplicateRecursively] = useState(true);
-  const [isDuplicateRecursivelyWithoutExistPath, setIsDuplicateRecursivelyWithoutExistPath] = useState(true);
-  const [isDuplicateExistList, setIsDuplicateExistList] = useState([]);
-
+  const [subordinatedPages, setSubordinatedPages] = useState([]);
+  const [isDuplicateRecursively, setIsDuplicateRecursively] = useState(false);
+  const [isDuplicateRecursivelyWithoutExistPath, setIsDuplicateRecursivelyWithoutExistPath] = useState(false);
 
   function getSubordinatedDuplicateList(value) {
 
@@ -46,7 +42,6 @@ const PageDuplicateModal = (props) => {
     // setIsDuplicateExist(duplicatedList);
 
     // ToDo: for now we use dummy path
-    setIsDuplicateExistList(['/test146/test147', value]);
   }
 
   /**
@@ -55,6 +50,8 @@ const PageDuplicateModal = (props) => {
    */
   function ppacInputChangeHandler(value) {
     getSubordinatedDuplicateList(value);
+    setErrorCode(null);
+    setErrorMessage(null);
     setPageNameInput(value);
   }
 
@@ -64,6 +61,8 @@ const PageDuplicateModal = (props) => {
    */
   function inputChangeHandler(value) {
     getSubordinatedDuplicateList(value);
+    setErrorCode(null);
+    setErrorMessage(null);
     setPageNameInput(value);
   }
 
@@ -74,10 +73,12 @@ const PageDuplicateModal = (props) => {
   const getSubordinatedList = useCallback(async() => {
     try {
       const res = await appContainer.apiv3Get('/pages/subordinated-list', { path });
-      setSubordinatedPaths(res.data.resultPaths);
+      const { subordinatedPaths } = res.data;
+      setSubordinatedPages(subordinatedPaths);
     }
     catch (err) {
-      setGetSuborinatedError(t('modal_duplicate.label.Fail to get subordinated pages'));
+      setErrs(err);
+      toastError(t('modal_duplicate.label.Fail to get subordinated pages'));
     }
   }, [appContainer, path, t]);
 
@@ -143,7 +144,7 @@ const PageDuplicateModal = (props) => {
             </div>
           </div>
         </div>
-        <div className="custom-control custom-checkbox custom-checkbox-warning">
+        <div className="custom-control custom-checkbox custom-checkbox-warning mb-3">
           <input
             className="custom-control-input"
             name="recursively"
@@ -156,30 +157,23 @@ const PageDuplicateModal = (props) => {
             { t('modal_duplicate.label.Duplicate with child') }
           </label>
         </div>
-
-        <div
-          className="custom-control custom-checkbox custom-checkbox-warning"
-          style={{ display: (isDuplicateExistList.length !== 0) && isDuplicateRecursively ? '' : 'none' }}
-        >
-          <input
-            className="custom-control-input"
-            name="withoutExistRecursively"
-            id="cbDuplicatewithoutExistRecursively"
-            type="checkbox"
-            checked={isDuplicateRecursivelyWithoutExistPath}
-            onChange={changeIsDuplicateRecursivelyWithoutExistPathHandler}
-          />
-          <label className="custom-control-label" htmlFor="cbDuplicatewithoutExistRecursively">
-            { t('modal_duplicate.label.Duplicate without exist path') }
-          </label>
-        </div>
-
-        <div>
-          <ul className="duplicate-name">
-            {isDuplicateRecursively && isDuplicateExistList.length !== 0 && isDuplicateExistList}
-          </ul>
-        </div>
-        <div> {getSubordinatedError} </div>
+        {isDuplicateRecursively && <ComparePathsTable subordinatedPages={subordinatedPages} newPagePath={pageNameInput} />}
+
+        {isDuplicateRecursively && (
+          <div className="custom-control custom-checkbox custom-checkbox-warning">
+            <input
+              className="custom-control-input"
+              name="withoutExistRecursively"
+              id="cbDuplicatewithoutExistRecursively"
+              type="checkbox"
+              checked={isDuplicateRecursivelyWithoutExistPath}
+              onChange={changeIsDuplicateRecursivelyWithoutExistPathHandler}
+            />
+            <label className="custom-control-label" htmlFor="cbDuplicatewithoutExistRecursively">
+              { t('modal_duplicate.label.Duplicate without exist path') }
+            </label>
+          </div>
+        )}
       </ModalBody>
       <ModalFooter>
         <ApiErrorMessageList errs={errs} targetPath={pageNameInput} />

Разница между файлами не показана из-за своего большого размера
+ 38 - 25
src/client/js/components/PageEditor/EditorIcon.jsx


+ 1 - 0
src/client/js/components/PageRenameModal.jsx

@@ -70,6 +70,7 @@ const PageRenameModal = (props) => {
    */
   function inputChangeHandler(value) {
     createSubordinatedList(value);
+    setErrs(null);
     setPageNameInput(value);
   }
 

+ 13 - 0
src/client/js/services/AdminAppContainer.js

@@ -238,6 +238,19 @@ export default class AdminAppContainer extends Container {
     return siteUrlSettingParams;
   }
 
+  /**
+   * Update from adress
+   * @memberOf AdminAppContainer
+   * @return {Array} Appearance
+   */
+  async updateFromAdressHandler() {
+    const response = await this.appContainer.apiv3.put('/app-settings/from-address', {
+      fromAddress: this.state.fromAddress,
+    });
+    const { mailSettingParams } = response.data;
+    return mailSettingParams;
+  }
+
   /**
    * Update mail setting
    * @memberOf AdminAppContainer

+ 2 - 2
src/client/js/services/PageHistoryContainer.js

@@ -55,7 +55,7 @@ export default class PageHistoryContainer extends Container {
       return;
     }
 
-    const res = await this.appContainer.apiv3Get('/revisions/list', { pageId, share_link_id: shareLinkId, selectedPage });
+    const res = await this.appContainer.apiv3Get('/revisions/list', { pageId, shareLinkId, selectedPage });
     const rev = res.data.docs;
 
     // set Pagination state
@@ -138,7 +138,7 @@ export default class PageHistoryContainer extends Container {
     }
 
     try {
-      const res = await this.appContainer.apiv3Get(`/revisions/${revision._id}`, { pageId, share_link_id: shareLinkId });
+      const res = await this.appContainer.apiv3Get(`/revisions/${revision._id}`, { pageId, shareLinkId });
       this.setState({
         revisions: this.state.revisions.map((rev) => {
           // comparing ObjectId

+ 0 - 2
src/client/js/services/PersonalContainer.js

@@ -29,7 +29,6 @@ export default class PersonalContainer extends Container {
       isUploadedPicture: false,
       uploadedPictureSrc: this.getUploadedPictureSrc(this.appContainer.currentUser),
       externalAccounts: [],
-      isPasswordSet: false,
       apiToken: '',
     };
 
@@ -55,7 +54,6 @@ export default class PersonalContainer extends Container {
         isEmailPublished: currentUser.isEmailPublished,
         lang: currentUser.lang,
         isGravatarEnabled: currentUser.isGravatarEnabled,
-        isPasswordSet: (currentUser.password != null),
         apiToken: currentUser.apiToken,
       });
     }

+ 1 - 1
src/client/styles/scss/_editor-navbar.scss

@@ -10,7 +10,7 @@
     }
 
     button {
-      padding: 8px;
+      padding: 0px;
       margin: 0 2px;
       font-size: 1rem;
       line-height: 1;

+ 7 - 4
src/client/styles/scss/_layout.scss

@@ -70,6 +70,10 @@ body {
     font-size: 24px;
 
     box-shadow: 2px 3px 6px #0000005d;
+    svg {
+      width: 28px;
+      height: 28px;
+    }
   }
 
   .btn-scroll-to-top {
@@ -77,10 +81,9 @@ body {
     height: 40px;
 
     opacity: 0.4;
-
-    i {
-      display: inline-block;
-      transform: rotate(90deg);
+    svg {
+      width: 18px;
+      height: 18px;
     }
   }
 }

+ 10 - 2
src/client/styles/scss/_navbar.scss

@@ -50,8 +50,16 @@
   }
 
   .grw-personal-dropdown {
-    .grw-email-sm {
-      font-size: 0.75em;
+    .dropdown-menu {
+      min-width: 15rem;
+
+      .grw-email-sm {
+        font-size: 0.75em;
+      }
+      .grw-icon-container svg {
+        width: 18px;
+        height: 18px;
+      }
     }
   }
 }

+ 36 - 1
src/client/styles/scss/_override-bootstrap-variables.scss

@@ -36,6 +36,9 @@ $font-family-base:        $font-family-sans-serif;
 $font-size-root: 14px;
 $line-height-base: 1.42857;
 
+$text-muted: $gray-500;
+$blockquote-small-color: $gray-500;
+
 //== Components
 //
 $border-radius:               .15rem;
@@ -43,16 +46,30 @@ $border-radius-sm:            .1rem;
 $border-radius-lg:            .25rem;
 $border-radius-xl:            .35rem;
 
+// Buttons
+//
+// For each of Bootstrap's buttons, define text, background, and border color.
+$btn-link-disabled-color: $gray-500;
 
 //== Forms
 //
+$input-border-color: $gray-300;
+
 $input-border-radius: $border-radius-sm;
 $input-border-radius-sm: $border-radius-sm;
 $input-border-radius-lg: $border-radius;
 
+$input-placeholder-color: $gray-500;
+
+$custom-control-indicator-border-color: $gray-400;
+$custom-control-label-disabled-color: $gray-500;
+$custom-select-disabled-color: $gray-500;
+$custom-range-thumb-disabled-bg: $gray-400;
+
 //== Navs
 $nav-link-padding-y: 0.75rem;
 $nav-link-padding-x: 1rem;
+$nav-link-disabled-color: $gray-500;
 
 //== Navbar
 $navbar-padding-y: 0;
@@ -61,11 +78,19 @@ $navbar-nav-link-padding-x: 1rem;
 
 //== Dropdowns
 $dropdown-border-radius: $border-radius-sm;
+$dropdown-link-disabled-color: $gray-500;
+$dropdown-header-color: $gray-500;
 
-//== card
+//== Pagination
+$pagination-disabled-color: $gray-500;
+
+//== Cards
 $card-spacer-y: 7px;
 $card-spacer-x: 15px;
 
+//== Toasts
+$toast-header-color: $gray-500;
+
 //== Modals
 $modal-content-border-width: 0;
 $modal-header-padding-y: 0.75rem;
@@ -82,6 +107,16 @@ $progress-border-radius: $border-radius-sm;
 $progress-bg: $gray-100;
 $progress-box-shadow: none;
 
+//== List group
+$list-group-disabled-color: $gray-500;
+
+//==  Figures
+$figure-caption-color: $gray-500;
+
+//==  Breadcrumbs
+$breadcrumb-divider-color: $gray-500;
+$breadcrumb-active-color: $gray-500;
+
 //== Code
 $pre-color: dummyinvalildcolor; // disable pre color specification with invalid value
 

+ 19 - 0
src/client/styles/scss/theme/_apply-colors.scss

@@ -79,6 +79,15 @@ pre:not(.hljs):not(.CodeMirror-line) {
   color: $color-global;
 }
 
+.grw-personal-dropdown {
+  .grw-icon-container svg {
+    fill: $color-global;
+  }
+  .grw-icon-container-muted svg {
+    fill: $secondary;
+  }
+}
+
 .dropdown-item {
   color: $color-global;
   &.active,
@@ -424,3 +433,13 @@ mark.rbt-highlight-text {
 .bg-box {
   background-color: $bgcolor-global;
 }
+
+.grw-fab {
+  .btn-create-page {
+    fill: color-yiq($primary);
+  }
+
+  .btn-scroll-to-top {
+    fill: $gray-900;
+  }
+}

+ 16 - 0
src/lib/util/path-utils.js

@@ -1,3 +1,5 @@
+const escapeStringRegexp = require('escape-string-regexp');
+
 /**
  * Whether path is the top page
  * @param {string} path
@@ -47,9 +49,23 @@ const userPageRoot = (user) => {
   return `/user/${user.username}`;
 };
 
+/**
+ * return user path
+ * @param {string} parentPath
+ * @param {string} childPath
+ * @param {string} newPath
+ *
+ * @return {string}
+ */
+const convertToNewAffiliationPath = (oldPath, newPath, childPath) => {
+  const pathRegExp = new RegExp(`^${escapeStringRegexp(oldPath)}`, 'i');
+  return childPath.replace(pathRegExp, newPath);
+};
+
 module.exports = {
   isTopPage,
   isTrashPage,
   isUserPage,
   userPageRoot,
+  convertToNewAffiliationPath,
 };

+ 2 - 2
src/server/middlewares/certify-shared-page.js

@@ -5,8 +5,8 @@ const logger = loggerFactory('growi:middleware:certify-shared-page');
 module.exports = (crowi) => {
 
   return async(req, res, next) => {
-    const pageId = req.query.page_id || req.body.page_id || null;
-    const shareLinkId = req.query.share_link_id || req.body.share_link_id || null;
+    const pageId = req.query.pageId || req.body.pageId || null;
+    const shareLinkId = req.query.shareLinkId || req.body.shareLinkId || null;
     if (pageId == null || shareLinkId == null) {
       return next();
     }

+ 3 - 28
src/server/models/user.js

@@ -20,7 +20,7 @@ module.exports = function(crowi) {
   const STATUS_DELETED = 4;
   const STATUS_INVITED = 5;
   const USER_PUBLIC_FIELDS = '_id image isEmailPublished isGravatarEnabled googleId name username email introduction'
-  + 'status lang createdAt lastLoginAt admin imageUrlCached';
+  + ' status lang createdAt lastLoginAt admin imageUrlCached';
 
   const PAGE_ITEMS = 50;
 
@@ -384,7 +384,7 @@ module.exports = function(crowi) {
     option = option || {};
 
     const sort = option.sort || { createdAt: -1 };
-    const fields = option.fields || USER_PUBLIC_FIELDS;
+    const fields = option.fields || {};
 
     let status = option.status || [STATUS_ACTIVE, STATUS_SUSPENDED];
     if (!Array.isArray(status)) {
@@ -403,7 +403,7 @@ module.exports = function(crowi) {
 
     const sort = option.sort || { createdAt: -1 };
     const status = option.status || STATUS_ACTIVE;
-    const fields = option.fields || USER_PUBLIC_FIELDS;
+    const fields = option.fields || {};
 
     return this.find({ _id: { $in: ids }, status })
       .select(fields)
@@ -414,31 +414,6 @@ module.exports = function(crowi) {
     return this.find({ admin: true });
   };
 
-  userSchema.statics.findUsersByPartOfEmail = function(emailPart, options) {
-    const status = options.status || null;
-    const emailPartRegExp = new RegExp(emailPart.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'));
-    const User = this;
-
-    return new Promise((resolve, reject) => {
-      const query = User.find({ email: emailPartRegExp }, USER_PUBLIC_FIELDS);
-
-      if (status) {
-        query.and({ status });
-      }
-
-      query
-        .limit(PAGE_ITEMS + 1)
-        .exec((err, userData) => {
-          if (err) {
-            return reject(err);
-          }
-
-          return resolve(userData);
-        });
-    });
-  };
-
-
   userSchema.statics.findUserByUsername = function(username) {
     if (username == null) {
       return Promise.resolve(null);

+ 0 - 17
src/server/routes/admin.js

@@ -337,23 +337,6 @@ module.exports = function(crowi, app) {
 
   actions.api = {};
 
-  // app.get('/_api/admin/users.search' , admin.api.userSearch);
-  actions.api.usersSearch = function(req, res) {
-    const User = crowi.model('User');
-    const email = req.query.email;
-
-    User.findUsersByPartOfEmail(email, {})
-      .then((users) => {
-        const result = {
-          data: users,
-        };
-        return res.json(ApiResponse.success(result));
-      })
-      .catch((err) => {
-        return res.json(ApiResponse.error());
-      });
-  };
-
   /**
    * save esa settings, update config cache, and response json
    *

+ 55 - 9
src/server/routes/apiv3/app-settings.js

@@ -50,13 +50,17 @@ const ErrorV3 = require('../../models/vo/error-apiv3');
  *          envSiteUrl:
  *            type: string
  *            description: environment variable 'APP_SITE_URL'
- *      MailSettingParams:
+ *      FromAddress:
  *        description: MailSettingParams
  *        type: object
  *        properties:
  *          fromAddress:
  *            type: string
  *            description: e-mail address used as from address of mail which sent from GROWI app
+ *      MailSettingParams:
+ *        description: MailSettingParams
+ *        type: object
+ *        properties:
  *          smtpHost:
  *            type: string
  *            description: host name of client's smtp server
@@ -115,16 +119,18 @@ module.exports = (crowi) => {
     siteUrlSetting: [
       body('siteUrl').trim().matches(/^(https?:\/\/[^/]+|)$/).isURL({ require_tld: false }),
     ],
+    fromAddress: [
+      body('fromAddress').trim().if(value => value !== '').isEmail(),
+    ],
     mailSetting: [
-      body('fromAddress').trim().isEmail(),
       body('smtpHost').trim(),
       body('smtpPort').trim().isPort(),
       body('smtpUser').trim(),
       body('smtpPassword').trim(),
     ],
     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('accessKeyId').trim().matches(/^[\da-zA-Z]+$/),
       body('secretAccessKey').trim(),
@@ -292,7 +298,12 @@ module.exports = (crowi) => {
    * validate mail setting send test mail
    */
   async function validateMailSetting(req) {
-    const { mailService } = crowi;
+    const { configManager, mailService } = crowi;
+    const fromAddress = configManager.getConfig('crowi', 'mail:from');
+    if (fromAddress == null) {
+      throw Error('fromAddress is not setup');
+    }
+
     const option = {
       host: req.body.smtpHost,
       port: req.body.smtpPort,
@@ -311,7 +322,7 @@ module.exports = (crowi) => {
     debug('mailer setup for validate SMTP setting', smtpClient);
 
     const mailOptions = {
-      from: req.body.fromAddress,
+      from: fromAddress,
       to: req.user.email,
       subject: 'Wiki管理設定のアップデートによるメール通知',
       text: 'このメールは、WikiのSMTP設定のアップデートにより送信されています。',
@@ -333,7 +344,6 @@ module.exports = (crowi) => {
     mailService.publishUpdatedMessage();
 
     return {
-      fromAddress: configManager.getConfig('crowi', 'mail:from'),
       smtpHost: configManager.getConfig('crowi', 'mail:smtpHost'),
       smtpPort: configManager.getConfig('crowi', 'mail:smtpPort'),
       smtpUser: configManager.getConfig('crowi', 'mail:smtpUser'),
@@ -341,6 +351,44 @@ module.exports = (crowi) => {
     };
   };
 
+  /**
+   * @swagger
+   *
+   *    /app-settings/from-address:
+   *      put:
+   *        tags: [AppSettings]
+   *        operationId: updateAppSettingFromAddress
+   *        summary: /app-settings/from-address
+   *        description: Update from address
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/FromAddress'
+   *        responses:
+   *          200:
+   *            description: Succeeded to update from adress
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  $ref: '#/components/schemas/FromAddress'
+   */
+  router.put('/from-address', loginRequiredStrictly, adminRequired, csrf, validator.fromAddress, apiV3FormValidator, async(req, res) => {
+
+    try {
+      const mailSettingParams = await updateMailSettinConfig({ 'mail:from': req.body.fromAddress });
+
+      return res.apiv3({ mailSettingParams });
+    }
+    catch (err) {
+      const msg = 'Error occurred in updating from adress';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'update-from-adress-failed'));
+    }
+
+  });
+
   /**
    * @swagger
    *
@@ -377,7 +425,6 @@ module.exports = (crowi) => {
 
 
     const requestMailSettingParams = {
-      'mail:from': req.body.fromAddress,
       'mail:smtpHost': req.body.smtpHost,
       'mail:smtpPort': req.body.smtpPort,
       'mail:smtpUser': req.body.smtpUser,
@@ -420,7 +467,6 @@ module.exports = (crowi) => {
    */
   router.delete('/mail-setting', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
     const requestMailSettingParams = {
-      'mail:from': null,
       'mail:smtpHost': null,
       'mail:smtpPort': null,
       'mail:smtpUser': null,

+ 1 - 4
src/server/routes/apiv3/pages.js

@@ -561,12 +561,9 @@ module.exports = (crowi) => {
 
     try {
       const pageData = await Page.findByPath(path);
-
       const result = await Page.findManageableListWithDescendants(pageData, req.user);
 
-      const resultPaths = result.map(element => element.path);
-
-      return res.apiv3({ resultPaths });
+      return res.apiv3({ subordinatedPaths: result });
     }
     catch (err) {
       return res.apiv3Err(new ErrorV3('Failed to update page.', 'unknown'), 500);

+ 46 - 2
src/server/routes/apiv3/personal-setting.js

@@ -123,8 +123,52 @@ module.exports = (crowi) => {
    *                      description: personal params
    */
   router.get('/', accessTokenParser, loginRequiredStrictly, async(req, res) => {
-    const currentUser = await User.findUserByUsername(req.user.username);
-    return res.apiv3({ currentUser });
+    const { username } = req.user;
+
+    try {
+      const user = await User.findUserByUsername(username);
+      const currentUser = user.toObject();
+      return res.apiv3({ currentUser });
+
+    }
+    catch (err) {
+      logger.error(err);
+      return res.apiv3Err('update-personal-settings-failed');
+    }
+  });
+
+  /**
+   * @swagger
+   *
+   *    /personal-setting/is-password-set:
+   *      get:
+   *        tags: [PersonalSetting]
+   *        operationId: getIsPasswordSet
+   *        summary: /personal-setting
+   *        description: Get whether a password has been set
+   *        responses:
+   *          200:
+   *            description: Whether a password has been set
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    isPasswordSet:
+   *                      type: boolean
+   */
+  router.get('/is-password-set', accessTokenParser, loginRequiredStrictly, async(req, res) => {
+    const { username } = req.user;
+
+    try {
+      const user = await User.findUserByUsername(username);
+      const isPasswordSet = user.isPasswordSet();
+      return res.apiv3({ isPasswordSet });
+    }
+    catch (err) {
+      logger.error(err);
+      return res.apiv3Err('fail-to-get-whether-password-is-set');
+    }
+
   });
 
   /**

+ 2 - 1
src/server/routes/apiv3/users.js

@@ -119,7 +119,7 @@ module.exports = (crowi) => {
    *            description: page number
    *            schema:
    *              type: number
-   *          - name:  selectedStatusList
+   *          - name: selectedStatusList
    *            in: query
    *            description: status list
    *            schema:
@@ -184,6 +184,7 @@ module.exports = (crowi) => {
           sort: sortOutput,
           page,
           limit: PAGE_ITEMS,
+          select: User.USER_PUBLIC_FIELDS,
         },
       );
       return res.apiv3({ paginateResult });

+ 4 - 1
src/server/routes/attachment.js

@@ -228,7 +228,10 @@ module.exports = function(crowi, app) {
     }
     // reference
     else {
-      res.set('Content-Type', attachment.fileFormat);
+      res.set({
+        'Content-Type': attachment.fileFormat,
+        'Content-Security-Policy': "script-src 'unsafe-hashes'",
+      });
     }
   }
 

+ 0 - 1
src/server/routes/index.js

@@ -89,7 +89,6 @@ module.exports = function(crowi, app) {
   app.get('/admin/notification'              , loginRequiredStrictly , adminRequired , admin.notification.index);
   app.get('/admin/notification/slackAuth'    , loginRequiredStrictly , adminRequired , admin.notification.slackAuth);
   app.get('/admin/notification/slackSetting/disconnect', loginRequiredStrictly , adminRequired , admin.notification.disconnectFromSlack);
-  app.get('/_api/admin/users.search'         , loginRequiredStrictly , adminRequired , admin.api.usersSearch);
   app.get('/admin/global-notification/new'   , loginRequiredStrictly , adminRequired , admin.globalNotification.detail);
   app.get('/admin/global-notification/:id'   , loginRequiredStrictly , adminRequired , admin.globalNotification.detail);
 

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

@@ -50,11 +50,17 @@ class ExportService {
    * @return {object} info for zip files and whether currentProgressingStatus exists
    */
   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);
       return this.growiBridgeService.parseZipFile(zipFile);
-    }));
+    });
+    for await (const stat of parseZipFilePromises) {
+      zipFileStats.push(stat);
+    }
 
     // filter null object (broken zip)
     const filtered = zipFileStats.filter(element => element != null);
@@ -166,9 +172,7 @@ class ExportService {
     const collection = mongoose.connection.collection(collectionName);
 
     const nativeCursor = collection.find();
-    const readStream = nativeCursor
-      .snapshot()
-      .stream({ transform: JSON.stringify });
+    const readStream = nativeCursor.stream({ transform: JSON.stringify });
 
     // get TransformStream
     const transformStream = this.generateTransformStream();
@@ -201,8 +205,12 @@ class ExportService {
   async exportCollectionsToZippedJson(collections) {
     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
     this.emitStartZippingEvent();

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

@@ -128,10 +128,16 @@ class ImportService {
    */
   async getStatus() {
     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);
       return this.growiBridgeService.parseZipFile(zipFile);
-    }));
+    });
+    for await (const stat of parseZipFilePromises) {
+      zipFileStats.push(stat);
+    }
 
     // filter null object (broken zip)
     const filtered = zipFileStats
@@ -158,23 +164,25 @@ class ImportService {
     // init status object
     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: 'revision', model: 'Revision', select: 'body' },
       ])
-      .snapshot()
       .lean()
       .cursor();
 

+ 0 - 7
src/test/models/user.test.js

@@ -42,13 +42,6 @@ describe('User', () => {
         expect(user.name).toBe('Example for User Test');
       });
 
-      test('should be found by findUsersByPartOfEmail', async() => {
-        const users = await User.findUsersByPartOfEmail('usert', {});
-        expect(users).toBeInstanceOf(Array);
-        expect(users.length).toBe(2);
-        expect(users[0]).toBeInstanceOf(User);
-        expect(users[1]).toBeInstanceOf(User);
-      });
     });
   });
 

Разница между файлами не показана из-за своего большого размера
+ 396 - 215
yarn.lock


Некоторые файлы не были показаны из-за большого количества измененных файлов