2
0
Эх сурвалжийг харах

Merge branch 'master' into fix/106917-107060-toc-rendering

jam411 3 жил өмнө
parent
commit
78221c91b9
100 өөрчлөгдсөн 555 нэмэгдсэн , 696 устгасан
  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. 0 4
      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. 0 4
      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. 0 5
      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. 2 2
      packages/app/src/components/Admin/App/AppSetting.jsx
  22. 1 1
      packages/app/src/components/Admin/App/AppSettingsPageContents.tsx
  23. 2 2
      packages/app/src/components/Admin/App/FileUploadSetting.tsx
  24. 2 2
      packages/app/src/components/Admin/App/MailSetting.tsx
  25. 1 1
      packages/app/src/components/Admin/App/PluginSetting.tsx
  26. 1 1
      packages/app/src/components/Admin/App/SiteUrlSetting.tsx
  27. 1 1
      packages/app/src/components/Admin/Common/AdminNavigation.jsx
  28. 0 4
      packages/app/src/components/Admin/Customize/Customize.jsx
  29. 1 1
      packages/app/src/components/Admin/Customize/CustomizeCssSetting.tsx
  30. 1 1
      packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.tsx
  31. 2 2
      packages/app/src/components/Admin/Customize/CustomizeHeaderSetting.tsx
  32. 0 148
      packages/app/src/components/Admin/Customize/CustomizeHighlightSetting.tsx
  33. 1 1
      packages/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx
  34. 3 3
      packages/app/src/components/Admin/Customize/CustomizeLogoSetting.tsx
  35. 2 2
      packages/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx
  36. 3 2
      packages/app/src/components/Admin/Customize/CustomizeSidebarSetting.tsx
  37. 1 1
      packages/app/src/components/Admin/Customize/CustomizeThemeSetting.tsx
  38. 1 1
      packages/app/src/components/Admin/Customize/CustomizeTitle.tsx
  39. 1 1
      packages/app/src/components/Admin/MarkdownSetting/IndentForm.tsx
  40. 1 1
      packages/app/src/components/Admin/MarkdownSetting/LineBreakForm.jsx
  41. 1 1
      packages/app/src/components/Admin/MarkdownSetting/PresentationForm.jsx
  42. 1 1
      packages/app/src/components/Admin/MarkdownSetting/XssForm.jsx
  43. 1 1
      packages/app/src/components/Admin/Notification/GlobalNotification.jsx
  44. 1 1
      packages/app/src/components/Admin/Security/GitHubSecuritySettingContents.jsx
  45. 1 1
      packages/app/src/components/Admin/Security/GoogleSecuritySettingContents.jsx
  46. 2 2
      packages/app/src/components/Admin/Security/OidcSecuritySettingContents.jsx
  47. 1 1
      packages/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx
  48. 1 1
      packages/app/src/components/Admin/Security/SecuritySetting.jsx
  49. 1 1
      packages/app/src/components/Admin/Security/TwitterSecuritySettingContents.jsx
  50. 2 2
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx
  51. 1 1
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySecretTokenSection.jsx
  52. 2 2
      packages/app/src/components/Admin/SlackIntegration/ManageCommandsProcess.jsx
  53. 2 2
      packages/app/src/components/Admin/SlackIntegration/ManageCommandsProcessWithoutProxy.jsx
  54. 1 1
      packages/app/src/components/Admin/SlackIntegration/OfficialBotSettings.jsx
  55. 1 1
      packages/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx
  56. 2 2
      packages/app/src/components/Admin/UserGroup/UserGroupPage.tsx
  57. 5 5
      packages/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx
  58. 38 0
      packages/app/src/components/AlertSiteUrlUndefined.tsx
  59. 1 1
      packages/app/src/components/CustomNavigation/CustomNav.jsx
  60. 1 1
      packages/app/src/components/Hotkeys/HotkeysDetector.jsx
  61. 3 4
      packages/app/src/components/Hotkeys/HotkeysManager.jsx
  62. 1 0
      packages/app/src/components/Hotkeys/Subscribers/SwitchToMirrorMode.jsx
  63. 11 6
      packages/app/src/components/InstallerForm.tsx
  64. 6 0
      packages/app/src/components/Layout/AdminLayout.tsx
  65. 2 0
      packages/app/src/components/Layout/BasicLayout.tsx
  66. 1 1
      packages/app/src/components/Layout/RawLayout.tsx
  67. 1 1
      packages/app/src/components/Me/ApiSettings.tsx
  68. 1 1
      packages/app/src/components/Me/BasicInfoSettings.tsx
  69. 44 31
      packages/app/src/components/Me/EditorSettings.tsx
  70. 1 1
      packages/app/src/components/Me/InAppNotificationSettings.tsx
  71. 2 2
      packages/app/src/components/Me/PasswordSettings.jsx
  72. 3 3
      packages/app/src/components/Me/ProfileImageSettings.tsx
  73. 1 1
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  74. 11 13
      packages/app/src/components/Page/DisplaySwitcher.tsx
  75. 1 1
      packages/app/src/components/PageComment.tsx
  76. 22 14
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx
  77. 1 12
      packages/app/src/components/PageEditor/CodeMirrorEditor.module.scss
  78. 1 1
      packages/app/src/components/PageEditor/Editor.module.scss
  79. 13 13
      packages/app/src/components/PagePathHierarchicalLink.tsx
  80. 2 2
      packages/app/src/components/PasswordResetExecutionForm.tsx
  81. 7 0
      packages/app/src/components/Sidebar/CustomSidebar.module.scss
  82. 4 2
      packages/app/src/components/Sidebar/CustomSidebar.tsx
  83. 1 1
      packages/app/src/components/Sidebar/RecentChanges.module.scss
  84. 9 7
      packages/app/src/components/Sidebar/Tag.tsx
  85. 0 143
      packages/app/src/components/StaffCredit/StaffCredit.jsx
  86. 5 4
      packages/app/src/components/StaffCredit/StaffCredit.module.scss
  87. 139 0
      packages/app/src/components/StaffCredit/StaffCredit.tsx
  88. 42 42
      packages/app/src/components/Theme/utils/ThemeProvider.tsx
  89. 0 22
      packages/app/src/interfaces/customize.ts
  90. 1 1
      packages/app/src/interfaces/editor-settings.ts
  91. 9 18
      packages/app/src/pages/[[...path]].page.tsx
  92. 6 0
      packages/app/src/pages/_document.page.tsx
  93. 4 4
      packages/app/src/pages/admin/[[...path]].page.tsx
  94. 1 1
      packages/app/src/pages/forgot-password.page.tsx
  95. 2 2
      packages/app/src/pages/installer.page.tsx
  96. 1 1
      packages/app/src/pages/me/[[...path]].page.tsx
  97. 1 1
      packages/app/src/pages/reset-password.page.tsx
  98. 1 1
      packages/app/src/pages/tags.page.tsx
  99. 11 1
      packages/app/src/pages/utils/commons.ts
  100. 0 17
      packages/app/src/server/routes/admin.js

