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

Merge branch 'master' into feat/can-upload-logo

Yohei-Shiina 3 лет назад
Родитель
Сommit
c75a7d61cf
100 измененных файлов с 655 добавлено и 699 удалено
  1. 33 11
      THIRD-PARTY-NOTICES.md
  2. BIN
      packages/app/public/static/fonts/Lato-Bold-latin-ext.woff2
  3. BIN
      packages/app/public/static/fonts/Lato-Bold-latin.woff2
  4. BIN
      packages/app/public/static/fonts/Lato-Regular-latin-ext.woff2
  5. BIN
      packages/app/public/static/fonts/Lato-Regular-latin.woff2
  6. BIN
      packages/app/public/static/fonts/PressStart2P-latin-ext.woff2
  7. BIN
      packages/app/public/static/fonts/PressStart2P-latin.woff2
  8. 0 4
      packages/app/public/static/locales/en_US/admin.json
  9. 17 0
      packages/app/public/static/locales/en_US/commons.json
  10. 2 5
      packages/app/public/static/locales/en_US/translation.json
  11. 0 4
      packages/app/public/static/locales/ja_JP/admin.json
  12. 17 0
      packages/app/public/static/locales/ja_JP/commons.json
  13. 2 5
      packages/app/public/static/locales/ja_JP/translation.json
  14. 0 4
      packages/app/public/static/locales/zh_CN/admin.json
  15. 17 0
      packages/app/public/static/locales/zh_CN/commons.json
  16. 2 6
      packages/app/public/static/locales/zh_CN/translation.json
  17. 2 2
      packages/app/resource/locales/en_US/welcome.md
  18. 2 2
      packages/app/resource/locales/ja_JP/welcome.md
  19. 2 2
      packages/app/resource/locales/zh_CN/welcome.md
  20. 0 73
      packages/app/src/client/services/AdminCustomizeContainer.js
  21. 23 30
      packages/app/src/client/services/PageContainer.js
  22. 2 2
      packages/app/src/components/Admin/App/AppSetting.jsx
  23. 1 1
      packages/app/src/components/Admin/App/AppSettingsPageContents.tsx
  24. 2 2
      packages/app/src/components/Admin/App/FileUploadSetting.tsx
  25. 2 2
      packages/app/src/components/Admin/App/MailSetting.tsx
  26. 1 1
      packages/app/src/components/Admin/App/PluginSetting.tsx
  27. 1 1
      packages/app/src/components/Admin/App/SiteUrlSetting.tsx
  28. 1 1
      packages/app/src/components/Admin/Common/AdminNavigation.jsx
  29. 0 4
      packages/app/src/components/Admin/Customize/Customize.jsx
  30. 1 1
      packages/app/src/components/Admin/Customize/CustomizeCssSetting.tsx
  31. 1 1
      packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.tsx
  32. 2 2
      packages/app/src/components/Admin/Customize/CustomizeHeaderSetting.tsx
  33. 0 148
      packages/app/src/components/Admin/Customize/CustomizeHighlightSetting.tsx
  34. 1 1
      packages/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx
  35. 3 3
      packages/app/src/components/Admin/Customize/CustomizeLogoSetting.tsx
  36. 2 2
      packages/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx
  37. 3 2
      packages/app/src/components/Admin/Customize/CustomizeSidebarSetting.tsx
  38. 1 1
      packages/app/src/components/Admin/Customize/CustomizeThemeSetting.tsx
  39. 1 1
      packages/app/src/components/Admin/Customize/CustomizeTitle.tsx
  40. 1 1
      packages/app/src/components/Admin/ElasticsearchManagement/ElasticsearchManagement.tsx
  41. 44 43
      packages/app/src/components/Admin/ImportData/GrowiArchive/ImportForm.jsx
  42. 3 4
      packages/app/src/components/Admin/ImportData/GrowiArchiveSection.jsx
  43. 1 1
      packages/app/src/components/Admin/MarkdownSetting/IndentForm.tsx
  44. 1 1
      packages/app/src/components/Admin/MarkdownSetting/LineBreakForm.jsx
  45. 1 1
      packages/app/src/components/Admin/MarkdownSetting/PresentationForm.jsx
  46. 1 1
      packages/app/src/components/Admin/MarkdownSetting/XssForm.jsx
  47. 1 1
      packages/app/src/components/Admin/Notification/GlobalNotification.jsx
  48. 1 1
      packages/app/src/components/Admin/Security/GitHubSecuritySettingContents.jsx
  49. 1 1
      packages/app/src/components/Admin/Security/GoogleSecuritySettingContents.jsx
  50. 2 2
      packages/app/src/components/Admin/Security/OidcSecuritySettingContents.jsx
  51. 1 1
      packages/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx
  52. 1 1
      packages/app/src/components/Admin/Security/SecuritySetting.jsx
  53. 1 1
      packages/app/src/components/Admin/Security/TwitterSecuritySettingContents.jsx
  54. 2 2
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx
  55. 1 1
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySecretTokenSection.jsx
  56. 2 2
      packages/app/src/components/Admin/SlackIntegration/ManageCommandsProcess.jsx
  57. 2 2
      packages/app/src/components/Admin/SlackIntegration/ManageCommandsProcessWithoutProxy.jsx
  58. 1 1
      packages/app/src/components/Admin/SlackIntegration/OfficialBotSettings.jsx
  59. 1 1
      packages/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx
  60. 2 2
      packages/app/src/components/Admin/UserGroup/UserGroupPage.tsx
  61. 5 5
      packages/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx
  62. 38 0
      packages/app/src/components/AlertSiteUrlUndefined.tsx
  63. 33 12
      packages/app/src/components/Common/ImageCropModal.tsx
  64. 1 1
      packages/app/src/components/CustomNavigation/CustomNav.jsx
  65. 1 1
      packages/app/src/components/Fab.tsx
  66. 1 1
      packages/app/src/components/Hotkeys/HotkeysDetector.jsx
  67. 3 4
      packages/app/src/components/Hotkeys/HotkeysManager.jsx
  68. 1 0
      packages/app/src/components/Hotkeys/Subscribers/SwitchToMirrorMode.jsx
  69. 11 6
      packages/app/src/components/InstallerForm.tsx
  70. 6 0
      packages/app/src/components/Layout/AdminLayout.tsx
  71. 2 0
      packages/app/src/components/Layout/BasicLayout.tsx
  72. 1 1
      packages/app/src/components/Layout/RawLayout.tsx
  73. 1 1
      packages/app/src/components/Me/ApiSettings.tsx
  74. 1 1
      packages/app/src/components/Me/BasicInfoSettings.tsx
  75. 44 31
      packages/app/src/components/Me/EditorSettings.tsx
  76. 1 1
      packages/app/src/components/Me/InAppNotificationSettings.tsx
  77. 2 2
      packages/app/src/components/Me/PasswordSettings.jsx
  78. 3 3
      packages/app/src/components/Me/ProfileImageSettings.tsx
  79. 1 1
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  80. 8 5
      packages/app/src/components/Page.tsx
  81. 2 1
      packages/app/src/components/Page/CopyDropdown.jsx
  82. 1 1
      packages/app/src/components/Page/CopyDropdown.module.scss
  83. 11 13
      packages/app/src/components/Page/DisplaySwitcher.tsx
  84. 1 1
      packages/app/src/components/PageComment.tsx
  85. 15 8
      packages/app/src/components/PageEditor.tsx
  86. 22 14
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx
  87. 1 12
      packages/app/src/components/PageEditor/CodeMirrorEditor.module.scss
  88. 1 1
      packages/app/src/components/PageEditor/Editor.module.scss
  89. 15 0
      packages/app/src/components/PageEditorByHackmd.tsx
  90. 13 13
      packages/app/src/components/PagePathHierarchicalLink.tsx
  91. 2 2
      packages/app/src/components/PasswordResetExecutionForm.tsx
  92. 22 0
      packages/app/src/components/RevisionComparer/RevisionComparer.module.scss
  93. 7 0
      packages/app/src/components/Sidebar/CustomSidebar.module.scss
  94. 4 2
      packages/app/src/components/Sidebar/CustomSidebar.tsx
  95. 1 1
      packages/app/src/components/Sidebar/RecentChanges.module.scss
  96. 9 7
      packages/app/src/components/Sidebar/Tag.tsx
  97. 0 143
      packages/app/src/components/StaffCredit/StaffCredit.jsx
  98. 5 4
      packages/app/src/components/StaffCredit/StaffCredit.module.scss
  99. 139 0
      packages/app/src/components/StaffCredit/StaffCredit.tsx
  100. 5 5
      packages/app/src/components/TableOfContents.tsx

+ 33 - 11
THIRD-PARTY-NOTICES.md

@@ -13,10 +13,12 @@ https://github.com/weseek/growi.
 
 
 
 
 1. Apache License, Version 2.0 Derivative Works
 1. Apache License, Version 2.0 Derivative Works
