Przeglądaj źródła

Merge branch 'support/apply-nextjs-2' into support/create-UsersHomePageFooter-integrate

jam411 3 lat temu
rodzic
commit
8c05e785ee
43 zmienionych plików z 566 dodań i 511 usunięć
  1. 6 4
      packages/app/public/static/locales/en_US/admin.json
  2. 1 3
      packages/app/public/static/locales/en_US/translation.json
  3. 21 5
      packages/app/public/static/locales/ja_JP/admin.json
  4. 0 9
      packages/app/public/static/locales/ja_JP/translation.json
  5. 3 3
      packages/app/public/static/locales/zh_CN/admin.json
  6. 1 1
      packages/app/public/static/locales/zh_CN/translation.json
  7. 1 1
      packages/app/src/components/Admin/Common/AdminNavigation.jsx
  8. 4 4
      packages/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx
  9. 6 6
      packages/app/src/components/Admin/Customize/CustomizeSidebarSetting.tsx
  10. 2 2
      packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx
  11. 5 5
      packages/app/src/components/Admin/ExportArchiveDataPage.jsx
  12. 1 1
      packages/app/src/components/Admin/ImportData/ImportDataPageContents.jsx
  13. 6 6
      packages/app/src/components/Admin/LegacySlackIntegration/SlackConfiguration.jsx
  14. 81 108
      packages/app/src/components/Admin/Notification/GlobalNotification.jsx
  15. 217 260
      packages/app/src/components/Admin/Notification/ManageGlobalNotification.jsx
  16. 3 3
      packages/app/src/components/Admin/Notification/NotificationSetting.jsx
  17. 2 2
      packages/app/src/components/Admin/Notification/TriggerEventCheckBox.jsx
  18. 1 1
      packages/app/src/components/Admin/Security/LocalSecuritySettingContents.jsx
  19. 1 1
      packages/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx
  20. 5 5
      packages/app/src/components/Admin/Security/SecurityManagement.tsx
  21. 2 2
      packages/app/src/components/Admin/Security/SecuritySetting.jsx
  22. 7 7
      packages/app/src/components/Admin/UserGroup/UserGroupForm.tsx
  23. 4 4
      packages/app/src/components/Admin/UserGroup/UserGroupModal.tsx
  24. 4 4
      packages/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx
  25. 2 2
      packages/app/src/components/Admin/UserGroupDetail/UserGroupPageList.tsx
  26. 1 1
      packages/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.tsx
  27. 1 1
      packages/app/src/components/Admin/Users/UserTable.jsx
  28. 4 2
      packages/app/src/components/Fab.jsx
  29. 31 0
      packages/app/src/components/Fab.module.scss
  30. 4 0
      packages/app/src/components/Layout/BasicLayout.tsx
  31. 3 2
      packages/app/src/components/PageEditor.tsx
  32. 6 5
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx
  33. 5 6
      packages/app/src/components/PageEditor/Preview.tsx
  34. 25 1
      packages/app/src/components/UncontrolledCodeMirror.tsx
  35. 12 18
      packages/app/src/pages/[[...path]].page.tsx
  36. 10 7
      packages/app/src/pages/admin/[[...path]].page.tsx
  37. 4 3
      packages/app/src/server/service/export.js
  38. 17 0
      packages/app/src/services/renderer/rehype-plugins/add-line-number-attribute.ts
  39. 39 6
      packages/app/src/services/renderer/renderer.tsx
  40. 15 2
      packages/app/src/stores/renderer.tsx
  41. 2 5
      packages/app/src/styles/_on-edit.scss
  42. 0 3
      packages/app/src/styles/bootstrap/_override.scss
  43. 1 0
      packages/app/src/styles/theme/_apply-colors.scss

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

@@ -268,9 +268,6 @@
     "delete_notification_pattern_desc2": "Once deleted, it cannot be recovered",
     "delete_notification_pattern_desc2": "Once deleted, it cannot be recovered",
     "toggle_notification": "Updated setting of {{path}}"
     "toggle_notification": "Updated setting of {{path}}"
   },
   },
-  "customize": "Customize",
-  "import_data": "Import Data",
-  "export_archive_data": "Export Archive Data",
   "mailer_setup_required":"<a href='/admin/app'>Email settings</a> are required to send.",
   "mailer_setup_required":"<a href='/admin/app'>Email settings</a> are required to send.",
   "admin_top": {
   "admin_top": {
     "management_wiki": "Management Wiki",
     "management_wiki": "Management Wiki",
@@ -421,7 +418,8 @@
       "import_recommended": "Import recommended {{target}}"
       "import_recommended": "Import recommended {{target}}"
     }
     }
   },
   },