+ 33 - 11
THIRD-PARTY-NOTICES.md

@@ -13,10 +13,12 @@ https://github.com/weseek/growi.
 
 
 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
@@ -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"
   },
   "wiki_management_home_page": "Wiki Management Home Page",
-  "app_settings": "App Settings",
   "security_settings": {
     "security_settings": "Security Settings",
     "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",
     "Authentication mechanism settings": "Authentication Mechanism Settings",
     "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_link": "Go to Markdown Settings",
     "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_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_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.",

+ 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"
+  }
+}

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

@@ -523,10 +523,6 @@
     "page_not_found_in_preview": "\"{{path}}\" is not a GROWI page."
   },
   "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}}",

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

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

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

@@ -514,10 +514,6 @@
     "page_not_found_in_preview": "\"{{path}}\" というページはありません。"
   },
   "toaster": {
-    "create_succeeded": "新しい{{target}}が作成されました",
-    "create_failed": "{{target}}の作成に失敗しました",
-    "update_successed": "{{target}}を更新しました",
-    "update_failed": "{{target}}の更新に失敗しました",
     "file_upload_succeeded": "ファイルをアップロードしました",
     "file_upload_failed": "ファイルのアップロードに失敗しました",
     "initialize_successed": "{{target}}を初期化しました",

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

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

+ 0 - 5
packages/app/public/static/locales/zh_CN/translation.json

@@ -496,10 +496,6 @@
     "page_not_found_in_preview": "\"{{path}}\" is not a GROWI page."
   },
 	"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_failed": "文件上传失败",
     "initialize_successed": "Succeeded to initialize {{target}}",
@@ -587,7 +583,6 @@
     "popover_title": "Slack Notification",
     "popover_desc": "Input channel name. You can notify multiple channels by entering a comma-separated list."
   },
-  "security_settings": "安全设置",
   "share_links": {
     "Shere this page link to public": "Shere this page link to public",
     "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 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.
-    - 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)
 