-2. crowi/crowi (https://github.com/crowi/crowi)
-3. Microsoft/vscode (https://github.com/Microsoft/vscode)
-4. stephenhutchings/typicons.font (https://github.com/stephenhutchings/typicons.font)
-5. Kuromoji.js (https://github.com/takuyaa/kuromoji.js)
+1. crowi/crowi (https://github.com/crowi/crowi)
+1. Microsoft/vscode (https://github.com/Microsoft/vscode)
+1. Kuromoji.js (https://github.com/takuyaa/kuromoji.js)
+1. Lato (https://fonts.google.com/specimen/Lato)
+1. Press Start 2P (https://fonts.google.com/specimen/Press+Start+2P)
+1. stephenhutchings/typicons.font (https://github.com/stephenhutchings/typicons.font)
 
 
 
 
 License Notice for Apache License, Version 2.0 Derivative Works
 License Notice for Apache License, Version 2.0 Derivative Works
@@ -90,21 +92,41 @@ SOFTWARE.
 ```
 ```
 
 
 
 
-License Notice for Typicons
+License Notice for Kuromoji.js
 ------------------------
 ------------------------
 
 
-https://creativecommons.org/licenses/by-sa/3.0/
+https://github.com/takuyaa/kuromoji.js/blob/master/LICENSE-2.0.txt
 
 
 ```
 ```
-Copyright (c) 2018 Stephen Hutchings
+author: "Takuya Asano <takuya.a@gmail.com>"
 ```
 ```
 
 
 
 
-License Notice for Kuromoji.js
-------------------------
+License Notice for Lato
+---------------------
 
 
-https://github.com/takuyaa/kuromoji.js/blob/master/LICENSE-2.0.txt
+https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL
 
 
 ```
 ```
-author: "Takuya Asano <takuya.a@gmail.com>"
+Designed by Łukasz Dziedzic 
 ```
 ```
+
+
+License Notice for Press Start 2P
+------------------------------
+
+https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL
+
+```
+Designed by CodeMan38
+```
+
+
+License Notice for Typicons
+------------------------
+
+https://creativecommons.org/licenses/by-sa/3.0/
+
+```
+Copyright (c) 2018 Stephen Hutchings
+```

BIN
packages/app/public/static/fonts/Lato-Bold-latin-ext.woff2


BIN
packages/app/public/static/fonts/Lato-Bold-latin.woff2


BIN
packages/app/public/static/fonts/Lato-Regular-latin-ext.woff2


BIN
packages/app/public/static/fonts/Lato-Regular-latin.woff2


BIN
packages/app/public/static/fonts/PressStart2P-latin-ext.woff2


BIN
packages/app/public/static/fonts/PressStart2P-latin.woff2


+ 0 - 4
packages/app/public/static/locales/en_US/admin.json

@@ -3,7 +3,6 @@
     "display_name": "English"
     "display_name": "English"
   },
   },
   "wiki_management_home_page": "Wiki Management Home Page",
   "wiki_management_home_page": "Wiki Management Home Page",
-  "app_settings": "App Settings",
   "security_settings": {
   "security_settings": {
     "security_settings": "Security Settings",
     "security_settings": "Security Settings",
     "Guest Users Access": "Guest users access",
     "Guest Users Access": "Guest users access",
@@ -46,7 +45,6 @@
     "page_delete_rights_caution": "The \"Delete / Delete All\" permission (including descendant pages) is forced to be stronger than the \"Delete / Completely Delete\" permission. <br> <br> Admin only > Admin and autor > Anyone",
     "page_delete_rights_caution": "The \"Delete / Delete All\" permission (including descendant pages) is forced to be stronger than the \"Delete / Completely Delete\" permission. <br> <br> Admin only > Admin and autor > Anyone",
     "Authentication mechanism settings": "Authentication Mechanism Settings",
     "Authentication mechanism settings": "Authentication Mechanism Settings",
     "setup_is_not_yet_complete": "Setup is not yet complete",
     "setup_is_not_yet_complete": "Setup is not yet complete",
-    "alert_siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the {{link}}",
     "xss_prevent_setting": "Prevent XSS(Cross Site Scripting)",
     "xss_prevent_setting": "Prevent XSS(Cross Site Scripting)",
     "xss_prevent_setting_link": "Go to Markdown Settings",
     "xss_prevent_setting_link": "Go to Markdown Settings",
     "callback_URL": "Callback URL",
     "callback_URL": "Callback URL",
@@ -467,8 +465,6 @@
       "select_search_scope_children_as_default": "Select 'Only children of this tree' as default value of search range",
       "select_search_scope_children_as_default": "Select 'Only children of this tree' as default value of search range",
       "select_search_scope_children_as_default_desc": "When the setting value is off, 'All pages' is used as default value of search range."
       "select_search_scope_children_as_default_desc": "When the setting value is off, 'All pages' is used as default value of search range."
     },
     },
-    "code_highlight": "Code highlight",
-    "nocdn_desc": "This function is disabled when the environment variable <code>NO_CDN=true</code>.<br>Github style has been forcibly applied.",
     "custom_title": "Custom title",
     "custom_title": "Custom title",
     "custom_title_detail": "You can customize <code>&lt;title&gt;</code> tag. Following placeholders will be automatically replaced:",
     "custom_title_detail": "You can customize <code>&lt;title&gt;</code> tag. Following placeholders will be automatically replaced:",
     "custom_title_detail_placeholder1": "<code>&#123;&#123;sitename&#125;&#125;</code> - The site name of this wiki.",
     "custom_title_detail_placeholder1": "<code>&#123;&#123;sitename&#125;&#125;</code> - The site name of this wiki.",

+ 17 - 0
packages/app/public/static/locales/en_US/commons.json

@@ -0,0 +1,17 @@
+{
+  "meta": {
+    "display_name": "English"
+  },
+  "toaster": {
+    "create_succeeded": "Succeeded to create {{target}}",
+    "create_failed": "Failed to create {{target}}",
+    "update_successed": "Succeeded to update {{target}}",
+    "update_failed": "Failed to update {{target}}"
+  },
+  "alert": {
+    "siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the {{link}}"
+  },
+  "headers": {
+    "app_settings": "App Settings"
+  }
+}

+ 2 - 5
packages/app/public/static/locales/en_US/translation.json

@@ -523,10 +523,6 @@
     "page_not_found_in_preview": "\"{{path}}\" is not a GROWI page."
     "page_not_found_in_preview": "\"{{path}}\" is not a GROWI page."
   },
   },
   "toaster": {
   "toaster": {
-    "create_succeeded": "Succeeded to create {{target}}",
-    "create_failed": "Failed to create {{target}}",
-    "update_successed": "Succeeded to update {{target}}",
-    "update_failed": "Failed to update {{target}}",
     "file_upload_succeeded": "File upload succeeded.",
     "file_upload_succeeded": "File upload succeeded.",
     "file_upload_failed": "File upload failed.",
     "file_upload_failed": "File upload failed.",
     "initialize_successed": "Succeeded to initialize {{target}}",
     "initialize_successed": "Succeeded to initialize {{target}}",
@@ -540,7 +536,8 @@
     "issue_share_link": "Succeeded to issue new share link",
     "issue_share_link": "Succeeded to issue new share link",
     "remove_share_link": "Succeeded to remove {{count}} share links",
     "remove_share_link": "Succeeded to remove {{count}} share links",
     "switch_disable_link_sharing_success": "Succeeded to update share link setting",
     "switch_disable_link_sharing_success": "Succeeded to update share link setting",
-    "failed_to_reset_password":"Failed to reset password"
+    "failed_to_reset_password":"Failed to reset password",
+    "save_succeeded": "Saved successfully"
   },
   },
   "template": {
   "template": {
     "modal_label": {
     "modal_label": {

+ 0 - 4
packages/app/public/static/locales/ja_JP/admin.json

@@ -12,7 +12,6 @@
   "Description": "説明",
   "Description": "説明",
   "last_login": "最終ログイン",
   "last_login": "最終ログイン",
   "wiki_management_home_page": "Wiki管理トップ",
   "wiki_management_home_page": "Wiki管理トップ",
-  "app_settings": "アプリ設定",
   "public": "公開",
   "public": "公開",
   "anyone_with_the_link": "リンクを知っている人のみ",
   "anyone_with_the_link": "リンクを知っている人のみ",
   "specified_users": "特定ユーザーのみ",
   "specified_users": "特定ユーザーのみ",
@@ -62,7 +61,6 @@
     "page_delete_rights_caution": "「(子孫ページを含む)ゴミ箱に入れる操作 / 完全に削除する」の権限は、「ゴミ箱に入れる操作 / 完全に削除する」よりも強い権限になるように強制されます。 <br><br> 管理者のみ可能 > 管理者とページ作者が可能 > 誰でも可能",
     "page_delete_rights_caution": "「(子孫ページを含む)ゴミ箱に入れる操作 / 完全に削除する」の権限は、「ゴミ箱に入れる操作 / 完全に削除する」よりも強い権限になるように強制されます。 <br><br> 管理者のみ可能 > 管理者とページ作者が可能 > 誰でも可能",
     "Authentication mechanism settings": "認証機構設定",
     "Authentication mechanism settings": "認証機構設定",
     "setup_is_not_yet_complete":"セットアップはまだ完了してません",
     "setup_is_not_yet_complete":"セットアップはまだ完了してません",
-    "alert_siteUrl_is_not_set": "'サイトURL' が設定されていません。{{link}} から設定してください。",
     "xss_prevent_setting": "XSS(Cross Site Scripting)対策設定",
     "xss_prevent_setting": "XSS(Cross Site Scripting)対策設定",
     "xss_prevent_setting_link": "マークダウン設定ページに移動",
     "xss_prevent_setting_link": "マークダウン設定ページに移動",
     "callback_URL": "コールバックURL",
     "callback_URL": "コールバックURL",
@@ -499,8 +497,6 @@
       "select_search_scope_children_as_default": "検索範囲のデフォルト設定を「この階層下の子ページ」にする",
       "select_search_scope_children_as_default": "検索範囲のデフォルト設定を「この階層下の子ページ」にする",
       "select_search_scope_children_as_default_desc": "OFFの場合、検索範囲のデフォルト設定は「全てのページ」になります。"
       "select_search_scope_children_as_default_desc": "OFFの場合、検索範囲のデフォルト設定は「全てのページ」になります。"
     },
     },
-    "code_highlight": "コードハイライト",
-    "nocdn_desc": "この機能は、環境変数 <code>NO_CDN=true</code> の時は無効化されます。<br>GitHub スタイルが適用されています。",
     "custom_title": "カスタム Title",
     "custom_title": "カスタム Title",
     "custom_title_detail": "<code>&lt;title&gt;</code>タグのコンテンツをカスタマイズできます。以下のプレースホルダーは自動的に置換されます:",
     "custom_title_detail": "<code>&lt;title&gt;</code>タグのコンテンツをカスタマイズできます。以下のプレースホルダーは自動的に置換されます:",
     "custom_title_detail_placeholder1": "<code>&#123;&#123;sitename&#125;&#125;</code> - この Wiki のサイト名",
     "custom_title_detail_placeholder1": "<code>&#123;&#123;sitename&#125;&#125;</code> - この Wiki のサイト名",

+ 17 - 0
packages/app/public/static/locales/ja_JP/commons.json

@@ -0,0 +1,17 @@
+{
+  "meta": {
+    "display_name": "日本語"
+  },
+  "toaster": {
+    "create_succeeded": "新しい{{target}}が作成されました",
+    "create_failed": "{{target}}の作成に失敗しました",
+    "update_successed": "{{target}}を更新しました",
+    "update_failed": "{{target}}の更新に失敗しました"
+  },
+  "alert": {
+    "siteUrl_is_not_set": "'サイトURL' が設定されていません。{{link}} から設定してください。"
+  },
+  "headers": {
+    "app_settings": "アプリ設定"
+  }
+}

+ 2 - 5
packages/app/public/static/locales/ja_JP/translation.json

@@ -514,10 +514,6 @@
     "page_not_found_in_preview": "\"{{path}}\" というページはありません。"
     "page_not_found_in_preview": "\"{{path}}\" というページはありません。"
   },
   },
   "toaster": {
   "toaster": {
-    "create_succeeded": "新しい{{target}}が作成されました",
-    "create_failed": "{{target}}の作成に失敗しました",
-    "update_successed": "{{target}}を更新しました",
-    "update_failed": "{{target}}の更新に失敗しました",
     "file_upload_succeeded": "ファイルをアップロードしました",
     "file_upload_succeeded": "ファイルをアップロードしました",
     "file_upload_failed": "ファイルのアップロードに失敗しました",
     "file_upload_failed": "ファイルのアップロードに失敗しました",
     "initialize_successed": "{{target}}を初期化しました",
     "initialize_successed": "{{target}}を初期化しました",
@@ -531,7 +527,8 @@
     "issue_share_link": "共有リンクを作成しました",
     "issue_share_link": "共有リンクを作成しました",
     "remove_share_link": "共有リンクを{{count}}件削除しました",
     "remove_share_link": "共有リンクを{{count}}件削除しました",
     "switch_disable_link_sharing_success": "共有リンクの設定を変更しました",
     "switch_disable_link_sharing_success": "共有リンクの設定を変更しました",
-    "failed_to_reset_password":"パスワードのリセットに失敗しました"
+    "failed_to_reset_password":"パスワードのリセットに失敗しました",
+    "save_succeeded": "保存に成功しました"
   },
   },
   "template": {
   "template": {
     "modal_label": {
     "modal_label": {

+ 0 - 4
packages/app/public/static/locales/zh_CN/admin.json

@@ -10,7 +10,6 @@
   "Edit": "编辑",
   "Edit": "编辑",
   "Description": "描述",
   "Description": "描述",
   "wiki_management_home_page": "Wiki管理首页",
   "wiki_management_home_page": "Wiki管理首页",
-  "app_settings": "系统设置",
   "public": "公共",
   "public": "公共",
   "anyone_with_the_link": "任何人",
   "anyone_with_the_link": "任何人",
   "specified_users": "仅指定用户",
   "specified_users": "仅指定用户",
@@ -60,7 +59,6 @@
     "page_delete_rights_caution": "\"删除/全部删除\"权限(包括后代页面)被强制强于\"删除/完全删除\"权限。 <br> <br> 仅管理员 > 管理员|作者 > 何人",
     "page_delete_rights_caution": "\"删除/全部删除\"权限(包括后代页面)被强制强于\"删除/完全删除\"权限。 <br> <br> 仅管理员 > 管理员|作者 > 何人",
 		"Authentication mechanism settings": "身份验证机制设置",
 		"Authentication mechanism settings": "身份验证机制设置",
 		"setup_is_not_yet_complete": "安装尚未完成",
 		"setup_is_not_yet_complete": "安装尚未完成",
-		"alert_siteUrl_is_not_set": "主页URL未设置,通过 {{link}} 设置",
 		"xss_prevent_setting": "阻止XSS(跨站点脚本)",
 		"xss_prevent_setting": "阻止XSS(跨站点脚本)",
 		"xss_prevent_setting_link": "转到Markdown设置",
 		"xss_prevent_setting_link": "转到Markdown设置",
 		"callback_URL": "回调URL",
 		"callback_URL": "回调URL",
@@ -464,8 +462,6 @@
       "select_search_scope_children_as_default": "选择“当前分支以下内容”, 作为搜索范围的默认值",
       "select_search_scope_children_as_default": "选择“当前分支以下内容”, 作为搜索范围的默认值",
       "select_search_scope_children_as_default_desc": "当设置值为“关”时,“所有页面”被作为搜索范围的默认值。"
       "select_search_scope_children_as_default_desc": "当设置值为“关”时,“所有页面”被作为搜索范围的默认值。"
     },
     },
-    "code_highlight": "代码突出显示",
-    "nocdn_desc": "当强制应用环境变量<code>NO_CDN=true</code><br>Github样式时,此函数被禁用。",
     "custom_title": "自定义标题",
     "custom_title": "自定义标题",
     "custom_title_detail": "您可以自定义<code>&lt;title&gt;</code>标记。<br><code>&123;&123;sitename&&125;&125;</code>将自动替换为应用程序名称,并且<code>&123;&123;page&&125;&125;</code>将替换为页面名称/路径。",
     "custom_title_detail": "您可以自定义<code>&lt;title&gt;</code>标记。<br><code>&123;&123;sitename&&125;&125;</code>将自动替换为应用程序名称,并且<code>&123;&123;page&&125;&125;</code>将替换为页面名称/路径。",
     "custom_title_detail_placeholder1": "<code>&#123;&#123;站点名称&#125;&#125;</code>-此wiki的站点名称。",
     "custom_title_detail_placeholder1": "<code>&#123;&#123;站点名称&#125;&#125;</code>-此wiki的站点名称。",

+ 17 - 0
packages/app/public/static/locales/zh_CN/commons.json

@@ -0,0 +1,17 @@
+{
+  "meta": {
+    "display_name": "简体中文"
+  },
+  "toaster": {
+    "create_succeeded": "Succeeded to create {{target}}",
+    "create_failed": "Failed to create {{target}}",
+    "update_successed": "Succeeded to update {{target}}",
+    "update_failed": "Failed to update {{target}}"
+  },
+  "alert": {
+    "siteUrl_is_not_set": "主页URL未设置,通过 {{link}} 设置"
+  },
+  "headers": {
+    "app_settings": "系统设置"
+  }
+}

+ 2 - 6
packages/app/public/static/locales/zh_CN/translation.json

@@ -496,10 +496,6 @@
     "page_not_found_in_preview": "\"{{path}}\" is not a GROWI page."
     "page_not_found_in_preview": "\"{{path}}\" is not a GROWI page."
   },
   },
 	"toaster": {
 	"toaster": {
-    "create_succeeded": "Succeeded to create {{target}}",
-    "create_failed": "Failed to create {{target}}",
-		"update_successed": "Succeeded to update {{target}}",
-    "update_failed": "Failed to update {{target}}",
     "file_upload_succeeded": "文件上传成功",
     "file_upload_succeeded": "文件上传成功",
     "file_upload_failed": "文件上传失败",
     "file_upload_failed": "文件上传失败",
     "initialize_successed": "Succeeded to initialize {{target}}",
     "initialize_successed": "Succeeded to initialize {{target}}",
@@ -510,7 +506,8 @@
 		"remove_user_success": "Succeeded to removing {{username}} ",
 		"remove_user_success": "Succeeded to removing {{username}} ",
     "remove_external_user_success": "Succeeded to remove {{accountId}} ",
     "remove_external_user_success": "Succeeded to remove {{accountId}} ",
     "switch_disable_link_sharing_success": "成功更新分享链接设置",
     "switch_disable_link_sharing_success": "成功更新分享链接设置",
-    "failed_to_reset_password":"Failed to reset password"
+    "failed_to_reset_password":"Failed to reset password",
+    "save_succeeded": "已成功保存"
   },
   },
 	"template": {
 	"template": {
 		"modal_label": {
 		"modal_label": {
@@ -586,7 +583,6 @@
     "popover_title": "Slack Notification",
     "popover_title": "Slack Notification",
     "popover_desc": "Input channel name. You can notify multiple channels by entering a comma-separated list."
     "popover_desc": "Input channel name. You can notify multiple channels by entering a comma-separated list."
   },
   },
-  "security_settings": "安全设置",
   "share_links": {
   "share_links": {
     "Shere this page link to public": "Shere this page link to public",
     "Shere this page link to public": "Shere this page link to public",
     "share_link_list": "Share link list",
     "share_link_list": "Share link list",

+ 2 - 2
packages/app/resource/locales/en_US/welcome.md

@@ -18,7 +18,7 @@ Let's increase the information exchange everyday.
 - We can create a bullet point by adding `-`  at the beginning of the line.
 - We can create a bullet point by adding `-`  at the beginning of the line.
 - We can also copy and paste, drag and drop attachments such as images, PDF, Word/Excel/PowerPoint, etc.
 - We can also copy and paste, drag and drop attachments such as images, PDF, Word/Excel/PowerPoint, etc.
 - Once we finished, press the "**Update**" button to publish the page.
 - Once we finished, press the "**Update**" button to publish the page.
-    - We can also save it by `Ctrl(⌘) +S`.
+    - We can also save it by `Ctrl(⌘) + S`.
 
 
 For more information: [Tutorial#Create New Page](https://docs.growi.org/en/guide/tutorial/create_page.html#create-new-page)
 For more information: [Tutorial#Create New Page](https://docs.growi.org/en/guide/tutorial/create_page.html#create-new-page)
 
 
@@ -29,7 +29,7 @@ For more information: [Tutorial#Create New Page](https://docs.growi.org/en/guide
   <div class="card-body">
   <div class="card-body">
     <ul>
     <ul>
       <li>Ctrl(⌘) + "/" to show quick help.</li>
       <li>Ctrl(⌘) + "/" to show quick help.</li>
-      <li>We can write HTML with <a href="https://getbootstrap.com/docs/4.5/components/">Bootstrap 4</a>.</li>
+      <li>We can write HTML with <a href="https://getbootstrap.com/docs/4.6/components/">Bootstrap 4</a>.</li>
     </ul>
     </ul>
   </div>
   </div>
 </div>
 </div>

+ 2 - 2
packages/app/resource/locales/ja_JP/welcome.md

@@ -17,7 +17,7 @@ GROWI は個人・法人向けの Wiki | ナレッジベースツールです。
 - `- ` を行頭につけると、この文章のような箇条書きを書くことができます
 - `- ` を行頭につけると、この文章のような箇条書きを書くことができます
 - 画像やPDF、Word/Excel/PowerPointなどの添付ファイルも、コピー&ペースト、ドラッグ&ドロップで貼ることができます
 - 画像やPDF、Word/Excel/PowerPointなどの添付ファイルも、コピー&ペースト、ドラッグ&ドロップで貼ることができます
 - 書けたら "**更新**" ボタンを押してページを公開しましょう
 - 書けたら "**更新**" ボタンを押してページを公開しましょう
-    - `Ctrl(⌘) +S` でも保存できます
+    - `Ctrl(⌘) + S` でも保存できます
 
 
 さらに詳しくはこちら: [チュートリアル#新規ページ作成](https://docs.growi.org/ja/guide/tutorial/create_page.html#新規ページ作成)
 さらに詳しくはこちら: [チュートリアル#新規ページ作成](https://docs.growi.org/ja/guide/tutorial/create_page.html#新規ページ作成)
 
 
@@ -25,7 +25,7 @@ GROWI は個人・法人向けの Wiki | ナレッジベースツールです。
   <div class="card-header bg-primary text-light">Tips</div>
   <div class="card-header bg-primary text-light">Tips</div>
   <div class="card-body"><ul>
   <div class="card-body"><ul>
     <li>Ctrl(⌘) + "/" でショートカットヘルプを表示します</li>
     <li>Ctrl(⌘) + "/" でショートカットヘルプを表示します</li>
-    <li>HTML/CSS の記述には、<a href="https://getbootstrap.com/docs/4.5/components/">Bootstrap 4</a> を利用できます</li>
+    <li>HTML/CSS の記述には、<a href="https://getbootstrap.com/docs/4.6/components/">Bootstrap 4</a> を利用できます</li>
   </ul></div>
   </ul></div>
 </div>
 </div>
 
 

+ 2 - 2
packages/app/resource/locales/zh_CN/welcome.md

@@ -18,7 +18,7 @@ GROWI是一个针对个人和公司的Wiki - 一个知识库工具。
 - 我们可以通过在行首添加`-`来创建一个要点。
 - 我们可以通过在行首添加`-`来创建一个要点。
 - 我们还可以复制和粘贴,拖放附件,如图片、PDF、Word/Excel/PowerPoint等。
 - 我们还可以复制和粘贴,拖放附件,如图片、PDF、Word/Excel/PowerPoint等。
 - 一旦我们完成了,按 "**更新**"按钮来发布页面。
 - 一旦我们完成了,按 "**更新**"按钮来发布页面。
-    - 我们也可以通过`Ctrl(⌘) +S`来保存。
+    - 我们也可以通过`Ctrl(⌘) + S`来保存。
 
 
 了解更多信息: [Tutorial#Create New Page](https://docs.growi.org/en/guide/tutorial/create_page.html#create-new-page)
 了解更多信息: [Tutorial#Create New Page](https://docs.growi.org/en/guide/tutorial/create_page.html#create-new-page)
 
 
@@ -29,7 +29,7 @@ GROWI是一个针对个人和公司的Wiki - 一个知识库工具。
   <div class="card-body">
   <div class="card-body">
     <ul>
     <ul>
       <li>Ctrl(⌘) + "/" 显示快速帮助。</li>
       <li>Ctrl(⌘) + "/" 显示快速帮助。</li>
-      <li>你可以用 <a href="https://getbootstrap.com/docs/4.5/components/">Bootstrap 4编写HTML</a>.</li>
+      <li>你可以用 <a href="https://getbootstrap.com/docs/4.6/components/">Bootstrap 4编写HTML</a>.</li>
     </ul>
     </ul>
   </div>
   </div>
 </div>
 </div>

+ 0 - 73
packages/app/src/client/services/AdminCustomizeContainer.js

@@ -35,26 +35,10 @@ export default class AdminCustomizeContainer extends Container {
       isEnabledStaleNotification: false,
       isEnabledStaleNotification: false,
       isAllReplyShown: false,
       isAllReplyShown: false,
       isSearchScopeChildrenAsDefault: false,
       isSearchScopeChildrenAsDefault: false,
-      currentHighlightJsStyleId: '',
-      isHighlightJsStyleBorderEnabled: false,
       currentCustomizeTitle: '',
       currentCustomizeTitle: '',
       currentCustomizeHeader: '',
       currentCustomizeHeader: '',
       currentCustomizeCss: '',
       currentCustomizeCss: '',
       currentCustomizeScript: '',
       currentCustomizeScript: '',
-      /* eslint-disable quote-props, no-multi-spaces */
-      highlightJsCssSelectorOptions: {
-        'github':           { name: '[Light] GitHub',         border: false },
-        'github-gist':      { name: '[Light] GitHub Gist',    border: true },
-        'atom-one-light':   { name: '[Light] Atom One Light', border: true },
-        'xcode':            { name: '[Light] Xcode',          border: true },
-        'vs':               { name: '[Light] Vs',             border: true },
-        'atom-one-dark':    { name: '[Dark] Atom One Dark',   border: false },
-        'hybrid':           { name: '[Dark] Hybrid',          border: false },
-        'monokai':          { name: '[Dark] Monokai',         border: false },
-        'tomorrow-night':   { name: '[Dark] Tomorrow Night',  border: false },
-        'vs2015':           { name: '[Dark] Vs 2015',         border: false },
-      },
-      /* eslint-enable quote-props, no-multi-spaces */
     };
     };
     this.switchPageListLimitationS = this.switchPageListLimitationS.bind(this);
     this.switchPageListLimitationS = this.switchPageListLimitationS.bind(this);
     this.switchPageListLimitationM = this.switchPageListLimitationM.bind(this);
     this.switchPageListLimitationM = this.switchPageListLimitationM.bind(this);
@@ -88,16 +72,11 @@ export default class AdminCustomizeContainer extends Container {
         isEnabledStaleNotification: customizeParams.isEnabledStaleNotification,
         isEnabledStaleNotification: customizeParams.isEnabledStaleNotification,
         isAllReplyShown: customizeParams.isAllReplyShown,
         isAllReplyShown: customizeParams.isAllReplyShown,
         isSearchScopeChildrenAsDefault: customizeParams.isSearchScopeChildrenAsDefault,
         isSearchScopeChildrenAsDefault: customizeParams.isSearchScopeChildrenAsDefault,
-        currentHighlightJsStyleId: customizeParams.styleName,
-        isHighlightJsStyleBorderEnabled: customizeParams.styleBorder,
         currentCustomizeTitle: customizeParams.customizeTitle,
         currentCustomizeTitle: customizeParams.customizeTitle,
         currentCustomizeHeader: customizeParams.customizeHeader,
         currentCustomizeHeader: customizeParams.customizeHeader,
         currentCustomizeCss: customizeParams.customizeCss,
         currentCustomizeCss: customizeParams.customizeCss,
         currentCustomizeScript: customizeParams.customizeScript,
         currentCustomizeScript: customizeParams.customizeScript,
       });
       });
-
-      // search style name from object for display
-      this.setState({ currentHighlightJsStyleName: this.state.highlightJsCssSelectorOptions[customizeParams.styleName].name });
     }
     }
     catch (err) {
     catch (err) {
       this.setState({ retrieveError: err });
       this.setState({ retrieveError: err });
@@ -171,25 +150,6 @@ export default class AdminCustomizeContainer extends Container {
     this.setState({ isSearchScopeChildrenAsDefault: !this.state.isSearchScopeChildrenAsDefault });
     this.setState({ isSearchScopeChildrenAsDefault: !this.state.isSearchScopeChildrenAsDefault });
   }
   }
 
 
-  /**
-   * Switch highlightJsStyle
-   */
-  switchHighlightJsStyle(styleId, styleName, isBorderEnable) {
-    this.setState({ currentHighlightJsStyleId: styleId });
-    this.setState({ currentHighlightJsStyleName: styleName });
-    // recommended settings are applied
-    this.setState({ isHighlightJsStyleBorderEnabled: isBorderEnable });
-
-    this.previewHighlightJsStyle(styleId);
-  }
-
-  /**
-   * Switch highlightJsStyleBorder
-   */
-  switchHighlightJsStyleBorder() {
-    this.setState({ isHighlightJsStyleBorderEnabled: !this.state.isHighlightJsStyleBorderEnabled });
-  }
-
   /**
   /**
    * Change customize Title
    * Change customize Title
    */
    */
@@ -218,17 +178,6 @@ export default class AdminCustomizeContainer extends Container {
     this.setState({ currentCustomizeScript: inpuValue });
     this.setState({ currentCustomizeScript: inpuValue });
   }
   }
 
 
-  /**
-   * Preview hljs style
-   * @param {string} styleId
-   */
-  previewHighlightJsStyle(styleId) {
-    const styleLInk = document.querySelectorAll('#grw-hljs-container-for-demo link')[0];
-    // replace css url
-    // see https://regex101.com/r/gBNZYu/4
-    styleLInk.href = styleLInk.href.replace(/[^/]+\.css$/, `${styleId}.css`);
-  }
-
 
 
   /**
   /**
    * Update function
    * Update function
@@ -266,28 +215,6 @@ export default class AdminCustomizeContainer extends Container {
     }
     }
   }
   }
 
 
-  /**
-   * Update code highlight
-   * @memberOf AdminCustomizeContainer
-   */
-  async updateHighlightJsStyle() {
-    try {
-      const response = await apiv3Put('/customize-setting/highlight', {
-        highlightJsStyle: this.state.currentHighlightJsStyleId,
-        highlightJsStyleBorder: this.state.isHighlightJsStyleBorderEnabled,
-      });
-      const { customizedParams } = response.data;
-      this.setState({
-        highlightJsStyle: customizedParams.highlightJsStyle,
-        highlightJsStyleBorder: customizedParams.highlightJsStyleBorder,
-      });
-    }
-    catch (err) {
-      logger.error(err);
-      throw new Error('Failed to update data');
-    }
-  }
-
   /**
   /**
    * Update customTitle
    * Update customTitle
    * @memberOf AdminCustomizeContainer
    * @memberOf AdminCustomizeContainer

+ 23 - 30
packages/app/src/client/services/PageContainer.js

@@ -174,38 +174,31 @@ export default class PageContainer extends Container {
    */
    */
   updateStateAfterSave(page, tags, revision, editorMode) {
   updateStateAfterSave(page, tags, revision, editorMode) {
     // update state of PageContainer
     // update state of PageContainer
-    const newState = {
-      pageId: page._id,
-      revisionId: revision._id,
-      revisionCreatedAt: new Date(revision.createdAt).getTime() / 1000,
-      remoteRevisionId: revision._id,
-      revisionAuthor: revision.author,
-      revisionIdHackmdSynced: page.revisionHackmdSynced,
-      hasDraftOnHackmd: page.hasDraftOnHackmd,
-      markdown: revision.body,
-      createdAt: page.createdAt,
-      updatedAt: page.updatedAt,
-    };
-    if (tags != null) {
-      newState.tags = tags;
-    }
-    this.setState(newState);
-
-    // Update PageEditor component
-    if (editorMode !== EditorMode.Editor) {
-      // eslint-disable-next-line no-undef
-      globalEmitter.emit('updateEditorValue', newState.markdown);
-    }
+    // const newState = {
+    //   pageId: page._id,
+    //   revisionId: revision._id,
+    //   revisionCreatedAt: new Date(revision.createdAt).getTime() / 1000,
+    //   remoteRevisionId: revision._id,
+    //   revisionAuthor: revision.author,
+    //   revisionIdHackmdSynced: page.revisionHackmdSynced,
+    //   hasDraftOnHackmd: page.hasDraftOnHackmd,
+    //   markdown: revision.body,
+    //   createdAt: page.createdAt,
+    //   updatedAt: page.updatedAt,
+    // };
+    // if (tags != null) {
+    //   newState.tags = tags;
+    // }
+    // this.setState(newState);
 
 
     // PageEditorByHackmd component
     // PageEditorByHackmd component
-    const pageEditorByHackmd = this.appContainer.getComponentInstance('PageEditorByHackmd');
-    if (pageEditorByHackmd != null) {
-      // reset
-      if (editorMode !== EditorMode.HackMD) {
-        pageEditorByHackmd.reset();
-      }
-    }
-
+    // const pageEditorByHackmd = this.appContainer.getComponentInstance('PageEditorByHackmd');
+    // if (pageEditorByHackmd != null) {
+    //   // reset
+    //   if (editorMode !== EditorMode.HackMD) {
+    //     pageEditorByHackmd.reset();
+    //   }
+    // }
   }
   }
 
 
   /**
   /**

+ 2 - 2
packages/app/src/components/Admin/App/AppSetting.jsx

@@ -18,12 +18,12 @@ const logger = loggerFactory('growi:appSettings');
 
 
 const AppSetting = (props) => {
 const AppSetting = (props) => {
   const { adminAppContainer } = props;
   const { adminAppContainer } = props;
-  const { t } = useTranslation('admin');
+  const { t } = useTranslation(['admin', 'commons']);
 
 
   const submitHandler = useCallback(async() => {
   const submitHandler = useCallback(async() => {
     try {
     try {
       await adminAppContainer.updateAppSettingHandler();
       await adminAppContainer.updateAppSettingHandler();
-      toastSuccess(t('toaster.update_successed', { target: t('app_settings') }));
+      toastSuccess(t('toaster.update_successed', { target: t('commons:headers.app_settings') }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 1 - 1
packages/app/src/components/Admin/App/AppSettingsPageContents.tsx

@@ -82,7 +82,7 @@ const AppSettingsPageContents = (props: Props) => {
 
 
       <div className="row">
       <div className="row">
         <div className="col-lg-12">
         <div className="col-lg-12">
-          <h2 className="admin-setting-header">{t('app_settings')}</h2>
+          <h2 className="admin-setting-header">{t('commons:headers.app_settings')}</h2>
           <AppSetting />
           <AppSetting />
         </div>
         </div>
       </div>
       </div>

+ 2 - 2
packages/app/src/components/Admin/App/FileUploadSetting.tsx

@@ -18,7 +18,7 @@ type Props = {
 
 
 
 
 const FileUploadSetting = (props: Props) => {
 const FileUploadSetting = (props: Props) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation(['admin', 'commons']);
   const { adminAppContainer } = props;
   const { adminAppContainer } = props;
   const { fileUploadType } = adminAppContainer.state;
   const { fileUploadType } = adminAppContainer.state;
   const fileUploadTypes = ['aws', 'gcs', 'gridfs', 'local'];
   const fileUploadTypes = ['aws', 'gcs', 'gridfs', 'local'];
@@ -26,7 +26,7 @@ const FileUploadSetting = (props: Props) => {
   const submitHandler = useCallback(async() => {
   const submitHandler = useCallback(async() => {
     try {
     try {
       await adminAppContainer.updateFileUploadSettingHandler();
       await adminAppContainer.updateFileUploadSettingHandler();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:app_setting.file_upload_settings') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:app_setting.file_upload_settings'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 2 - 2
packages/app/src/components/Admin/App/MailSetting.tsx

@@ -17,7 +17,7 @@ type Props = {
 
 
 
 
 const MailSetting = (props: Props) => {
 const MailSetting = (props: Props) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation(['admin', 'commons']);
   const { adminAppContainer } = props;
   const { adminAppContainer } = props;
 
 
   const transmissionMethods = ['smtp', 'ses'];
   const transmissionMethods = ['smtp', 'ses'];
@@ -25,7 +25,7 @@ const MailSetting = (props: Props) => {
   async function submitHandler() {
   async function submitHandler() {
     try {
     try {
       await adminAppContainer.updateMailSettingHandler();
       await adminAppContainer.updateMailSettingHandler();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:app_setting.ses_settings') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:app_setting.ses_settings'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 1 - 1
packages/app/src/components/Admin/App/PluginSetting.tsx

@@ -23,7 +23,7 @@ const PluginSetting = (props: Props) => {
   const submitHandler = useCallback(async() => {
   const submitHandler = useCallback(async() => {
     try {
     try {
       await adminAppContainer.updatePluginSettingHandler();
       await adminAppContainer.updatePluginSettingHandler();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:app_setting.plugin_settings') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:app_setting.plugin_settings'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 1 - 1
packages/app/src/components/Admin/App/SiteUrlSetting.tsx

@@ -24,7 +24,7 @@ const SiteUrlSetting = (props: Props) => {
   const submitHandler = useCallback(async() => {
   const submitHandler = useCallback(async() => {
     try {
     try {
       await adminAppContainer.updateSiteUrlSettingHandler();
       await adminAppContainer.updateSiteUrlSettingHandler();
-      toastSuccess(t('toaster.update_successed', { target: t('Site URL settings') }));
+      toastSuccess(t('toaster.update_successed', { target: t('Site URL settings'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 1 - 1
packages/app/src/components/Admin/Common/AdminNavigation.jsx

@@ -23,7 +23,7 @@ const AdminNavigation = (props) => {
   const MenuLabel = ({ menu }) => {
   const MenuLabel = ({ menu }) => {
     switch (menu) {
     switch (menu) {
       /* eslint-disable no-multi-spaces, max-len */
       /* eslint-disable no-multi-spaces, max-len */
-      case 'app':                      return <><i className="mr-1 icon-fw icon-settings"></i>{        t('app_settings') }</>;
+      case 'app':                      return <><i className="mr-1 icon-fw icon-settings"></i>{        t('commons:headers.app_settings') }</>;
       case 'security':                 return <><i className="mr-1 icon-fw icon-shield"></i>{          t('security_settings.security_settings') }</>;
       case 'security':                 return <><i className="mr-1 icon-fw icon-shield"></i>{          t('security_settings.security_settings') }</>;
       case 'markdown':                 return <><i className="mr-1 icon-fw icon-note"></i>{            t('markdown_settings.markdown_settings') }</>;
       case 'markdown':                 return <><i className="mr-1 icon-fw icon-note"></i>{            t('markdown_settings.markdown_settings') }</>;
       case 'customize':                return <><i className="mr-1 icon-fw icon-wrench"></i>{          t('customize_settings.customize_settings') }</>;
       case 'customize':                return <><i className="mr-1 icon-fw icon-wrench"></i>{          t('customize_settings.customize_settings') }</>;

+ 0 - 4
packages/app/src/components/Admin/Customize/Customize.jsx

@@ -13,7 +13,6 @@ import { withUnstatedContainers } from '../../UnstatedUtils';
 import CustomizeCssSetting from './CustomizeCssSetting';
 import CustomizeCssSetting from './CustomizeCssSetting';
 import CustomizeFunctionSetting from './CustomizeFunctionSetting';
 import CustomizeFunctionSetting from './CustomizeFunctionSetting';
 import CustomizeHeaderSetting from './CustomizeHeaderSetting';
 import CustomizeHeaderSetting from './CustomizeHeaderSetting';
-import CustomizeHighlightSetting from './CustomizeHighlightSetting';
 import CustomizeLayoutSetting from './CustomizeLayoutSetting';
 import CustomizeLayoutSetting from './CustomizeLayoutSetting';
 import CustomizeLogoSetting from './CustomizeLogoSetting';
 import CustomizeLogoSetting from './CustomizeLogoSetting';
 import CustomizeScriptSetting from './CustomizeScriptSetting';
 import CustomizeScriptSetting from './CustomizeScriptSetting';
@@ -56,9 +55,6 @@ function Customize(props) {
       <div className="mb-5">
       <div className="mb-5">
         <CustomizeFunctionSetting />
         <CustomizeFunctionSetting />
       </div>
       </div>
-      <div className="mb-5">
-        <CustomizeHighlightSetting />
-      </div>
       <div className="mb-5">
       <div className="mb-5">
         <CustomizeTitle />
         <CustomizeTitle />
       </div>
       </div>

+ 1 - 1
packages/app/src/components/Admin/Customize/CustomizeCssSetting.tsx

@@ -21,7 +21,7 @@ const CustomizeCssSetting = (props: Props): JSX.Element => {
   const onClickSubmit = useCallback(async() => {
   const onClickSubmit = useCallback(async() => {
     try {
     try {
       await adminCustomizeContainer.updateCustomizeCss();
       await adminCustomizeContainer.updateCustomizeCss();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_css') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_css'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 1 - 1
packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.tsx

@@ -25,7 +25,7 @@ const CustomizeFunctionSetting = (props: Props): JSX.Element => {
 
 
     try {
     try {
       await adminCustomizeContainer.updateCustomizeFunction();
       await adminCustomizeContainer.updateCustomizeFunction();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.function') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.function'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 2 - 2
packages/app/src/components/Admin/Customize/CustomizeHeaderSetting.tsx

@@ -21,7 +21,7 @@ const CustomizeHeaderSetting = (props: Props): JSX.Element => {
   const onClickSubmit = useCallback(async() => {
   const onClickSubmit = useCallback(async() => {
     try {
     try {
       await adminCustomizeContainer.updateCustomizeHeader();
       await adminCustomizeContainer.updateCustomizeHeader();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_header') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_header'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
@@ -44,7 +44,7 @@ const CustomizeHeaderSetting = (props: Props): JSX.Element => {
           </Card>
           </Card>
           <div className="form-text text-muted">
           <div className="form-text text-muted">
             { t('Example') }:
             { t('Example') }:
-            <pre className="hljs">
+            <pre>
               {/* eslint-disable-next-line react/no-unescaped-entities */}
               {/* eslint-disable-next-line react/no-unescaped-entities */}
               <code className="text-wrap">&lt;script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.13.0/build/languages/yaml.min.js"
               <code className="text-wrap">&lt;script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.13.0/build/languages/yaml.min.js"
                 defer&gt;&lt;/script&gt;
                 defer&gt;&lt;/script&gt;

+ 0 - 148
packages/app/src/components/Admin/Customize/CustomizeHighlightSetting.tsx

@@ -1,148 +0,0 @@
-/* eslint-disable no-useless-escape */
-import React, { useCallback, useState } from 'react';
-
-
-import { useTranslation } from 'next-i18next';
-import {
-  Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
-} from 'reactstrap';
-
-import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { IHighlightJsCssSelectorOptions } from '~/interfaces/customize';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
-type Props = {
-  adminCustomizeContainer: AdminCustomizeContainer
-}
-
-type HljsDemoProps = {
-  isHighlightJsStyleBorderEnabled: boolean
-}
-
-const HljsDemo = React.memo((props: HljsDemoProps): JSX.Element => {
-
-  const { isHighlightJsStyleBorderEnabled } = props;
-
-  /* eslint-disable max-len */
-  const html = `<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">MersenneTwister</span>(<span class="hljs-params">seed</span>) </span>{
-<span class="hljs-keyword">if</span> (<span class="hljs-built_in">arguments</span>.length == <span class="hljs-number">0</span>) {
-  seed = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().getTime();
-}
-
-<span class="hljs-keyword">this</span>._mt = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Array</span>(<span class="hljs-number">624</span>);
-<span class="hljs-keyword">this</span>.setSeed(seed);
-}</span>`;
-  /* eslint-enable max-len */
-
-  return (
-    <pre className={`hljs ${!isHighlightJsStyleBorderEnabled && 'hljs-no-border'}`}>
-      {/* eslint-disable-next-line react/no-danger */}
-      <code dangerouslySetInnerHTML={{ __html: html }}></code>
-    </pre>
-  );
-});
-HljsDemo.displayName = 'HljsDemo';
-
-const CustomizeHighlightSetting = (props: Props): JSX.Element => {
-  const { adminCustomizeContainer } = props;
-  const { t } = useTranslation();
-  const [isDropdownOpen, setIsDropdownOpen] = useState(false);
-  const options: IHighlightJsCssSelectorOptions = adminCustomizeContainer.state.highlightJsCssSelectorOptions;
-
-  const onToggleDropdown = useCallback(() => {
-    setIsDropdownOpen(!isDropdownOpen);
-  }, [isDropdownOpen]);
-
-  const onClickSubmit = useCallback(async() => {
-    try {
-      await adminCustomizeContainer.updateHighlightJsStyle();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.code_highlight') }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [t, adminCustomizeContainer]);
-
-  const renderMenuItems = useCallback(() => {
-
-    const items = Object.entries(options).map((option) => {
-      const styleId = option[0];
-      const styleName = option[1].name;
-      const isBorderEnable = option[1].border;
-
-      return (
-        <DropdownItem
-          key={styleId}
-          role="presentation"
-          onClick={() => adminCustomizeContainer.switchHighlightJsStyle(styleId, styleName, isBorderEnable)}
-        >
-          <a role="menuitem">{styleName}</a>
-        </DropdownItem>
-      );
-    });
-    return items;
-  }, [adminCustomizeContainer, options]);
-
-  return (
-    <React.Fragment>
-      <div className="row">
-        <div className="col-12">
-          <h2 className="admin-setting-header">{t('admin:customize_settings.code_highlight')}</h2>
-
-          <div className="form-group row">
-            <div className="offset-md-3 col-md-6 text-left">
-              <div className="my-0">
-                <label>{t('admin:customize_settings.theme')}</label>
-              </div>
-              <Dropdown isOpen={isDropdownOpen} toggle={onToggleDropdown}>
-                <DropdownToggle className="text-right col-6" caret>
-                  <span className="float-left">{adminCustomizeContainer.state.currentHighlightJsStyleName}</span>
-                </DropdownToggle>
-                <DropdownMenu className="dropdown-menu" role="menu">
-                  {renderMenuItems()}
-                </DropdownMenu>
-              </Dropdown>
-              <p className="form-text text-warning">
-                {/* eslint-disable-next-line react/no-danger */}
-                <span dangerouslySetInnerHTML={{ __html: t('admin:customize_settings.nocdn_desc') }} />
-              </p>
-            </div>
-          </div>
-
-          <div className="form-group row">
-            <div className="offset-md-3 col-md-6 text-left">
-              <div className="custom-control custom-switch custom-checkbox-success">
-                <input
-                  type="checkbox"
-                  className="custom-control-input"
-                  id="highlightBorder"
-                  checked={adminCustomizeContainer.state.isHighlightJsStyleBorderEnabled}
-                  onChange={() => { adminCustomizeContainer.switchHighlightJsStyleBorder() }}
-                />
-                <label className="custom-control-label" htmlFor="highlightBorder">
-                  <strong>Border</strong>
-                </label>
-              </div>
-            </div>
-          </div>
-
-          <div className="form-text text-muted">
-            <label>Examples:</label>
-            <div className="wiki">
-              <HljsDemo isHighlightJsStyleBorderEnabled={adminCustomizeContainer.state.isHighlightJsStyleBorderEnabled} />
-            </div>
-          </div>
-
-          <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
-        </div>
-      </div>
-    </React.Fragment>
-  );
-};
-
-const CustomizeHighlightSettingWrapper = withUnstatedContainers(CustomizeHighlightSetting, [AdminCustomizeContainer]);
-
-export default CustomizeHighlightSettingWrapper;

+ 1 - 1
packages/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx

@@ -19,7 +19,7 @@ const CustomizeLayoutSetting = (): JSX.Element => {
   const onClickSubmit = async() => {
   const onClickSubmit = async() => {
     try {
     try {
       await apiv3Put('/customize-setting/layout', { isContainerFluid });
       await apiv3Put('/customize-setting/layout', { isContainerFluid });
-      toastSuccess(t('toaster.update_successed', { target: t('customize_settings.layout') }));
+      toastSuccess(t('toaster.update_successed', { target: t('customize_settings.layout'), ns: 'commons' }));
       mutateLayoutSetting();
       mutateLayoutSetting();
     }
     }
     catch (err) {
     catch (err) {

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

@@ -59,7 +59,7 @@ const CustomizeLogoSetting = (): JSX.Element => {
       const { customizedParams } = response.data;
       const { customizedParams } = response.data;
       setIsDefaultLogo(customizedParams.isDefaultLogo);
       setIsDefaultLogo(customizedParams.isDefaultLogo);
       setCustomizedLogoSrc(customizedParams.customizedLogoSrc);
       setCustomizedLogoSrc(customizedParams.customizedLogoSrc);
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_logo') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_logo'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
@@ -70,7 +70,7 @@ const CustomizeLogoSetting = (): JSX.Element => {
     try {
     try {
       await apiv3Delete('/customize-setting/delete-brand-logo');
       await apiv3Delete('/customize-setting/delete-brand-logo');
       setCustomizedLogoSrc(null);
       setCustomizedLogoSrc(null);
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.current_logo') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.current_logo'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
@@ -86,7 +86,7 @@ const CustomizeLogoSetting = (): JSX.Element => {
       formData.append('file', croppedImage);
       formData.append('file', croppedImage);
       const { data } = await apiv3PostForm('/customize-setting/upload-brand-logo', formData);
       const { data } = await apiv3PostForm('/customize-setting/upload-brand-logo', formData);
       setCustomizedLogoSrc(data.attachment.filePathProxied);
       setCustomizedLogoSrc(data.attachment.filePathProxied);
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.current_logo') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.current_logo'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 2 - 2
packages/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx

@@ -21,7 +21,7 @@ const CustomizeScriptSetting = (props: Props): JSX.Element => {
   const onClickSubmit = useCallback(async() => {
   const onClickSubmit = useCallback(async() => {
     try {
     try {
       await adminCustomizeContainer.updateCustomizeScript();
       await adminCustomizeContainer.updateCustomizeScript();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_script') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_script'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
@@ -79,7 +79,7 @@ const CustomizeScriptSetting = (props: Props): JSX.Element => {
 
 
           <div className="form-text text-muted">
           <div className="form-text text-muted">
             Examples:
             Examples:
-            <pre className="hljs"><code>{getExampleCode()}</code></pre>
+            <pre><code className='language-javascript'>{getExampleCode()}</code></pre>
           </div>
           </div>
 
 
           <div className="form-group">
           <div className="form-group">

+ 3 - 2
packages/app/src/components/Admin/Customize/CustomizeSidebarSetting.tsx

@@ -8,7 +8,8 @@ import { useSWRxSidebarConfig } from '~/stores/ui';
 import { useNextThemes } from '~/stores/use-next-themes';
 import { useNextThemes } from '~/stores/use-next-themes';
 
 
 const CustomizeSidebarsetting = (): JSX.Element => {
 const CustomizeSidebarsetting = (): JSX.Element => {
-  const { t } = useTranslation('admin');
+  const { t } = useTranslation(['admin', 'commons']);
+
   const {
   const {
     update, isSidebarDrawerMode, isSidebarClosedAtDockMode, setIsSidebarDrawerMode, setIsSidebarClosedAtDockMode,
     update, isSidebarDrawerMode, isSidebarClosedAtDockMode, setIsSidebarDrawerMode, setIsSidebarClosedAtDockMode,
   } = useSWRxSidebarConfig();
   } = useSWRxSidebarConfig();
@@ -20,7 +21,7 @@ const CustomizeSidebarsetting = (): JSX.Element => {
   const onClickSubmit = useCallback(async() => {
   const onClickSubmit = useCallback(async() => {
     try {
     try {
       await update();
       await update();
-      toastSuccess(t('toaster.update_successed', { target: t('customize_settings.default_sidebar_mode.title') }));
+      toastSuccess(t('toaster.update_successed', { target: t('customize_settings.default_sidebar_mode.title'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 1 - 1
packages/app/src/components/Admin/Customize/CustomizeThemeSetting.tsx

@@ -35,7 +35,7 @@ const CustomizeThemeSetting = (props: Props): JSX.Element => {
         });
         });
       }
       }
 
 
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.theme') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.theme'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 1 - 1
packages/app/src/components/Admin/Customize/CustomizeTitle.tsx

@@ -22,7 +22,7 @@ export const CustomizeTitle: FC = () => {
       await apiv3Put('/customize-setting/customize-title', {
       await apiv3Put('/customize-setting/customize-title', {
         customizeTitle: currentCustomizeTitle,
         customizeTitle: currentCustomizeTitle,
       });
       });
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_title') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_title'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 1 - 1
packages/app/src/components/Admin/ElasticsearchManagement/ElasticsearchManagement.tsx

@@ -146,7 +146,7 @@ const ElasticsearchManagement = () => {
 
 
   return (
   return (
     <>
     <>
-      <div className="row">
+      <div data-testid="admin-full-text-search" className="row">
         <div className="col-md-12">
         <div className="col-md-12">
           <StatusTable
           <StatusTable
             isInitialized={isInitialized}
             isInitialized={isInitialized}

+ 44 - 43
packages/app/src/components/Admin/ImportData/GrowiArchive/ImportForm.jsx

@@ -3,14 +3,12 @@ import React from 'react';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-import AdminSocketIoContainer from '~/client/services/AdminSocketIoContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import GrowiArchiveImportOption from '~/models/admin/growi-archive-import-option';
 import GrowiArchiveImportOption from '~/models/admin/growi-archive-import-option';
 import ImportOptionForPages from '~/models/admin/import-option-for-pages';
 import ImportOptionForPages from '~/models/admin/import-option-for-pages';
 import ImportOptionForRevisions from '~/models/admin/import-option-for-revisions';
 import ImportOptionForRevisions from '~/models/admin/import-option-for-revisions';
-
-import { withUnstatedContainers } from '../../../UnstatedUtils';
+import { useAdminSocket } from '~/stores/socket-io';
 
 
 
 
 import ErrorViewer from './ErrorViewer';
 import ErrorViewer from './ErrorViewer';
@@ -103,50 +101,56 @@ class ImportForm extends React.Component {
   }
   }
 
 
   setupWebsocketEventHandler() {
   setupWebsocketEventHandler() {
-    const socket = this.props.adminSocketIoContainer.getSocket();
-
-    // websocket event
-    // eslint-disable-next-line object-curly-newline
-    socket.on('admin:onProgressForImport', ({ collectionName, collectionProgress, appendedErrors }) => {
-      const { progressMap, errorsMap } = this.state;
-      progressMap[collectionName] = collectionProgress;
+    const { socket } = this.props;
+
+    if (socket != null) {
+      // websocket event
+      // eslint-disable-next-line object-curly-newline
+      socket.on('admin:onProgressForImport', ({ collectionName, collectionProgress, appendedErrors }) => {
+        const { progressMap, errorsMap } = this.state;
+        progressMap[collectionName] = collectionProgress;
+
+        const errors = errorsMap[collectionName] || [];
+        errorsMap[collectionName] = errors.concat(appendedErrors);
+
+        this.setState({
+          isImporting: true,
+          progressMap,
+          errorsMap,
+        });
+      });
 
 
-      const errors = errorsMap[collectionName] || [];
-      errorsMap[collectionName] = errors.concat(appendedErrors);
+      // websocket event
+      socket.on('admin:onTerminateForImport', () => {
+        this.setState({
+          isImporting: false,
+          isImported: true,
+        });
 
 
-      this.setState({
-        isImporting: true,
-        progressMap,
-        errorsMap,
+        toastSuccess(undefined, 'Import process has completed.');
       });
       });
-    });
 
 
-    // websocket event
-    socket.on('admin:onTerminateForImport', () => {
-      this.setState({
-        isImporting: false,
-        isImported: true,
-      });
+      // websocket event
+      socket.on('admin:onErrorForImport', (err) => {
+        this.setState({
+          isImporting: false,
+          isImported: false,
+        });
 
 
-      toastSuccess(undefined, 'Import process has completed.');
-    });
-
-    // websocket event
-    socket.on('admin:onErrorForImport', (err) => {
-      this.setState({
-        isImporting: false,
-        isImported: false,
+        toastError(err, 'Import process has failed.');
       });
       });
 
 
-      toastError(err, 'Import process has failed.');
-    });
+    }
+
   }
   }
 
 
   teardownWebsocketEventHandler() {
   teardownWebsocketEventHandler() {
-    const socket = this.props.adminSocketIoContainer.getSocket();
+    const { socket } = this.props;
 
 
-    socket.removeAllListeners('admin:onProgressForImport');
-    socket.removeAllListeners('admin:onTerminateForImport');
+    if (socket != null) {
+      socket.removeAllListeners('admin:onProgressForImport');
+      socket.removeAllListeners('admin:onTerminateForImport');
+    }
   }
   }
 
 
   async toggleCheckbox(collectionName, bool) {
   async toggleCheckbox(collectionName, bool) {
@@ -496,7 +500,7 @@ class ImportForm extends React.Component {
 
 
 ImportForm.propTypes = {
 ImportForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
-  adminSocketIoContainer: PropTypes.instanceOf(AdminSocketIoContainer).isRequired,
+  socket: PropTypes.object,
 
 
   fileName: PropTypes.string,
   fileName: PropTypes.string,
   innerFileStats: PropTypes.arrayOf(PropTypes.object).isRequired,
   innerFileStats: PropTypes.arrayOf(PropTypes.object).isRequired,
@@ -506,13 +510,10 @@ ImportForm.propTypes = {
 
 
 const ImportFormWrapperFc = (props) => {
 const ImportFormWrapperFc = (props) => {
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
+  const { data: socket } = useAdminSocket();
 
 
-  return <ImportForm t={t} {...props} />;
+  return <ImportForm t={t} socket={socket} {...props} />;
 };
 };
 
 
-/**
- * Wrapper component for using unstated
- */
-const ImportFormWrapper = withUnstatedContainers(ImportFormWrapperFc, [AdminSocketIoContainer]);
 
 
-export default ImportFormWrapper;
+export default ImportFormWrapperFc;

+ 3 - 4
packages/app/src/components/Admin/ImportData/GrowiArchiveSection.jsx

@@ -8,7 +8,7 @@ import { apiv3Delete, apiv3Get } from '~/client/util/apiv3-client';
 
 
 // import { toastSuccess, toastError } from '~/client/util/apiNotification';
 // import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 
-// import ImportForm from './GrowiArchive/ImportForm';
+import ImportForm from './GrowiArchive/ImportForm';
 import UploadForm from './GrowiArchive/UploadForm';
 import UploadForm from './GrowiArchive/UploadForm';
 
 
 class GrowiArchiveSection extends React.Component {
 class GrowiArchiveSection extends React.Component {
@@ -129,12 +129,11 @@ class GrowiArchiveSection extends React.Component {
         {isTheSameVersion === false && this.renderDefferentVersionAlert()}
         {isTheSameVersion === false && this.renderDefferentVersionAlert()}
         {this.state.fileName != null && isTheSameVersion === true ? (
         {this.state.fileName != null && isTheSameVersion === true ? (
           <div className="px-4">
           <div className="px-4">
-            {/* show ImportForm by https://redmine.weseek.co.jp/issues/100061 */}
-            {/* <ImportForm
+            <ImportForm
               fileName={this.state.fileName}
               fileName={this.state.fileName}
               innerFileStats={this.state.innerFileStats}
               innerFileStats={this.state.innerFileStats}
               onDiscard={this.discardData}
               onDiscard={this.discardData}
-            /> */}
+            />
           </div>
           </div>
         )
         )
           : (
           : (

+ 1 - 1
packages/app/src/components/Admin/MarkdownSetting/IndentForm.tsx

@@ -26,7 +26,7 @@ const IndentForm = (props: Props) => {
   const onClickSubmit = useCallback(async(props) => {
   const onClickSubmit = useCallback(async(props) => {
     try {
     try {
       await props.adminMarkDownContainer.updateIndentSetting();
       await props.adminMarkDownContainer.updateIndentSetting();
-      toastSuccess(t('toaster.update_successed', { target: t('markdown_settings.indent_header') }));
+      toastSuccess(t('toaster.update_successed', { target: t('markdown_settings.indent_header'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 1 - 1
packages/app/src/components/Admin/MarkdownSetting/LineBreakForm.jsx

@@ -27,7 +27,7 @@ class LineBreakForm extends React.Component {
 
 
     try {
     try {
       await this.props.adminMarkDownContainer.updateLineBreakSetting();
       await this.props.adminMarkDownContainer.updateLineBreakSetting();
-      toastSuccess(t('toaster.update_successed', { target: t('markdown_settings.lineBreak_header') }));
+      toastSuccess(t('toaster.update_successed', { target: t('markdown_settings.lineBreak_header'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 1 - 1
packages/app/src/components/Admin/MarkdownSetting/PresentationForm.jsx

@@ -25,7 +25,7 @@ class PresentationForm extends React.Component {
 
 
     try {
     try {
       await this.props.adminMarkDownContainer.updatePresentationSetting();
       await this.props.adminMarkDownContainer.updatePresentationSetting();
-      toastSuccess(t('toaster.update_successed', { target: t('markdown_settings.presentation_header') }));
+      toastSuccess(t('toaster.update_successed', { target: t('markdown_settings.presentation_header'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 1 - 1
packages/app/src/components/Admin/MarkdownSetting/XssForm.jsx

@@ -28,7 +28,7 @@ class XssForm extends React.Component {
 
 
     try {
     try {
       await this.props.adminMarkDownContainer.updateXssSetting();
       await this.props.adminMarkDownContainer.updateXssSetting();
-      toastSuccess(t('toaster.update_successed', { target: t('markdown_settings.xss_header') }));
+      toastSuccess(t('toaster.update_successed', { target: t('markdown_settings.xss_header'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 1 - 1
packages/app/src/components/Admin/Notification/GlobalNotification.jsx

@@ -22,7 +22,7 @@ const GlobalNotification = (props) => {
   const onClickSubmit = useCallback(async() => {
   const onClickSubmit = useCallback(async() => {
     try {
     try {
       await adminNotificationContainer.updateGlobalNotificationForPages();
       await adminNotificationContainer.updateGlobalNotificationForPages();
-      toastSuccess(t('toaster.update_successed', { target: t('external_notification.external_notification') }));
+      toastSuccess(t('toaster.update_successed', { target: t('external_notification.external_notification'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 1 - 1
packages/app/src/components/Admin/Security/GitHubSecuritySettingContents.jsx

@@ -90,7 +90,7 @@ class GitHubSecurityManagementContents extends React.Component {
                 <i
                 <i
                   className="icon-exclamation"
                   className="icon-exclamation"
                   // eslint-disable-next-line max-len
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('security_settings.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('app_settings')}<i class="icon-login"></i></a>` }) }}
+                  dangerouslySetInnerHTML={{ __html: t('commons:alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('commons:headers.app_settings')}<i class="icon-login"></i></a>` }) }}
                 />
                 />
               </div>
               </div>
             )}
             )}

+ 1 - 1
packages/app/src/components/Admin/Security/GoogleSecuritySettingContents.jsx

@@ -88,7 +88,7 @@ class GoogleSecurityManagementContents extends React.Component {
                 <i
                 <i
                   className="icon-exclamation"
                   className="icon-exclamation"
                   // eslint-disable-next-line max-len
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('security_settings.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('app_settings')}<i class="icon-login"></i></a>` }) }}
+                  dangerouslySetInnerHTML={{ __html: t('commons:alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('commons:headers.app_settings')}<i class="icon-login"></i></a>` }) }}
                 />
                 />
               </div>
               </div>
             )}
             )}

+ 2 - 2
packages/app/src/components/Admin/Security/OidcSecuritySettingContents.jsx

@@ -82,7 +82,7 @@ class OidcSecurityManagementContents extends React.Component {
                 <i
                 <i
                   className="icon-exclamation"
                   className="icon-exclamation"
                   // eslint-disable-next-line max-len
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('security_settings.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('app_settings')}<i class="icon-login"></i></a>` }) }}
+                  dangerouslySetInnerHTML={{ __html: t('commons:alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('commons:headers.app_settings')}<i class="icon-login"></i></a>` }) }}
                 />
                 />
               </div>
               </div>
             )}
             )}
@@ -378,7 +378,7 @@ class OidcSecurityManagementContents extends React.Component {
                     <i
                     <i
                       className="icon-exclamation"
                       className="icon-exclamation"
                       // eslint-disable-next-line max-len
                       // eslint-disable-next-line max-len
-                      dangerouslySetInnerHTML={{ __html: t('security_settings.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('app_settings')}<i class="icon-login"></i></a>` }) }}
+                      dangerouslySetInnerHTML={{ __html: t('commons:alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('commons:headers.app_settings')}<i class="icon-login"></i></a>` }) }}
                     />
                     />
                   </div>
                   </div>
                 )}
                 )}

+ 1 - 1
packages/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx

@@ -99,7 +99,7 @@ class SamlSecurityManagementContents extends React.Component {
                 <i
                 <i
                   className="icon-exclamation"
                   className="icon-exclamation"
                   // eslint-disable-next-line max-len
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('security_settings.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('app_settings')}<i class="icon-login"></i></a>` }) }}
+                  dangerouslySetInnerHTML={{ __html: t('commons:alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('commons:headers.app_settings')}<i class="icon-login"></i></a>` }) }}
                 />
                 />
               </div>
               </div>
             )}
             )}

+ 1 - 1
packages/app/src/components/Admin/Security/SecuritySetting.jsx

@@ -453,7 +453,7 @@ class SecuritySetting extends React.Component {
           ].map(arr => this.renderPageDeletePermission(arr[0], arr[1], arr[2], arr[3]))
           ].map(arr => this.renderPageDeletePermission(arr[0], arr[1], arr[2], arr[3]))
         }
         }
 
 
-        <h4>{t('security_settings.session')}aa</h4>
+        <h4>{t('security_settings.session')}</h4>
         <div className="form-group row">
         <div className="form-group row">
           <label className="text-left text-md-right col-md-3 col-form-label">{t('security_settings.max_age')}</label>
           <label className="text-left text-md-right col-md-3 col-form-label">{t('security_settings.max_age')}</label>
           <div className="col-md-6">
           <div className="col-md-6">

+ 1 - 1
packages/app/src/components/Admin/Security/TwitterSecuritySettingContents.jsx

@@ -90,7 +90,7 @@ class TwitterSecuritySettingContents extends React.Component {
                 <i
                 <i
                   className="icon-exclamation"
                   className="icon-exclamation"
                   // eslint-disable-next-line max-len
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('security_settings.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('app_settings')}<i class="icon-login"></i></a>` }) }}
+                  dangerouslySetInnerHTML={{ __html: t('commons:alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('commons:headers.app_settings')}<i class="icon-login"></i></a>` }) }}
                 />
                 />
               </div>
               </div>
             )}
             )}

+ 2 - 2
packages/app/src/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx

@@ -50,7 +50,7 @@ const CustomBotWithProxySettings = (props) => {
       if (onPrimaryUpdated != null) {
       if (onPrimaryUpdated != null) {
         onPrimaryUpdated();
         onPrimaryUpdated();
       }
       }
-      toastSuccess(t('toaster.update_successed', { target: 'Primary' }));
+      toastSuccess(t('toaster.update_successed', { target: 'Primary', ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err, 'Failed to change isPrimary');
       toastError(err, 'Failed to change isPrimary');
@@ -77,7 +77,7 @@ const CustomBotWithProxySettings = (props) => {
       await apiv3Put('/slack-integration-settings/proxy-uri', {
       await apiv3Put('/slack-integration-settings/proxy-uri', {
         proxyUri: newProxyServerUri,
         proxyUri: newProxyServerUri,
       });
       });
-      toastSuccess(t('toaster.update_successed', { target: 'Proxy URL' }));
+      toastSuccess(t('toaster.update_successed', { target: 'Proxy URL', ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err, 'Failed to update');
       toastError(err, 'Failed to update');

+ 1 - 1
packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySecretTokenSection.jsx

@@ -38,7 +38,7 @@ const CustomBotWithoutProxySecretTokenSection = (props) => {
         onUpdatedSecretToken(inputSigningSecret, inputBotToken);
         onUpdatedSecretToken(inputSigningSecret, inputBotToken);
       }
       }
 
 
-      toastSuccess(t('toaster.update_successed', { target: t('admin:slack_integration.custom_bot_without_proxy_settings') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:slack_integration.custom_bot_without_proxy_settings'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 2 - 2
packages/app/src/components/Admin/SlackIntegration/ManageCommandsProcess.jsx

@@ -1,8 +1,8 @@
 import React, { useCallback, useState } from 'react';
 import React, { useCallback, useState } from 'react';
 
 
 import { defaultSupportedCommandsNameForBroadcastUse, defaultSupportedCommandsNameForSingleUse, defaultSupportedSlackEventActions } from '@growi/slack';
 import { defaultSupportedCommandsNameForBroadcastUse, defaultSupportedCommandsNameForSingleUse, defaultSupportedSlackEventActions } from '@growi/slack';
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
@@ -265,7 +265,7 @@ const ManageCommandsProcess = ({
         permissionsForSingleUseCommands: permissionsForSingleUseCommandsState,
         permissionsForSingleUseCommands: permissionsForSingleUseCommandsState,
         permissionsForSlackEventActions: permissionsForEventsState,
         permissionsForSlackEventActions: permissionsForEventsState,
       });
       });
-      toastSuccess(t('toaster.update_successed', { target: 'Token' }));
+      toastSuccess(t('toaster.update_successed', { target: 'Token', ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 2 - 2
packages/app/src/components/Admin/SlackIntegration/ManageCommandsProcessWithoutProxy.jsx

@@ -1,8 +1,8 @@
 import React, { useCallback, useEffect, useState } from 'react';
 import React, { useCallback, useEffect, useState } from 'react';
 
 
 import { defaultSupportedCommandsNameForBroadcastUse, defaultSupportedCommandsNameForSingleUse, defaultSupportedSlackEventActions } from '@growi/slack';
 import { defaultSupportedCommandsNameForBroadcastUse, defaultSupportedCommandsNameForSingleUse, defaultSupportedSlackEventActions } from '@growi/slack';
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
@@ -207,7 +207,7 @@ const ManageCommandsProcessWithoutProxy = ({ commandPermission, eventActionsPerm
         commandPermission: editingCommandPermission,
         commandPermission: editingCommandPermission,
         eventActionsPermission: editingEventActionsPermission,
         eventActionsPermission: editingEventActionsPermission,
       });
       });
-      toastSuccess(t('toaster.update_successed', { target: 'the permission for commands' }));
+      toastSuccess(t('toaster.update_successed', { target: 'the permission for commands', ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 1 - 1
packages/app/src/components/Admin/SlackIntegration/OfficialBotSettings.jsx

@@ -46,7 +46,7 @@ const OfficialBotSettings = (props) => {
       if (onPrimaryUpdated != null) {
       if (onPrimaryUpdated != null) {
         onPrimaryUpdated();
         onPrimaryUpdated();
       }
       }
-      toastSuccess(t('toaster.update_successed', { target: 'Primary' }));
+      toastSuccess(t('toaster.update_successed', { target: 'Primary', ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err, 'Failed to change isPrimary');
       toastError(err, 'Failed to change isPrimary');

+ 1 - 1
packages/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx

@@ -153,7 +153,7 @@ const GeneratingTokensAndRegisteringProxyServiceProcess = withUnstatedContainers
       if (props.onUpdateTokens != null) {
       if (props.onUpdateTokens != null) {
         props.onUpdateTokens();
         props.onUpdateTokens();
       }
       }
-      toastSuccess(t('toaster.update_successed', { target: 'Token' }));
+      toastSuccess(t('toaster.update_successed', { target: 'Token', ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 2 - 2
packages/app/src/components/Admin/UserGroup/UserGroupPage.tsx

@@ -93,7 +93,7 @@ export const UserGroupPage: FC = () => {
         description: userGroupData.description,
         description: userGroupData.description,
       });
       });
 
 
-      toastSuccess(t('toaster.update_successed', { target: t('UserGroup') }));
+      toastSuccess(t('toaster.update_successed', { target: t('UserGroup'), ns: 'commons' }));
 
 
       // mutate
       // mutate
       await mutateUserGroups();
       await mutateUserGroups();
@@ -112,7 +112,7 @@ export const UserGroupPage: FC = () => {
         description: userGroupData.description,
         description: userGroupData.description,
       });
       });
 
 
-      toastSuccess(t('toaster.update_successed', { target: t('UserGroup') }));
+      toastSuccess(t('toaster.update_successed', { target: t('UserGroup'), ns: 'commons' }));
 
 
       // mutate
       // mutate
       await mutateUserGroups();
       await mutateUserGroups();

+ 5 - 5
packages/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx

@@ -124,10 +124,10 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
     async(targetGroup: IUserGroupHasId, userGroupData: Partial<IUserGroupHasId>, forceUpdateParents: boolean): Promise<void> => {
     async(targetGroup: IUserGroupHasId, userGroupData: Partial<IUserGroupHasId>, forceUpdateParents: boolean): Promise<void> => {
       try {
       try {
         await updateUserGroup(targetGroup, userGroupData, forceUpdateParents);
         await updateUserGroup(targetGroup, userGroupData, forceUpdateParents);
-        toastSuccess(t('toaster.update_successed', { target: t('UserGroup') }));
+        toastSuccess(t('toaster.update_successed', { target: t('UserGroup'), ns: 'commons' }));
       }
       }
       catch {
       catch {
-        toastError(t('toaster.update_failed', { target: t('UserGroup') }));
+        toastError(t('toaster.update_failed', { target: t('UserGroup'), ns: 'commons' }));
       }
       }
     },
     },
     [t, updateUserGroup],
     [t, updateUserGroup],
@@ -205,7 +205,7 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
         parentId: userGroupData.parent,
         parentId: userGroupData.parent,
       });
       });
 
 
-      toastSuccess(t('toaster.update_successed', { target: t('UserGroup') }));
+      toastSuccess(t('toaster.update_successed', { target: t('UserGroup'), ns: 'commons' }));
 
 
       // mutate
       // mutate
       mutateChildUserGroups();
       mutateChildUserGroups();
@@ -244,7 +244,7 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
         parentId: currentUserGroupId,
         parentId: currentUserGroupId,
       });
       });
 
 
-      toastSuccess(t('toaster.update_successed', { target: t('UserGroup') }));
+      toastSuccess(t('toaster.update_successed', { target: t('UserGroup'), ns: 'commons' }));
 
 
       // mutate
       // mutate
       mutateChildUserGroups();
       mutateChildUserGroups();
@@ -296,7 +296,7 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
         parentId: null,
         parentId: null,
       });
       });
 
 
-      toastSuccess(t('toaster.update_successed', { target: t('UserGroup') }));
+      toastSuccess(t('toaster.update_successed', { target: t('UserGroup'), ns: 'commons' }));
 
 
       // mutate
       // mutate
       mutateChildUserGroups();
       mutateChildUserGroups();

+ 38 - 0
packages/app/src/components/AlertSiteUrlUndefined.tsx

@@ -0,0 +1,38 @@
+import { useTranslation } from 'next-i18next';
+
+import { useSiteUrl } from '~/stores/context';
+
+const isValidUrl = (str: string): boolean => {
+  try {
+    // eslint-disable-next-line no-new
+    new URL(str);
+    return true;
+  }
+  catch {
+    return false;
+  }
+};
+
+export const AlertSiteUrlUndefined = (): JSX.Element => {
+  const { t } = useTranslation();
+  const { data: siteUrl, error: errorSiteUrl } = useSiteUrl();
+  const isLoadingSiteUrl = siteUrl === undefined && errorSiteUrl === undefined;
+
+  if (isLoadingSiteUrl) {
+    return <></>;
+  }
+
+  if (typeof siteUrl === 'string' && isValidUrl(siteUrl)) {
+    return <></>;
+  }
+
+  return (
+    <div className="alert alert-danger rounded-0 d-edit-none mb-0 px-4 py-2">
+      <i className="icon-exclamation"></i>
+      {
+        t('commons:alert.siteUrl_is_not_set', { link: t('commons:headers.app_settings') })
+      } &gt;&gt; <a href="/admin/app">{t('commons:headers.app_settings')}<i className="icon-login"></i></a>
+    </div>
+  );
+};
+AlertSiteUrlUndefined.displayName = 'AlertSiteUrlUndefined';

+ 33 - 12
packages/app/src/components/Common/ImageCropModal.tsx

@@ -49,6 +49,14 @@ const ImageCropModal: FC<Props> = (props: Props) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const reset = useCallback(() => {
   const reset = useCallback(() => {
     if (imageRef) {
     if (imageRef) {
+      // Some SVG files may not have width and height properties, causing the render size to be 0x0
+      // Force imageRef to have width and height by create temporary image element then set the imageRef width with tempImage width
+      // Set imageRef width & height by natural width / height if image has no dimension
+      if (imageRef.width === 0 || imageRef.height === 0) {
+        imageRef.width = imageRef.naturalWidth;
+        imageRef.height = imageRef.naturalHeight;
+      }
+      // Get size of Image, min value of width and height
       const size = Math.min(imageRef.width, imageRef.height);
       const size = Math.min(imageRef.width, imageRef.height);
       setCropOtions({
       setCropOtions({
         aspect: 1,
         aspect: 1,
@@ -63,6 +71,7 @@ const ImageCropModal: FC<Props> = (props: Props) => {
 
 
   useEffect(() => {
   useEffect(() => {
     document.body.style.position = 'static';
     document.body.style.position = 'static';
+    setIsCropImage(true);
     reset();
     reset();
   }, [reset]);
   }, [reset]);
 
 
@@ -73,18 +82,22 @@ const ImageCropModal: FC<Props> = (props: Props) => {
   };
   };
 
 
 
 
-  const onCropChange = (crop) => {
-    setCropOtions(crop);
-  };
+  const getCroppedImg = async(image: HTMLImageElement, crop: ICropOptions) => {
+    const {
+      naturalWidth: imageNaturalWidth, naturalHeight: imageNaturalHeight, width: imageWidth, height: imageHeight,
+    } = image;
+
+    const {
+      width: cropWidth, height: cropHeight, x, y,
+    } = crop;
 
 
-  const getCroppedImg = async(image, crop) => {
     const canvas = document.createElement('canvas');
     const canvas = document.createElement('canvas');
-    const scaleX = image.naturalWidth / image.width;
-    const scaleY = image.naturalHeight / image.height;
-    canvas.width = crop.width;
-    canvas.height = crop.height;
+    const scaleX = imageNaturalWidth / imageWidth;
+    const scaleY = imageNaturalHeight / imageHeight;
+    canvas.width = cropWidth;
+    canvas.height = cropHeight;
     const ctx = canvas.getContext('2d');
     const ctx = canvas.getContext('2d');
-    ctx?.drawImage(image, crop.x * scaleX, crop.y * scaleY, crop.width * scaleX, crop.height * scaleY, 0, 0, crop.width, crop.height);
+    ctx?.drawImage(image, x * scaleX, y * scaleY, cropWidth * scaleX, cropHeight * scaleY, 0, 0, cropWidth, cropHeight);
     try {
     try {
       const blob = await canvasToBlob(canvas);
       const blob = await canvasToBlob(canvas);
       return blob;
       return blob;
@@ -105,7 +118,6 @@ const ImageCropModal: FC<Props> = (props: Props) => {
   // Clear image and set isImageCrop true on modal close
   // Clear image and set isImageCrop true on modal close
   const onModalCloseHandler = async() => {
   const onModalCloseHandler = async() => {
     setImageRef(null);
     setImageRef(null);
-    setIsCropImage(true);
     onModalClose();
     onModalClose();
   };
   };
 
 
@@ -129,12 +141,21 @@ const ImageCropModal: FC<Props> = (props: Props) => {
       <ModalBody className="my-4">
       <ModalBody className="my-4">
         {
         {
           isCropImage
           isCropImage
-            ? (<ReactCrop src={src} crop={cropOptions} onImageLoaded={onImageLoaded} onChange={onCropChange} circularCrop={isCircular} />)
+            ? (
+              <ReactCrop
+                style={{ backgroundColor: 'transparent' }}
+                src={src}
+                crop={cropOptions}
+                onImageLoaded={onImageLoaded}
+                onChange={crop => setCropOtions(crop)}
+                circularCrop={isCircular}
+              />
+            )
             : (<img style={{ maxWidth: imageRef?.width }} src={imageRef?.src} />)
             : (<img style={{ maxWidth: imageRef?.width }} src={imageRef?.src} />)
         }
         }
       </ModalBody>
       </ModalBody>
       <ModalFooter>
       <ModalFooter>
-        <button type="button" className="btn btn-outline-danger rounded-pill mr-auto" onClick={reset}>
+        <button type="button" className="btn btn-outline-danger rounded-pill mr-auto" disabled={!isCropImage} onClick={reset}>
           {t('crop_image_modal.reset')}
           {t('crop_image_modal.reset')}
         </button>
         </button>
         { !showCropOption && (
         { !showCropOption && (

+ 1 - 1
packages/app/src/components/CustomNavigation/CustomNav.jsx

@@ -20,7 +20,7 @@ function getBreakpointOneLevelLarger(breakpoint) {
       return 'xl';
       return 'xl';
     case 'xl':
     case 'xl':
     default:
     default:
-      return '2xl';
+      return 'xxl';
   }
   }
 }
 }
 
 

+ 1 - 1
packages/app/src/components/Fab.tsx

@@ -81,7 +81,7 @@ export const Fab = (): JSX.Element => {
   };
   };
 
 
   return (
   return (
-    <div className={`${styles['grw-fab']} grw-fab d-none d-md-block d-edit-none`} data-testid="grw-fab">
+    <div className={`${styles['grw-fab']} grw-fab d-none d-md-block d-edit-none`} data-testid="grw-fab-container">
       {currentUser != null && renderPageCreateButton()}
       {currentUser != null && renderPageCreateButton()}
       <div className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: 0, right: 0 }} data-testid="grw-fab-return-to-top">
       <div className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: 0, right: 0 }} data-testid="grw-fab-return-to-top">
         <button
         <button

+ 1 - 1
packages/app/src/components/Hotkeys/HotkeysDetector.jsx

@@ -1,6 +1,6 @@
 import React, { useMemo, useCallback } from 'react';
 import React, { useMemo, useCallback } from 'react';
-import PropTypes from 'prop-types';
 
 
+import PropTypes from 'prop-types';
 import { GlobalHotKeys } from 'react-hotkeys';
 import { GlobalHotKeys } from 'react-hotkeys';
 
 
 import HotkeyStroke from '~/client/models/HotkeyStroke';
 import HotkeyStroke from '~/client/models/HotkeyStroke';

+ 3 - 4
packages/app/src/components/Hotkeys/HotkeysManager.jsx

@@ -1,13 +1,12 @@
 import React, { useState } from 'react';
 import React, { useState } from 'react';
 
 
 import HotkeysDetector from './HotkeysDetector';
 import HotkeysDetector from './HotkeysDetector';
-
-import ShowStaffCredit from './Subscribers/ShowStaffCredit';
-import SwitchToMirrorMode from './Subscribers/SwitchToMirrorMode';
-import ShowShortcutsModal from './Subscribers/ShowShortcutsModal';
 import CreatePage from './Subscribers/CreatePage';
 import CreatePage from './Subscribers/CreatePage';
 import EditPage from './Subscribers/EditPage';
 import EditPage from './Subscribers/EditPage';
 import FocusToGlobalSearch from './Subscribers/FocusToGlobalSearch';
 import FocusToGlobalSearch from './Subscribers/FocusToGlobalSearch';
+import ShowShortcutsModal from './Subscribers/ShowShortcutsModal';
+import ShowStaffCredit from './Subscribers/ShowStaffCredit';
+import SwitchToMirrorMode from './Subscribers/SwitchToMirrorMode';
 
 
 // define supported components list
 // define supported components list
 const SUPPORTED_COMPONENTS = [
 const SUPPORTED_COMPONENTS = [

+ 1 - 0
packages/app/src/components/Hotkeys/Subscribers/SwitchToMirrorMode.jsx

@@ -1,4 +1,5 @@
 import React, { useEffect } from 'react';
 import React, { useEffect } from 'react';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 const SwitchToMirrorMode = (props) => {
 const SwitchToMirrorMode = (props) => {

+ 11 - 6
packages/app/src/components/InstallerForm.tsx

@@ -2,8 +2,7 @@ import {
   FormEventHandler, memo, useCallback, useState,
   FormEventHandler, memo, useCallback, useState,
 } from 'react';
 } from 'react';
 
 
-import i18next from 'i18next';
-import { useTranslation, i18n } from 'next-i18next';
+import { useTranslation } from 'next-i18next';
 
 
 import { i18n as i18nConfig } from '^/config/next-i18next.config';
 import { i18n as i18nConfig } from '^/config/next-i18next.config';
 
 
@@ -11,10 +10,11 @@ import { toastError } from '~/client/util/apiNotification';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
 
 
 const InstallerForm = memo((): JSX.Element => {
 const InstallerForm = memo((): JSX.Element => {
-  const { t } = useTranslation();
+  const { t, i18n } = useTranslation();
 
 
   const [isValidUserName, setValidUserName] = useState(true);
   const [isValidUserName, setValidUserName] = useState(true);
   const [isSubmittingDisabled, setSubmittingDisabled] = useState(false);
   const [isSubmittingDisabled, setSubmittingDisabled] = useState(false);
+  const [currentLocale, setCurrentLocale] = useState('en_US');
 
 
   const checkUserName = useCallback(async(event) => {
   const checkUserName = useCallback(async(event) => {
     const axios = require('axios').create({
     const axios = require('axios').create({
@@ -28,6 +28,11 @@ const InstallerForm = memo((): JSX.Element => {
     setValidUserName(res.data.valid);
     setValidUserName(res.data.valid);
   }, []);
   }, []);
 
 
+  const onClickLanguageItem = useCallback((locale) => {
+    i18n.changeLanguage(locale);
+    setCurrentLocale(locale);
+  }, [i18n]);
+
   const submitHandler: FormEventHandler = useCallback(async(e: any) => {
   const submitHandler: FormEventHandler = useCallback(async(e: any) => {
     e.preventDefault();
     e.preventDefault();
 
 
@@ -59,7 +64,7 @@ const InstallerForm = memo((): JSX.Element => {
         name,
         name,
         email,
         email,
         password,
         password,
-        'app:globalLang': formData['registerForm[app:globalLang]'].value,
+        'app:globalLang': currentLocale,
       },
       },
     };
     };
 
 
@@ -78,7 +83,7 @@ const InstallerForm = memo((): JSX.Element => {
 
 
       toastError(t('installer.failed_to_install'));
       toastError(t('installer.failed_to_install'));
     }
     }
-  }, [isSubmittingDisabled, t]);
+  }, [isSubmittingDisabled, t, currentLocale]);
 
 
   const hasErrorClass = isValidUserName ? '' : ' has-error';
   const hasErrorClass = isValidUserName ? '' : ' has-error';
   const unavailableUserId = isValidUserName
   const unavailableUserId = isValidUserName
@@ -132,7 +137,7 @@ const InstallerForm = memo((): JSX.Element => {
                         data-testid={`dropdownLanguageMenu-${locale}`}
                         data-testid={`dropdownLanguageMenu-${locale}`}
                         className="dropdown-item"
                         className="dropdown-item"
                         type="button"
                         type="button"
-                        onClick={() => { i18next.changeLanguage(locale) }}
+                        onClick={() => { onClickLanguageItem(locale) }}
                       >
                       >
                         {fixedT?.('meta.display_name')}
                         {fixedT?.('meta.display_name')}
                       </button>
                       </button>

+ 6 - 0
packages/app/src/components/Layout/AdminLayout.tsx

@@ -8,6 +8,9 @@ import { RawLayout } from './RawLayout';
 
 
 import styles from './Admin.module.scss';
 import styles from './Admin.module.scss';
 
 
+
+const HotkeysManager = dynamic(() => import('../Hotkeys/HotkeysManager'), { ssr: false });
+
 const AdminNotFoundPage = dynamic(() => import('../Admin/NotFoundPage').then(mod => mod.AdminNotFoundPage), { ssr: false });
 const AdminNotFoundPage = dynamic(() => import('../Admin/NotFoundPage').then(mod => mod.AdminNotFoundPage), { ssr: false });
 
 
 
 
@@ -54,6 +57,9 @@ const AdminLayout = ({
 
 
         <SystemVersion />
         <SystemVersion />
       </div>
       </div>
+
+      <HotkeysManager />
+
     </RawLayout>
     </RawLayout>
   );
   );
 };
 };

+ 2 - 0
packages/app/src/components/Layout/BasicLayout.tsx

@@ -9,6 +9,7 @@ import Sidebar from '../Sidebar';
 
 
 import { RawLayout } from './RawLayout';
 import { RawLayout } from './RawLayout';
 
 
+const AlertSiteUrlUndefined = dynamic(() => import('../AlertSiteUrlUndefined').then(mod => mod.AlertSiteUrlUndefined), { ssr: false });
 const HotkeysManager = dynamic(() => import('../Hotkeys/HotkeysManager'), { ssr: false });
 const HotkeysManager = dynamic(() => import('../Hotkeys/HotkeysManager'), { ssr: false });
 // const PageCreateModal = dynamic(() => import('../client/js/components/PageCreateModal'), { ssr: false });
 // const PageCreateModal = dynamic(() => import('../client/js/components/PageCreateModal'), { ssr: false });
 const GrowiNavbarBottom = dynamic(() => import('../Navbar/GrowiNavbarBottom').then(mod => mod.GrowiNavbarBottom), { ssr: false });
 const GrowiNavbarBottom = dynamic(() => import('../Navbar/GrowiNavbarBottom').then(mod => mod.GrowiNavbarBottom), { ssr: false });
@@ -51,6 +52,7 @@ export const BasicLayout = ({
           </div>
           </div>
 
 
           <div className="flex-fill mw-0" style={{ position: 'relative' }}>
           <div className="flex-fill mw-0" style={{ position: 'relative' }}>
+            <AlertSiteUrlUndefined />
             {children}
             {children}
           </div>
           </div>
         </div>
         </div>

+ 1 - 1
packages/app/src/components/Layout/RawLayout.tsx

@@ -21,7 +21,7 @@ type Props = {
 }
 }
 
 
 export const RawLayout = ({ children, title, className }: Props): JSX.Element => {
 export const RawLayout = ({ children, title, className }: Props): JSX.Element => {
-  const classNames: string[] = ['wrapper'];
+  const classNames: string[] = ['layout-root'];
   if (className != null) {
   if (className != null) {
     classNames.push(className);
     classNames.push(className);
   }
   }

+ 1 - 1
packages/app/src/components/Me/ApiSettings.tsx

@@ -19,7 +19,7 @@ const ApiSettings = React.memo((): JSX.Element => {
       await apiv3Put('/personal-setting/api-token');
       await apiv3Put('/personal-setting/api-token');
       mutateDatabaseData();
       mutateDatabaseData();
 
 
-      toastSuccess(t('toaster.update_successed', { target: t('page_me_apitoken.api_token') }));
+      toastSuccess(t('toaster.update_successed', { target: t('page_me_apitoken.api_token'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 1 - 1
packages/app/src/components/Me/BasicInfoSettings.tsx

@@ -22,7 +22,7 @@ export const BasicInfoSettings = (): JSX.Element => {
     try {
     try {
       await updateBasicInfo();
       await updateBasicInfo();
       sync();
       sync();
-      toastSuccess(t('toaster.update_successed', { target: t('Basic Info') }));
+      toastSuccess(t('toaster.update_successed', { target: t('Basic Info'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 44 - 31
packages/app/src/components/Me/EditorSettings.tsx

@@ -1,12 +1,13 @@
 import React, {
 import React, {
-  Dispatch,
+  Dispatch, memo,
   FC, SetStateAction, useCallback, useEffect, useState,
   FC, SetStateAction, useCallback, useEffect, useState,
+  useMemo,
 } from 'react';
 } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
+import { useEditorSettings } from '~/stores/editor';
 
 
 
 
 type EditorSettingsBodyProps = Record<string, never>;
 type EditorSettingsBodyProps = Record<string, never>;
@@ -194,51 +195,62 @@ const RuleListGroup: FC<RuleListGroupProps> = ({
   );
   );
 };
 };
 
 
+const createRulesFromDefaultList = (rule: { name: string }) => (
+  {
+    name: rule.name,
+    isEnabled: true,
+  }
+);
+
 
 
-export const EditorSettings: FC<EditorSettingsBodyProps> = () => {
+export const EditorSettings = memo((): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const [textlintRules, setTextlintRules] = useState<LintRule[]>([]);
   const [textlintRules, setTextlintRules] = useState<LintRule[]>([]);
 
 
-  const initializeEditorSettings = useCallback(async() => {
-    const { data } = await apiv3Get('/personal-setting/editor-settings');
-    const retrievedRules: LintRule[] = data?.textlintSettings?.textlintRules;
+  const { data: dataEditorSettings, update: updateEditorSettings } = useEditorSettings();
 
 
-    // If database is empty, add default rules to state
-    if (retrievedRules != null && retrievedRules.length > 0) {
-      setTextlintRules(retrievedRules);
-    }
-    else {
-      const createRulesFromDefaultList = (rule: { name: string }) => (
-        {
-          name: rule.name,
-          isEnabled: true,
-        }
-      );
+  const defaultRules = useMemo(() => {
+    const defaultCommonRules = commonRulesMenuItems.map(rule => createRulesFromDefaultList(rule));
+    const defaultJapaneseRules = japaneseRulesMenuItems.map(rule => createRulesFromDefaultList(rule));
+
+    return [...defaultCommonRules, ...defaultJapaneseRules];
+  }, []);
 
 
-      const defaultCommonRules = commonRulesMenuItems.map(rule => createRulesFromDefaultList(rule));
-      const defaultJapaneseRules = japaneseRulesMenuItems.map(rule => createRulesFromDefaultList(rule));
-      setTextlintRules([...defaultCommonRules, ...defaultJapaneseRules]);
+  const initializeEditorSettings = useCallback(() => {
+    if (dataEditorSettings == null) {
+      return;
     }
     }
 
 
-  }, []);
+    const retrievedRules: LintRule[] | undefined = dataEditorSettings?.textlintSettings?.textlintRules;
 
 
-  useEffect(() => {
-    initializeEditorSettings();
-  }, [initializeEditorSettings]);
+    // If database is empty, add default rules to state
+    if (retrievedRules != null && retrievedRules.length > 0) {
+      setTextlintRules(retrievedRules);
+      return;
+    }
+    setTextlintRules(defaultRules);
+  }, [dataEditorSettings, defaultRules]);
 
 
-  const updateRulesHandler = async() => {
+  const updateRulesHandler = useCallback(async() => {
     try {
     try {
-      const { data } = await apiv3Put('/personal-setting/editor-settings', { textlintSettings: { textlintRules: [...textlintRules] } });
-      setTextlintRules(data.textlintSettings.textlintRules);
-      toastSuccess(t('toaster.update_successed', { target: 'Updated Textlint Settings' }));
+      await updateEditorSettings({ textlintSettings: { textlintRules } });
+      toastSuccess(t('toaster.update_successed', { target: 'Updated Textlint Settings', ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
-  };
+  }, [t, textlintRules, updateEditorSettings]);
+
+  useEffect(() => {
+    initializeEditorSettings();
+  }, [initializeEditorSettings]);
 
 
   if (textlintRules == null) {
   if (textlintRules == null) {
-    return <></>;
+    return (
+      <div className="text-muted text-center">
+        <i className="fa fa-2x fa-spinner fa-pulse"></i>
+      </div>
+    );
   }
   }
 
 
   return (
   return (
@@ -270,4 +282,5 @@ export const EditorSettings: FC<EditorSettingsBodyProps> = () => {
       </div>
       </div>
     </div>
     </div>
   );
   );
-};
+});
+EditorSettings.displayName = 'EditorSettings';

+ 1 - 1
packages/app/src/components/Me/InAppNotificationSettings.tsx

@@ -54,7 +54,7 @@ const InAppNotificationSettings: FC = () => {
     try {
     try {
       const { data } = await apiv3Put('/personal-setting/in-app-notification-settings', { subscribeRules });
       const { data } = await apiv3Put('/personal-setting/in-app-notification-settings', { subscribeRules });
       setSubscribeRules(data.subscribeRules);
       setSubscribeRules(data.subscribeRules);
-      toastSuccess(t('toaster.update_successed', { target: 'InAppNotification Settings' }));
+      toastSuccess(t('toaster.update_successed', { target: 'InAppNotification Settings', ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 2 - 2
packages/app/src/components/Me/PasswordSettings.jsx

@@ -1,7 +1,7 @@
 import React, { useCallback } from 'react';
 import React, { useCallback } from 'react';
 
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
@@ -52,7 +52,7 @@ class PasswordSettings extends React.Component {
       if (onSubmit != null) {
       if (onSubmit != null) {
         onSubmit();
         onSubmit();
       }
       }
-      toastSuccess(t('toaster.update_successed', { target: t('Password') }));
+      toastSuccess(t('toaster.update_successed', { target: t('Password'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 3 - 3
packages/app/src/components/Me/ProfileImageSettings.tsx

@@ -48,7 +48,7 @@ const ProfileImageSettings = (): JSX.Element => {
       formData.append('file', croppedImage);
       formData.append('file', croppedImage);
       const response = await apiPostForm('/attachments.uploadProfileImage', formData);
       const response = await apiPostForm('/attachments.uploadProfileImage', formData);
 
 
-      toastSuccess(t('toaster.update_successed', { target: t('Current Image') }));
+      toastSuccess(t('toaster.update_successed', { target: t('Current Image'), ns: 'commons' }));
 
 
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       setUploadedPictureSrc((response as any).attachment.filePathProxied);
       setUploadedPictureSrc((response as any).attachment.filePathProxied);
@@ -64,7 +64,7 @@ const ProfileImageSettings = (): JSX.Element => {
       await apiPost('/attachments.removeProfileImage');
       await apiPost('/attachments.removeProfileImage');
 
 
       setUploadedPictureSrc(undefined);
       setUploadedPictureSrc(undefined);
-      toastSuccess(t('toaster.update_successed', { target: t('Current Image') }));
+      toastSuccess(t('toaster.update_successed', { target: t('Current Image'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
@@ -78,7 +78,7 @@ const ProfileImageSettings = (): JSX.Element => {
       const { userData } = response.data;
       const { userData } = response.data;
       setGravatarEnabled(userData.isGravatarEnabled);
       setGravatarEnabled(userData.isGravatarEnabled);
 
 
-      toastSuccess(t('toaster.update_successed', { target: t('Set Profile Image') }));
+      toastSuccess(t('toaster.update_successed', { target: t('Set Profile Image'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 1 - 1
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -330,7 +330,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
                     pageId={pageId}
                     pageId={pageId}
                     revisionId={revisionId}
                     revisionId={revisionId}
                     shareLinkId={shareLinkId}
                     shareLinkId={shareLinkId}
-                    path={path}
+                    path={path ?? currentPathname} // If the page is empty, "path" is undefined
                     disableSeenUserInfoPopover={isSharedUser}
                     disableSeenUserInfoPopover={isSharedUser}
                     showPageControlDropdown={isAbleToShowPageManagement}
                     showPageControlDropdown={isAbleToShowPageManagement}
                     additionalMenuItemRenderer={additionalMenuItemsRenderer}
                     additionalMenuItemRenderer={additionalMenuItemsRenderer}

+ 8 - 5
packages/app/src/components/Page.tsx

@@ -10,6 +10,7 @@ import dynamic from 'next/dynamic';
 
 
 import { HtmlElementNode } from 'rehype-toc';
 import { HtmlElementNode } from 'rehype-toc';
 
 
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { getOptionsToSave } from '~/client/util/editor';
 import { getOptionsToSave } from '~/client/util/editor';
 import {
 import {
   useIsGuestUser, useCurrentPageTocNode, useShareLinkId,
   useIsGuestUser, useCurrentPageTocNode, useShareLinkId,
@@ -124,11 +125,12 @@ class PageSubstance extends React.Component<PageSubstanceProps> {
     //   const { page, tags } = await pageContainer.save(newMarkdown, this.props.editorMode, optionsToSave);
     //   const { page, tags } = await pageContainer.save(newMarkdown, this.props.editorMode, optionsToSave);
     //   logger.debug('success to save');
     //   logger.debug('success to save');
 
 
-    //   pageContainer.showSuccessToastr();
+    // // Todo: add translation
+    // toastSuccess(t(''));
     // }
     // }
     // catch (error) {
     // catch (error) {
     //   logger.error('failed to save', error);
     //   logger.error('failed to save', error);
-    //   pageContainer.showErrorToastr(error);
+    // toastError(error);
     // }
     // }
     // finally {
     // finally {
     //   this.setState({ currentTargetTableArea: null });
     //   this.setState({ currentTargetTableArea: null });
@@ -156,11 +158,12 @@ class PageSubstance extends React.Component<PageSubstanceProps> {
     //     const { page, tags } = await pageContainer.save(newMarkdown, this.props.editorMode, optionsToSave);
     //     const { page, tags } = await pageContainer.save(newMarkdown, this.props.editorMode, optionsToSave);
     //     logger.debug('success to save');
     //     logger.debug('success to save');
 
 
-  //     pageContainer.showSuccessToastr();
-  //   }
+    // // Todo: add translation
+    //   toastSuccess(t(''));
+    //   }
   //   catch (error) {
   //   catch (error) {
   //     logger.error('failed to save', error);
   //     logger.error('failed to save', error);
-  //     pageContainer.showErrorToastr(error);
+  //     toastError(error);
   //   }
   //   }
   //   finally {
   //   finally {
   //     this.setState({ currentTargetDrawioArea: null });
   //     this.setState({ currentTargetDrawioArea: null });

+ 2 - 1
packages/app/src/components/Page/CopyDropdown.jsx

@@ -11,6 +11,7 @@ import {
   Tooltip,
   Tooltip,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
+import styles from './CopyDropdown.module.scss';
 
 
 const { encodeSpaces } = pagePathUtils;
 const { encodeSpaces } = pagePathUtils;
 
 
@@ -109,7 +110,7 @@ const CopyDropdown = (props) => {
 
 
   return (
   return (
     <>
     <>
-      <Dropdown className="grw-copy-dropdown" isOpen={dropdownOpen} toggle={toggleDropdown}>
+      <Dropdown className={`${styles['grw-copy-dropdown']} grw-copy-dropdown`} isOpen={dropdownOpen} toggle={toggleDropdown}>
         <DropdownToggle
         <DropdownToggle
           caret
           caret
           className={dropdownToggleClassName}
           className={dropdownToggleClassName}

+ 1 - 1
packages/app/src/styles/molecules/copy-dropdown.scss → packages/app/src/components/Page/CopyDropdown.module.scss

@@ -1,4 +1,4 @@
-.grw-copy-dropdown {
+.grw-copy-dropdown :global {
   .dropdown-menu {
   .dropdown-menu {
     min-width: 310px;
     min-width: 310px;
 
 

+ 11 - 13
packages/app/src/components/Page/DisplaySwitcher.tsx

@@ -3,8 +3,8 @@ import React, { useMemo } from 'react';
 import { pagePathUtils } from '@growi/core';
 import { pagePathUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import { Link } from 'react-scroll';
 
 
-// import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
 import {
 import {
   useCurrentPagePath, useIsSharedUser, useIsEditable, useShareLinkId, useIsNotFound,
   useCurrentPagePath, useIsSharedUser, useIsEditable, useShareLinkId, useIsNotFound,
 } from '~/stores/context';
 } from '~/stores/context';
@@ -79,18 +79,18 @@ const PageView = React.memo((): JSX.Element => {
             </div>
             </div>
 
 
             {/* Comments */}
             {/* Comments */}
-            {/* { getCommentListDom != null && !isTopPagePath && ( */}
             { !isTopPagePath && (
             { !isTopPagePath && (
               <div className={`mt-2 grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
               <div className={`mt-2 grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
-                <button
-                  type="button"
-                  className="btn btn-block btn-outline-secondary grw-btn-page-accessories rounded-pill d-flex justify-content-between align-items-center"
-                  // onClick={() => smoothScrollIntoView(getCommentListDom, WIKI_HEADER_LINK)}
-                >
-                  <i className="icon-fw icon-bubbles grw-page-accessories-control-icon"></i>
-                  <span>Comments</span>
-                  <CountBadge count={currentPage?.commentCount} />
-                </button>
+                <Link to={'page-comments'} smooth="easeOutQuart" offset={-100} duration={800}>
+                  <button
+                    type="button"
+                    className="btn btn-block btn-outline-secondary grw-btn-page-accessories rounded-pill d-flex justify-content-between align-items-center"
+                  >
+                    <i className="icon-fw icon-bubbles grw-page-accessories-control-icon"></i>
+                    <span>Comments</span>
+                    <CountBadge count={currentPage?.commentCount} />
+                  </button>
+                </Link>
               </div>
               </div>
             ) }
             ) }
 
 
@@ -109,8 +109,6 @@ PageView.displayName = 'PageView';
 
 
 
 
 const DisplaySwitcher = React.memo((): JSX.Element => {
 const DisplaySwitcher = React.memo((): JSX.Element => {
-  // get element for smoothScroll
-  // const getCommentListDom = useMemo(() => { return document.getElementById('page-comments-list') }, []);
 
 
   const { data: isEditable } = useIsEditable();
   const { data: isEditable } = useIsEditable();
 
 

+ 1 - 1
packages/app/src/components/PageComment.tsx

@@ -152,7 +152,7 @@ export const PageComment: FC<PageCommentProps> = memo((props:PageCommentProps):
 
 
   return (
   return (
     <>
     <>
-      <div className={`${styles['page-comment-styles']} page-comments-row comment-list`}>
+      <div id="page-comments" className={`${styles['page-comment-styles']} page-comments-row comment-list`}>
         <div className="container-lg">
         <div className="container-lg">
           <div className="page-comments">
           <div className="page-comments">
             <h2 className={commentTitleClasses}><i className="icon-fw icon-bubbles"></i>Comments</h2>
             <h2 className={commentTitleClasses}><i className="icon-fw icon-bubbles"></i>Comments</h2>

+ 15 - 8
packages/app/src/components/PageEditor.tsx

@@ -6,9 +6,11 @@ import EventEmitter from 'events';
 
 
 import { envUtils, PageGrant } from '@growi/core';
 import { envUtils, PageGrant } from '@growi/core';
 import detectIndent from 'detect-indent';
 import detectIndent from 'detect-indent';
+import { useTranslation } from 'next-i18next';
 import { throttle, debounce } from 'throttle-debounce';
 import { throttle, debounce } from 'throttle-debounce';
 
 
 import { saveOrUpdate } from '~/client/services/page-operation';
 import { saveOrUpdate } from '~/client/services/page-operation';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
 import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
 import { getOptionsToSave } from '~/client/util/editor';
 import { getOptionsToSave } from '~/client/util/editor';
 import { IEditorMethods } from '~/interfaces/editor-methods';
 import { IEditorMethods } from '~/interfaces/editor-methods';
@@ -48,6 +50,7 @@ let isOriginOfScrollSyncPreview = false;
 
 
 const PageEditor = React.memo((): JSX.Element => {
 const PageEditor = React.memo((): JSX.Element => {
 
 
+  const { t } = useTranslation();
   const { data: pageId } = useCurrentPageId();
   const { data: pageId } = useCurrentPageId();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPathname } = useCurrentPathname();
   const { data: currentPathname } = useCurrentPathname();
@@ -95,7 +98,8 @@ const PageEditor = React.memo((): JSX.Element => {
     setMarkdownWithDebounce(value, isClean);
     setMarkdownWithDebounce(value, isClean);
   }, [setMarkdownWithDebounce]);
   }, [setMarkdownWithDebounce]);
 
 
-  const save = useCallback(async(opts?: {overwriteScopesOfDescendants: boolean}) => {
+  // return true if the save succeeds, otherwise false.
+  const save = useCallback(async(opts?: {overwriteScopesOfDescendants: boolean}): Promise<boolean> => {
     if (grantData == null || isSlackEnabled == null || currentPathname == null) {
     if (grantData == null || isSlackEnabled == null || currentPathname == null) {
       logger.error('Some materials to save are invalid', { grantData, isSlackEnabled, currentPathname });
       logger.error('Some materials to save are invalid', { grantData, isSlackEnabled, currentPathname });
       throw new Error('Some materials to save are invalid');
       throw new Error('Some materials to save are invalid');
@@ -113,10 +117,11 @@ const PageEditor = React.memo((): JSX.Element => {
       await saveOrUpdate(optionsToSave, { pageId, path: currentPagePath || currentPathname, revisionId: currentRevisionId }, markdownToSave.current);
       await saveOrUpdate(optionsToSave, { pageId, path: currentPagePath || currentPathname, revisionId: currentRevisionId }, markdownToSave.current);
       await mutateCurrentPage();
       await mutateCurrentPage();
       mutateIsEnabledUnsavedWarning(false);
       mutateIsEnabledUnsavedWarning(false);
+      return true;
     }
     }
     catch (error) {
     catch (error) {
       logger.error('failed to save', error);
       logger.error('failed to save', error);
-      // pageContainer.showErrorToastr(error);
+      toastError(error);
       if (error.code === 'conflict') {
       if (error.code === 'conflict') {
         // pageContainer.setState({
         // pageContainer.setState({
         //   remoteRevisionId: error.data.revisionId,
         //   remoteRevisionId: error.data.revisionId,
@@ -125,6 +130,7 @@ const PageEditor = React.memo((): JSX.Element => {
         //   lastUpdateUser: error.data.user,
         //   lastUpdateUser: error.data.user,
         // });
         // });
       }
       }
+      return false;
     }
     }
 
 
   // eslint-disable-next-line max-len
   // eslint-disable-next-line max-len
@@ -144,11 +150,12 @@ const PageEditor = React.memo((): JSX.Element => {
       return;
       return;
     }
     }
 
 
-    await save();
+    const isSuccess = await save();
+    if (isSuccess) {
+      toastSuccess(t('toaster.save_succeeded'));
+    }
 
 
-    // TODO: show toastr
-    // pageContainer.showErrorToastr(error);
-  }, [editorMode, save]);
+  }, [editorMode, save, t]);
 
 
 
 
   /**
   /**
@@ -195,13 +202,13 @@ const PageEditor = React.memo((): JSX.Element => {
       // when if created newly
       // when if created newly
       if (res.pageCreated) {
       if (res.pageCreated) {
         logger.info('Page is created', res.page._id);
         logger.info('Page is created', res.page._id);
-        // pageContainer.updateStateAfterSave(res.page, res.tags, res.revision, editorMode);
+        globalEmitter.emit('resetInitializedHackMdStatus');
         mutateGrant(res.page.grant);
         mutateGrant(res.page.grant);
       }
       }
     }
     }
     catch (e) {
     catch (e) {
       logger.error('failed to upload', e);
       logger.error('failed to upload', e);
-      // pageContainer.showErrorToastr(e);
+      toastError(e);
     }
     }
     finally {
     finally {
       editorRef.current.terminateUploadingState();
       editorRef.current.terminateUploadingState();

+ 22 - 14
packages/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -23,7 +23,8 @@ import EditorIcon from './EditorIcon';
 import EmojiPicker from './EmojiPicker';
 import EmojiPicker from './EmojiPicker';
 import EmojiPickerHelper from './EmojiPickerHelper';
 import EmojiPickerHelper from './EmojiPickerHelper';
 import GridEditModal from './GridEditModal';
 import GridEditModal from './GridEditModal';
-import geu from './GridEditorUtil';
+// TODO: re-impl with https://redmine.weseek.co.jp/issues/107248
+// import geu from './GridEditorUtil';
 // import HandsontableModal from './HandsontableModal';
 // import HandsontableModal from './HandsontableModal';
 import LinkEditModal from './LinkEditModal';
 import LinkEditModal from './LinkEditModal';
 import mdu from './MarkdownDrawioUtil';
 import mdu from './MarkdownDrawioUtil';
@@ -152,7 +153,8 @@ class CodeMirrorEditor extends AbstractEditor {
     this.renderCheatsheetModalButton = this.renderCheatsheetModalButton.bind(this);
     this.renderCheatsheetModalButton = this.renderCheatsheetModalButton.bind(this);
 
 
     this.makeHeaderHandler = this.makeHeaderHandler.bind(this);
     this.makeHeaderHandler = this.makeHeaderHandler.bind(this);
-    this.showGridEditorHandler = this.showGridEditorHandler.bind(this);
+    // TODO: re-impl with https://redmine.weseek.co.jp/issues/107248
+    // this.showGridEditorHandler = this.showGridEditorHandler.bind(this);
     this.showLinkEditHandler = this.showLinkEditHandler.bind(this);
     this.showLinkEditHandler = this.showLinkEditHandler.bind(this);
     this.showHandsonTableHandler = this.showHandsonTableHandler.bind(this);
     this.showHandsonTableHandler = this.showHandsonTableHandler.bind(this);
 
 
@@ -858,9 +860,10 @@ class CodeMirrorEditor extends AbstractEditor {
     cm.focus();
     cm.focus();
   }
   }
 
 
-  showGridEditorHandler() {
-    this.gridEditModal.current.show(geu.getGridHtml(this.getCodeMirror()));
-  }
+  // TODO: re-impl with https://redmine.weseek.co.jp/issues/107248
+  // showGridEditorHandler() {
+  //   this.gridEditModal.current.show(geu.getGridHtml(this.getCodeMirror()));
+  // }
 
 
   showLinkEditHandler() {
   showLinkEditHandler() {
     this.linkEditModal.current.show(markdownLinkUtil.getMarkdownLink(this.getCodeMirror()));
     this.linkEditModal.current.show(markdownLinkUtil.getMarkdownLink(this.getCodeMirror()));
@@ -998,15 +1001,16 @@ class CodeMirrorEditor extends AbstractEditor {
       >
       >
         <EditorIcon icon="Image" />
         <EditorIcon icon="Image" />
       </Button>,
       </Button>,
-      <Button
-        key="nav-item-grid"
-        color={null}
-        size="sm"
-        title="Grid"
-        onClick={this.showGridEditorHandler}
-      >
-        <EditorIcon icon="Grid" />
-      </Button>,
+      // TODO: re-impl with https://redmine.weseek.co.jp/issues/107248
+      // <Button
+      //   key="nav-item-grid"
+      //   color={null}
+      //   size="sm"
+      //   title="Grid"
+      //   onClick={this.showGridEditorHandler}
+      // >
+      //   <EditorIcon icon="Grid" />
+      // </Button>,
       <Button
       <Button
         key="nav-item-table"
         key="nav-item-table"
         color={null}
         color={null}
@@ -1119,10 +1123,14 @@ class CodeMirrorEditor extends AbstractEditor {
         { this.renderCheatsheetOverlay() }
         { this.renderCheatsheetOverlay() }
         { this.renderEmojiPicker() }
         { this.renderEmojiPicker() }
 
 
+        {/*
+        // TODO: re-impl with https://redmine.weseek.co.jp/issues/107248
         <GridEditModal
         <GridEditModal
           ref={this.gridEditModal}
           ref={this.gridEditModal}
           onSave={(grid) => { return geu.replaceGridWithHtmlWithEditor(this.getCodeMirror(), grid) }}
           onSave={(grid) => { return geu.replaceGridWithHtmlWithEditor(this.getCodeMirror(), grid) }}
         />
         />
+         */}
+
         <LinkEditModal
         <LinkEditModal
           ref={this.linkEditModal}
           ref={this.linkEditModal}
           onSave={(linkText) => { return markdownLinkUtil.replaceFocusedMarkdownLinkWithEditor(this.getCodeMirror(), linkText) }}
           onSave={(linkText) => { return markdownLinkUtil.replaceFocusedMarkdownLinkWithEditor(this.getCodeMirror(), linkText) }}

+ 1 - 12
packages/app/src/components/PageEditor/CodeMirrorEditor.module.scss

@@ -1,4 +1,3 @@
-@use '~/styles/variables' as var;
 @use '~/styles/bootstrap/init' as bs;
 @use '~/styles/bootstrap/init' as bs;
 
 
 .grw-codemirror-editor :global {
 .grw-codemirror-editor :global {
@@ -17,7 +16,7 @@
     pre.CodeMirror-line.grw-cm-header-line {
     pre.CodeMirror-line.grw-cm-header-line {
       padding-top: 0.16em;
       padding-top: 0.16em;
       padding-bottom: 0.08em;
       padding-bottom: 0.08em;
-      font-family: bs.$font-family-monospace;
+      font-family: var(--font-family-monospace);
 
 
       // '#'
       // '#'
       .cm-formatting-header {
       .cm-formatting-header {
@@ -76,16 +75,6 @@
   .CodeMirror-hints {
   .CodeMirror-hints {
     max-height: 30em !important;
     max-height: 30em !important;
 
 
-    .CodeMirror-hint.crowi-emoji-autocomplete {
-      font-family: var.$font-family-monospace-not-strictly;
-      line-height: 1.6em;
-
-      .img-container {
-        display: inline-block;
-        width: 30px;
-      }
-    }
-
     // active line
     // active line
     .CodeMirror-hint-active.crowi-emoji-autocomplete {
     .CodeMirror-hint-active.crowi-emoji-autocomplete {
       .img-container {
       .img-container {

+ 1 - 1
packages/app/src/components/PageEditor/Editor.module.scss

@@ -158,7 +158,7 @@
 .modal-gfm-cheatsheet :global {
 .modal-gfm-cheatsheet :global {
   .modal-body {
   .modal-body {
     .hljs {
     .hljs {
-      font-family: bs.$font-family-monospace;
+      font-family: var(--font-family-monospace);
     }
     }
   }
   }
 }
 }

+ 15 - 0
packages/app/src/components/PageEditorByHackmd.tsx

@@ -131,6 +131,21 @@ export const PageEditorByHackmd = (): JSX.Element => {
     };
     };
   }, [saveAndReturnToViewHandler]);
   }, [saveAndReturnToViewHandler]);
 
 
+  const resetInitializedStatusHandler = useCallback(() => {
+    setIsInitialized(false);
+  }, []);
+
+
+  // set handler to save and reload Page
+  useEffect(() => {
+    globalEmitter.on('resetInitializedHackMdStatus', resetInitializedStatusHandler);
+
+    return function cleanup() {
+      globalEmitter.removeListener('resetInitializedHackMdStatus', resetInitializedStatusHandler);
+    };
+  }, [resetInitializedStatusHandler]);
+
+
   const isResume = useCallback(() => {
   const isResume = useCallback(() => {
     const isPageExistsOnHackmd = (pageIdOnHackmd != null);
     const isPageExistsOnHackmd = (pageIdOnHackmd != null);
     return (isPageExistsOnHackmd && hasDraftOnHackmd) || isHackmdDraftUpdatingInRealtime;
     return (isPageExistsOnHackmd && hasDraftOnHackmd) || isHackmdDraftUpdatingInRealtime;

+ 13 - 13
packages/app/src/components/PagePathHierarchicalLink.tsx

@@ -1,4 +1,4 @@
-import React, { memo } from 'react';
+import React, { memo, useCallback } from 'react';
 
 
 import Link from 'next/link';
 import Link from 'next/link';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
@@ -19,9 +19,16 @@ type PagePathHierarchicalLinkProps = {
 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkProps): JSX.Element => {
 const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkProps): JSX.Element => {
   const {
   const {
-    linkedPagePath, linkedPagePathByHtml, basePath, isInTrash,
+    linkedPagePath, linkedPagePathByHtml, basePath, isInTrash, isInnerElem,
   } = props;
   } = props;
 
 
+  // eslint-disable-next-line react/prop-types
+  const RootElm = useCallback(({ children }) => {
+    return isInnerElem
+      ? <>{children}</>
+      : <span className="grw-page-path-hierarchical-link text-break">{children}</span>;
+  }, [isInnerElem]);
+
   // render root element
   // render root element
   if (linkedPagePath.isRoot) {
   if (linkedPagePath.isRoot) {
     if (basePath != null) {
     if (basePath != null) {
@@ -30,17 +37,17 @@ const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkProps): JS
 
 
     return isInTrash
     return isInTrash
       ? (
       ? (
-        <>
+        <RootElm>
           <span className="path-segment">
           <span className="path-segment">
             <Link href="/trash" prefetch={false}>
             <Link href="/trash" prefetch={false}>
               <a ><i className="icon-trash"></i></a>
               <a ><i className="icon-trash"></i></a>
             </Link>
             </Link>
           </span>
           </span>
           <span className="separator"><a href="/">/</a></span>
           <span className="separator"><a href="/">/</a></span>
-        </>
+        </RootElm>
       )
       )
       : (
       : (
-        <>
+        <RootElm>
           <span className="path-segment">
           <span className="path-segment">
             <Link href="/" prefetch={false}>
             <Link href="/" prefetch={false}>
               <a >
               <a >
@@ -49,7 +56,7 @@ const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkProps): JS
               </a>
               </a>
             </Link>
             </Link>
           </span>
           </span>
-        </>
+        </RootElm>
       );
       );
   }
   }
 
 
@@ -61,13 +68,6 @@ const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkProps): JS
 
 
   const href = encodeURI(urljoin(basePath || '/', linkedPagePath.href));
   const href = encodeURI(urljoin(basePath || '/', linkedPagePath.href));
 
 
-  // eslint-disable-next-line react/prop-types
-  const RootElm = ({ children }) => {
-    return props.isInnerElem
-      ? <>{children}</>
-      : <span className="grw-page-path-hierarchical-link text-break">{children}</span>;
-  };
-
   return (
   return (
     <RootElm>
     <RootElm>
       { isParentExists && (
       { isParentExists && (

+ 2 - 2
packages/app/src/components/PasswordResetExecutionForm.tsx

@@ -11,7 +11,7 @@ const logger = loggerFactory('growi:passwordReset');
 
 
 
 
 const PasswordResetExecutionForm: FC = () => {
 const PasswordResetExecutionForm: FC = () => {
-  const { t } = useTranslation();
+  const { t } = useTranslation(['translation', 'commons']);
 
 
   const [newPassword, setNewPassword] = useState('');
   const [newPassword, setNewPassword] = useState('');
   const [newPasswordConfirm, setNewPasswordConfirm] = useState('');
   const [newPasswordConfirm, setNewPasswordConfirm] = useState('');
@@ -41,7 +41,7 @@ const PasswordResetExecutionForm: FC = () => {
 
 
       setValidationErrorI18n('');
       setValidationErrorI18n('');
 
 
-      toastSuccess(t('toaster.update_successed', { target: t('Password') }));
+      toastSuccess(t('toaster.update_successed', { target: t('Password'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 22 - 0
packages/app/src/components/RevisionComparer/RevisionComparer.module.scss

@@ -11,4 +11,26 @@
   .d2h-file-header {
   .d2h-file-header {
     display: none;
     display: none;
   }
   }
+
+  .grw-copy-dropdown {
+    .dropdown-menu {
+      min-width: 310px;
+
+      .dropdown-header {
+        margin-bottom: 0.5em;
+        font-size: 1.1em;
+      }
+
+      // unset active styles
+      .dropdown-item:active {
+        color: unset;
+        background-color: unset;
+      }
+
+      .well {
+        font-size: 0.7em;
+        word-break: break-all;
+      }
+    }
+  }
 }
 }

+ 7 - 0
packages/app/src/components/Sidebar/CustomSidebar.module.scss

@@ -0,0 +1,7 @@
+@use '~/styles/organisms/wiki-custom-sidebar.scss';
+
+.grw-custom-sidebar-content :global {
+  .wiki {
+    @extend %grw-custom-sidebar-content;
+  }
+}

+ 4 - 2
packages/app/src/components/Sidebar/CustomSidebar.tsx

@@ -10,6 +10,9 @@ import loggerFactory from '~/utils/logger';
 import RevisionRenderer from '../Page/RevisionRenderer';
 import RevisionRenderer from '../Page/RevisionRenderer';
 
 
 
 
+import styles from './CustomSidebar.module.scss';
+
+
 const logger = loggerFactory('growi:cli:CustomSidebar');
 const logger = loggerFactory('growi:cli:CustomSidebar');
 
 
 
 
@@ -58,11 +61,10 @@ const CustomSidebar: FC = () => {
 
 
       {
       {
         (!isLoading && markdown != null) && (
         (!isLoading && markdown != null) && (
-          <div className="p-3">
+          <div className={`p-3 grw-custom-sidebar-content ${styles['grw-custom-sidebar-content']}`}>
             <RevisionRenderer
             <RevisionRenderer
               rendererOptions={rendererOptions}
               rendererOptions={rendererOptions}
               markdown={markdown}
               markdown={markdown}
-              additionalClassName="grw-custom-sidebar-content"
             />
             />
           </div>
           </div>
         )
         )

+ 1 - 1
packages/app/src/components/Sidebar/RecentChanges.module.scss

@@ -4,7 +4,7 @@
   transform: translateY(-2px);
   transform: translateY(-2px);
 
 
   .custom-control-label::before {
   .custom-control-label::before {
-    padding-left: 16px;
+    padding-left: 19px;
     content: 'L';
     content: 'L';
   }
   }
 
 

+ 9 - 7
packages/app/src/components/Sidebar/Tag.tsx

@@ -58,13 +58,15 @@ const Tag: FC = () => {
           </div>
           </div>
         )
         )
         : (
         : (
-          <TagList
-            tagData={tagData}
-            totalTags={totalCount}
-            activePage={activePage}
-            onChangePage={setOffsetByPageNumber}
-            pagingLimit={PAGING_LIMIT}
-          />
+          <div data-testid="grw-tags-list">
+            <TagList
+              tagData={tagData}
+              totalTags={totalCount}
+              activePage={activePage}
+              onChangePage={setOffsetByPageNumber}
+              pagingLimit={PAGING_LIMIT}
+            />
+          </div>
         )
         )
       }
       }
 
 

+ 0 - 143
packages/app/src/components/StaffCredit/StaffCredit.jsx

@@ -1,143 +0,0 @@
-import React from 'react';
-
-import PropTypes from 'prop-types';
-import {
-  Modal, ModalBody,
-} from 'reactstrap';
-
-import { apiv3Get } from '~/client/util/apiv3-client';
-import loggerFactory from '~/utils/logger';
-
-
-/**
- * Page staff credit component
- *
- * @export
- * @class StaffCredit
- * @extends {React.Component}
- */
-
-// eslint-disable-next-line no-unused-vars
-const logger = loggerFactory('growi:cli:StaffCredit');
-
-class StaffCredit extends React.Component {
-
-  constructor(props) {
-
-    super(props);
-    this.state = {
-      isShown: true,
-      contributors: null,
-    };
-    this.deleteCredit = this.deleteCredit.bind(this);
-  }
-
-  // to delete the staffCredit and to inform that to Hotkeys.jsx
-  deleteCredit() {
-    if (this.state.isShown) {
-      this.setState({ isShown: false });
-    }
-  }
-
-  renderMembers(memberGroup, keyPrefix) {
-    // construct members elements
-    const members = memberGroup.members.map((member) => {
-      return (
-        <div className={memberGroup.additionalClass} key={`${keyPrefix}-${member.name}-container`}>
-          <span className="dev-position" key={`${keyPrefix}-${member.name}-position`}>
-            {/* position or '&nbsp;' */}
-            { member.position || '\u00A0' }
-          </span>
-          <p className="dev-name" key={`${keyPrefix}-${member.name}`}>{member.name}</p>
-        </div>
-      );
-    });
-    return (
-      <React.Fragment key={`${keyPrefix}-fragment`}>
-        {members}
-      </React.Fragment>
-    );
-  }
-
-  renderContributors() {
-    if (this.state.isShown) {
-      const credit = this.state.contributors.map((contributor) => {
-        // construct members elements
-        const memberGroups = contributor.memberGroups.map((memberGroup, idx) => {
-          return this.renderMembers(memberGroup, `${contributor.sectionName}-group${idx}`);
-        });
-        return (
-          <React.Fragment key={`${contributor.sectionName}-fragment`}>
-            <div className={`row ${contributor.additionalClass}`} key={`${contributor.sectionName}-row`}>
-              <h2 className="col-md-12 dev-team staff-credit-mt-10rem staff-credit-mb-6rem" key={contributor.sectionName}>{contributor.sectionName}</h2>
-              {memberGroups}
-            </div>
-            <div className="clearfix"></div>
-          </React.Fragment>
-        );
-      });
-      return (
-        <div className="text-center staff-credit-content" onClick={this.deleteCredit}>
-          <h1 className="staff-credit-mb-6rem">GROWI Contributors</h1>
-          <div className="clearfix"></div>
-          {credit}
-        </div>
-      );
-    }
-    return null;
-  }
-
-  async componentDidMount() {
-    const res = await apiv3Get('/staffs');
-    const contributors = res.data.contributors;
-    this.setState({ contributors });
-
-    setTimeout(() => {
-      // px / sec
-      const scrollSpeed = 200;
-      const target = $('.credit-curtain');
-      const scrollTargetHeight = target.children().innerHeight();
-      const duration = scrollTargetHeight / scrollSpeed * 1000;
-      target.animate({ scrollTop: scrollTargetHeight }, duration, 'linear');
-      target.slimScroll({
-        height: target.innerHeight(),
-        // Able to scroll after automatic schooling is complete so set "bottom" to allow scrolling from the bottom.
-        start: 'bottom',
-        color: '#FFFFFF',
-      });
-    }, 10);
-  }
-
-  render() {
-    const { onClosed } = this.props;
-
-    if (this.state.contributors === null) {
-      return <></>;
-    }
-
-    return (
-      <Modal
-        isOpen={this.state.isShown}
-        onClosed={() => {
-          if (onClosed != null) {
-            onClosed();
-          }
-        }}
-        toggle={this.deleteCredit}
-        scrollable
-        className="staff-credit"
-      >
-        <ModalBody className="credit-curtain">
-          {this.renderContributors()}
-        </ModalBody>
-      </Modal>
-    );
-  }
-
-}
-
-StaffCredit.propTypes = {
-  onClosed: PropTypes.func,
-};
-
-export default StaffCredit;

+ 5 - 4
packages/app/src/styles/_staff_credit.scss → packages/app/src/components/StaffCredit/StaffCredit.module.scss

@@ -1,5 +1,5 @@
 // Staff Credit
 // Staff Credit
-.staff-credit {
+.staff-credit :global {
   // attached !important for updating from .modal-dialog class style
   // attached !important for updating from .modal-dialog class style
   width: 80vw !important;
   width: 80vw !important;
   max-width: unset !important;
   max-width: unset !important;
@@ -17,13 +17,14 @@
     background-color: black;
     background-color: black;
     background-image: radial-gradient(rgba(50, 100, 100, 0.75), black 120%);
     background-image: radial-gradient(rgba(50, 100, 100, 0.75), black 120%);
   }
   }
-  &::after {
+
+  .background {
     position: absolute;
     position: absolute;
     top: 0;
     top: 0;
     left: 0;
     left: 0;
     width: 100%;
     width: 100%;
     height: 100%;
     height: 100%;
-    content: '';
+    pointer-events: none;
     background: repeating-linear-gradient(0deg, rgba(black, 0.15), rgba(black, 0.15) 2px, transparent 2px, transparent 4px);
     background: repeating-linear-gradient(0deg, rgba(black, 0.15), rgba(black, 0.15) 2px, transparent 2px, transparent 4px);
   }
   }
 
 
@@ -35,7 +36,7 @@
   h6,
   h6,
   .dev-position,
   .dev-position,
   .dev-name {
   .dev-name {
-    font-family: 'Press Start 2P', $font-family-for-staff-credit;
+    font-family: 'Press Start 2P', Lato, -apple-system, BlinkMacSystemFont, 'Hiragino Kaku Gothic ProN', Meiryo, sans-serif;
     color: white;
     color: white;
   }
   }
 
 

+ 139 - 0
packages/app/src/components/StaffCredit/StaffCredit.tsx

@@ -0,0 +1,139 @@
+import React, { useCallback, useState } from 'react';
+
+import { animateScroll } from 'react-scroll';
+import {
+  Modal, ModalBody,
+} from 'reactstrap';
+
+import { useSWRxStaffs } from '~/stores/staff';
+import loggerFactory from '~/utils/logger';
+
+
+import styles from './StaffCredit.module.scss';
+
+
+// eslint-disable-next-line no-unused-vars
+const logger = loggerFactory('growi:cli:StaffCredit');
+
+
+type Props = {
+  onClosed?: () => void,
+}
+
+const StaffCredit = (props: Props): JSX.Element => {
+
+  const { onClosed } = props;
+
+  const { data: contributors } = useSWRxStaffs();
+
+  const [isScrolling, setScrolling] = useState(false);
+
+
+  const closeHandler = useCallback(() => {
+    if (onClosed != null) {
+      onClosed();
+    }
+  }, [onClosed]);
+
+  const contentsClickedHandler = useCallback(() => {
+    if (isScrolling) {
+      setScrolling(false);
+    }
+    else {
+      closeHandler();
+    }
+  }, [closeHandler, isScrolling]);
+
+  const renderMembers = useCallback((memberGroup, keyPrefix) => {
+    // construct members elements
+    const members = memberGroup.members.map((member) => {
+      return (
+        <div className={memberGroup.additionalClass} key={`${keyPrefix}-${member.name}-container`}>
+          <span className="dev-position" key={`${keyPrefix}-${member.name}-position`}>
+            {/* position or '&nbsp;' */}
+            { member.position || '\u00A0' }
+          </span>
+          <p className="dev-name" key={`${keyPrefix}-${member.name}`}>{member.name}</p>
+        </div>
+      );
+    });
+    return (
+      <React.Fragment key={`${keyPrefix}-fragment`}>
+        {members}
+      </React.Fragment>
+    );
+  }, []);
+
+  const renderContributors = useCallback(() => {
+    if (contributors == null) {
+      return <></>;
+    }
+
+    const credit = contributors.map((contributor) => {
+      // construct members elements
+      const memberGroups = contributor.memberGroups.map((memberGroup, idx) => {
+        return renderMembers(memberGroup, `${contributor.sectionName}-group${idx}`);
+      });
+      return (
+        <React.Fragment key={`${contributor.sectionName}-fragment`}>
+          <div className={`row ${contributor.additionalClass}`} key={`${contributor.sectionName}-row`}>
+            <h2 className="col-md-12 dev-team staff-credit-mt-10rem staff-credit-mb-6rem" key={contributor.sectionName}>{contributor.sectionName}</h2>
+            {memberGroups}
+          </div>
+          <div className="clearfix"></div>
+        </React.Fragment>
+      );
+    });
+    return (
+      <div className="text-center staff-credit-content" onClick={contentsClickedHandler}>
+        <h1 className="staff-credit-mb-6rem">GROWI Contributors</h1>
+        <div className="clearfix"></div>
+        {credit}
+      </div>
+    );
+  }, [contentsClickedHandler, contributors, renderMembers]);
+
+
+  const openedHandler = useCallback(() => {
+    // init
+    animateScroll.scrollTo(0, { containerId: 'modalBody', duration: 0 });
+
+    setScrolling(true);
+
+    // start scrolling
+    animateScroll.scrollToBottom({
+      containerId: 'modalBody',
+      smooth: 'linear',
+      delay: 200,
+      duration: (scrollDistanceInPx: number) => {
+        const scrollSpeed = 200;
+        return scrollDistanceInPx / scrollSpeed * 1000;
+      },
+    });
+  }, []);
+
+
+  const isLoaded = contributors !== undefined;
+
+  if (contributors == null) {
+    return <></>;
+  }
+
+  return (
+    <Modal
+      isOpen={isLoaded}
+      toggle={closeHandler}
+      scrollable
+      className={`staff-credit ${styles['staff-credit']}`}
+      onOpened={openedHandler}
+    >
+      <ModalBody id="modalBody" className="credit-curtain">
+        {renderContributors()}
+      </ModalBody>
+      <div className="background"></div>
+    </Modal>
+  );
+
+};
+
+export default StaffCredit;

+ 5 - 5
packages/app/src/components/TableOfContents.tsx

@@ -6,13 +6,10 @@ import { useIsUserPage } from '~/stores/context';
 import { useTocOptions } from '~/stores/renderer';
 import { useTocOptions } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-
 import { StickyStretchableScroller } from './StickyStretchableScroller';
 import { StickyStretchableScroller } from './StickyStretchableScroller';
 
 
-
 import styles from './TableOfContents.module.scss';
 import styles from './TableOfContents.module.scss';
 
 
-
 // eslint-disable-next-line no-unused-vars
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:TableOfContents');
 const logger = loggerFactory('growi:TableOfContents');
 
 
@@ -28,7 +25,10 @@ const TableOfContents = (): JSX.Element => {
     // calculate absolute top of '#revision-toc' element
     // calculate absolute top of '#revision-toc' element
     const parentElem = document.querySelector('.grw-side-contents-container');
     const parentElem = document.querySelector('.grw-side-contents-container');
     const containerElem = document.querySelector('#revision-toc');
     const containerElem = document.querySelector('#revision-toc');
-    if (parentElem == null || containerElem == null) {
+
+    // rendererOptions for redo calcViewHeight()
+    // see: https://github.com/weseek/growi/pull/6791
+    if (parentElem == null || containerElem == null || rendererOptions == null) {
       return 0;
       return 0;
     }
     }
     const parentBottom = parentElem.getBoundingClientRect().bottom;
     const parentBottom = parentElem.getBoundingClientRect().bottom;
@@ -45,7 +45,7 @@ const TableOfContents = (): JSX.Element => {
     }
     }
     // bottom - revisionToc top
     // bottom - revisionToc top
     return bottom - (containerTop + containerPaddingTop);
     return bottom - (containerTop + containerPaddingTop);
-  }, [isUserPage]);
+  }, [isUserPage, rendererOptions]);
 
 
   return (
   return (
     <div id="revision-toc" className={`revision-toc ${styles['revision-toc']}`}>
     <div id="revision-toc" className={`revision-toc ${styles['revision-toc']}`}>

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