-  "customize_setting": {
+  "customize_settings": {
+    "customize_settings": "Customize",
     "default_sidebar_mode": {
     "default_sidebar_mode": {
       "title": "Default sidebar mode",
       "title": "Default sidebar mode",
       "desc": "You can set the sidebar mode for new users and guests visiting the page.",
       "desc": "You can set the sidebar mode for new users and guests visiting the page.",
@@ -489,6 +487,7 @@
     "delete_logo": "Delete Logo"
     "delete_logo": "Delete Logo"
   },
   },
   "importer_management": {
   "importer_management": {
+    "import_data": "Import Data",
     "beta_warning": "This function is Beta.",
     "beta_warning": "This function is Beta.",
     "import_from": "Import from {{from}}",
     "import_from": "Import from {{from}}",
     "import_growi_archive": "Import GROWI archive",
     "import_growi_archive": "Import GROWI archive",
@@ -560,6 +559,7 @@
     "Directory_hierarchy_tag": "Directory hierarchy tag"
     "Directory_hierarchy_tag": "Directory hierarchy tag"
   },
   },
   "export_management": {
   "export_management": {
+    "export_archive_data": "Export Archive Data",
     "exporting_collection_list": "Exporting Collection List",
     "exporting_collection_list": "Exporting Collection List",
     "exported_data_list": "Exported Archive Data List",
     "exported_data_list": "Exported Archive Data List",
     "export_collections": "Export Collections",
     "export_collections": "Export Collections",
@@ -706,6 +706,7 @@
     "user_management": "User Management",
     "user_management": "User Management",
     "invite_users": "Temporarily issue a new user",
     "invite_users": "Temporarily issue a new user",
     "click_twice_same_checkbox": "You should check at least one checkbox.",
     "click_twice_same_checkbox": "You should check at least one checkbox.",
+    "status": "Status",
     "invite_modal": {
     "invite_modal": {
       "emails": "Emails (Possible to issue multiple people with new lines)",
       "emails": "Emails (Possible to issue multiple people with new lines)",
       "description1":"Temporarily issue new users by email addresses.",
       "description1":"Temporarily issue new users by email addresses.",
@@ -768,6 +769,7 @@
     "deny_create_group": "You can't create a new group with the current settings.",
     "deny_create_group": "You can't create a new group with the current settings.",
     "group_name": "Group name",
     "group_name": "Group name",
     "group_example": "e.g. : Group1",
     "group_example": "e.g. : Group1",
+    "child_user_group": "Child User Group",
     "parent_group": "Parent Group",
     "parent_group": "Parent Group",
     "select_parent_group": "Select Parent Group",
     "select_parent_group": "Select Parent Group",
     "release_parent_group": "Release parent group",
     "release_parent_group": "Release parent group",

+ 1 - 3
packages/app/public/static/locales/en_US/translation.json

@@ -40,7 +40,6 @@
   "Page Path": "Page path",
   "Page Path": "Page path",
   "Category": "Category",
   "Category": "Category",
   "User": "User",
   "User": "User",
-  "status": "Status",
   "account_id": "Account Id",
   "account_id": "Account Id",
   "Update": "Update",
   "Update": "Update",
   "Update Page": "Update Page",
   "Update Page": "Update Page",
@@ -79,7 +78,7 @@
   "username": "Username",
   "username": "Username",
   "Created": "Created",
   "Created": "Created",
   "Last updated": "Updated",
   "Last updated": "Updated",
-  "Last_Login": "Last login",
+  "last_login": "Last login",
   "Share": "Share",
   "Share": "Share",
   "Markdown Link": "Markdown Link",
   "Markdown Link": "Markdown Link",
   "Create/Edit Template": "Create/Edit template page",
   "Create/Edit Template": "Create/Edit template page",
@@ -120,7 +119,6 @@
   "Site URL settings": "Site URL settings",
   "Site URL settings": "Site URL settings",
   "external_account_management": "External Account Management",
   "external_account_management": "External Account Management",
   "UserGroup": "UserGroup",
   "UserGroup": "UserGroup",
-  "ChildUserGroup": "ChildUserGroup",
   "Basic Settings": "Basic Settings",
   "Basic Settings": "Basic Settings",
   "Basic authentication": "Basic authentication",
   "Basic authentication": "Basic authentication",
   "Register limitation": "Register limitation",
   "Register limitation": "Register limitation",

+ 21 - 5
packages/app/public/static/locales/ja_JP/admin.json

@@ -1,4 +1,13 @@
 {
 {
+  "Update": "更新",
+  "Delete": "削除",
+  "User": "ユーザー",
+  "Name": "名前",
+  "Page": "ページ",
+  "Created": "作成日",
+  "Edit": "編集",
+  "Description": "説明",
+  "last_login": "最終ログイン",
   "wiki_management_home_page": "Wiki管理トップ",
   "wiki_management_home_page": "Wiki管理トップ",
   "app_settings": "アプリ設定",
   "app_settings": "アプリ設定",
   "public": "公開",
   "public": "公開",
@@ -268,9 +277,6 @@
     "delete_notification_pattern_desc2": "Once deleted, it cannot be recovered",
     "delete_notification_pattern_desc2": "Once deleted, it cannot be recovered",
     "toggle_notification": "{{path}}の通知設定を変更しました"
     "toggle_notification": "{{path}}の通知設定を変更しました"
   },
   },
-  "customize": "カスタマイズ",
-  "import_data": "データインポート",
-  "export_archive_data": "データアーカイブ",
   "full_text_search_management": {
   "full_text_search_management": {
     "full_text_search_management": "全文検索管理",
     "full_text_search_management": "全文検索管理",
     "elasticsearch_management": "Elasticsearch 管理",
     "elasticsearch_management": "Elasticsearch 管理",
@@ -400,7 +406,7 @@
     "use_env_var_if_empty": "データベース側の値が空の場合、環境変数 <code>{{variable}}</code> の値を利用します",
     "use_env_var_if_empty": "データベース側の値が空の場合、環境変数 <code>{{variable}}</code> の値を利用します",
     "note_for_the_only_env_option": "現在GCS設定は環境変数の値によって制限されています<br>この設定を変更する場合は環境変数 <code>{{env}}</code> の値をfalseに変更もしくは削除してください"
     "note_for_the_only_env_option": "現在GCS設定は環境変数の値によって制限されています<br>この設定を変更する場合は環境変数 <code>{{env}}</code> の値をfalseに変更もしくは削除してください"
   },
   },
-  "markdown_setting": {
+  "markdown_settings": {
     "markdown_settings": "マークダウン設定",
     "markdown_settings": "マークダウン設定",
     "lineBreak_header": "Line Break設定",
     "lineBreak_header": "Line Break設定",
     "lineBreak_desc": "Line Breakの設定を変更できます。",
     "lineBreak_desc": "Line Breakの設定を変更できます。",
@@ -444,7 +450,8 @@
       "import_recommended": "{{target}} のおすすめをインポート"
       "import_recommended": "{{target}} のおすすめをインポート"
     }
     }
   },
   },
-  "customize_setting": {
+  "customize_settings": {
+    "customize_settings": "カスタマイズ",
     "default_sidebar_mode": {
     "default_sidebar_mode": {
       "title": "デフォルトのサイドバーモード",
       "title": "デフォルトのサイドバーモード",
       "desc": "新規ユーザー、ページを訪れたゲストのサイドバーモードを設定できます。",
       "desc": "新規ユーザー、ページを訪れたゲストのサイドバーモードを設定できます。",
@@ -512,6 +519,7 @@
     "delete_logo": "ロゴを削除"
     "delete_logo": "ロゴを削除"
   },
   },
   "export_management": {
   "export_management": {
+    "export_archive_data": "データアーカイブ",
     "exporting_collection_list": "エクスポート中のコレクション",
     "exporting_collection_list": "エクスポート中のコレクション",
     "exported_data_list": "エクスポートされたアーカイブリスト",
     "exported_data_list": "エクスポートされたアーカイブリスト",
     "export_collections": "コレクションのエクスポート",
     "export_collections": "コレクションのエクスポート",
@@ -530,6 +538,12 @@
     "delete": "削除"
     "delete": "削除"
   },
   },
   "importer_management": {
   "importer_management": {
+    "import_data": "データインポート",
+    "article": "記事",
+    "category": "カテゴリー",
+    "tag": "タグ",
+    "page": "ページ",
+    "page_path": "ページパス",
     "beta_warning": "この機能はベータ版です",
     "beta_warning": "この機能はベータ版です",
     "import_from": "{{from}} からインポート",
     "import_from": "{{from}} からインポート",
     "import_growi_archive": "GROWI アーカイブをインポート",
     "import_growi_archive": "GROWI アーカイブをインポート",
@@ -727,6 +741,7 @@
     "user_management": "ユーザー管理",
     "user_management": "ユーザー管理",
     "invite_users": "新規ユーザーの仮発行",
     "invite_users": "新規ユーザーの仮発行",
     "click_twice_same_checkbox": "少なくとも一つはチェックしてください。",
     "click_twice_same_checkbox": "少なくとも一つはチェックしてください。",
+    "status": "ステータス",
     "invite_modal": {
     "invite_modal": {
       "emails": "メールアドレス (複数行入力で複数人発行可能)",
       "emails": "メールアドレス (複数行入力で複数人発行可能)",
       "description1": "メールアドレスを使用して新規ユーザーを仮発行します。",
       "description1": "メールアドレスを使用して新規ユーザーを仮発行します。",
@@ -789,6 +804,7 @@
     "deny_create_group": "新規グループの作成はできません。",
     "deny_create_group": "新規グループの作成はできません。",
     "group_name": "グループ名",
     "group_name": "グループ名",
     "group_example": "例: Group1",
     "group_example": "例: Group1",
+    "child_user_group": "子グループ",
     "parent_group": "親グループ",
     "parent_group": "親グループ",
     "select_parent_group": "親グループを選択",
     "select_parent_group": "親グループを選択",
     "release_parent_group": "親グループの解除",
     "release_parent_group": "親グループの解除",

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

@@ -35,12 +35,6 @@
   "eg": "例:",
   "eg": "例:",
   "add": "追加",
   "add": "追加",
   "Undo": "元に戻す",
   "Undo": "元に戻す",
-  "Article": "記事",
-  "Page": "ページ",
-  "Page Path": "ページパス",
-  "Category": "カテゴリー",
-  "User": "ユーザー",
-  "status": "ステータス",
   "account_id": "アカウントID",
   "account_id": "アカウントID",
   "Initialize": "初期化",
   "Initialize": "初期化",
   "Update": "更新",
   "Update": "更新",
@@ -79,7 +73,6 @@
   "username": "ユーザー名",
   "username": "ユーザー名",
   "Created": "作成日",
   "Created": "作成日",
   "Last updated": "最終更新",
   "Last updated": "最終更新",
-  "Last_Login": "最終ログイン",
   "Share": "共有",
   "Share": "共有",
   "Markdown Link": "Markdown形式のリンク",
   "Markdown Link": "Markdown形式のリンク",
   "Create/Edit Template": "テンプレートページの作成/編集",
   "Create/Edit Template": "テンプレートページの作成/編集",
@@ -120,9 +113,7 @@
   "Site URL settings": "サイトURL設定",
   "Site URL settings": "サイトURL設定",
   "external_account_management": "外部アカウント管理",
   "external_account_management": "外部アカウント管理",
   "UserGroup": "グループ",
   "UserGroup": "グループ",
-  "ChildUserGroup": "子グループ",
   "Basic Settings": "基本設定",
   "Basic Settings": "基本設定",
-  "Register limitation": "登録の制限",
   "The contents entered here will be shown in the header etc": "ここに入力した内容は、ヘッダー等に表示されます。",
   "The contents entered here will be shown in the header etc": "ここに入力した内容は、ヘッダー等に表示されます。",
   "Public": "公開",
   "Public": "公開",
   "Anyone with the link": "リンクを知っている人のみ",
   "Anyone with the link": "リンクを知っている人のみ",

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

@@ -404,9 +404,8 @@
       "import_recommended": "导入建议 {{target}}"
       "import_recommended": "导入建议 {{target}}"
     }
     }
   },
   },
-  "export_archive_data": "导出主题数据",
-  "customize_setting": {
-    "customize_setting": "页面定制",
+  "customize_settings": {
+    "customize_settings": "页面定制",
     "default_sidebar_mode": {
     "default_sidebar_mode": {
       "title": "默认的侧边栏模式",
       "title": "默认的侧边栏模式",
       "desc": "你可以为新用户和访问该网页的客人设置侧边栏模式。",
       "desc": "你可以为新用户和访问该网页的客人设置侧边栏模式。",
@@ -562,6 +561,7 @@
     "Directory_hierarchy_tag": "Directory hierarchy tag"
     "Directory_hierarchy_tag": "Directory hierarchy tag"
   },
   },
   "export_management": {
   "export_management": {
+    "export_archive_data": "导出主题数据",
     "exporting_collection_list": "正在导出集合列表",
     "exporting_collection_list": "正在导出集合列表",
     "exported_data_list": "导出的存档数据列表",
     "exported_data_list": "导出的存档数据列表",
     "export_collections": "导出集合",
     "export_collections": "导出集合",

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

@@ -73,7 +73,7 @@
   "username": "用户名",
   "username": "用户名",
 	"Created": "创建",
 	"Created": "创建",
 	"Last updated": "上次更新",
 	"Last updated": "上次更新",
-  "Last_Login": "上次登录",
+  "last_login": "上次登录",
 	"Share": "分享",
 	"Share": "分享",
   "Share Link": "分享链接",
   "Share Link": "分享链接",
 	"Markdown Link": "Markdown链接",
 	"Markdown Link": "Markdown链接",

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

@@ -28,7 +28,7 @@ const AdminNavigation = (props) => {
       case 'app':                      return <><i className="icon-fw icon-settings"></i>        { t('app_settings') }</>;
       case 'app':                      return <><i className="icon-fw icon-settings"></i>        { t('app_settings') }</>;
       case 'security':                 return <><i className="icon-fw icon-shield"></i>          { t('security_settings.security_settings') }</>;
       case 'security':                 return <><i className="icon-fw icon-shield"></i>          { t('security_settings.security_settings') }</>;
       case 'markdown':                 return <><i className="icon-fw icon-note"></i>            { t('markdown_settings.markdown_settings') }</>;
       case 'markdown':                 return <><i className="icon-fw icon-note"></i>            { t('markdown_settings.markdown_settings') }</>;
-      case 'customize':                return <><i className="icon-fw icon-wrench"></i>          { t('customize') }</>;
+      case 'customize':                return <><i className="icon-fw icon-wrench"></i>          { t('customize_settings.customize_settings') }</>;
       case 'importer':                 return <><i className="icon-fw icon-cloud-upload"></i>    { t('importer_management.import_data') }</>;
       case 'importer':                 return <><i className="icon-fw icon-cloud-upload"></i>    { t('importer_management.import_data') }</>;
       case 'export':                   return <><i className="icon-fw icon-cloud-download"></i>  { t('export_archive_data') }</>;
       case 'export':                   return <><i className="icon-fw icon-cloud-download"></i>  { t('export_archive_data') }</>;
       case 'notification':             return <><i className="icon-fw icon-bell"></i>            { t('external_notification.external_notification')}</>;
       case 'notification':             return <><i className="icon-fw icon-bell"></i>            { t('external_notification.external_notification')}</>;

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

@@ -32,7 +32,7 @@ const CustomizeLayoutSetting = (): JSX.Element => {
   const onClickSubmit = async() => {
   const onClickSubmit = async() => {
     try {
     try {
       await apiv3Put('/customize-setting/layout', { isContainerFluid });
       await apiv3Put('/customize-setting/layout', { isContainerFluid });
-      toastSuccess(t('toaster.update_successed', { target: t('customize_setting.layout') }));
+      toastSuccess(t('toaster.update_successed', { target: t('customize_settings.layout') }));
       retrieveData();
       retrieveData();
     }
     }
     catch (err) {
     catch (err) {
@@ -44,7 +44,7 @@ const CustomizeLayoutSetting = (): JSX.Element => {
     <React.Fragment>
     <React.Fragment>
       <div className="row">
       <div className="row">
         <div className="col-12">
         <div className="col-12">
-          <h2 className="admin-setting-header">{t('customize_setting.layout')}</h2>
+          <h2 className="admin-setting-header">{t('customize_settings.layout')}</h2>
 
 
           <div className="d-flex justify-content-around mt-5">
           <div className="d-flex justify-content-around mt-5">
             <div id="layoutOptions" className="card-deck">
             <div id="layoutOptions" className="card-deck">
@@ -55,7 +55,7 @@ const CustomizeLayoutSetting = (): JSX.Element => {
               >
               >
                 <img src={`/images/customize-settings/default-${resolvedTheme}.svg`} />
                 <img src={`/images/customize-settings/default-${resolvedTheme}.svg`} />
                 <div className="card-body text-center">
                 <div className="card-body text-center">
-                  {t('customize_setting.layout_options.default')}
+                  {t('customize_settings.layout_options.default')}
                 </div>
                 </div>
               </div>
               </div>
               <div
               <div
@@ -65,7 +65,7 @@ const CustomizeLayoutSetting = (): JSX.Element => {
               >
               >
                 <img src={`/images/customize-settings/fluid-${resolvedTheme}.svg`} />
                 <img src={`/images/customize-settings/fluid-${resolvedTheme}.svg`} />
                 <div className="card-body  text-center">
                 <div className="card-body  text-center">
-                  {t('customize_setting.layout_options.expanded')}
+                  {t('customize_settings.layout_options.expanded')}
                 </div>
                 </div>
               </div>
               </div>
             </div>
             </div>

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

@@ -20,7 +20,7 @@ const CustomizeSidebarsetting = (): JSX.Element => {
   const onClickSubmit = useCallback(async() => {
   const onClickSubmit = useCallback(async() => {
     try {
     try {
       await update();
       await update();
-      toastSuccess(t('toaster.update_successed', { target: t('customize_setting.default_sidebar_mode.title') }));
+      toastSuccess(t('toaster.update_successed', { target: t('customize_settings.default_sidebar_mode.title') }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
@@ -32,11 +32,11 @@ const CustomizeSidebarsetting = (): JSX.Element => {
       <div className="row">
       <div className="row">
         <div className="col-12">
         <div className="col-12">
 
 
-          <h2 className="admin-setting-header">{t('customize_setting.default_sidebar_mode.title')}</h2>
+          <h2 className="admin-setting-header">{t('customize_settings.default_sidebar_mode.title')}</h2>
 
 
           <Card className="card well my-3">
           <Card className="card well my-3">
             <CardBody className="px-0 py-2">
             <CardBody className="px-0 py-2">
-              {t('customize_setting.default_sidebar_mode.desc')}
+              {t('customize_settings.default_sidebar_mode.desc')}
             </CardBody>
             </CardBody>
           </Card>
           </Card>
 
 
@@ -67,7 +67,7 @@ const CustomizeSidebarsetting = (): JSX.Element => {
 
 
           <Card className="card well my-5">
           <Card className="card well my-5">
             <CardBody className="px-0 py-2">
             <CardBody className="px-0 py-2">
-              {t('customize_setting.default_sidebar_mode.dock_mode_default_desc')}
+              {t('customize_settings.default_sidebar_mode.dock_mode_default_desc')}
             </CardBody>
             </CardBody>
           </Card>
           </Card>
 
 
@@ -83,7 +83,7 @@ const CustomizeSidebarsetting = (): JSX.Element => {
                 onChange={() => setIsSidebarClosedAtDockMode(false)}
                 onChange={() => setIsSidebarClosedAtDockMode(false)}
               />
               />
               <label className="custom-control-label" htmlFor="is-open">
               <label className="custom-control-label" htmlFor="is-open">
-                {t('customize_setting.default_sidebar_mode.dock_mode_default_open')}
+                {t('customize_settings.default_sidebar_mode.dock_mode_default_open')}
               </label>
               </label>
             </div>
             </div>
             <div className="custom-control custom-radio my-3">
             <div className="custom-control custom-radio my-3">
@@ -97,7 +97,7 @@ const CustomizeSidebarsetting = (): JSX.Element => {
                 onChange={() => setIsSidebarClosedAtDockMode(true)}
                 onChange={() => setIsSidebarClosedAtDockMode(true)}
               />
               />
               <label className="custom-control-label" htmlFor="is-closed">
               <label className="custom-control-label" htmlFor="is-closed">
-                {t('customize_setting.default_sidebar_mode.dock_mode_default_close')}
+                {t('customize_settings.default_sidebar_mode.dock_mode_default_close')}
               </label>
               </label>
             </div>
             </div>
           </div>
           </div>

+ 2 - 2
packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx

@@ -59,7 +59,7 @@ const CustomizeThemeOptions = (props) => {
     <div id="themeOptions" className={`${currentLayout === 'kibela' && 'disabled'}`}>
     <div id="themeOptions" className={`${currentLayout === 'kibela' && 'disabled'}`}>
       {/* Light and Dark Themes */}
       {/* Light and Dark Themes */}
       <div>
       <div>
-        <h3>{t('customize_setting.theme_desc.light_and_dark')}</h3>
+        <h3>{t('customize_settings.theme_desc.light_and_dark')}</h3>
         <div className="d-flex flex-wrap">
         <div className="d-flex flex-wrap">
           {lightNDarkTheme.map((theme) => {
           {lightNDarkTheme.map((theme) => {
             return (
             return (
@@ -75,7 +75,7 @@ const CustomizeThemeOptions = (props) => {
       </div>
       </div>
       {/* Unique Theme */}
       {/* Unique Theme */}
       <div className="mt-3">
       <div className="mt-3">
-        <h3>{t('customize_setting.theme_desc.unique')}</h3>
+        <h3>{t('customize_settings.theme_desc.unique')}</h3>
         <div className="d-flex flex-wrap">
         <div className="d-flex flex-wrap">
           {uniqueTheme.map((theme) => {
           {uniqueTheme.map((theme) => {
             return (
             return (

+ 5 - 5
packages/app/src/components/Admin/ExportArchiveDataPage.jsx

@@ -212,22 +212,22 @@ class ExportArchiveDataPage extends React.Component {
 
 
     return (
     return (
       <div data-testid="admin-export-archive-data">
       <div data-testid="admin-export-archive-data">
-        <h2>{t('export_archive_data')}</h2>
+        <h2>{t('export_management.export_archive_data')}</h2>
 
 
         <button type="button" className="btn btn-outline-secondary" disabled={isExporting} onClick={this.openExportModal}>
         <button type="button" className="btn btn-outline-secondary" disabled={isExporting} onClick={this.openExportModal}>
-          {t('admin:export_management.create_new_archive_data')}
+          {t('export_management.create_new_archive_data')}
         </button>
         </button>
 
 
         { showExportingData && (
         { showExportingData && (
           <div className="mt-5">
           <div className="mt-5">
-            <h3>{t('admin:export_management.exporting_collection_list')}</h3>
+            <h3>{t('export_management.exporting_collection_list')}</h3>
             { this.renderProgressBarsForCollections() }
             { this.renderProgressBarsForCollections() }
             { this.renderProgressBarForZipping() }
             { this.renderProgressBarForZipping() }
           </div>
           </div>
         ) }
         ) }
 
 
         <div className="mt-5">
         <div className="mt-5">
-          <h3>{t('admin:export_management.exported_data_list')}</h3>
+          <h3>{t('export_management.exported_data_list')}</h3>
           <ArchiveFilesTable
           <ArchiveFilesTable
             zipFileStats={this.state.zipFileStats}
             zipFileStats={this.state.zipFileStats}
             onZipFileStatRemove={this.onZipFileStatRemove}
             onZipFileStatRemove={this.onZipFileStatRemove}
@@ -252,7 +252,7 @@ ExportArchiveDataPage.propTypes = {
 };
 };
 
 
 const ExportArchiveDataPageWrapperFC = (props) => {
 const ExportArchiveDataPageWrapperFC = (props) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
   const { data: socket } = useAdminSocket();
   const { data: socket } = useAdminSocket();
 
 
   return <ExportArchiveDataPage t={t} socket={socket} {...props} />;
   return <ExportArchiveDataPage t={t} socket={socket} {...props} />;

+ 1 - 1
packages/app/src/components/Admin/ImportData/ImportDataPageContents.jsx

@@ -144,7 +144,7 @@ class ImportDataPageContents extends React.Component {
                 <tr>
                 <tr>
                   <th>{t('importer_management.article')}</th>
                   <th>{t('importer_management.article')}</th>
                   <th><i className="icon-arrow-right-circle text-success"></i></th>
                   <th><i className="icon-arrow-right-circle text-success"></i></th>
-                  <th>{t('page')}</th>
+                  <th>{t('importer_management.page')}</th>
                 </tr>
                 </tr>
                 <tr>
                 <tr>
                   <th>{t('importer_management.tag')}</th>
                   <th>{t('importer_management.tag')}</th>

+ 6 - 6
packages/app/src/components/Admin/LegacySlackIntegration/SlackConfiguration.jsx

@@ -62,7 +62,7 @@ class SlackConfiguration extends React.Component {
         </div>
         </div>
         {adminSlackIntegrationLegacyContainer.state.selectSlackOption === 'Incoming Webhooks' ? (
         {adminSlackIntegrationLegacyContainer.state.selectSlackOption === 'Incoming Webhooks' ? (
           <React.Fragment>
           <React.Fragment>
-            <h2 className="border-bottom mb-5">{t('admin:notification_settings.slack_incoming_configuration')}</h2>
+            <h2 className="border-bottom mb-5">{t('notification_settings.slack_incoming_configuration')}</h2>
 
 
             <div className="row mb-3">
             <div className="row mb-3">
               <label className="col-md-3 text-left text-md-right">Webhook URL</label>
               <label className="col-md-3 text-left text-md-right">Webhook URL</label>
@@ -87,11 +87,11 @@ class SlackConfiguration extends React.Component {
                     onChange={() => { adminSlackIntegrationLegacyContainer.switchIsIncomingWebhookPrioritized() }}
                     onChange={() => { adminSlackIntegrationLegacyContainer.switchIsIncomingWebhookPrioritized() }}
                   />
                   />
                   <label className="custom-control-label" htmlFor="cbPrioritizeIWH">
                   <label className="custom-control-label" htmlFor="cbPrioritizeIWH">
-                    {t('admin:notification_settings.prioritize_webhook')}
+                    {t('notification_settings.prioritize_webhook')}
                   </label>
                   </label>
                 </div>
                 </div>
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
-                  {t('admin:notification_settings.prioritize_webhook_desc')}
+                  {t('notification_settings.prioritize_webhook_desc')}
                 </p>
                 </p>
               </div>
               </div>
             </div>
             </div>
@@ -112,7 +112,7 @@ class SlackConfiguration extends React.Component {
                   data-toggle="tab"
                   data-toggle="tab"
                   onClick={() => adminSlackIntegrationLegacyContainer.switchSlackOption('Incoming Webhooks')}
                   onClick={() => adminSlackIntegrationLegacyContainer.switchSlackOption('Incoming Webhooks')}
                 >
                 >
-                  {t('admin:notification_settings.use_instead')}
+                  {t('notification_settings.use_instead')}
                 </a>
                 </a>
               </div>
               </div>
 
 
@@ -141,7 +141,7 @@ class SlackConfiguration extends React.Component {
 
 
         <h3>
         <h3>
           <i className="icon-question" aria-hidden="true"></i>{' '}
           <i className="icon-question" aria-hidden="true"></i>{' '}
-          <a href="#collapseHelpForIwh" data-toggle="collapse">{t('admin:notification_settings.how_to.header')}</a>
+          <a href="#collapseHelpForIwh" data-toggle="collapse">{t('notification_settings.how_to.header')}</a>
         </h3>
         </h3>
 
 
         <ol id="collapseHelpForIwh" className="collapse">
         <ol id="collapseHelpForIwh" className="collapse">
@@ -177,7 +177,7 @@ SlackConfiguration.propTypes = {
 };
 };
 
 
 const SlackConfigurationWrapperFc = (props) => {
 const SlackConfigurationWrapperFc = (props) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
 
 
   return <SlackConfiguration t={t} {...props} />;
   return <SlackConfiguration t={t} {...props} />;
 };
 };

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

@@ -1,6 +1,7 @@
-import React from 'react';
+import { React, useCallback } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import { useRouter } from 'next/router';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
@@ -13,17 +14,12 @@ import GlobalNotificationList from './GlobalNotificationList';
 
 
 const logger = loggerFactory('growi:GlobalNotification');
 const logger = loggerFactory('growi:GlobalNotification');
 
 
-class GlobalNotification extends React.Component {
+const GlobalNotification = (props) => {
 
 
-  constructor() {
-    super();
-
-    this.onClickSubmit = this.onClickSubmit.bind(this);
-  }
-
-  async onClickSubmit() {
-    const { t, adminNotificationContainer } = this.props;
+  const { adminNotificationContainer } = props;
+  const { t } = useTranslation('admin');
 
 
+  const onClickSubmit = useCallback(async() => {
     try {
     try {
       await adminNotificationContainer.updateGlobalNotificationForPages();
       await adminNotificationContainer.updateGlobalNotificationForPages();
       toastSuccess(t('toaster.update_successed', { target: t('external_notification.external_notification') }));
       toastSuccess(t('toaster.update_successed', { target: t('external_notification.external_notification') }));
@@ -32,113 +28,90 @@ class GlobalNotification extends React.Component {
       toastError(err);
       toastError(err);
       logger.error(err);
       logger.error(err);
     }
     }
-  }
-
-  render() {
-    const { t, adminNotificationContainer } = this.props;
-    const { globalNotifications } = adminNotificationContainer.state;
-    return (
-      <React.Fragment>
-
-        <h2 className="border-bottom my-4">{t('notification_settings.valid_page')}</h2>
-
-        <p className="card well">
-          {/* eslint-disable-next-line react/no-danger */}
-          <span dangerouslySetInnerHTML={{ __html: t('notification_settings.link_notification_help') }} />
-        </p>
-
-
-        <div className="row mb-4">
-          <div className="col-md-8 offset-md-2">
-            <div className="custom-control custom-checkbox custom-checkbox-success">
-              <input
-                id="isNotificationForOwnerPageEnabled"
-                className="custom-control-input"
-                type="checkbox"
-                checked={adminNotificationContainer.state.isNotificationForOwnerPageEnabled || false}
-                onChange={() => { adminNotificationContainer.switchIsNotificationForOwnerPageEnabled() }}
-              />
-              <label className="custom-control-label" htmlFor="isNotificationForOwnerPageEnabled">
-                {/* eslint-disable-next-line react/no-danger */}
-                <span dangerouslySetInnerHTML={{ __html: t('notification_settings.just_me_notification_help') }} />
-              </label>
-            </div>
+  }, [adminNotificationContainer, t]);
+
+  const router = useRouter();
+  const { globalNotifications } = adminNotificationContainer.state;
+  return (
+    <>
+      <h2 className="border-bottom my-4">{t('notification_settings.valid_page')}</h2>
+
+      <p className="card well">
+        {/* eslint-disable-next-line react/no-danger */}
+        <span dangerouslySetInnerHTML={{ __html: t('notification_settings.link_notification_help') }} />
+      </p><div className="row mb-4">
+        <div className="col-md-8 offset-md-2">
+          <div className="custom-control custom-checkbox custom-checkbox-success">
+            <input
+              id="isNotificationForOwnerPageEnabled"
+              className="custom-control-input"
+              type="checkbox"
+              checked={adminNotificationContainer.state.isNotificationForOwnerPageEnabled || false}
+              onChange={() => { adminNotificationContainer.switchIsNotificationForOwnerPageEnabled() } } />
+            <label className="custom-control-label" htmlFor="isNotificationForOwnerPageEnabled">
+              {/* eslint-disable-next-line react/no-danger */}
+              <span dangerouslySetInnerHTML={{ __html: t('notification_settings.just_me_notification_help') }} />
+            </label>
           </div>
           </div>
         </div>
         </div>
-
-
-        <div className="row mb-4">
-          <div className="col-md-8 offset-md-2">
-            <div className="custom-control custom-checkbox custom-checkbox-success">
-              <input
-                id="isNotificationForGroupPageEnabled"
-                className="custom-control-input"
-                type="checkbox"
-                checked={adminNotificationContainer.state.isNotificationForGroupPageEnabled || false}
-                onChange={() => { adminNotificationContainer.switchIsNotificationForGroupPageEnabled() }}
-              />
-              <label className="custom-control-label" htmlFor="isNotificationForGroupPageEnabled">
-                {/* eslint-disable-next-line react/no-danger */}
-                <span dangerouslySetInnerHTML={{ __html: t('notification_settings.group_notification_help') }} />
-              </label>
-            </div>
+      </div><div className="row mb-4">
+        <div className="col-md-8 offset-md-2">
+          <div className="custom-control custom-checkbox custom-checkbox-success">
+            <input
+              id="isNotificationForGroupPageEnabled"
+              className="custom-control-input"
+              type="checkbox"
+              checked={adminNotificationContainer.state.isNotificationForGroupPageEnabled || false}
+              onChange={() => { adminNotificationContainer.switchIsNotificationForGroupPageEnabled() } } />
+            <label className="custom-control-label" htmlFor="isNotificationForGroupPageEnabled">
+              {/* eslint-disable-next-line react/no-danger */}
+              <span dangerouslySetInnerHTML={{ __html: t('notification_settings.group_notification_help') }} />
+            </label>
           </div>
           </div>
         </div>
         </div>
-
-        <div className="row my-3">
-          <div className="col-sm-5 offset-sm-4">
-            <button
-              type="button"
-              className="btn btn-primary"
-              onClick={this.onClickSubmit}
-              disabled={adminNotificationContainer.state.retrieveError}
-            >{t('Update')}
-            </button>
-          </div>
+      </div>
+      <div className="row my-3">
+        <div className="col-sm-5 offset-sm-4">
+          <button
+            type="button"
+            className="btn btn-primary"
+            onClick={onClickSubmit}
+            disabled={adminNotificationContainer.state.retrieveError}
+          >{t('Update')}
+          </button>
         </div>
         </div>
-
-        <h2 className="border-bottom mb-5">{t('notification_settings.notification_list')}
-          <a href="/admin/global-notification/new">
-            <p className="btn btn-outline-secondary pull-right">{t('notification_settings.add_notification')}</p>
-          </a>
-        </h2>
-
-        <table className="table table-bordered">
-          <thead>
-            <tr>
-              <th>ON/OFF</th>
-              {/* eslint-disable-next-line react/no-danger */}
-              <th>{t('notification_settings.trigger_path')} <span dangerouslySetInnerHTML={{ __html: t('notification_settings.trigger_path_help') }} /></th>
-              <th>{t('notification_settings.trigger_events')}</th>
-              <th>{t('notification_settings.notify_to')}</th>
-              <th></th>
-            </tr>
-          </thead>
-          {globalNotifications.length !== 0 && (
-            <tbody className="admin-notif-list">
-              <GlobalNotificationList />
-            </tbody>
-          )}
-        </table>
-
-      </React.Fragment>
-    );
-  }
-
-}
+      </div>
+      <h2 className="border-bottom mb-5">{t('notification_settings.notification_list')}
+        <button className="btn btn-outline-secondary pull-right"
+          type="button" onClick={() => router.push('/admin/global-notification/new')}>{t('notification_settings.add_notification')}</button>
+        {/* <a href="/admin/global-notification/new">
+      <p className="btn btn-outline-secondary pull-right">{t('notification_setting.add_notification')}</p>
+    </a> */}
+      </h2><table className="table table-bordered">
+        <thead>
+          <tr>
+            <th>ON/OFF</th>
+            {/* eslint-disable-next-line react/no-danger */}
+            <th>{t('notification_settings.trigger_path')} <span dangerouslySetInnerHTML={{ __html: t('notification_settings.trigger_path_help') }} /></th>
+            <th>{t('notification_settings.trigger_events')}</th>
+            <th>{t('notification_settings.notify_to')}</th>
+            <th></th>
+          </tr>
+        </thead>
+        {globalNotifications.length !== 0 && (
+          <tbody className="admin-notif-list">
+            <GlobalNotificationList />
+          </tbody>
+        )}
+      </table>
+    </>
+  );
+};
 
 
 GlobalNotification.propTypes = {
 GlobalNotification.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
   adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
   adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
-
-};
-
-const GlobalNotificationWrapperFC = (props) => {
-  const { t } = useTranslation('admin');
-
-  return <GlobalNotification t={t} {...props} />;
 };
 };
 
 
-const GlobalNotificationWrapper = withUnstatedContainers(GlobalNotificationWrapperFC, [AdminNotificationContainer]);
+const GlobalNotificationWrapper = withUnstatedContainers(GlobalNotification, [AdminNotificationContainer]);
 
 
 export default GlobalNotificationWrapper;
 export default GlobalNotificationWrapper;

+ 217 - 260
packages/app/src/components/Admin/Notification/ManageGlobalNotification.jsx

@@ -1,12 +1,13 @@
-import React from 'react';
+import React, { useCallback, useState } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
-import AppContainer from '~/client/services/AppContainer';
+import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
 import { apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
+import { useIsMailerSetup } from '~/stores/context';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 
 
@@ -18,74 +19,44 @@ import TriggerEventCheckBox from './TriggerEventCheckBox';
 
 
 const logger = loggerFactory('growi:manageGlobalNotification');
 const logger = loggerFactory('growi:manageGlobalNotification');
 
 
-class ManageGlobalNotification extends React.Component {
+const ManageGlobalNotification = (props) => {
 
 
-  constructor() {
-    super();
+  let globalNotification;
+  // TODO: securely fetch the data of globalNotification variable without using swig. URL https://redmine.weseek.co.jp/issues/103901
+  // globalNotification = JSON.parse(document.getElementById('admin-global-notification-setting').getAttribute('data-global-notification'));
 
 
-    let globalNotification;
-    try {
-      globalNotification = JSON.parse(document.getElementById('admin-global-notification-setting').getAttribute('data-global-notification'));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-
-    this.state = {
-      globalNotificationId: globalNotification._id || null,
-      triggerPath: globalNotification.triggerPath || '',
-      notifyToType: globalNotification.__t || 'mail',
-      emailToSend: globalNotification.toEmail || '',
-      slackChannelToSend: globalNotification.slackChannels || '',
-      triggerEvents: new Set(globalNotification.triggerEvents),
-    };
-
-    this.submitHandler = this.submitHandler.bind(this);
-  }
-
-  onChangeTriggerPath(inputValue) {
-    this.setState({ triggerPath: inputValue });
-  }
+  const [globalNotificationId, setGlobalNotificationId] = useState(null);
+  const [triggerPath, setTriggerPath] = useState('');
+  const [notifyToType, setNotifyToType] = useState('mail');
+  const [emailToSend, setEmailToSend] = useState('');
+  const [slackChannelToSend, setSlackChannelToSend] = useState('');
+  const [triggerEvents, setTriggerEvents] = useState(new Set(globalNotification?.triggerEvents));
 
 
-  onChangeNotifyToType(notifyToType) {
-    this.setState({ notifyToType });
-  }
-
-  onChangeEmailToSend(inputValue) {
-    this.setState({ emailToSend: inputValue });
-  }
-
-  onChangeSlackChannelToSend(inputValue) {
-    this.setState({ slackChannelToSend: inputValue });
-  }
-
-  onChangeTriggerEvents(triggerEvent) {
-    const { triggerEvents } = this.state;
+  const onChangeTriggerEvents = (triggerEvent) => {
 
 
     if (triggerEvents.has(triggerEvent)) {
     if (triggerEvents.has(triggerEvent)) {
       triggerEvents.delete(triggerEvent);
       triggerEvents.delete(triggerEvent);
-      this.setState({ triggerEvents });
+      setTriggerEvents(triggerEvents);
     }
     }
     else {
     else {
       triggerEvents.add(triggerEvent);
       triggerEvents.add(triggerEvent);
-      this.setState({ triggerEvents });
+      setTriggerEvents(triggerEvents);
     }
     }
-  }
+  };
 
 
-  async submitHandler() {
+  const submitHandler = useCallback(async() => {
 
 
     const requestParams = {
     const requestParams = {
-      triggerPath: this.state.triggerPath,
-      notifyToType: this.state.notifyToType,
-      toEmail: this.state.emailToSend,
-      slackChannels: this.state.slackChannelToSend,
-      triggerEvents: [...this.state.triggerEvents],
+      triggerPath,
+      notifyToType,
+      emailToSend,
+      slackChannelToSend,
+      triggerEvents,
     };
     };
 
 
     try {
     try {
-      if (this.state.globalNotificationId != null) {
-        await apiv3Put(`/notification-setting/global-notification/${this.state.globalNotificationId}`, requestParams);
+      if (globalNotificationId != null) {
+        await apiv3Put(`/notification-setting/global-notification/${globalNotificationId}`, requestParams);
       }
       }
       else {
       else {
         await apiv3Post('/notification-setting/global-notification', requestParams);
         await apiv3Post('/notification-setting/global-notification', requestParams);
@@ -96,235 +67,221 @@ class ManageGlobalNotification extends React.Component {
       toastError(err);
       toastError(err);
       logger.error(err);
       logger.error(err);
     }
     }
-  }
+  }, [emailToSend, globalNotificationId, notifyToType, slackChannelToSend, triggerEvents, triggerPath]);
 
 
+  const { data: isMailerSetup } = useIsMailerSetup();
+  const { adminNotificationContainer } = props;
+  const { t } = useTranslation('admin');
 
 
-  render() {
-    const { t, appContainer } = this.props;
-    const { isMailerSetup } = appContainer.config;
+  return (
+    <>
+      <div className="my-3">
+        <a href="/admin/notification#global-notification" className="btn btn-outline-secondary">
+          <i className="icon-fw ti-arrow-left" aria-hidden="true"></i>
+          {t('notification_settings.back_to_list')}
+        </a>
+      </div>
 
 
-    return (
-      <React.Fragment>
 
 
-        <div className="my-3">
-          <a href="/admin/notification#global-notification" className="btn btn-outline-secondary">
-            <i className="icon-fw ti-arrow-left" aria-hidden="true"></i>
-            {t('notification_settings.back_to_list')}
-          </a>
+      <div className="row">
+        <div className="form-box col-md-12">
+          <h2 className="border-bottom mb-5">{t('notification_settings.notification_detail')}</h2>
         </div>
         </div>
 
 
-
-        <div className="row">
-          <div className="form-box col-md-12">
-            <h2 className="border-bottom mb-5">{t('notification_settings.notification_detail')}</h2>
+        <div className="col-sm-4">
+          <h3 htmlFor="triggerPath">{t('notification_settings.trigger_path')}
+            {/* eslint-disable-next-line react/no-danger */}
+            <small dangerouslySetInnerHTML={{ __html: t('notification_settings.trigger_path_help', '<code>*</code>') }} />
+          </h3>
+          <div className="form-group">
+            <input
+              className="form-control"
+              type="text"
+              name="triggerPath"
+              value={triggerPath}
+              onChange={(e) => { setTriggerPath(e.target.value) }}
+              required
+            />
           </div>
           </div>
 
 
-          <div className="col-sm-4">
-            <h3 htmlFor="triggerPath">{t('notification_settings.trigger_path')}
-              {/* eslint-disable-next-line react/no-danger */}
-              <small dangerouslySetInnerHTML={{ __html: t('notification_settings.trigger_path_help', '<code>*</code>') }} />
-            </h3>
-            <div className="form-group">
+          <h3>{t('notification_settings.notify_to')}</h3>
+          <div className="form-group form-inline">
+            <div className="custom-control custom-radio">
               <input
               <input
-                className="form-control"
-                type="text"
-                name="triggerPath"
-                value={this.state.triggerPath}
-                onChange={(e) => { this.onChangeTriggerPath(e.target.value) }}
-                required
+                className="custom-control-input"
+                type="radio"
+                id="mail"
+                name="notifyToType"
+                value="mail"
+                checked={notifyToType === 'mail'}
+                onChange={() => { setNotifyToType('mail') }}
               />
               />
+              <label className="custom-control-label" htmlFor="mail">
+                <p className="font-weight-bold">Email</p>
+              </label>
             </div>
             </div>
-
-            <h3>{t('notification_settings.notify_to')}</h3>
-            <div className="form-group form-inline">
-              <div className="custom-control custom-radio">
-                <input
-                  className="custom-control-input"
-                  type="radio"
-                  id="mail"
-                  name="notifyToType"
-                  value="mail"
-                  checked={this.state.notifyToType === 'mail'}
-                  onChange={() => { this.onChangeNotifyToType('mail') }}
-                />
-                <label className="custom-control-label" htmlFor="mail">
-                  <p className="font-weight-bold">Email</p>
-                </label>
-              </div>
-              <div className="custom-control custom-radio ml-2">
-                <input
-                  className="custom-control-input"
-                  type="radio"
-                  id="slack"
-                  name="notifyToType"
-                  value="slack"
-                  checked={this.state.notifyToType === 'slack'}
-                  onChange={() => { this.onChangeNotifyToType('slack') }}
-                />
-                <label className="custom-control-label" htmlFor="slack">
-                  <p className="font-weight-bold">Slack</p>
-                </label>
-              </div>
+            <div className="custom-control custom-radio ml-2">
+              <input
+                className="custom-control-input"
+                type="radio"
+                id="slack"
+                name="notifyToType"
+                value="slack"
+                checked={notifyToType === 'slack'}
+                onChange={() => { setNotifyToType('slack') }}
+              />
+              <label className="custom-control-label" htmlFor="slack">
+                <p className="font-weight-bold">Slack</p>
+              </label>
             </div>
             </div>
+          </div>
 
 
-            {this.state.notifyToType === 'mail'
-              ? (
-                <>
-                  <div className="input-group notify-to-option" id="mail-input">
-                    <div className="input-group-prepend">
-                      <span className="input-group-text" id="mail-addon"><i className="ti ti-email" /></span>
-                    </div>
-                    <input
-                      className="form-control"
-                      type="text"
-                      aria-describedby="mail-addon"
-                      name="toEmail"
-                      placeholder="Email"
-                      value={this.state.emailToSend}
-                      onChange={(e) => { this.onChangeEmailToSend(e.target.value) }}
-                    />
-
+          {notifyToType === 'mail'
+            ? (
+              <>
+                <div className="input-group notify-to-option" id="mail-input">
+                  <div className="input-group-prepend">
+                    <span className="input-group-text" id="mail-addon"><i className="ti ti-email" /></span>
                   </div>
                   </div>
-
-                  <p className="p-2">
-                    {/* eslint-disable-next-line react/no-danger */}
-                    {!isMailerSetup && <span className="form-text text-muted" dangerouslySetInnerHTML={{ __html: t('admin:mailer_setup_required') }} />}
-                    <b>Hint: </b>
-                    <a href="https://ifttt.com/create" target="blank">{t('notification_settings.email.ifttt_link')}
-                      <i className="icon-share-alt" />
-                    </a>
-                  </p>
-                </>
-              )
-              : (
-                <>
-                  <div className="input-group notify-to-option" id="slack-input">
-                    <div className="input-group-prepend">
-                      <span className="input-group-text" id="slack-channel-addon"><i className="fa fa-hashtag" /></span>
-                    </div>
-                    <input
-                      className="form-control"
-                      type="text"
-                      aria-describedby="slack-channel-addon"
-                      name="notificationGlobal[slackChannels]"
-                      placeholder="Slack Channel"
-                      value={this.state.slackChannelToSend}
-                      onChange={(e) => { this.onChangeSlackChannelToSend(e.target.value) }}
-                    />
+                  <input
+                    className="form-control"
+                    type="text"
+                    aria-describedby="mail-addon"
+                    name="toEmail"
+                    placeholder="Email"
+                    value={emailToSend}
+                    onChange={(e) => { setEmailToSend(e.target.value) }}
+                  />
+
+                </div>
+
+                <p className="p-2">
+                  {/* eslint-disable-next-line react/no-danger */}
+                  {!isMailerSetup && <span className="form-text text-muted" dangerouslySetInnerHTML={{ __html: t('admin:mailer_setup_required') }} />}
+                  <b>Hint: </b>
+                  <a href="https://ifttt.com/create" target="blank">{t('notification_settings.email.ifttt_link')}
+                    <i className="icon-share-alt" />
+                  </a>
+                </p>
+              </>
+            )
+            : (
+              <>
+                <div className="input-group notify-to-option" id="slack-input">
+                  <div className="input-group-prepend">
+                    <span className="input-group-text" id="slack-channel-addon"><i className="fa fa-hashtag" /></span>
                   </div>
                   </div>
-                  <p className="p-2">
-                    {/* eslint-disable-next-line react/no-danger */}
-                    <span dangerouslySetInnerHTML={{ __html: t('notification_settings.channel_desc') }} />
-                  </p>
-                </>
-              )}
-          </div>
-
-          <div className="offset-1 col-sm-5">
-            <div className="form-group">
-              <h3>{t('notification_settings.trigger_events')}</h3>
-              <div className="my-1">
-                <TriggerEventCheckBox
-                  checkbox="success"
-                  event="pageCreate"
-                  checked={this.state.triggerEvents.has('pageCreate')}
-                  onChange={() => this.onChangeTriggerEvents('pageCreate')}
-                >
-                  <span className="badge badge-pill badge-success">
-                    <i className="icon-doc mr-1" /> CREATE
-                  </span>
-                </TriggerEventCheckBox>
-              </div>
-              <div className="my-1">
-                <TriggerEventCheckBox
-                  checkbox="warning"
-                  event="pageEdit"
-                  checked={this.state.triggerEvents.has('pageEdit')}
-                  onChange={() => this.onChangeTriggerEvents('pageEdit')}
-                >
-                  <span className="badge badge-pill badge-warning">
-                    <i className="icon-pencil mr-1" />EDIT
-                  </span>
-                </TriggerEventCheckBox>
-              </div>
-              <div className="my-1">
-                <TriggerEventCheckBox
-                  checkbox="pink"
-                  event="pageMove"
-                  checked={this.state.triggerEvents.has('pageMove')}
-                  onChange={() => this.onChangeTriggerEvents('pageMove')}
-                >
-                  <span className="badge badge-pill badge-pink">
-                    <i className="icon-action-redo mr-1" />MOVE
-                  </span>
-                </TriggerEventCheckBox>
-              </div>
-              <div className="my-1">
-                <TriggerEventCheckBox
-                  checkbox="danger"
-                  event="pageDelete"
-                  checked={this.state.triggerEvents.has('pageDelete')}
-                  onChange={() => this.onChangeTriggerEvents('pageDelete')}
-                >
-                  <span className="badge badge-pill badge-danger">
-                    <i className="icon-fire mr-1" />DELETE
-                  </span>
-                </TriggerEventCheckBox>
-              </div>
-              <div className="my-1">
-                <TriggerEventCheckBox
-                  checkbox="info"
-                  event="pageLike"
-                  checked={this.state.triggerEvents.has('pageLike')}
-                  onChange={() => this.onChangeTriggerEvents('pageLike')}
-                >
-                  <span className="badge badge-pill badge-info">
-                    <i className="fa fa-heart-o mr-1" />LIKE
-                  </span>
-                </TriggerEventCheckBox>
-              </div>
-              <div className="my-1">
-                <TriggerEventCheckBox
-                  checkbox="secondary"
-                  event="comment"
-                  checked={this.state.triggerEvents.has('comment')}
-                  onChange={() => this.onChangeTriggerEvents('comment')}
-                >
-                  <span className="badge badge-pill badge-secondary">
-                    <i className="icon-bubble mr-1" />POST
-                  </span>
-                </TriggerEventCheckBox>
-              </div>
+                  <input
+                    className="form-control"
+                    type="text"
+                    aria-describedby="slack-channel-addon"
+                    name="notificationGlobal[slackChannels]"
+                    placeholder="Slack Channel"
+                    value={slackChannelToSend}
+                    onChange={(e) => { setSlackChannelToSend(e.target.value) }}
+                  />
+                </div>
+                <p className="p-2">
+                  {/* eslint-disable-next-line react/no-danger */}
+                  <span dangerouslySetInnerHTML={{ __html: t('notification_settings.channel_desc') }} />
+                </p>
+              </>
+            )}
+        </div>
 
 
+        <div className="offset-1 col-sm-5">
+          <div className="form-group">
+            <h3>{t('notification_settings.trigger_events')}</h3>
+            <div className="my-1">
+              <TriggerEventCheckBox
+                checkbox="success"
+                event="pageCreate"
+                checked={triggerEvents.has('pageCreate')}
+                onChange={() => onChangeTriggerEvents('pageCreate')}
+              >
+                <span className="badge badge-pill badge-success">
+                  <i className="icon-doc mr-1" /> CREATE
+                </span>
+              </TriggerEventCheckBox>
+            </div>
+            <div className="my-1">
+              <TriggerEventCheckBox
+                checkbox="warning"
+                event="pageEdit"
+                checked={triggerEvents.has('pageEdit')}
+                onChange={() => onChangeTriggerEvents('pageEdit')}
+              >
+                <span className="badge badge-pill badge-warning">
+                  <i className="icon-pencil mr-1" />EDIT
+                </span>
+              </TriggerEventCheckBox>
             </div>
             </div>
+            <div className="my-1">
+              <TriggerEventCheckBox
+                checkbox="pink"
+                event="pageMove"
+                checked={triggerEvents.has('pageMove')}
+                onChange={() => onChangeTriggerEvents('pageMove')}
+              >
+                <span className="badge badge-pill badge-pink">
+                  <i className="icon-action-redo mr-1" />MOVE
+                </span>
+              </TriggerEventCheckBox>
+            </div>
+            <div className="my-1">
+              <TriggerEventCheckBox
+                checkbox="danger"
+                event="pageDelete"
+                checked={triggerEvents.has('pageDelete')}
+                onChange={() => onChangeTriggerEvents('pageDelete')}
+              >
+                <span className="badge badge-pill badge-danger">
+                  <i className="icon-fire mr-1" />DELETE
+                </span>
+              </TriggerEventCheckBox>
+            </div>
+            <div className="my-1">
+              <TriggerEventCheckBox
+                checkbox="info"
+                event="pageLike"
+                checked={triggerEvents.has('pageLike')}
+                onChange={() => onChangeTriggerEvents('pageLike')}
+              >
+                <span className="badge badge-pill badge-info">
+                  <i className="fa fa-heart-o mr-1" />LIKE
+                </span>
+              </TriggerEventCheckBox>
+            </div>
+            <div className="my-1">
+              <TriggerEventCheckBox
+                checkbox="secondary"
+                event="comment"
+                checked={triggerEvents.has('comment')}
+                onChange={() => onChangeTriggerEvents('comment')}
+              >
+                <span className="badge badge-pill badge-secondary">
+                  <i className="icon-bubble mr-1" />POST
+                </span>
+              </TriggerEventCheckBox>
+            </div>
+
           </div>
           </div>
         </div>
         </div>
-
-        <AdminUpdateButtonRow
-          onClick={this.submitHandler}
-          disabled={this.state.retrieveError != null}
-        />
-
-      </React.Fragment>
-
-    );
-  }
-
-}
-
-ManageGlobalNotification.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
+      </div>
+
+      <AdminUpdateButtonRow
+        onClick={submitHandler}
+        disabled={adminNotificationContainer.state.retrieveError != null}
+      />
+    </>
+  );
 };
 };
 
 
-const ManageGlobalNotificationWrapperFC = (props) => {
-  const { t } = useTranslation();
-
-  return <ManageGlobalNotification t={t} {...props} />;
+ManageGlobalNotification.propTypes = {
+  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
 };
 };
 
 
-const ManageGlobalNotificationWrapper = withUnstatedContainers(ManageGlobalNotificationWrapperFC, [AppContainer]);
+const ManageGlobalNotificationWrapper = withUnstatedContainers(ManageGlobalNotification, [AdminNotificationContainer]);
 
 
 
 
 export default ManageGlobalNotificationWrapper;
 export default ManageGlobalNotificationWrapper;

+ 3 - 3
packages/app/src/components/Admin/Notification/NotificationSetting.jsx

@@ -46,7 +46,7 @@ const SkeltonListItem = () => (
 
 
 // eslint-disable-next-line react/prop-types
 // eslint-disable-next-line react/prop-types
 const SlackIntegrationListItem = ({ isEnabled, currentBotType }) => {
 const SlackIntegrationListItem = ({ isEnabled, currentBotType }) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
 
 
   const isCautionVisible = currentBotType === SlackbotType.OFFICIAL || currentBotType === SlackbotType.CUSTOM_WITH_PROXY;
   const isCautionVisible = currentBotType === SlackbotType.OFFICIAL || currentBotType === SlackbotType.CUSTOM_WITH_PROXY;
 
 
@@ -68,7 +68,7 @@ const SlackIntegrationListItem = ({ isEnabled, currentBotType }) => {
 
 
 // eslint-disable-next-line react/prop-types
 // eslint-disable-next-line react/prop-types
 const LegacySlackIntegrationListItem = ({ isEnabled }) => {
 const LegacySlackIntegrationListItem = ({ isEnabled }) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
 
 
   return (
   return (
     <li className="list-group-item">
     <li className="list-group-item">
@@ -91,7 +91,7 @@ const LegacySlackIntegrationListItem = ({ isEnabled }) => {
 function NotificationSetting(props) {
 function NotificationSetting(props) {
   const { adminNotificationContainer } = props;
   const { adminNotificationContainer } = props;
 
 
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
 
 
   const [isMounted, setMounted] = useState(false);
   const [isMounted, setMounted] = useState(false);
   const [activeTab, setActiveTab] = useState('user_trigger_notification');
   const [activeTab, setActiveTab] = useState('user_trigger_notification');

+ 2 - 2
packages/app/src/components/Admin/Notification/TriggerEventCheckBox.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import React from 'react';
 
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 
 const TriggerEventCheckBox = (props) => {
 const TriggerEventCheckBox = (props) => {
   const { t } = props;
   const { t } = props;
@@ -36,7 +36,7 @@ TriggerEventCheckBox.propTypes = {
 
 
 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 const TriggerEventCheckBoxWrapperFC = (props) => {
 const TriggerEventCheckBoxWrapperFC = (props) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
 
 
   return <TriggerEventCheckBox t={t} {...props} />;
   return <TriggerEventCheckBox t={t} {...props} />;
 };
 };

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

@@ -152,7 +152,7 @@ class LocalSecuritySettingContents extends React.Component {
             </div>
             </div>
             <div className="row">
             <div className="row">
               <div className="col-12 col-md-3 text-left text-md-right">
               <div className="col-12 col-md-3 text-left text-md-right">
-                <strong dangerouslySetInnerHTML={{ __html: t('The whitelist of registration permission E-mail address') }} />
+                <strong dangerouslySetInnerHTML={{ __html: t('security_settings.The whitelist of registration permission E-mail address') }} />
               </div>
               </div>
               <div className="col-12 col-md-6">
               <div className="col-12 col-md-6">
                 <textarea
                 <textarea

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

@@ -115,7 +115,7 @@ class SamlSecurityManagementContents extends React.Component {
                 <ul>
                 <ul>
                   {adminSamlSecurityContainer.state.missingMandatoryConfigKeys.map((configKey) => {
                   {adminSamlSecurityContainer.state.missingMandatoryConfigKeys.map((configKey) => {
                     const key = configKey.replace('security:passport-saml:', '');
                     const key = configKey.replace('security:passport-saml:', '');
-                    return <li key={configKey}>{t(`security_setting.form_item_name.${key}`)}</li>;
+                    return <li key={configKey}>{t(`security_settings.form_item_name.${key}`)}</li>;
                   })}
                   })}
                 </ul>
                 </ul>
               </div>
               </div>

+ 5 - 5
packages/app/src/components/Admin/Security/SecurityManagement.jsx → packages/app/src/components/Admin/Security/SecurityManagement.tsx

@@ -10,7 +10,11 @@ import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
 import SecurityManagementContents from './SecurityManagementContents';
 import SecurityManagementContents from './SecurityManagementContents';
 
 
-function SecurityManagement(props) {
+type Props = {
+  adminGeneralSecurityContainer: AdminGeneralSecurityContainer
+}
+
+const SecurityManagement = (props: Props) => {
   const { adminGeneralSecurityContainer } = props;
   const { adminGeneralSecurityContainer } = props;
 
 
   const fetchGeneralSecuritySettingsData = useCallback(async() => {
   const fetchGeneralSecuritySettingsData = useCallback(async() => {
@@ -28,10 +32,6 @@ function SecurityManagement(props) {
   }, [adminGeneralSecurityContainer, fetchGeneralSecuritySettingsData]);
   }, [adminGeneralSecurityContainer, fetchGeneralSecuritySettingsData]);
 
 
   return <SecurityManagementContents />;
   return <SecurityManagementContents />;
-}
-
-SecurityManagement.propTypes = {
-  adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
 };
 };
 
 
 const SecurityManagementWithUnstatedContainer = withUnstatedContainers(SecurityManagement, [AdminGeneralSecurityContainer]);
 const SecurityManagementWithUnstatedContainer = withUnstatedContainers(SecurityManagement, [AdminGeneralSecurityContainer]);

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

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

+ 7 - 7
packages/app/src/components/Admin/UserGroup/UserGroupForm.tsx

@@ -15,7 +15,7 @@ type Props = {
 
 
 export const UserGroupForm: FC<Props> = (props: Props) => {
 export const UserGroupForm: FC<Props> = (props: Props) => {
 
 
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
 
 
   const {
   const {
     userGroup, selectableParentUserGroups, submitButtonLabel, onSubmit,
     userGroup, selectableParentUserGroups, submitButtonLabel, onSubmit,
@@ -57,7 +57,7 @@ export const UserGroupForm: FC<Props> = (props: Props) => {
     >
     >
 
 
       <fieldset>
       <fieldset>
-        <h2 className="admin-setting-header">{t('admin:user_group_management.basic_info')}</h2>
+        <h2 className="admin-setting-header">{t('user_group_management.basic_info')}</h2>
 
 
         {
         {
           userGroup?.createdAt != null && (
           userGroup?.createdAt != null && (
@@ -70,14 +70,14 @@ export const UserGroupForm: FC<Props> = (props: Props) => {
 
 
         <div className="form-group row">
         <div className="form-group row">
           <label htmlFor="name" className="col-md-2 col-form-label">
           <label htmlFor="name" className="col-md-2 col-form-label">
-            {t('admin:user_group_management.group_name')}
+            {t('user_group_management.group_name')}
           </label>
           </label>
           <div className="col-md-4">
           <div className="col-md-4">
             <input
             <input
               className="form-control"
               className="form-control"
               type="text"
               type="text"
               name="name"
               name="name"
-              placeholder={t('admin:user_group_management.group_example')}
+              placeholder={t('user_group_management.group_example')}
               value={currentName}
               value={currentName}
               onChange={onChangeNameHandler}
               onChange={onChangeNameHandler}
               required
               required
@@ -96,7 +96,7 @@ export const UserGroupForm: FC<Props> = (props: Props) => {
 
 
         <div className="form-group row">
         <div className="form-group row">
           <label htmlFor="parent" className="col-md-2 col-form-label">
           <label htmlFor="parent" className="col-md-2 col-form-label">
-            {t('admin:user_group_management.parent_group')}
+            {t('user_group_management.parent_group')}
           </label>
           </label>
           <div className="dropdown col-md-4">
           <div className="dropdown col-md-4">
             <button
             <button
@@ -107,7 +107,7 @@ export const UserGroupForm: FC<Props> = (props: Props) => {
                 btn btn-outline-secondary dropdown-toggle mb-3 ${selectableParentUserGroups != null && selectableParentUserGroups.length > 0 ? '' : 'disabled'}
                 btn btn-outline-secondary dropdown-toggle mb-3 ${selectableParentUserGroups != null && selectableParentUserGroups.length > 0 ? '' : 'disabled'}
               `}
               `}
             >
             >
-              {selectedParent?.name ?? t('admin:user_group_management.select_parent_group')}
+              {selectedParent?.name ?? t('user_group_management.select_parent_group')}
             </button>
             </button>
             <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
             <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
               {
               {
@@ -135,7 +135,7 @@ export const UserGroupForm: FC<Props> = (props: Props) => {
                 className="dropdown-item"
                 className="dropdown-item"
                 type="button"
                 type="button"
                 onClick={() => { setSelectedParent(undefined) }}
                 onClick={() => { setSelectedParent(undefined) }}
-              >{t('admin:user_group_management.release_parent_group')}
+              >{t('user_group_management.release_parent_group')}
               </button>
               </button>
             </div>
             </div>
           </div>
           </div>

+ 4 - 4
packages/app/src/components/Admin/UserGroup/UserGroupModal.tsx

@@ -21,7 +21,7 @@ type Props = {
 
 
 export const UserGroupModal: FC<Props> = (props: Props) => {
 export const UserGroupModal: FC<Props> = (props: Props) => {
 
 
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
 
 
   const {
   const {
     userGroup, buttonLabel, onClickSubmit, isShow, onHide,
     userGroup, buttonLabel, onClickSubmit, isShow, onHide,
@@ -73,19 +73,19 @@ export const UserGroupModal: FC<Props> = (props: Props) => {
     <Modal className="modal-md" isOpen={isShow} toggle={onHide}>
     <Modal className="modal-md" isOpen={isShow} toggle={onHide}>
       <form onSubmit={onSubmitHandler}>
       <form onSubmit={onSubmitHandler}>
         <ModalHeader tag="h4" toggle={onHide} className="bg-primary text-light">
         <ModalHeader tag="h4" toggle={onHide} className="bg-primary text-light">
-          {t('admin:user_group_management.basic_info')}
+          {t('user_group_management.basic_info')}
         </ModalHeader>
         </ModalHeader>
 
 
         <ModalBody>
         <ModalBody>
           <div className="form-group">
           <div className="form-group">
             <label htmlFor="name">
             <label htmlFor="name">
-              {t('admin:user_group_management.group_name')}
+              {t('user_group_management.group_name')}
             </label>
             </label>
             <input
             <input
               className="form-control"
               className="form-control"
               type="text"
               type="text"
               name="name"
               name="name"
-              placeholder={t('admin:user_group_management.group_example')}
+              placeholder={t('user_group_management.group_example')}
               value={currentName}
               value={currentName}
               onChange={onChangeNameHandler}
               onChange={onChangeNameHandler}
               required
               required

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

@@ -41,7 +41,7 @@ type Props = {
 }
 }
 
 
 const UserGroupDetailPage = (props: Props): JSX.Element => {
 const UserGroupDetailPage = (props: Props): JSX.Element => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
   const router = useRouter();
   const router = useRouter();
   const xss = useMemo(() => new Xss(), []);
   const xss = useMemo(() => new Xss(), []);
   const { userGroupId: currentUserGroupId } = props;
   const { userGroupId: currentUserGroupId } = props;
@@ -323,7 +323,7 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
     <div>
     <div>
       <nav aria-label="breadcrumb">
       <nav aria-label="breadcrumb">
         <ol className="breadcrumb">
         <ol className="breadcrumb">
-          <li className="breadcrumb-item"><a href="/admin/user-groups">{t('admin:user_group_management.group_list')}</a></li>
+          <li className="breadcrumb-item"><a href="/admin/user-groups">{t('user_group_management.group_list')}</a></li>
           {
           {
             ancestorUserGroups != null && ancestorUserGroups.length > 0 && (
             ancestorUserGroups != null && ancestorUserGroups.length > 0 && (
               ancestorUserGroups.map((ancestorUserGroup: IUserGroupHasId) => (
               ancestorUserGroups.map((ancestorUserGroup: IUserGroupHasId) => (
@@ -349,7 +349,7 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
           onSubmit={onClickSubmitForm}
           onSubmit={onClickSubmitForm}
         />
         />
       </div>
       </div>
-      <h2 className="admin-setting-header mt-4">{t('admin:user_group_management.user_list')}</h2>
+      <h2 className="admin-setting-header mt-4">{t('user_group_management.user_list')}</h2>
       <UserGroupUserTable
       <UserGroupUserTable
         userGroup={currentUserGroup}
         userGroup={currentUserGroup}
         userGroupRelations={childUserGroupRelations}
         userGroupRelations={childUserGroupRelations}
@@ -370,7 +370,7 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
         onToggleIsAlsoNameSearched={toggleAlsoNameSearched}
         onToggleIsAlsoNameSearched={toggleAlsoNameSearched}
       />
       />
 
 
-      <h2 className="admin-setting-header mt-4">{t('admin:user_group_management.child_group_list')}</h2>
+      <h2 className="admin-setting-header mt-4">{t('user_group_management.child_group_list')}</h2>
       <UserGroupDropdown
       <UserGroupDropdown
         selectableUserGroups={selectableChildUserGroups}
         selectableUserGroups={selectableChildUserGroups}
         onClickAddExistingUserGroupButton={onClickAddExistingUserGroupButtonHandler}
         onClickAddExistingUserGroupButton={onClickAddExistingUserGroupButtonHandler}

+ 2 - 2
packages/app/src/components/Admin/UserGroupDetail/UserGroupPageList.tsx

@@ -17,7 +17,7 @@ type Props = {
 }
 }
 
 
 const UserGroupPageList = (props: Props): JSX.Element => {
 const UserGroupPageList = (props: Props): JSX.Element => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
   const { userGroupId, relatedPages } = props;
   const { userGroupId, relatedPages } = props;
 
 
   const [currentPages, setCurrentPages] = useState<IPageHasId[]>([]);
   const [currentPages, setCurrentPages] = useState<IPageHasId[]>([]);
@@ -52,7 +52,7 @@ const UserGroupPageList = (props: Props): JSX.Element => {
       <ul className="page-list-ul page-list-ul-flat mb-3">
       <ul className="page-list-ul page-list-ul-flat mb-3">
         {currentPages.map(page => <li key={page._id}><PageListItemS page={page} /></li>)}
         {currentPages.map(page => <li key={page._id}><PageListItemS page={page} /></li>)}
       </ul>
       </ul>
-      {relatedPages != null && relatedPages.length === 0 ? <p>{t('admin:user_group_management.no_pages')}</p> : (
+      {relatedPages != null && relatedPages.length === 0 ? <p>{t('user_group_management.no_pages')}</p> : (
         <PaginationWrapper
         <PaginationWrapper
           activePage={activePage}
           activePage={activePage}
           changePage={handlePageChange}
           changePage={handlePageChange}

+ 1 - 1
packages/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.tsx

@@ -33,7 +33,7 @@ export const UserGroupUserTable = (props: Props): JSX.Element => {
           </th>
           </th>
           <th>{t('Name')}</th>
           <th>{t('Name')}</th>
           <th style={{ width: '100px' }}>{t('Created')}</th>
           <th style={{ width: '100px' }}>{t('Created')}</th>
-          <th style={{ width: '160px' }}>{t('Last_Login')}</th>
+          <th style={{ width: '160px' }}>{t('last_login')}</th>
           <th style={{ width: '70px' }}></th>
           <th style={{ width: '70px' }}></th>
         </tr>
         </tr>
       </thead>
       </thead>

+ 1 - 1
packages/app/src/components/Admin/Users/UserTable.jsx

@@ -169,7 +169,7 @@ class UserTable extends React.Component {
                 <th width="150px">
                 <th width="150px">
                   <div className="d-flex align-items-center">
                   <div className="d-flex align-items-center">
                     <div className="mr-3">
                     <div className="mr-3">
-                      {t('Last_Login')}
+                      {t('last_login')}
                     </div>
                     </div>
                     <SortIcons
                     <SortIcons
                       isSelected={adminUsersContainer.state.sort === 'lastLoginAt'}
                       isSelected={adminUsersContainer.state.sort === 'lastLoginAt'}

+ 4 - 2
packages/app/src/components/Fab.jsx

@@ -13,6 +13,8 @@ import loggerFactory from '~/utils/logger';
 import CreatePageIcon from './Icons/CreatePageIcon';
 import CreatePageIcon from './Icons/CreatePageIcon';
 import ReturnTopIcon from './Icons/ReturnTopIcon';
 import ReturnTopIcon from './Icons/ReturnTopIcon';
 
 
+import styles from './Fab.module.scss';
+
 const logger = loggerFactory('growi:cli:Fab');
 const logger = loggerFactory('growi:cli:Fab');
 
 
 const Fab = () => {
 const Fab = () => {
@@ -71,9 +73,9 @@ const Fab = () => {
   }
   }
 
 
   return (
   return (
-    <div className="grw-fab d-none d-md-block d-edit-none" data-testid="grw-fab">
+    <div className={`${styles['grw-fab']} grw-fab d-none d-md-block d-edit-none`} data-testid="grw-fab">
       {currentUser != null && renderPageCreateButton()}
       {currentUser != null && renderPageCreateButton()}
-      <div data-testid="grw-fab-return-to-top" className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: 0, right: 0 }}>
+      <div className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: 0, right: 0 }} data-testid="grw-fab-return-to-top">
         <button
         <button
           type="button"
           type="button"
           className={`btn btn-light btn-scroll-to-top rounded-circle p-0 ${buttonClasses}`}
           className={`btn btn-light btn-scroll-to-top rounded-circle p-0 ${buttonClasses}`}

+ 31 - 0
packages/app/src/components/Fab.module.scss

@@ -0,0 +1,31 @@
+.grw-fab :global {
+  // workaround
+  // https://stackoverflow.com/a/57667536
+  .fadeInUp {
+    & :local {
+      animation: fab-fadeinup 1s ease 0s;
+    }
+  }
+  .fadeOut {
+    & :local {
+      animation: fab-fadeout 0.5s ease 0s forwards;
+    }
+  }
+}
+
+@keyframes fab-fadeinup {
+  0% {
+    opacity: 0;
+    transform: translateY(100px);
+  }
+  100% {
+    opacity: 1;
+    transform: translateY(0px);
+  }
+}
+
+@keyframes fab-fadeout {
+  100% {
+    opacity: 0
+  }
+}

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

@@ -19,6 +19,8 @@ const PageDeleteModal = dynamic(() => import('../PageDeleteModal'), { ssr: false
 const PageRenameModal = dynamic(() => import('../PageRenameModal'), { ssr: false });
 const PageRenameModal = dynamic(() => import('../PageRenameModal'), { ssr: false });
 const PagePresentationModal = dynamic(() => import('../PagePresentationModal'), { ssr: false });
 const PagePresentationModal = dynamic(() => import('../PagePresentationModal'), { ssr: false });
 const PageAccessoriesModal = dynamic(() => import('../PageAccessoriesModal'), { ssr: false });
 const PageAccessoriesModal = dynamic(() => import('../PageAccessoriesModal'), { ssr: false });
+// Fab
+const Fab = dynamic(() => import('../Fab'), { ssr: false });
 
 
 
 
 type Props = {
 type Props = {
@@ -58,6 +60,8 @@ export const BasicLayout = ({
       <PageAccessoriesModal />
       <PageAccessoriesModal />
       {/* <HotkeysManager /> */}
       {/* <HotkeysManager /> */}
 
 
+      <Fab />
+
       <ShortcutsModal />
       <ShortcutsModal />
       <SystemVersion showShortcutsButton />
       <SystemVersion showShortcutsButton />
     </RawLayout>
     </RawLayout>

+ 3 - 2
packages/app/src/components/PageEditor.tsx

@@ -462,9 +462,10 @@ const PageEditor = React.memo((props: Props): JSX.Element => {
       </div>
       </div>
       <div className="d-none d-lg-block page-editor-preview-container flex-grow-1 flex-basis-0 mw-0">
       <div className="d-none d-lg-block page-editor-preview-container flex-grow-1 flex-basis-0 mw-0">
         <Preview
         <Preview
-          markdown={markdown}
-          rendererOptions={rendererOptions}
           ref={previewRef}
           ref={previewRef}
+          rendererOptions={rendererOptions}
+          markdown={markdown}
+          pagePath={currentPagePath}
           renderMathJaxOnInit={false}
           renderMathJaxOnInit={false}
           onScroll={offset => scrollEditorByPreviewScrollWithThrottle(offset)}
           onScroll={offset => scrollEditorByPreviewScrollWithThrottle(offset)}
         />
         />

+ 6 - 5
packages/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -996,11 +996,12 @@ class CodeMirrorEditor extends AbstractEditor {
           ref={this.cm}
           ref={this.cm}
           className={additionalClasses}
           className={additionalClasses}
           placeholder="search"
           placeholder="search"
-          editorDidMount={(editor) => {
-          // add event handlers
-            editor.on('paste', this.pasteHandler);
-            editor.on('scrollCursorIntoView', this.scrollCursorIntoViewHandler);
-          }}
+          // == temporary deactivate editorDidMount to use https://github.com/scniro/react-codemirror2/issues/284#issuecomment-1155928554
+          // editorDidMount={(editor) => {
+          // // add event handlers
+          //   editor.on('paste', this.pasteHandler);
+          //   editor.on('scrollCursorIntoView', this.scrollCursorIntoViewHandler);
+          // }}
           // temporary set props.value
           // temporary set props.value
           // value={this.state.value}
           // value={this.state.value}
           value={this.props.value}
           value={this.props.value}

+ 5 - 6
packages/app/src/components/PageEditor/Preview.tsx

@@ -2,19 +2,16 @@ import React, {
   SyntheticEvent, RefObject,
   SyntheticEvent, RefObject,
 } from 'react';
 } from 'react';
 
 
-import ReactMarkdown from 'react-markdown';
-
-
 import { RendererOptions } from '~/services/renderer/renderer';
 import { RendererOptions } from '~/services/renderer/renderer';
 import { useEditorSettings } from '~/stores/editor';
 import { useEditorSettings } from '~/stores/editor';
 
 
-import RevisionBody from '../Page/RevisionBody';
+import RevisionRenderer from '../Page/RevisionRenderer';
 
 
 
 
 type Props = {
 type Props = {
   rendererOptions: RendererOptions,
   rendererOptions: RendererOptions,
   markdown?: string,
   markdown?: string,
-  pagePath?: string,
+  pagePath?: string | null,
   renderMathJaxOnInit?: boolean,
   renderMathJaxOnInit?: boolean,
   onScroll?: (scrollTop: number) => void,
   onScroll?: (scrollTop: number) => void,
 }
 }
@@ -39,7 +36,9 @@ const Preview = React.forwardRef((props: Props, ref: RefObject<HTMLDivElement>):
         }
         }
       }}
       }}
     >
     >
-      <ReactMarkdown {...rendererOptions} className='wiki'>{markdown || ''}</ReactMarkdown>
+      { markdown != null && pagePath != null && (
+        <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} pagePath={pagePath}></RevisionRenderer>
+      ) }
     </div>
     </div>
   );
   );
 
 

+ 25 - 1
packages/app/src/components/UncontrolledCodeMirror.tsx

@@ -1,5 +1,8 @@
-import React, { forwardRef, ReactNode, Ref } from 'react';
+import React, {
+  forwardRef, ReactNode, Ref,
+} from 'react';
 
 
+import { Editor } from 'codemirror';
 import { ICodeMirror, UnControlled as CodeMirror } from 'react-codemirror2';
 import { ICodeMirror, UnControlled as CodeMirror } from 'react-codemirror2';
 
 
 import AbstractEditor, { AbstractEditorProps } from '~/components/PageEditor/AbstractEditor';
 import AbstractEditor, { AbstractEditorProps } from '~/components/PageEditor/AbstractEditor';
@@ -21,6 +24,25 @@ interface UncontrolledCodeMirrorCoreProps extends UncontrolledCodeMirrorProps {
 
 
 export class UncontrolledCodeMirrorCore extends AbstractEditor<UncontrolledCodeMirrorCoreProps> {
 export class UncontrolledCodeMirrorCore extends AbstractEditor<UncontrolledCodeMirrorCoreProps> {
 
 
+  editor: Editor;
+
+  // wrapperRef: RefObject<any>;
+
+  constructor(props: UncontrolledCodeMirrorCoreProps) {
+    super(props);
+    this.editorDidMount = this.editorDidMount.bind(this);
+    this.editorWillUnmount = this.editorWillUnmount.bind(this);
+  }
+
+  editorDidMount(e: Editor): void {
+    this.editor = e;
+  }
+
+  editorWillUnmount(): void {
+    // workaround to fix editor duplicating by https://github.com/scniro/react-codemirror2/issues/284#issuecomment-1155928554
+    (this.editor as any).display.wrapper.remove();
+  }
+
   override render(): ReactNode {
   override render(): ReactNode {
 
 
     const {
     const {
@@ -38,6 +60,8 @@ export class UncontrolledCodeMirrorCore extends AbstractEditor<UncontrolledCodeM
           tabSize: 4,
           tabSize: 4,
           ...options,
           ...options,
         }}
         }}
+        editorDidMount={this.editorDidMount}
+        editorWillUnmount={this.editorWillUnmount}
         {...rest}
         {...rest}
       />
       />
     );
     );

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

@@ -307,24 +307,18 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
           <div className="flex-grow-1">
           <div className="flex-grow-1">
             <div id="main" className={`main ${isUsersHomePage(props.currentPathname) && 'user-page'}`}>
             <div id="main" className={`main ${isUsersHomePage(props.currentPathname) && 'user-page'}`}>
               <div id="content-main" className="content-main grw-container-convertible">
               <div id="content-main" className="content-main grw-container-convertible">
-                <div className="row">
-                  <div className="col">
-                    { props.isIdenticalPathPage && <IdenticalPathPage /> }
-
-                    { !props.isIdenticalPathPage && (
-                      <>
-                        <PageAlerts />
-                        { props.isForbidden && <ForbiddenPage /> }
-                        { props.IsNotCreatable && <NotCreatablePage />}
-                        { !props.isForbidden && !props.IsNotCreatable && <DisplaySwitcher />}
-                        {/* <DisplaySwitcher /> */}
-                        <div id="page-editor-navbar-bottom-container" className="d-none d-edit-block"></div>
-                        {/* <PageStatusAlert /> */}
-                      </>
-                    ) }
-
-                  </div>
-                </div>
+                { props.isIdenticalPathPage && <IdenticalPathPage /> }
+
+                { !props.isIdenticalPathPage && (
+                  <>
+                    <PageAlerts />
+                    { props.isForbidden && <ForbiddenPage /> }
+                    { props.IsNotCreatable && <NotCreatablePage />}
+                    { !props.isForbidden && !props.IsNotCreatable && <DisplaySwitcher />}
+                    {/* <DisplaySwitcher /> */}
+                    {/* <PageStatusAlert /> */}
+                  </>
+                ) }
 
 
                 {/* <div className="col-xl-2 col-lg-3 d-none d-lg-block revision-toc-container">
                 {/* <div className="col-xl-2 col-lg-3 d-none d-lg-block revision-toc-container">
                   <div id="revision-toc" className="revision-toc mt-3 sps sps--abv" data-sps-offset="123">
                   <div id="revision-toc" className="revision-toc mt-3 sps sps--abv" data-sps-offset="123">

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

@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React from 'react';
 
 
 import { isClient, objectIdUtils } from '@growi/core';
 import { isClient, objectIdUtils } from '@growi/core';
 import {
 import {
@@ -46,12 +46,13 @@ import {
 
 
 const AdminHome = dynamic(() => import('../../components/Admin/AdminHome/AdminHome'), { ssr: false });
 const AdminHome = dynamic(() => import('../../components/Admin/AdminHome/AdminHome'), { ssr: false });
 const AppSettingsPageContents = dynamic(() => import('../../components/Admin/App/AppSettingsPageContents'), { ssr: false });
 const AppSettingsPageContents = dynamic(() => import('../../components/Admin/App/AppSettingsPageContents'), { ssr: false });
-const SecurityManagementContents = dynamic(() => import('../../components/Admin/Security/SecurityManagementContents'), { ssr: false });
+const SecurityManagement = dynamic(() => import('../../components/Admin/Security/SecurityManagement'), { ssr: false });
 const MarkDownSettingContents = dynamic(() => import('../../components/Admin/MarkdownSetting/MarkDownSettingContents'), { ssr: false });
 const MarkDownSettingContents = dynamic(() => import('../../components/Admin/MarkdownSetting/MarkDownSettingContents'), { ssr: false });
 const CustomizeSettingContents = dynamic(() => import('../../components/Admin/Customize/Customize'), { ssr: false });
 const CustomizeSettingContents = dynamic(() => import('../../components/Admin/Customize/Customize'), { ssr: false });
 const DataImportPageContents = dynamic(() => import('../../components/Admin/ImportData/ImportDataPageContents'), { ssr: false });
 const DataImportPageContents = dynamic(() => import('../../components/Admin/ImportData/ImportDataPageContents'), { ssr: false });
 const ExportArchiveDataPage = dynamic(() => import('../../components/Admin/ExportArchiveDataPage'), { ssr: false });
 const ExportArchiveDataPage = dynamic(() => import('../../components/Admin/ExportArchiveDataPage'), { ssr: false });
 const NotificationSetting = dynamic(() => import('../../components/Admin/Notification/NotificationSetting'), { ssr: false });
 const NotificationSetting = dynamic(() => import('../../components/Admin/Notification/NotificationSetting'), { ssr: false });
+const ManageGlobalNotification = dynamic(() => import('../../components/Admin/Notification/ManageGlobalNotification'), { ssr: false });
 const SlackIntegration = dynamic(() => import('../../components/Admin/SlackIntegration/SlackIntegration'), { ssr: false });
 const SlackIntegration = dynamic(() => import('../../components/Admin/SlackIntegration/SlackIntegration'), { ssr: false });
 const LegacySlackIntegration = dynamic(() => import('../../components/Admin/LegacySlackIntegration/LegacySlackIntegration'), { ssr: false });
 const LegacySlackIntegration = dynamic(() => import('../../components/Admin/LegacySlackIntegration/LegacySlackIntegration'), { ssr: false });
 const UserManagement = dynamic(() => import('../../components/Admin/UserManagement'), { ssr: false });
 const UserManagement = dynamic(() => import('../../components/Admin/UserManagement'), { ssr: false });
@@ -117,14 +118,14 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
     },
     },
     security: {
     security: {
       title: t('security_settings.security_settings'),
       title: t('security_settings.security_settings'),
-      component: <SecurityManagementContents />,
+      component: <SecurityManagement />,
     },
     },
     markdown: {
     markdown: {
       title: t('markdown_settings.markdown_settings'),
       title: t('markdown_settings.markdown_settings'),
       component: <MarkDownSettingContents />,
       component: <MarkDownSettingContents />,
     },
     },
     customize: {
     customize: {
-      title: t('customize_setting.customize_setting'),
+      title: t('customize_settings.customize_settings'),
       component: <CustomizeSettingContents />,
       component: <CustomizeSettingContents />,
     },
     },
     importer: {
     importer: {
@@ -140,8 +141,10 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
       component: <NotificationSetting />,
       component: <NotificationSetting />,
     },
     },
     'global-notification': {
     'global-notification': {
-      title: '',
-      component: <>global-notification</>,
+      new: {
+        title: t('external_notification.external_notification'),
+        component: <ManageGlobalNotification />,
+      },
     },
     },
     'slack-integration': {
     'slack-integration': {
       title: t('slack_integration.slack_integration'),
       title: t('slack_integration.slack_integration'),
@@ -155,7 +158,7 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
       title: t('user_management.user_management'),
       title: t('user_management.user_management'),
       component: <UserManagement />,
       component: <UserManagement />,
       'external-accounts': {
       'external-accounts': {
-        title: t('external_account_management'),
+        title: t('user_management.external_account'),
         component: <ManageExternalAccount />,
         component: <ManageExternalAccount />,
       },
       },
     },
     },

+ 4 - 3
packages/app/src/server/service/export.js

@@ -1,5 +1,5 @@
-import loggerFactory from '~/utils/logger';
 import { toArrayIfNot } from '~/utils/array-utils';
 import { toArrayIfNot } from '~/utils/array-utils';
+import loggerFactory from '~/utils/logger';
 
 
 import ConfigLoader from './config-loader';
 import ConfigLoader from './config-loader';
 
 
@@ -7,10 +7,11 @@ const logger = loggerFactory('growi:services:ExportService'); // eslint-disable-
 
 
 const fs = require('fs');
 const fs = require('fs');
 const path = require('path');
 const path = require('path');
-const mongoose = require('mongoose');
 const { Transform } = require('stream');
 const { Transform } = require('stream');
-const streamToPromise = require('stream-to-promise');
+
 const archiver = require('archiver');
 const archiver = require('archiver');
+const mongoose = require('mongoose');
+const streamToPromise = require('stream-to-promise');
 
 
 const CollectionProgressingStatus = require('../models/vo/collection-progressing-status');
 const CollectionProgressingStatus = require('../models/vo/collection-progressing-status');
 
 

+ 17 - 0
packages/app/src/services/renderer/rehype-plugins/add-line-number-attribute.ts

@@ -0,0 +1,17 @@
+import { Element } from 'hast-util-select';
+import { Plugin } from 'unified';
+import { visit } from 'unist-util-visit';
+
+const REGEXP_TARGET_TAGNAMES = new RegExp(/h1|h2|h3|h4|h5|h6|p|img|pre|blockquote|hr|ol|ul/);
+
+export const addLineNumberAttribute: Plugin = () => {
+  return (tree) => {
+    visit(tree, 'element', (node: Element) => {
+      if (REGEXP_TARGET_TAGNAMES.test(node.tagName as string)) {
+        if (node.properties != null) {
+          node.properties['data-line'] = node.position?.start.line;
+        }
+      }
+    });
+  };
+};

+ 39 - 6
packages/app/src/services/renderer/renderer.tsx

@@ -28,6 +28,7 @@ import { RendererConfig } from '~/interfaces/services/renderer';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { addClass } from './rehype-plugins/add-class';
 import { addClass } from './rehype-plugins/add-class';
+import { addLineNumberAttribute } from './rehype-plugins/add-line-number-attribute';
 import { relativeLinks } from './rehype-plugins/relative-links';
 import { relativeLinks } from './rehype-plugins/relative-links';
 import { relativeLinksByPukiwikiLikeLinker } from './rehype-plugins/relative-links-by-pukiwiki-like-linker';
 import { relativeLinksByPukiwikiLikeLinker } from './rehype-plugins/relative-links-by-pukiwiki-like-linker';
 import { pukiwikiLikeLinker } from './remark-plugins/pukiwiki-like-linker';
 import { pukiwikiLikeLinker } from './remark-plugins/pukiwiki-like-linker';
@@ -286,7 +287,6 @@ const generateCommonOptions = (pagePath: string|undefined, config: RendererConfi
       growiPlugin,
       growiPlugin,
     ],
     ],
     rehypePlugins: [
     rehypePlugins: [
-      slug,
       [relativeLinksByPukiwikiLikeLinker, { pagePath }],
       [relativeLinksByPukiwikiLikeLinker, { pagePath }],
       [relativeLinks, { pagePath }],
       [relativeLinks, { pagePath }],
       raw,
       raw,
@@ -323,6 +323,7 @@ export const generateViewOptions = (
 
 
   // add rehype plugins
   // add rehype plugins
   rehypePlugins.push(
   rehypePlugins.push(
+    slug,
     katex,
     katex,
     [toc, {
     [toc, {
       nav: false,
       nav: false,
@@ -409,10 +410,7 @@ export const generateTocOptions = (config: RendererConfig, tocNode: HtmlElementN
   return options;
   return options;
 };
 };
 
 
-export const generatePreviewOptions = (config: RendererConfig): RendererOptions => {
-  const options = generateCommonOptions(undefined, config);
-  const { rehypePlugins } = options;
-
+export const generatePreviewOptions = (pagePath: string, config: RendererConfig): RendererOptions => {
   // // Add configurers for preview
   // // Add configurers for preview
   // renderer.addConfigurers([
   // renderer.addConfigurers([
   //   new FooternoteConfigurer(),
   //   new FooternoteConfigurer(),
@@ -423,11 +421,46 @@ export const generatePreviewOptions = (config: RendererConfig): RendererOptions
   // renderer.setMarkdownSettings({ breaks: rendererSettings?.isEnabledLinebreaks });
   // renderer.setMarkdownSettings({ breaks: rendererSettings?.isEnabledLinebreaks });
   // renderer.configure();
   // renderer.configure();
 
 
+  const options = generateCommonOptions(pagePath, config);
+
+  const { remarkPlugins, rehypePlugins, components } = options;
+
+  // add remark plugins
+  remarkPlugins.push(
+    emoji,
+    math,
+    lsxGrowiPlugin.remarkPlugin,
+  );
+  if (config.isEnabledLinebreaks) {
+    remarkPlugins.push(breaks);
+  }
+
   // add rehype plugins
   // add rehype plugins
   rehypePlugins.push(
   rehypePlugins.push(
-    [sanitize, commonSanitizeOption],
+    katex,
+    [lsxGrowiPlugin.rehypePlugin, { pagePath }],
+    addLineNumberAttribute,
+    // [autoLinkHeadings, {
+    //   behavior: 'append',
+    // }]
   );
   );
 
 
+  const sanitizeOption = deepmerge(
+    commonSanitizeOption,
+    lsxGrowiPlugin.sanitizeOption,
+    {
+      attributes: {
+        '*': ['data-line'],
+      },
+    },
+  );
+  rehypePlugins.push([sanitize, sanitizeOption]);
+
+  // add components
+  if (components != null) {
+    components.lsx = props => <Lsx {...props} />;
+  }
+
   verifySanitizePlugin(options);
   verifySanitizePlugin(options);
   return options;
   return options;
 };
 };

+ 15 - 2
packages/app/src/stores/renderer.tsx

@@ -77,9 +77,22 @@ export const useTocOptions = (): SWRResponse<RendererOptions, Error> => {
 };
 };
 
 
 export const usePreviewOptions = (): SWRResponse<RendererOptions, Error> => {
 export const usePreviewOptions = (): SWRResponse<RendererOptions, Error> => {
-  const key = 'previewOptions';
+  const { data: currentPagePath } = useCurrentPagePath();
+  const { data: rendererConfig } = useRendererConfig();
+
+  const isAllDataValid = currentPagePath != null && rendererConfig != null;
+
+  const key = isAllDataValid
+    ? ['previewOptions', currentPagePath, rendererConfig]
+    : null;
 
 
-  return _useOptionsBase(key, generatePreviewOptions);
+  return useSWRImmutable<RendererOptions, Error>(
+    key,
+    (rendererId, currentPagePath, rendererConfig) => generatePreviewOptions(currentPagePath, rendererConfig),
+    {
+      fallbackData: isAllDataValid ? generatePreviewOptions(currentPagePath, rendererConfig) : undefined,
+    },
+  );
 };
 };
 
 
 export const useCommentPreviewOptions = (): SWRResponse<RendererOptions, Error> => {
 export const useCommentPreviewOptions = (): SWRResponse<RendererOptions, Error> => {

+ 2 - 5
packages/app/src/styles/_on-edit.scss

@@ -7,11 +7,6 @@
 body.on-edit {
 body.on-edit {
   overflow-y: hidden !important;
   overflow-y: hidden !important;
 
 
-  .container-fluid {
-    padding-right: 15px;
-    padding-left: 15px;
-  }
-
   .grw-navbar {
   .grw-navbar {
     position: fixed !important;
     position: fixed !important;
     width: 100vw;
     width: 100vw;
@@ -22,6 +17,8 @@ body.on-edit {
     height: $grw-subnav-height-on-edit;
     height: $grw-subnav-height-on-edit;
     min-height: unset;
     min-height: unset;
     padding-top: 0;
     padding-top: 0;
+    padding-right: 15px;
+    padding-left: 15px;
 
 
     @include media-breakpoint-up(lg) {
     @include media-breakpoint-up(lg) {
       height: $grw-subnav-height-lg-on-edit;
       height: $grw-subnav-height-lg-on-edit;

+ 0 - 3
packages/app/src/styles/bootstrap/_override.scss

@@ -12,9 +12,6 @@ body {
 .container-lg,
 .container-lg,
 .container-xl,
 .container-xl,
 .container-fluid {
 .container-fluid {
-  // default: 15px
-  // padding-right: 15px;
-  // padding-left: 15px;
   @include media-breakpoint-down(xs) {
   @include media-breakpoint-down(xs) {
     padding-right: 10px;
     padding-right: 10px;
     padding-left: 10px;
     padding-left: 10px;

+ 1 - 0
packages/app/src/styles/theme/_apply-colors.scss

@@ -37,6 +37,7 @@ $nav-tabs-link-hover-border-color: $bordercolor-nav-tabs-hover;
 $nav-tabs-link-active-color: $color-nav-tabs-link-active;
 $nav-tabs-link-active-color: $color-nav-tabs-link-active;
 $nav-tabs-link-active-bg: $bgcolor-global;
 $nav-tabs-link-active-bg: $bgcolor-global;
 $nav-tabs-link-active-border-color: $bordercolor-nav-tabs-active;
 $nav-tabs-link-active-border-color: $bordercolor-nav-tabs-active;
+$theme-colors: map-merge($theme-colors, ( primary: $primary ));
 
 
 @import 'reboot-bootstrap-buttons';
 @import 'reboot-bootstrap-buttons';
 @import 'reboot-bootstrap-colors';
 @import 'reboot-bootstrap-colors';