@@ -29,7 +29,7 @@ For more information: [Tutorial#Create New Page](https://docs.growi.org/en/guide
   <div class="card-body">
     <ul>
       <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>
   </div>
 </div>

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

@@ -17,7 +17,7 @@ GROWI は個人・法人向けの Wiki | ナレッジベースツールです。
 - `- ` を行頭につけると、この文章のような箇条書きを書くことができます
 - 画像やPDF、Word/Excel/PowerPointなどの添付ファイルも、コピー&ペースト、ドラッグ&ドロップで貼ることができます
 - 書けたら "**更新**" ボタンを押してページを公開しましょう
-    - `Ctrl(⌘) +S` でも保存できます
+    - `Ctrl(⌘) + S` でも保存できます
 
 さらに詳しくはこちら: [チュートリアル#新規ページ作成](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-body"><ul>
     <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>
 </div>
 

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

@@ -18,7 +18,7 @@ GROWI是一个针对个人和公司的Wiki - 一个知识库工具。
 - 我们可以通过在行首添加`-`来创建一个要点。
 - 我们还可以复制和粘贴,拖放附件,如图片、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)
 
@@ -29,7 +29,7 @@ GROWI是一个针对个人和公司的Wiki - 一个知识库工具。
   <div class="card-body">
     <ul>
       <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>
   </div>
 </div>

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

@@ -35,26 +35,10 @@ export default class AdminCustomizeContainer extends Container {
       isEnabledStaleNotification: false,
       isAllReplyShown: false,
       isSearchScopeChildrenAsDefault: false,
-      currentHighlightJsStyleId: '',
-      isHighlightJsStyleBorderEnabled: false,
       currentCustomizeTitle: '',
       currentCustomizeHeader: '',
       currentCustomizeCss: '',
       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.switchPageListLimitationM = this.switchPageListLimitationM.bind(this);
@@ -88,16 +72,11 @@ export default class AdminCustomizeContainer extends Container {
         isEnabledStaleNotification: customizeParams.isEnabledStaleNotification,
         isAllReplyShown: customizeParams.isAllReplyShown,
         isSearchScopeChildrenAsDefault: customizeParams.isSearchScopeChildrenAsDefault,
-        currentHighlightJsStyleId: customizeParams.styleName,
-        isHighlightJsStyleBorderEnabled: customizeParams.styleBorder,
         currentCustomizeTitle: customizeParams.customizeTitle,
         currentCustomizeHeader: customizeParams.customizeHeader,
         currentCustomizeCss: customizeParams.customizeCss,
         currentCustomizeScript: customizeParams.customizeScript,
       });
-
-      // search style name from object for display
-      this.setState({ currentHighlightJsStyleName: this.state.highlightJsCssSelectorOptions[customizeParams.styleName].name });
     }
     catch (err) {
       this.setState({ retrieveError: err });
@@ -171,25 +150,6 @@ export default class AdminCustomizeContainer extends Container {
     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
    */
@@ -218,17 +178,6 @@ export default class AdminCustomizeContainer extends Container {
     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
@@ -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
    * @memberOf AdminCustomizeContainer

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

@@ -18,12 +18,12 @@ const logger = loggerFactory('growi:appSettings');
 
 const AppSetting = (props) => {
   const { adminAppContainer } = props;
-  const { t } = useTranslation('admin');
+  const { t } = useTranslation(['admin', 'commons']);
 
   const submitHandler = useCallback(async() => {
     try {
       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) {
       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="col-lg-12">
-          <h2 className="admin-setting-header">{t('app_settings')}</h2>
+          <h2 className="admin-setting-header">{t('commons:headers.app_settings')}</h2>
           <AppSetting />
         </div>
       </div>

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

@@ -18,7 +18,7 @@ type Props = {
 
 
 const FileUploadSetting = (props: Props) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation(['admin', 'commons']);
   const { adminAppContainer } = props;
   const { fileUploadType } = adminAppContainer.state;
   const fileUploadTypes = ['aws', 'gcs', 'gridfs', 'local'];
@@ -26,7 +26,7 @@ const FileUploadSetting = (props: Props) => {
   const submitHandler = useCallback(async() => {
     try {
       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) {
       toastError(err);

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

@@ -17,7 +17,7 @@ type Props = {
 
 
 const MailSetting = (props: Props) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation(['admin', 'commons']);
   const { adminAppContainer } = props;
 
   const transmissionMethods = ['smtp', 'ses'];
@@ -25,7 +25,7 @@ const MailSetting = (props: Props) => {
   async function submitHandler() {
     try {
       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) {
       toastError(err);

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

@@ -23,7 +23,7 @@ const PluginSetting = (props: Props) => {
   const submitHandler = useCallback(async() => {
     try {
       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) {
       toastError(err);

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

@@ -24,7 +24,7 @@ const SiteUrlSetting = (props: Props) => {
   const submitHandler = useCallback(async() => {
     try {
       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) {
       toastError(err);

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

@@ -23,7 +23,7 @@ const AdminNavigation = (props) => {
   const MenuLabel = ({ menu }) => {
     switch (menu) {
       /* 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 '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') }</>;

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

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

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

@@ -25,7 +25,7 @@ const CustomizeFunctionSetting = (props: Props): JSX.Element => {
 
     try {
       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) {
       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() => {
     try {
       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) {
       toastError(err);
@@ -44,7 +44,7 @@ const CustomizeHeaderSetting = (props: Props): JSX.Element => {
           </Card>
           <div className="form-text text-muted">
             { t('Example') }:
-            <pre className="hljs">
+            <pre>
               {/* 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"
                 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() => {
     try {
       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();
     }
     catch (err) {

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

@@ -59,7 +59,7 @@ const CustomizeLogoSetting = (): JSX.Element => {
       const { customizedParams } = response.data;
       setIsDefaultLogo(customizedParams.isDefaultLogo);
       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) {
       toastError(err);
@@ -70,7 +70,7 @@ const CustomizeLogoSetting = (): JSX.Element => {
     try {
       await apiv3Delete('/customize-setting/delete-brand-logo');
       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) {
       toastError(err);
@@ -86,7 +86,7 @@ const CustomizeLogoSetting = (): JSX.Element => {
       formData.append('file', croppedImage);
       const { data } = await apiv3PostForm('/customize-setting/upload-brand-logo', formData);
       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) {
       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() => {
     try {
       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) {
       toastError(err);
@@ -79,7 +79,7 @@ const CustomizeScriptSetting = (props: Props): JSX.Element => {
 
           <div className="form-text text-muted">
             Examples:
-            <pre className="hljs"><code>{getExampleCode()}</code></pre>
+            <pre><code className='language-javascript'>{getExampleCode()}</code></pre>
           </div>
 
           <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';
 
 const CustomizeSidebarsetting = (): JSX.Element => {
-  const { t } = useTranslation('admin');
+  const { t } = useTranslation(['admin', 'commons']);
+
   const {
     update, isSidebarDrawerMode, isSidebarClosedAtDockMode, setIsSidebarDrawerMode, setIsSidebarClosedAtDockMode,
   } = useSWRxSidebarConfig();
@@ -20,7 +21,7 @@ const CustomizeSidebarsetting = (): JSX.Element => {
   const onClickSubmit = useCallback(async() => {
     try {
       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) {
       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) {
       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', {
         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) {
       toastError(err);

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

@@ -26,7 +26,7 @@ const IndentForm = (props: Props) => {
   const onClickSubmit = useCallback(async(props) => {
     try {
       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) {
       toastError(err);

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

@@ -27,7 +27,7 @@ class LineBreakForm extends React.Component {
 
     try {
       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) {
       toastError(err);

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

@@ -25,7 +25,7 @@ class PresentationForm extends React.Component {
 
     try {
       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) {
       toastError(err);

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

@@ -28,7 +28,7 @@ class XssForm extends React.Component {
 
     try {
       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) {
       toastError(err);

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

@@ -22,7 +22,7 @@ const GlobalNotification = (props) => {
   const onClickSubmit = useCallback(async() => {
     try {
       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) {
       toastError(err);

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

@@ -90,7 +90,7 @@ class GitHubSecurityManagementContents extends React.Component {
                 <i
                   className="icon-exclamation"
                   // 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('security_settings.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('commons:headers.app_settings')}<i class="icon-login"></i></a>` }) }}
                 />
               </div>
             )}

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

@@ -88,7 +88,7 @@ class GoogleSecurityManagementContents extends React.Component {
                 <i
                   className="icon-exclamation"
                   // 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('security_settings.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('commons:headers.app_settings')}<i class="icon-login"></i></a>` }) }}
                 />
               </div>
             )}

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

@@ -82,7 +82,7 @@ class OidcSecurityManagementContents extends React.Component {
                 <i
                   className="icon-exclamation"
                   // 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('security_settings.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('commons:headers.app_settings')}<i class="icon-login"></i></a>` }) }}
                 />
               </div>
             )}
@@ -378,7 +378,7 @@ class OidcSecurityManagementContents extends React.Component {
                     <i
                       className="icon-exclamation"
                       // 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('security_settings.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('commons:headers.app_settings')}<i class="icon-login"></i></a>` }) }}
                     />
                   </div>
                 )}

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

@@ -99,7 +99,7 @@ class SamlSecurityManagementContents extends React.Component {
                 <i
                   className="icon-exclamation"
                   // 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('security_settings.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('commons:headers.app_settings')}<i class="icon-login"></i></a>` }) }}
                 />
               </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]))
         }
 
-        <h4>{t('security_settings.session')}aa</h4>
+        <h4>{t('security_settings.session')}</h4>
         <div className="form-group row">
           <label className="text-left text-md-right col-md-3 col-form-label">{t('security_settings.max_age')}</label>
           <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
                   className="icon-exclamation"
                   // 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('security_settings.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('commons:headers.app_settings')}<i class="icon-login"></i></a>` }) }}
                 />
               </div>
             )}

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

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

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

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

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

@@ -46,7 +46,7 @@ const OfficialBotSettings = (props) => {
       if (onPrimaryUpdated != null) {
         onPrimaryUpdated();
       }
-      toastSuccess(t('toaster.update_successed', { target: 'Primary' }));
+      toastSuccess(t('toaster.update_successed', { target: 'Primary', ns: 'commons' }));
     }
     catch (err) {
       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) {
         props.onUpdateTokens();
       }
-      toastSuccess(t('toaster.update_successed', { target: 'Token' }));
+      toastSuccess(t('toaster.update_successed', { target: 'Token', ns: 'commons' }));
     }
     catch (err) {
       toastError(err);

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

@@ -93,7 +93,7 @@ export const UserGroupPage: FC = () => {
         description: userGroupData.description,
       });
 
-      toastSuccess(t('toaster.update_successed', { target: t('UserGroup') }));
+      toastSuccess(t('toaster.update_successed', { target: t('UserGroup'), ns: 'commons' }));
 
       // mutate
       await mutateUserGroups();
@@ -112,7 +112,7 @@ export const UserGroupPage: FC = () => {
         description: userGroupData.description,
       });
 
-      toastSuccess(t('toaster.update_successed', { target: t('UserGroup') }));
+      toastSuccess(t('toaster.update_successed', { target: t('UserGroup'), ns: 'commons' }));
 
       // mutate
       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> => {
       try {
         await updateUserGroup(targetGroup, userGroupData, forceUpdateParents);
-        toastSuccess(t('toaster.update_successed', { target: t('UserGroup') }));
+        toastSuccess(t('toaster.update_successed', { target: t('UserGroup'), ns: 'commons' }));
       }
       catch {
-        toastError(t('toaster.update_failed', { target: t('UserGroup') }));
+        toastError(t('toaster.update_failed', { target: t('UserGroup'), ns: 'commons' }));
       }
     },
     [t, updateUserGroup],
@@ -205,7 +205,7 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
         parentId: userGroupData.parent,
       });
 
-      toastSuccess(t('toaster.update_successed', { target: t('UserGroup') }));
+      toastSuccess(t('toaster.update_successed', { target: t('UserGroup'), ns: 'commons' }));
 
       // mutate
       mutateChildUserGroups();
@@ -244,7 +244,7 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
         parentId: currentUserGroupId,
       });
 
-      toastSuccess(t('toaster.update_successed', { target: t('UserGroup') }));
+      toastSuccess(t('toaster.update_successed', { target: t('UserGroup'), ns: 'commons' }));
 
       // mutate
       mutateChildUserGroups();
@@ -296,7 +296,7 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
         parentId: null,
       });
 
-      toastSuccess(t('toaster.update_successed', { target: t('UserGroup') }));
+      toastSuccess(t('toaster.update_successed', { target: t('UserGroup'), ns: 'commons' }));
 
       // mutate
       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('common:alert.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';

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

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

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

@@ -1,6 +1,6 @@
 import React, { useMemo, useCallback } from 'react';
-import PropTypes from 'prop-types';
 
+import PropTypes from 'prop-types';
 import { GlobalHotKeys } from 'react-hotkeys';
 
 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 HotkeysDetector from './HotkeysDetector';
-
-import ShowStaffCredit from './Subscribers/ShowStaffCredit';
-import SwitchToMirrorMode from './Subscribers/SwitchToMirrorMode';
-import ShowShortcutsModal from './Subscribers/ShowShortcutsModal';
 import CreatePage from './Subscribers/CreatePage';
 import EditPage from './Subscribers/EditPage';
 import FocusToGlobalSearch from './Subscribers/FocusToGlobalSearch';
+import ShowShortcutsModal from './Subscribers/ShowShortcutsModal';
+import ShowStaffCredit from './Subscribers/ShowStaffCredit';
+import SwitchToMirrorMode from './Subscribers/SwitchToMirrorMode';
 
 // define supported components list
 const SUPPORTED_COMPONENTS = [

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

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

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

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

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

@@ -8,6 +8,9 @@ import { RawLayout } from './RawLayout';
 
 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 });
 
 
@@ -54,6 +57,9 @@ const AdminLayout = ({
 
         <SystemVersion />
       </div>
+
+      <HotkeysManager />
+
     </RawLayout>
   );
 };

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

@@ -9,6 +9,7 @@ import Sidebar from '../Sidebar';
 
 import { RawLayout } from './RawLayout';
 
+const AlertSiteUrlUndefined = dynamic(() => import('../AlertSiteUrlUndefined').then(mod => mod.AlertSiteUrlUndefined), { ssr: false });
 const HotkeysManager = dynamic(() => import('../Hotkeys/HotkeysManager'), { ssr: false });
 // const PageCreateModal = dynamic(() => import('../client/js/components/PageCreateModal'), { ssr: false });
 const GrowiNavbarBottom = dynamic(() => import('../Navbar/GrowiNavbarBottom').then(mod => mod.GrowiNavbarBottom), { ssr: false });
@@ -51,6 +52,7 @@ export const BasicLayout = ({
           </div>
 
           <div className="flex-fill mw-0" style={{ position: 'relative' }}>
+            <AlertSiteUrlUndefined />
             {children}
           </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 => {
-  const classNames: string[] = ['wrapper'];
+  const classNames: string[] = ['layout-root'];
   if (className != null) {
     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');
       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) {
       toastError(err);

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

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

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

@@ -1,12 +1,13 @@
 import React, {
-  Dispatch,
+  Dispatch, memo,
   FC, SetStateAction, useCallback, useEffect, useState,
+  useMemo,
 } from 'react';
 
 import { useTranslation } from 'next-i18next';
 
 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>;
@@ -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 [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 {
-      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) {
       toastError(err);
     }
-  };
+  }, [t, textlintRules, updateEditorSettings]);
+
+  useEffect(() => {
+    initializeEditorSettings();
+  }, [initializeEditorSettings]);
 
   if (textlintRules == null) {
-    return <></>;
+    return (
+      <div className="text-muted text-center">
+        <i className="fa fa-2x fa-spinner fa-pulse"></i>
+      </div>
+    );
   }
 
   return (
@@ -270,4 +282,5 @@ export const EditorSettings: FC<EditorSettingsBodyProps> = () => {
       </div>
     </div>
   );
-};
+});
+EditorSettings.displayName = 'EditorSettings';

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

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

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

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

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

@@ -48,7 +48,7 @@ const ProfileImageSettings = (): JSX.Element => {
       formData.append('file', croppedImage);
       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
       setUploadedPictureSrc((response as any).attachment.filePathProxied);
@@ -64,7 +64,7 @@ const ProfileImageSettings = (): JSX.Element => {
       await apiPost('/attachments.removeProfileImage');
 
       setUploadedPictureSrc(undefined);
-      toastSuccess(t('toaster.update_successed', { target: t('Current Image') }));
+      toastSuccess(t('toaster.update_successed', { target: t('Current Image'), ns: 'commons' }));
     }
     catch (err) {
       toastError(err);
@@ -78,7 +78,7 @@ const ProfileImageSettings = (): JSX.Element => {
       const { userData } = response.data;
       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) {
       toastError(err);

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

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

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

@@ -3,8 +3,8 @@ import React, { useMemo } from 'react';
 import { pagePathUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
+import { Link } from 'react-scroll';
 
-// import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
 import {
   useCurrentPagePath, useIsSharedUser, useIsEditable, useShareLinkId, useIsNotFound,
 } from '~/stores/context';
@@ -79,18 +79,18 @@ const PageView = React.memo((): JSX.Element => {
             </div>
 
             {/* Comments */}
-            {/* { getCommentListDom != null && !isTopPagePath && ( */}
             { !isTopPagePath && (
               <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>
             ) }
 
@@ -109,8 +109,6 @@ PageView.displayName = 'PageView';
 
 
 const DisplaySwitcher = React.memo((): JSX.Element => {
-  // get element for smoothScroll
-  // const getCommentListDom = useMemo(() => { return document.getElementById('page-comments-list') }, []);
 
   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 (
     <>
-      <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="page-comments">
             <h2 className={commentTitleClasses}><i className="icon-fw icon-bubbles"></i>Comments</h2>

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

@@ -23,7 +23,8 @@ import EditorIcon from './EditorIcon';
 import EmojiPicker from './EmojiPicker';
 import EmojiPickerHelper from './EmojiPickerHelper';
 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 LinkEditModal from './LinkEditModal';
 import mdu from './MarkdownDrawioUtil';
@@ -152,7 +153,8 @@ class CodeMirrorEditor extends AbstractEditor {
     this.renderCheatsheetModalButton = this.renderCheatsheetModalButton.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.showHandsonTableHandler = this.showHandsonTableHandler.bind(this);
 
@@ -858,9 +860,10 @@ class CodeMirrorEditor extends AbstractEditor {
     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() {
     this.linkEditModal.current.show(markdownLinkUtil.getMarkdownLink(this.getCodeMirror()));
@@ -998,15 +1001,16 @@ class CodeMirrorEditor extends AbstractEditor {
       >
         <EditorIcon icon="Image" />
       </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
         key="nav-item-table"
         color={null}
@@ -1119,10 +1123,14 @@ class CodeMirrorEditor extends AbstractEditor {
         { this.renderCheatsheetOverlay() }
         { this.renderEmojiPicker() }
 
+        {/*
+        // TODO: re-impl with https://redmine.weseek.co.jp/issues/107248
         <GridEditModal
           ref={this.gridEditModal}
           onSave={(grid) => { return geu.replaceGridWithHtmlWithEditor(this.getCodeMirror(), grid) }}
         />
+         */}
+
         <LinkEditModal
           ref={this.linkEditModal}
           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;
 
 .grw-codemirror-editor :global {
@@ -17,7 +16,7 @@
     pre.CodeMirror-line.grw-cm-header-line {
       padding-top: 0.16em;
       padding-bottom: 0.08em;
-      font-family: bs.$font-family-monospace;
+      font-family: var(--font-family-monospace);
 
       // '#'
       .cm-formatting-header {
@@ -76,16 +75,6 @@
   .CodeMirror-hints {
     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
     .CodeMirror-hint-active.crowi-emoji-autocomplete {
       .img-container {

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

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

+ 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 urljoin from 'url-join';
@@ -19,9 +19,16 @@ type PagePathHierarchicalLinkProps = {
 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkProps): JSX.Element => {
   const {
-    linkedPagePath, linkedPagePathByHtml, basePath, isInTrash,
+    linkedPagePath, linkedPagePathByHtml, basePath, isInTrash, isInnerElem,
   } = 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
   if (linkedPagePath.isRoot) {
     if (basePath != null) {
@@ -30,17 +37,17 @@ const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkProps): JS
 
     return isInTrash
       ? (
-        <>
+        <RootElm>
           <span className="path-segment">
             <Link href="/trash" prefetch={false}>
               <a ><i className="icon-trash"></i></a>
             </Link>
           </span>
           <span className="separator"><a href="/">/</a></span>
-        </>
+        </RootElm>
       )
       : (
-        <>
+        <RootElm>
           <span className="path-segment">
             <Link href="/" prefetch={false}>
               <a >
@@ -49,7 +56,7 @@ const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkProps): JS
               </a>
             </Link>
           </span>
-        </>
+        </RootElm>
       );
   }
 
@@ -61,13 +68,6 @@ const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkProps): JS
 
   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 (
     <RootElm>
       { isParentExists && (

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

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

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

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

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

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

@@ -58,13 +58,15 @@ const Tag: FC = () => {
           </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 :global {
   // attached !important for updating from .modal-dialog class style
   width: 80vw !important;
   max-width: unset !important;
@@ -17,13 +17,14 @@
     background-color: black;
     background-image: radial-gradient(rgba(50, 100, 100, 0.75), black 120%);
   }
-  &::after {
+
+  .background {
     position: absolute;
     top: 0;
     left: 0;
     width: 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);
   }
 
@@ -35,7 +36,7 @@
   h6,
   .dev-position,
   .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;
   }
 

+ 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;

+ 42 - 42
packages/app/src/components/Theme/utils/ThemeProvider.tsx

@@ -7,21 +7,21 @@ import { GrowiThemes } from '~/interfaces/theme';
 import { Themes } from '~/stores/use-next-themes';
 
 
-const ThemeAntarctic = dynamic(() => import('../ThemeAntarctic'));
-const ThemeBlackboard = dynamic(() => import('../ThemeBlackboard'));
-const ThemeChristmas = dynamic(() => import('../ThemeChristmas'));
+// const ThemeAntarctic = dynamic(() => import('../ThemeAntarctic'));
+// const ThemeBlackboard = dynamic(() => import('../ThemeBlackboard'));
+// const ThemeChristmas = dynamic(() => import('../ThemeChristmas'));
 const ThemeDefault = dynamic(() => import('../ThemeDefault'));
-const ThemeFireRed = dynamic(() => import('../ThemeFireRed'));
-const ThemeFuture = dynamic(() => import('../ThemeFuture'));
-const ThemeHalloween = dynamic(() => import('../ThemeHalloween'));
-const ThemeHufflepuff = dynamic(() => import('../ThemeHufflepuff'));
-const ThemeIsland = dynamic(() => import('../ThemeIsland'));
-const ThemeJadeGreen = dynamic(() => import('../ThemeJadeGreen'));
-const ThemeKibela = dynamic(() => import('../ThemeKibela'));
-const ThemeMonoBlue = dynamic(() => import('../ThemeMonoBlue'));
-const ThemeNature = dynamic(() => import('../ThemeNature'));
-const ThemeSpring = dynamic(() => import('../ThemeSpring'));
-const ThemeWood = dynamic(() => import('../ThemeWood'));
+// const ThemeFireRed = dynamic(() => import('../ThemeFireRed'));
+// const ThemeFuture = dynamic(() => import('../ThemeFuture'));
+// const ThemeHalloween = dynamic(() => import('../ThemeHalloween'));
+// const ThemeHufflepuff = dynamic(() => import('../ThemeHufflepuff'));
+// const ThemeIsland = dynamic(() => import('../ThemeIsland'));
+// const ThemeJadeGreen = dynamic(() => import('../ThemeJadeGreen'));
+// const ThemeKibela = dynamic(() => import('../ThemeKibela'));
+// const ThemeMonoBlue = dynamic(() => import('../ThemeMonoBlue'));
+// const ThemeNature = dynamic(() => import('../ThemeNature'));
+// const ThemeSpring = dynamic(() => import('../ThemeSpring'));
+// const ThemeWood = dynamic(() => import('../ThemeWood'));
 
 
 type Props = {
@@ -32,34 +32,34 @@ type Props = {
 
 export const ThemeProvider = ({ theme, children, colorScheme }: Props): JSX.Element => {
   switch (theme) {
-    case GrowiThemes.ANTARCTIC:
-      return <ThemeAntarctic colorScheme={colorScheme}>{children}</ThemeAntarctic>;
-    case GrowiThemes.BLACKBOARD:
-      return <ThemeBlackboard>{children}</ThemeBlackboard>;
-    case GrowiThemes.CHRISTMAS:
-      return <ThemeChristmas colorScheme={colorScheme}>{children}</ThemeChristmas>;
-    case GrowiThemes.FIRE_RED:
-      return <ThemeFireRed>{children}</ThemeFireRed>;
-    case GrowiThemes.FUTURE:
-      return <ThemeFuture>{children}</ThemeFuture>;
-    case GrowiThemes.HALLOWEEN:
-      return <ThemeHalloween colorScheme={colorScheme}>{children}</ThemeHalloween>;
-    case GrowiThemes.HUFFLEPUFF:
-      return <ThemeHufflepuff colorScheme={colorScheme}>{children}</ThemeHufflepuff>;
-    case GrowiThemes.ISLAND:
-      return <ThemeIsland colorScheme={colorScheme}>{children}</ThemeIsland>;
-    case GrowiThemes.JADE_GREEN:
-      return <ThemeJadeGreen>{children}</ThemeJadeGreen>;
-    case GrowiThemes.KIBELA:
-      return <ThemeKibela>{children}</ThemeKibela>;
-    case GrowiThemes.MONO_BLUE:
-      return <ThemeMonoBlue>{children}</ThemeMonoBlue>;
-    case GrowiThemes.NATURE:
-      return <ThemeNature>{children}</ThemeNature>;
-    case GrowiThemes.SPRING:
-      return <ThemeSpring colorScheme={colorScheme}>{children}</ThemeSpring>;
-    case GrowiThemes.WOOD:
-      return <ThemeWood colorScheme={colorScheme}>{children}</ThemeWood>;
+    // case GrowiThemes.ANTARCTIC:
+    //   return <ThemeAntarctic colorScheme={colorScheme}>{children}</ThemeAntarctic>;
+    // case GrowiThemes.BLACKBOARD:
+    //   return <ThemeBlackboard>{children}</ThemeBlackboard>;
+    // case GrowiThemes.CHRISTMAS:
+    //   return <ThemeChristmas colorScheme={colorScheme}>{children}</ThemeChristmas>;
+    // case GrowiThemes.FIRE_RED:
+    //   return <ThemeFireRed>{children}</ThemeFireRed>;
+    // case GrowiThemes.FUTURE:
+    //   return <ThemeFuture>{children}</ThemeFuture>;
+    // case GrowiThemes.HALLOWEEN:
+    //   return <ThemeHalloween colorScheme={colorScheme}>{children}</ThemeHalloween>;
+    // case GrowiThemes.HUFFLEPUFF:
+    //   return <ThemeHufflepuff colorScheme={colorScheme}>{children}</ThemeHufflepuff>;
+    // case GrowiThemes.ISLAND:
+    //   return <ThemeIsland colorScheme={colorScheme}>{children}</ThemeIsland>;
+    // case GrowiThemes.JADE_GREEN:
+    //   return <ThemeJadeGreen>{children}</ThemeJadeGreen>;
+    // case GrowiThemes.KIBELA:
+    //   return <ThemeKibela>{children}</ThemeKibela>;
+    // case GrowiThemes.MONO_BLUE:
+    //   return <ThemeMonoBlue>{children}</ThemeMonoBlue>;
+    // case GrowiThemes.NATURE:
+    //   return <ThemeNature>{children}</ThemeNature>;
+    // case GrowiThemes.SPRING:
+    //   return <ThemeSpring colorScheme={colorScheme}>{children}</ThemeSpring>;
+    // case GrowiThemes.WOOD:
+    //   return <ThemeWood colorScheme={colorScheme}>{children}</ThemeWood>;
     default:
       return <ThemeDefault>{children}</ThemeDefault>;
   }

+ 0 - 22
packages/app/src/interfaces/customize.ts

@@ -1,25 +1,3 @@
-const IHighlightJsCssSelectorThemes = {
-  GITHUB: 'github',
-  GITHUB_GIST: 'github-gist',
-  ATOM_ONE_LIGHT: 'atom-one-light',
-  XCIDE: 'xcode',
-  VS: 'vs',
-  ATOM_ONE_DARK: 'atom-one-dark',
-  HYBRID: 'hybrid',
-  MONOKAI: 'monokai',
-  TOMMORROW_NIGHT: 'tomorrow-night',
-  VS_2015: 'vs2015',
-} as const;
-
-type IHighlightJsCssSelectorThemes = typeof IHighlightJsCssSelectorThemes[keyof typeof IHighlightJsCssSelectorThemes];
-
-export type IHighlightJsCssSelectorOptions = {
-  [theme in IHighlightJsCssSelectorThemes]: {
-    name: string,
-    border: boolean
-  }
-}
-
 export type IResLayoutSetting = {
   isContainerFluid: boolean,
 };

+ 1 - 1
packages/app/src/interfaces/editor-settings.ts

@@ -5,7 +5,7 @@ export interface ILintRule {
 }
 
 export interface ITextlintSettings {
-  neverAskBeforeDownloadLargeFiles: boolean;
+  neverAskBeforeDownloadLargeFiles?: boolean;
   textlintRules: ILintRule[];
 }
 

+ 9 - 18
packages/app/src/pages/[[...path]].page.tsx

@@ -39,7 +39,9 @@ import type { UserUISettingsModel } from '~/server/models/user-ui-settings';
 import { useSWRxCurrentPage, useSWRxIsGrantNormalized, useSWRxPageInfo } from '~/stores/page';
 import { useRedirectFrom } from '~/stores/page-redirect';
 import {
-  usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth, useSelectedGrant,
+  EditorMode,
+  useEditorMode, useSelectedGrant,
+  usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
 } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 
@@ -230,8 +232,6 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   useIsUploadableFile(props.editorConfig.upload.isUploadableFile);
   useIsUploadableImage(props.editorConfig.upload.isUploadableImage);
 
-  // const { data: editorMode } = useEditorMode();
-
   const { pageWithMeta, userUISettings } = props;
 
   const pageId = pageWithMeta?.data._id;
@@ -246,13 +246,13 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
 
   useSWRxCurrentPage(undefined, pageWithMeta?.data ?? null); // store initial data
   useEditingMarkdown(pageWithMeta?.data.revision?.body ?? '');
-
-  const { data: layoutSetting } = useLayoutSetting({ isContainerFluid: props.isContainerFluid });
   const { data: dataPageInfo } = useSWRxPageInfo(pageId);
-
   const { data: grantData } = useSWRxIsGrantNormalized(pageId);
   const { mutate: mutateSelectedGrant } = useSelectedGrant();
 
+  const { data: layoutSetting } = useLayoutSetting({ isContainerFluid: props.isContainerFluid });
+  const { getClassNamesByEditorMode } = useEditorMode();
+
   const shouldRenderPutbackPageModal = pageWithMeta != null
     ? _isTrashPage(pageWithMeta.data.path)
     : false;
@@ -271,17 +271,9 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   }, [props.currentPathname, router]);
 
   const classNames: string[] = [];
-  // switch (editorMode) {
-  //   case EditorMode.Editor:
-  //     classNames.push('on-edit', 'builtin-editor');
-  //     break;
-  //   case EditorMode.HackMD:
-  //     classNames.push('on-edit', 'hackmd');
-  //     break;
-  // }
-  // if (page == null) {
-  //   classNames.push('not-found-page');
-  // }
+
+  const isSidebar = pagePath === '/Sidebar';
+  classNames.push(...getClassNamesByEditorMode(isSidebar));
 
   const isTopPagePath = isTopPage(pageWithMeta?.data.path ?? '');
 
@@ -301,7 +293,6 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
         {renderHighlightJsStyleTag(props.highlightJsStyle)}
         */}
       </Head>
-      {/* <BasicLayout title={useCustomTitle(props, t('GROWI'))} className={classNames.join(' ')}> */}
       <BasicLayout title={useCustomTitle(props, 'GROWI')} className={classNames.join(' ')} expandContainer={isContainerFluid}>
         <div className="h-100 d-flex flex-column justify-content-between">
           <header className="py-0 position-relative">

+ 6 - 0
packages/app/src/pages/_document.page.tsx

@@ -28,6 +28,12 @@ class GrowiDocument extends Document {
           {renderScriptTagsByGroup('basis')}
           {renderStyleTagsByGroup('basis')}
           */}
+          <link rel='preload' href="/static/fonts/PressStart2P-latin.woff2" as="font" type="font/woff2" />
+          <link rel='preload' href="/static/fonts/PressStart2P-latin-ext.woff2" as="font" type="font/woff2" />
+          <link rel='preload' href="/static/fonts/Lato-Regular-latin.woff2" as="font" type="font/woff2" />
+          <link rel='preload' href="/static/fonts/Lato-Regular-latin-ext.woff2" as="font" type="font/woff2" />
+          <link rel='preload' href="/static/fonts/Lato-Bold-latin.woff2" as="font" type="font/woff2" />
+          <link rel='preload' href="/static/fonts/Lato-Bold-latin-ext.woff2" as="font" type="font/woff2" />
         </Head>
         <body>
           <Main />

+ 4 - 4
packages/app/src/pages/admin/[[...path]].page.tsx

@@ -86,7 +86,7 @@ type Props = CommonProps & {
   siteUrl: string,
 };
 
-const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
+const AdminPage: NextPage<Props> = (props: Props) => {
 
   const { t } = useTranslation('admin');
   const router = useRouter();
@@ -115,7 +115,7 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
       />,
     },
     app: {
-      title: t('app_settings'),
+      title: t('commons:headers.app_settings'),
       component: <AppSettingsPageContents />,
     },
     security: {
@@ -335,11 +335,11 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
   }
 
   injectServerConfigurations(context, props);
-  await injectNextI18NextConfigurations(context, props, ['admin']);
+  await injectNextI18NextConfigurations(context, props, ['admin', 'commons']);
 
   return {
     props,
   };
 };
 
-export default AdminMarkdownSettingsPage;
+export default AdminPage;

+ 1 - 1
packages/app/src/pages/forgot-password.page.tsx

@@ -50,7 +50,7 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
 
   const props: CommonProps = result.props as CommonProps;
 
-  await injectNextI18NextConfigurations(context, props, ['translation']);
+  await injectNextI18NextConfigurations(context, props, ['translation', 'commons']);
 
   return {
     props,

+ 2 - 2
packages/app/src/pages/installer.page.tsx

@@ -23,7 +23,7 @@ import {
 const { isTrashPage: _isTrashPage } = pagePathUtils;
 
 async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
-  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired, true);
   props._nextI18Next = nextI18NextConfig._nextI18Next;
 }
 
@@ -65,7 +65,7 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
 
   const props: Props = result.props as Props;
 
-  injectNextI18NextConfigurations(context, props, ['translation']);
+  await injectNextI18NextConfigurations(context, props, ['translation']);
 
   return {
     props,

+ 1 - 1
packages/app/src/pages/me/[[...path]].page.tsx

@@ -198,7 +198,7 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
 
   await injectUserUISettings(context, props);
   await injectServerConfigurations(context, props);
-  await injectNextI18NextConfigurations(context, props, ['translation', 'admin']);
+  await injectNextI18NextConfigurations(context, props, ['translation', 'admin', 'commons']);
 
   return {
     props,

+ 1 - 1
packages/app/src/pages/reset-password.page.tsx

@@ -62,7 +62,7 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
     props.email = email;
   }
 
-  await injectNextI18NextConfigurations(context, props, ['translation']);
+  await injectNextI18NextConfigurations(context, props, ['translation', 'commons']);
 
   return {
     props,

+ 1 - 1
packages/app/src/pages/tags.page.tsx

@@ -63,7 +63,7 @@ const TagPage: NextPage<CommonProps> = (props: Props) => {
       <Head>
       </Head>
       <BasicLayout title={useCustomTitle(props, 'GROWI')} className={classNames.join(' ')}>
-        <div className="grw-container-convertible mb-5 pb-5">
+        <div className="grw-container-convertible mb-5 pb-5" data-testid="tags-page">
           <h2 className="my-3">{`${t('Tags')}(${totalCount})`}</h2>
           <div className="px-3 mb-5 text-center">
             <TagCloudBox tags={tagData} minSize={20} />

+ 11 - 1
packages/app/src/pages/utils/commons.ts

@@ -76,7 +76,17 @@ export const getNextI18NextConfig = async(
     ?? configManager.getConfig('crowi', 'app:globalLang') as Lang
     ?? Lang.en_US;
 
-  return serverSideTranslations(locale, namespacesRequired ?? ['translation'], nextI18NextConfig, preloadAllLang ? AllLang : false);
+  // TODO: Consider to not include translation as default or other architecture idea
+  // see: https://redmine.weseek.co.jp/issues/107092
+  const namespaces = ['commons'];
+  if (namespacesRequired != null) {
+    namespaces.push(...namespacesRequired);
+  }
+  else {
+    namespaces.push('translation');
+  }
+
+  return serverSideTranslations(locale, namespaces, nextI18NextConfig, preloadAllLang ? AllLang : false);
 };
 
 /**

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

@@ -124,25 +124,8 @@ module.exports = function(crowi, app) {
   actions.customize.index = function(req, res) {
     const settingForm = configManager.getConfigByPrefix('crowi', 'customize:');
 
-    // TODO delete after apiV3
-    /* eslint-disable quote-props, no-multi-spaces */
-    const 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 */
-
     return res.render('admin/customize', {
       settingForm,
-      highlightJsCssSelectorOptions,
     });
   };
 

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно