Kaynağa Gözat

Merge branch 'master' into reactify/personal-settings

# Conflicts:
#	src/client/js/app.jsx
#	src/server/routes/apiv3/index.js
itizawa 6 yıl önce
ebeveyn
işleme
500a9dda87
100 değiştirilmiş dosya ile 2427 ekleme ve 1187 silme
  1. 1 1
      .github/workflows/build-rc.yml
  2. 2 2
      .github/workflows/build.yml
  3. 49 11
      .github/workflows/ci.yml
  4. 1 3
      .github/workflows/release.yml
  5. 25 2
      CHANGES.md
  6. 7 4
      README.md
  7. 2 2
      config/webpack.common.js
  8. 1 1
      package.json
  9. 298 0
      resource/locales/en-US/admin/admin.json
  10. 0 10
      resource/locales/en-US/admin/admin_top.json
  11. 0 38
      resource/locales/en-US/admin/app_setting.json
  12. 0 59
      resource/locales/en-US/admin/customize_setting.json
  13. 0 18
      resource/locales/en-US/admin/export_management.json
  14. 0 58
      resource/locales/en-US/admin/importer_management.json
  15. 0 35
      resource/locales/en-US/admin/markdown_setting.json
  16. 0 32
      resource/locales/en-US/admin/user_group_management.json
  17. 0 46
      resource/locales/en-US/admin/user_management.json
  18. 38 2
      resource/locales/en-US/translation.json
  19. 298 0
      resource/locales/ja/admin/admin.json
  20. 0 10
      resource/locales/ja/admin/admin_top.json
  21. 0 38
      resource/locales/ja/admin/app_setting.json
  22. 0 59
      resource/locales/ja/admin/customize_setting.json
  23. 0 18
      resource/locales/ja/admin/export_management.json
  24. 0 58
      resource/locales/ja/admin/importer_management.json
  25. 0 35
      resource/locales/ja/admin/markdown_setting.json
  26. 0 32
      resource/locales/ja/admin/user_group_management.json
  27. 0 46
      resource/locales/ja/admin/user_management.json
  28. 38 2
      resource/locales/ja/translation.json
  29. 99 0
      src/client/js/admin.jsx
  30. 11 139
      src/client/js/app.jsx
  31. 44 0
      src/client/js/bootstrap.jsx
  32. 4 4
      src/client/js/components/Admin/AdminHome/AdminHome.jsx
  33. 3 3
      src/client/js/components/Admin/AdminHome/InstalledPluginTable.jsx
  34. 19 19
      src/client/js/components/Admin/App/AppSetting.jsx
  35. 3 3
      src/client/js/components/Admin/App/AppSettingsPage.jsx
  36. 14 14
      src/client/js/components/Admin/App/AwsSetting.jsx
  37. 12 12
      src/client/js/components/Admin/App/MailSetting.jsx
  38. 3 3
      src/client/js/components/Admin/App/PluginSetting.jsx
  39. 8 7
      src/client/js/components/Admin/App/SiteUrlSetting.jsx
  40. 59 0
      src/client/js/components/Admin/Common/AdminNavigation.jsx
  41. 1 0
      src/client/js/components/Admin/CustomCssEditor.jsx
  42. 1 0
      src/client/js/components/Admin/CustomHeaderEditor.jsx
  43. 1 0
      src/client/js/components/Admin/CustomScriptEditor.jsx
  44. 13 6
      src/client/js/components/Admin/Customize/Customize.jsx
  45. 11 15
      src/client/js/components/Admin/Customize/CustomizeBehaviorSetting.jsx
  46. 6 21
      src/client/js/components/Admin/Customize/CustomizeCssSetting.jsx
  47. 16 21
      src/client/js/components/Admin/Customize/CustomizeFunctionSetting.jsx
  48. 5 20
      src/client/js/components/Admin/Customize/CustomizeHeaderSetting.jsx
  49. 25 29
      src/client/js/components/Admin/Customize/CustomizeHighlightSetting.jsx
  50. 13 13
      src/client/js/components/Admin/Customize/CustomizeLayoutOptions.jsx
  51. 3 9
      src/client/js/components/Admin/Customize/CustomizeLayoutSetting.jsx
  52. 6 21
      src/client/js/components/Admin/Customize/CustomizeScriptSetting.jsx
  53. 4 9
      src/client/js/components/Admin/Customize/CustomizeTitle.jsx
  54. 15 3
      src/client/js/components/Admin/ElasticsearchManagement/ElasticsearchManagement.jsx
  55. 3 2
      src/client/js/components/Admin/ElasticsearchManagement/ReconnectControls.jsx
  56. 6 2
      src/client/js/components/Admin/ElasticsearchManagement/StatusTable.jsx
  57. 4 6
      src/client/js/components/Admin/ExportArchiveData/ArchiveFilesTable.jsx
  58. 3 3
      src/client/js/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.jsx
  59. 6 6
      src/client/js/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx
  60. 3 3
      src/client/js/components/Admin/ExportArchiveDataPage.jsx
  61. 2 2
      src/client/js/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx
  62. 10 10
      src/client/js/components/Admin/ImportData/GrowiArchive/ImportForm.jsx
  63. 2 2
      src/client/js/components/Admin/ImportData/GrowiArchive/UploadForm.jsx
  64. 1 1
      src/client/js/components/Admin/ImportData/GrowiArchiveSection.jsx
  65. 13 13
      src/client/js/components/Admin/ImportDataPage.jsx
  66. 2 2
      src/client/js/components/Admin/ManageExternalAccount.jsx
  67. 5 5
      src/client/js/components/Admin/MarkdownSetting/LineBreakForm.jsx
  68. 6 6
      src/client/js/components/Admin/MarkdownSetting/MarkDownSetting.jsx
  69. 10 10
      src/client/js/components/Admin/MarkdownSetting/PresentationForm.jsx
  70. 4 4
      src/client/js/components/Admin/MarkdownSetting/WhiteListInput.jsx
  71. 8 8
      src/client/js/components/Admin/MarkdownSetting/XssForm.jsx
  72. 60 0
      src/client/js/components/Admin/Notification/GlobalNotification.jsx
  73. 175 0
      src/client/js/components/Admin/Notification/GlobalNotificationList.jsx
  74. 279 0
      src/client/js/components/Admin/Notification/ManageGlobalNotification.jsx
  75. 48 0
      src/client/js/components/Admin/Notification/NotificationDeleteModal.jsx
  76. 80 0
      src/client/js/components/Admin/Notification/NotificationSetting.jsx
  77. 184 0
      src/client/js/components/Admin/Notification/SlackAppConfiguration.jsx
  78. 36 0
      src/client/js/components/Admin/Notification/TriggerEventCheckBox.jsx
  79. 49 0
      src/client/js/components/Admin/Notification/UserNotificationRow.jsx
  80. 151 0
      src/client/js/components/Admin/Notification/UserTriggerNotification.jsx
  81. 5 5
      src/client/js/components/Admin/UserGroup/UserGroupCreateForm.jsx
  82. 9 8
      src/client/js/components/Admin/UserGroup/UserGroupDeleteModal.jsx
  83. 4 4
      src/client/js/components/Admin/UserGroup/UserGroupPage.jsx
  84. 3 3
      src/client/js/components/Admin/UserGroup/UserGroupTable.jsx
  85. 1 1
      src/client/js/components/Admin/UserGroupDetail/CheckBoxForSerchUserOption.jsx
  86. 1 1
      src/client/js/components/Admin/UserGroupDetail/RadioButtonForSerchUserOption.jsx
  87. 2 2
      src/client/js/components/Admin/UserGroupDetail/UserGroupDetailPage.jsx
  88. 12 9
      src/client/js/components/Admin/UserGroupDetail/UserGroupEditForm.jsx
  89. 6 6
      src/client/js/components/Admin/UserGroupDetail/UserGroupPageList.jsx
  90. 15 10
      src/client/js/components/Admin/UserGroupDetail/UserGroupUserFormByInput.jsx
  91. 17 17
      src/client/js/components/Admin/UserGroupDetail/UserGroupUserModal.jsx
  92. 10 10
      src/client/js/components/Admin/UserGroupDetail/UserGroupUserTable.jsx
  93. 1 1
      src/client/js/components/Admin/UserManagement.jsx
  94. 9 9
      src/client/js/components/Admin/Users/ExternalAccountTable.jsx
  95. 3 3
      src/client/js/components/Admin/Users/GiveAdminButton.jsx
  96. 1 1
      src/client/js/components/Admin/Users/InviteUserControl.jsx
  97. 8 8
      src/client/js/components/Admin/Users/PasswordResetModal.jsx
  98. 5 5
      src/client/js/components/Admin/Users/RemoveAdminButton.jsx
  99. 2 2
      src/client/js/components/Admin/Users/StatusActivateButton.jsx
  100. 4 4
      src/client/js/components/Admin/Users/StatusSuspendedButton.jsx

+ 1 - 1
.github/workflows/build-rc.yml

@@ -12,7 +12,7 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-    - uses: actions/checkout@v1
+    - uses: actions/checkout@v2
 
     - name: Cache/Restore yarn cache
       uses: actions/cache@v1

+ 2 - 2
.github/workflows/build.yml

@@ -17,7 +17,7 @@ jobs:
         flavor: [default, nocdn]
 
     steps:
-    - uses: actions/checkout@v1
+    - uses: actions/checkout@v2
 
     - name: Determine suffix
       run: |
@@ -72,7 +72,7 @@ jobs:
     needs: build
 
     steps:
-    - uses: actions/checkout@v1
+    - uses: actions/checkout@v2
 
     - name: Update Docker Hub Description
       uses: peter-evans/dockerhub-description@v2.1.0

+ 49 - 11
.github/workflows/ci.yml

@@ -16,7 +16,7 @@ jobs:
         node-version: [12.x]
 
     steps:
-    - uses: actions/checkout@v1
+    - uses: actions/checkout@v2
     - name: Use Node.js ${{ matrix.node-version }}
       uses: actions/setup-node@v1
       with:
@@ -70,7 +70,7 @@ jobs:
         node-version: [12.x]
 
     steps:
-    - uses: actions/checkout@v1
+    - uses: actions/checkout@v2
     - name: Use Node.js ${{ matrix.node-version }}
       uses: actions/setup-node@v1
       with:
@@ -130,7 +130,7 @@ jobs:
         node-version: [12.x]
 
     steps:
-    - uses: actions/checkout@v1
+    - uses: actions/checkout@v2
     - name: Use Node.js ${{ matrix.node-version }}
       uses: actions/setup-node@v1
       with:
@@ -140,7 +140,23 @@ jobs:
       uses: actions/cache@v1
       with:
         path: node_modules
-        key: ${{ runner.OS }}-node_modules-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
+        key: ${{ runner.OS }}-node_modules_dev-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
+    - name: Get Date
+      id: date
+      run: |
+        echo ::set-output name=YmdH::$(date '+%Y%m%d%H')
+        echo ::set-output name=Ymd::$(date '+%Y%m%d')
+        echo ::set-output name=Ym::$(date '+%Y%m')
+        echo ::set-output name=Y::$(date '+%Y')
+    - name: Cache/Restore node_modules/.cache/hard-source
+      uses: actions/cache@v1
+      with:
+        path: node_modules/.cache
+        key: ${{ runner.OS }}-hard_source_webpack_dev-${{ matrix.node-version }}-${{ steps.date.outputs.YmdH }}
+        restore-keys: |
+          ${{ runner.os }}-hard_source_webpack_dev-${{ matrix.node-version }}-${{ steps.date.outputs.Ymd }}
+          ${{ runner.os }}-hard_source_webpack_dev-${{ matrix.node-version }}-${{ steps.date.outputs.Ym }}
+          ${{ runner.os }}-hard_source_webpack_dev-${{ matrix.node-version }}-${{ steps.date.outputs.Y }}
     - name: Get yarn cache dir
       if: steps.cache-dependencies.outputs.cache-hit != 'true'
       id: cache-yarn
@@ -150,9 +166,9 @@ jobs:
       uses: actions/cache@v1
       with:
         path: ${{ steps.cache-yarn.outputs.dir }}
-        key: ${{ runner.os }}-yarn-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
+        key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
         restore-keys: |
-          ${{ runner.os }}-yarn-${{ matrix.node-version }}-
+          ${{ runner.os }}-yarn-
     - name: Install dependencies
       if: steps.cache-dependencies.outputs.cache-hit != 'true'
       run: |
@@ -184,11 +200,27 @@ jobs:
         node-version: [10.x, 12.x]
 
     steps:
-    - uses: actions/checkout@v1
+    - uses: actions/checkout@v2
     - name: Use Node.js ${{ matrix.node-version }}
       uses: actions/setup-node@v1
       with:
         node-version: ${{ matrix.node-version }}
+    - name: Get Date
+      id: date
+      run: |
+        echo ::set-output name=YmdH::$(date '+%Y%m%d%H')
+        echo ::set-output name=Ymd::$(date '+%Y%m%d')
+        echo ::set-output name=Ym::$(date '+%Y%m')
+        echo ::set-output name=Y::$(date '+%Y')
+    - name: Cache/Restore node_modules
+      uses: actions/cache@v1
+      with:
+        path: node_modules
+        key: ${{ runner.OS }}-node_modules_prod-${{ matrix.node-version }}-${{ steps.date.outputs.YmdH }}
+        restore-keys: |
+          ${{ runner.os }}-node_modules_prod-${{ matrix.node-version }}-${{ steps.date.outputs.Ymd }}
+          ${{ runner.os }}-node_modules_prod-${{ matrix.node-version }}-${{ steps.date.outputs.Ym }}
+          ${{ runner.os }}-node_modules_prod-${{ matrix.node-version }}-${{ steps.date.outputs.Y }}
     - name: Get yarn cache dir
       id: cache-yarn
       run: echo "::set-output name=dir::$(yarn cache dir)"
@@ -196,9 +228,9 @@ jobs:
       uses: actions/cache@v1
       with:
         path: ${{ steps.cache-yarn.outputs.dir }}
-        key: ${{ runner.os }}-yarn-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
+        key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
         restore-keys: |
-          ${{ runner.os }}-yarn-${{ matrix.node-version }}-
+          ${{ runner.os }}-yarn-
     - name: Install dependencies
       run: |
         yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs
@@ -208,9 +240,9 @@ jobs:
         echo -n "node " && node -v
         echo -n "npm " && npm -v
         yarn list --depth=0
-    - name: yarn build:prod
+    - name: yarn build:prod:analyze
       run: |
-        yarn build:prod
+        yarn build:prod:analyze
     - name: yarn install --production
       run: |
         yarn install --production
@@ -229,6 +261,12 @@ jobs:
       env:
         MONGO_URI: mongodb://localhost:27017/growi
 
+    - name: Upload report as artifact
+      uses: actions/upload-artifact@v1
+      with:
+        name: Report
+        path: report
+
     - name: Slack Notification
       uses: homoluctus/slatify@master
       if: failure()

+ 1 - 3
.github/workflows/release.yml

@@ -11,9 +11,7 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-    - uses: actions/checkout@v1
-      with:
-        fetch-depth: 1
+    - uses: actions/checkout@v2
 
     - name: Init Git
       run: |

+ 25 - 2
CHANGES.md

@@ -1,15 +1,38 @@
 # CHANGES
 
-## v3.6.5-RC
+## v3.6.8-RC
+
+* Improvement: Optimize markdown rendering
+
+## v3.6.7
+
+* Feature: Anchor link for comments
+* Improvement: Show error toastr when saving page is failed because of empty document
+* Fix: Admin Customise couldn't restore stored config value
+    * Introduced by 3.6.2
+* Fix: Admin Customise missed preview functions
+    * Introduced by 3.6.2
+* Fix: AWS doesn't work
+    * Introduced by 3.6.4
+* Fix: Ensure not to get unrelated indices information in Elasticsearch Management
+    * Introduced by 3.6.6
+* Support: Optimize bundles
+* Support: Optimize build-prod job with caching node_modules/.cache
+
+## v3.6.6
 
 * Feature: Reconnect to Elasticsearch from Full Text Search Management
 * Feature: Normalize indices of Elasticsearch from Full Text Search Management
 * Improvement: Add 'spring' theme
-* Impromvement: Add `checkMiddlewaresStrictly` query option to Healthcheck API
+* Improvement: Reactify admin pages (Notification)
+* Impromvement: Add `checkMiddlewaresStrictly` option to Healthcheck API
 * Improvement: Accessibility for History component under dark themes
+* Fix: Warning on client console when developing /admin/app
 * Support: Upgrade libs
     * react-bootstrap-typeahead
 
+## v3.6.5 (Missing number)
+
 ## v3.6.4
 
 * Feature: Alert for stale page

+ 7 - 4
README.md

@@ -28,6 +28,9 @@ GROWI
 | :-: |
 |![sample image](https://user-images.githubusercontent.com/42988650/70600974-6b29cc80-1c34-11ea-94ef-33c39c6a00dc.gif)|
 
+Table Of Contents
+---------------
+
 - [Features](#features)
 - [Quick Start for Production](#quick-start-for-production)
     - [Heroku](#heroku)
@@ -75,13 +78,13 @@ Quick Start for Production
 
 
 Configuration
-============
+------------
 
 See [GROWI Docs: Admin Guide](https://docs.growi.org/en/admin-guide/) ([en](https://docs.growi.org/en/admin-guide/)/[ja](https://docs.growi.org/ja/admin-guide/)).
 
-## Environment Variables
+### Environment Variables
 
-- [GROWI Docs: Environment Variables](https://docs.growi.org/en/admin-guide/admin-cookbook/env-vars.html)
+See [GROWI Docs: Environment Variables](https://docs.growi.org/en/admin-guide/admin-cookbook/env-vars.html) ([en](https://docs.growi.org/en/admin-guide/admin-cookbook/env-vars.html)/[ja](https://docs.growi.org/ja/admin-guide/admin-cookbook/env-vars.html)).
 
 
 Development
@@ -89,7 +92,7 @@ Development
 
 ## Dependencies
 
-- Node.js v10.x (DON'T USE 11.x)
+- Node.js v12.x (DON'T USE 13.x)
 - npm 6.x
 - yarn
 - MongoDB 3.x

+ 2 - 2
config/webpack.common.js

@@ -21,9 +21,9 @@ module.exports = (options) => {
     mode: options.mode,
     entry: Object.assign({
       'js/app':                       './src/client/js/app',
+      'js/admin':                     './src/client/js/admin',
       'js/installer':                 './src/client/js/installer',
       'js/legacy':                    './src/client/js/legacy/crowi',
-      'js/legacy-admin':              './src/client/js/legacy/crowi-admin',
       'js/legacy-presentation':       './src/client/js/legacy/crowi-presentation',
       'js/plugin':                    './src/client/js/plugin',
       'js/ie11-polyfill':             './src/client/js/ie11-polyfill',
@@ -158,7 +158,7 @@ module.exports = (options) => {
             test: /\.(sc|sa|c)ss$/,
             chunks: (chunk) => {
               // ignore patterns
-              return chunk.name != null && !chunk.name.match(/style-|theme-|legacy-admin|legacy-presentation/);
+              return chunk.name != null && !chunk.name.match(/style-|theme-|legacy-presentation/);
             },
             name: 'styles/style-commons',
             minSize: 1,

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.6.5-RC",
+  "version": "3.6.8-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",

+ 298 - 0
resource/locales/en-US/admin/admin.json

@@ -0,0 +1,298 @@
+{
+  "admin_top": {
+    "management_wiki": "Management Wiki",
+    "system_information": "System Information",
+    "wiki_administrator": "Only Wiki administrator can access this page",
+    "assign_administrator": "You can assign administrator from Assign administrator button in the User management page",
+    "list_of_installed_plugins": "List of installed plugins",
+    "package_name": "Package name",
+    "specified_version": "Specified version",
+    "installed_version": "Installed version"
+  },
+  "app_setting": {
+    "site_name": "Site name",
+    "sitename_change": "You can change Site Name which is used for header and HTML title.",
+    "header_content": "The contents entered here will be shown in the header etc.",
+    "site_url_desc": "This is for the site URL setting.",
+    "site_url_warn": "Some features don't work because the site URL is not set.",
+    "siteurl_help": "Site full URL beginning from <code>http://</code> or <code>https://</code>.",
+    "confidential_name": "Confidential name",
+    "confidential_example": "ex): internal use only",
+    "default_language": "Default Language for new users",
+    "file_uploading": "File Uploading",
+    "enable_files_except_image": "Enable file upload other than image files.",
+    "attach_enable": "You can attach files other than image files if you enable this option.",
+    "update": "Update",
+    "mail_settings": "Mail settings",
+    "smtp_used": "If you have SMTP settings, it will be used.",
+    "smtp_but_aws": "If you do not have SMTP settings but AWS settings,  e-mails will be sent by SES.",
+    "neihter_of": "If you do not of neither of these, e-mails will not be sent.",
+    "from_e-mail_address": "From e-mail address",
+    "smtp_settings": "SMTP settings",
+    "host": "Host",
+    "port": "Port",
+    "user": "User",
+    "aws_settings": "AWS settings",
+    "aws_access": "This is for AWS settings. If you complete AWS settings, file upload function, profile picture function etc will be enabled.",
+    "no_smtp_setting": "If you do not have SMTP settings, e-mails will be sent via SES. You need to verify from e-mail address and production settings.",
+    "change_setting": "Caution:if you change this setting not completed, you will not be able to access files you have uploaded so far.",
+    "region": "Region",
+    "bucket_name": "Bucket name",
+    "custom_endpoint": "Custom endpoint",
+    "custom_endpoint_change": "Input the URL of the endpoint of an object storage service like MinIO that has a S3-compatible API.  Amazon S3 is used if empty.",
+    "plugin_settings": "Plugin settings",
+    "enable_plugin_loading": "Enable plugin loading",
+    "load_plugins": "Load_plugins",
+    "enable": "Enable",
+    "disable": "Disable",
+    "use_env_var_if_empty": "If the value in the database is empty, the value of the environment variable <cod>{{variable}}</code> is used."
+  },
+  "markdown_setting": {
+    "lineBreak_header": "Line Break Setting",
+    "lineBreak_desc": "You can change line break settings.",
+    "lineBreak_options": {
+      "enable_lineBreak": "Enable Line Break",
+      "enable_lineBreak_desc": "Treat line break in the text page as<code>&lt;br&gt;</code>in HTML",
+      "enable_lineBreak_for_comment": "Enable Line Break in comment",
+      "enable_lineBreak_for_comment_desc": "Treat line break in comment as<code>&lt;br&gt;</code>in HTML"
+    },
+    "presentation_header": "Presentation Setting",
+    "presentation_desc": "You can change presentation settings.",
+    "presentation_options": {
+      "page_break_setting": "Page break Setting",
+      "preset_one_separator": "Preset 1",
+      "preset_one_separator_desc": "3 Blank lines",
+      "preset_one_separator_value": "\\n\\n\\n",
+      "preset_two_separator": "Preset 2",
+      "preset_two_separator_desc": "5 Hyphens",
+      "preset_two_separator_value": "-----",
+      "custom_separator": "Custom",
+      "custom_separator_desc": "Regular Expression"
+    },
+    "xss_header": "Prevent XSS(Cross Site Scripting) Setting",
+    "xss_desc": "You can change the handling of HTML tags in markdown text.",
+    "xss_options": {
+      "enable_xss_prevention": "Enable XSS Prevention",
+      "ignore_all_tags": "Ignore All Tags",
+      "ignore_all_tags_desc": "Stripe all HTML tags and attributes",
+      "recommended_setting": "Recommended Setting",
+      "custom_whitelist": "Custom Whitelist",
+      "tag_names": "Tag names",
+      "tag_attributes": "Tag attributes",
+      "import_recommended": "Import recommended {{target}}"
+    }
+  },
+  "customize_setting": {
+    "recommended": "Recommended",
+    "layout": "Layout",
+    "theme": "Theme",
+    "layout_desc": {
+      "growi_title": "Simple and Clear",
+      "growi_text1": "Full screen layout and thin margins/paddings",
+      "growi_text2": "Show and post comments at the bottom of the page",
+      "growi_text3": "Affix Table-of-contents",
+      "kibela_title": "Easy Viewing Structure",
+      "kibela_text1": "Center aligned contents",
+      "kibela_text2": "Show and post comments at the bottom of the page",
+      "kibela_text3": "Affix Table-of-contents",
+      "crowi_title": "Separated Functions",
+      "crowi_text1": "Collapsible Sidebar",
+      "crowi_text2": "Show and post comments in Sidebar",
+      "crowi_text3": "Collapsible Table-of-contents"
+    },
+    "behavior": "Behavior",
+    "behavior_desc": {
+      "growi_text1": "Both of <code>/page</code> and <code>/page/</code> shows the same page。",
+      "growi_text2": "<code>/nonexistent_page</code> shows editing form",
+      "growi_text3": "All pages shows the list of sub pages <b>if using GROWI Enhanced Layout</b>",
+      "crowi_text1": "<code>/page</code> shows the page",
+      "crowi_text2": "<code>/page/</code> shows the list of sub pages",
+      "crowi_text3": "If portal is applied to <code>/page/</code> , the portal and the list of sub pages are shown",
+      "crowi_text4": "<code>/nonexistent_page</code> shows editing form<",
+      "crowi_text5": "<code>/nonexistent_page/</code> the list of sub pages"
+    },
+    "function": "Function",
+    "function_desc": "You can choose Valid/Invalid of the function",
+    "function_options": {
+      "timeline": "Timeline function",
+      "timeline_desc1": "You can show the timeline of the subpages.",
+      "timeline_desc2": "If there are many subpages, performance decreases while page loading.",
+      "timeline_desc3": "You can speed up list page display by invalidating.",
+      "tab_switch": "Save tab-switching in the browser",
+      "tab_switch_desc1": "Save edit tab and history tab switching in the browser and make it object for forward/back command of the browser.",
+      "tab_switch_desc2": "By invalidating, you can make page transition as the only object for forward/back command of the browser.",
+      "attach_title_header": "Add h1 section when create new page automatically",
+      "attach_title_header_desc": "Add page path to the first line as h1 section when create new page",
+      "recent_created__n_draft_num_desc": "Number of Recently Created Pages & Drafts Displayed",
+      "recently_created_n_draft_num_desc": "Number of recently created pages and drafts displayed on user page",
+      "stale_notification": "Display Notification on Stale Pages",
+      "stale_notification_desc": "Displays the notification to pages more than 1 year since the last update."
+    },
+    "code_highlight": "Code Highlight",
+    "nocdn_desc": "This function is disabled when the environment variable <code>NO_CDN=true</code>.<br>Github style has been forcibly applied.",
+    "custom_title": "Custom Title",
+    "custom_title_detail": "You can customize <code>&lt;title&gt;</code> tag.<br><code>&#123;&#123;sitename&#125;&#125;</code> will be automatically replaced with the app name, and <code>&#123;&#123;page&#125;&#125;</code> will be replaced with the page name/path.",
+    "custom_header": "Custom HTML Header",
+    "custom_header_detail": "You can customize HTML header that applies all pages. Your custom script will be inserted in <code>&lt;header&gt;</code> but above other <code>&lt;script&gt;</code> tags.<br>Relaod page to see changes.",
+    "custom_css": "Custom CSS",
+    "write_css": "You can write CSS that is applied to whole system.",
+    "ctrl_space": "Ctrl+Space to Autocomplete",
+    "custom_script": "Custom script",
+    "write_java": "You can write Javascript that is applied to whole system.",
+    "reflect_change": "You need to reload the page to reflect the change."
+  },
+  "importer_management": {
+    "beta_warning": "This function is Beta.",
+    "import_from": "Import from {{from}}",
+    "import_growi_archive": "Import GROWI Archive",
+    "growi_settings": {
+      "overwrite_documents": "Imported documents will overwrite existing documents",
+      "growi_archive_file": "GROWI Archive File",
+      "uploaded_data": "Uploaded Data",
+      "extracted_file": "Extracted File",
+      "collection": "Collection",
+      "upload": "Upload",
+      "discard": "Discard Uploaded Data",
+      "errors": {
+        "at_least_one": "Select one or more collections.",
+        "page_and_revision": "'Pages' and 'Revisions' must be imported both.",
+        "depends": "'{{target}}' must be selected when '{{condition}}' is selected."
+      },
+      "configuration": {
+        "pages": {
+          "overwrite_author": {
+            "label": "Overwrite page's author with the current user",
+            "desc": "Recommended <span class=\"text-danger\">NOT</span> to check this when users will also be restored."
+          },
+          "set_public_to_page": {
+            "label": "Set 'Public' to the pages that is '{{from}}'",
+            "desc": "Make sure that this configuration makes all <b>'{{from}}'</b> pages readable from <span class=\"text-danger\">ANY users</span>."
+          },
+          "initialize_meta_datas": {
+            "label": "Initialize page's like, read users and comment count",
+            "desc": "Recommended <span class=\"text-danger\">NOT</span> to check this when users will also be restored."
+          },
+          "initialize_hackmd_related_datas": {
+            "label": "Initialize HackMD related data",
+            "desc": "Recommended to check this unless there is important drafts on HackMD."
+          }
+        },
+        "revisions": {
+          "overwrite_author": {
+            "label": "Overwrite revision's author with the current user",
+            "desc": "Recommended <span class=\"text-danger\">NOT</span> to check this when users will also be restored."
+          }
+        }
+      }
+    },
+    "esa_settings": {
+      "team_name": "Team name",
+      "access_token": "Access token",
+      "test_connection": "Test connection to esa"
+    },
+    "qiita_settings": {
+      "team_name": "Team name",
+      "access_token": "Access token",
+      "test_connection": "Test connection to qiita:team"
+    },
+    "import": "Import",
+    "page_skip": "Pages with a name that already exists on GROWI are not imported",
+    "Directory_hierarchy_tag": "Directory Hierarchy Tag"
+  },
+  "export_management": {
+    "exporting_collection_list": "Exporting Collection List",
+    "exported_data_list": "Exported Archive Data List",
+    "export_collections": "Export Collections",
+    "check_all": "Check All",
+    "uncheck_all": "Uncheck All",
+    "desc_password_seed": "DO NOT FORGET to set current <code>PASSWORD_SEED</code> to your new GROWI system when restoring user data, or users will NOT be able to login with their password.<br><br><strong>HINT:</strong><br>The current <code>PASSWORD_SEED</code> will be stored in <code>meta.json</code> in exported ZIP.",
+    "create_new_archive_data": "Create New Archive Data",
+    "export": "Export",
+    "cancel": "Cancel",
+    "file": "File",
+    "growi_version": "Growi Version",
+    "collections": "Collections",
+    "exported_at": "Exported At",
+    "export_menu": "Export Menu",
+    "download": "Download",
+    "delete": "Delete"
+  },
+  "user_management": {
+    "invite_users": "Invite New Users",
+    "invite_modal": {
+      "emails": "Emails",
+      "invite_thru_email": "Send Invitation Email",
+      "valid_email": "Valid email address is required",
+      "temporary_password": "The created user has a temporary password",
+      "send_new_password": "Please send the new password to the user.",
+      "send_temporary_password": "Be sure to copy the temporary password ON THIS SCREEN and send it to the user.",
+      "existing_email": "The following emails already exist"
+    },
+    "user_table": {
+      "administrator": "Administrator",
+      "edit_menu": "Edit Menu",
+      "reset_password": "Reset Password",
+      "administrator_menu": "Administrator Menu",
+      "accept": "Accept",
+      "deactivate_account": "Deactivate Account",
+      "your_own": "You cannot deactivate your own account",
+      "remove_admin_access": "Remove Admin Access",
+      "cannot_remove": "You cannot remove yourself from administrator",
+      "give_admin_access": "Give Admin Access"
+    },
+    "reset_password": "Reset Password",
+    "reset_password_modal": {
+      "password_never_seen": "The temporary password can never be retrieved after this screen is closed.",
+      "password_reset_message": "Let the user know the new password below and strongly recommend to change another one immediately.",
+      "send_new_password": "Please send the new password to the user.",
+      "target_user": "Target User",
+      "new_password": "New Password"
+    },
+    "external_account": "External Account Management",
+    "external_account_list": "External Account List",
+    "invite": "Invite",
+    "invited": "User was invited",
+    "back_to_user_management": "Back to User Management",
+    "authentication_provider": "Authentication Provider",
+    "manage": "Manage",
+    "password_setting": "Password Setting",
+    "password_setting_help": "Is password set?",
+    "set": "Yes",
+    "unset": "No",
+    "related_username": "Related user's ",
+    "cannot_invite_maximum_users": "Can not invite more than the maximum number of users.",
+    "current_users": "Current users:"
+  },
+  "user_group_management": {
+    "create_group": "Create New Group",
+    "deny_create_group": "You can't create a new group with the current settings",
+    "group_name": "Group Name",
+    "group_example": "e.g. : Group1",
+    "add_modal": {
+      "add_user": "Add a User to the Created Group",
+      "search_option": "Search Option",
+      "enable_option": "Enable {{option}}",
+      "forward_match": "forword match",
+      "partial_match": "partial match",
+      "backward_match": "backward match"
+    },
+    "group_list": "Group List",
+    "back_to_list": "Go Back to Group List",
+    "basic_info": "Basic Info",
+    "user_list": "User List",
+    "created_group": "Group was created",
+    "is_loading_data": "fetch data...",
+    "no_pages": "There are no pages the group has view permission",
+    "remove_from_group": "Remove this user",
+    "delete_modal": {
+      "header": "Delete Group",
+      "desc": "Once deleted, the deleted group and its private pages cannot be retrieved",
+      "dropdown_desc": "Choose an action for private pages",
+      "select_group": "Select a group",
+      "no_groups": "No groups to select",
+      "publish_pages": "Publish All",
+      "delete_pages": "Delete All",
+      "transfer_pages": "Transfer to another group"
+    }
+  }
+}

+ 0 - 10
resource/locales/en-US/admin/admin_top.json

@@ -1,10 +0,0 @@
-{
-  "management_wiki": "Management Wiki",
-  "system_information": "System Information",
-  "wiki_administrator": "Only Wiki administrator can access this page",
-  "assign_administrator": "You can assign administrator from Assign administrator button in the User management page",
-  "list_of_installed_plugins": "List of installed plugins",
-  "package_name": "Package name",
-  "specified_version": "Specified version",
-  "installed_version": "Installed version"
-}

+ 0 - 38
resource/locales/en-US/admin/app_setting.json

@@ -1,38 +0,0 @@
-  {
-    "site_name": "Site name",
-    "sitename_change": "You can change Site Name which is used for header and HTML title.",
-    "header_content": "The contents entered here will be shown in the header etc.",
-    "site_url_desc": "This is for the site URL setting.",
-    "site_url_warn": "Some features don't work because the site URL is not set.",
-    "siteurl_help": "Site full URL beginning from <code>http://</code> or <code>https://</code>.",
-    "confidential_name": "Confidential name",
-    "confidential_example": "ex): internal use only",
-    "default_language": "Default Language for new users",
-    "file_uploading": "File Uploading",
-    "enable_files_except_image": "Enable file upload other than image files.",
-    "attach_enable": "You can attach files other than image files if you enable this option.",
-    "update": "Update",
-    "mail_settings": "Mail settings",
-    "smtp_used": "If you have SMTP settings, it will be used.",
-    "smtp_but_aws": "If you do not have SMTP settings but AWS settings,  e-mails will be sent by SES.",
-    "neihter_of": "If you do not of neither of these, e-mails will not be sent.",
-    "from_e-mail_address": "From e-mail address",
-    "smtp_settings": "SMTP settings",
-    "host": "Host",
-    "port": "Port",
-    "user": "User",
-    "aws_settings": "AWS settings",
-    "aws_access": "This is for AWS settings. If you complete AWS settings, file upload function, profile picture function etc will be enabled.",
-    "no_smtp_setting": "If you do not have SMTP settings, e-mails will be sent via SES. You need to verify from e-mail address and production settings.",
-    "change_setting": "Caution:if you change this setting not completed, you will not be able to access files you have uploaded so far.",
-    "region": "Region",
-    "bucket_name": "Bucket name",
-    "custom_endpoint": "Custom endpoint",
-    "custom_endpoint_change": "Input the URL of the endpoint of an object storage service like MinIO that has a S3-compatible API.  Amazon S3 is used if empty.",
-    "plugin_settings": "Plugin settings",
-    "enable_plugin_loading": "Enable plugin loading",
-    "load_plugins": "Load_plugins",
-    "enable": "Enable",
-    "disable": "Disable",
-    "use_env_var_if_empty": "If the value in the database is empty, the value of the environment variable <cod>{{variable}}</code> is used."
-  }

+ 0 - 59
resource/locales/en-US/admin/customize_setting.json

@@ -1,59 +0,0 @@
-{
-  "recommended": "Recommended",
-  "layout": "Layout",
-  "theme": "Theme",
-  "layout_desc": {
-    "growi_title": "Simple and Clear",
-    "growi_text1": "Full screen layout and thin margins/paddings",
-    "growi_text2": "Show and post comments at the bottom of the page",
-    "growi_text3": "Affix Table-of-contents",
-    "kibela_title": "Easy Viewing Structure",
-    "kibela_text1": "Center aligned contents",
-    "kibela_text2": "Show and post comments at the bottom of the page",
-    "kibela_text3": "Affix Table-of-contents",
-    "crowi_title": "Separated Functions",
-    "crowi_text1": "Collapsible Sidebar",
-    "crowi_text2": "Show and post comments in Sidebar",
-    "crowi_text3": "Collapsible Table-of-contents"
-  },
-  "behavior": "Behavior",
-  "behavior_desc": {
-    "growi_text1": "Both of <code>/page</code> and <code>/page/</code> shows the same page。",
-    "growi_text2": "<code>/nonexistent_page</code> shows editing form",
-    "growi_text3": "All pages shows the list of sub pages <b>if using GROWI Enhanced Layout</b>",
-    "crowi_text1": "<code>/page</code> shows the page",
-    "crowi_text2": "<code>/page/</code> shows the list of sub pages",
-    "crowi_text3": "If portal is applied to <code>/page/</code> , the portal and the list of sub pages are shown",
-    "crowi_text4": "<code>/nonexistent_page</code> shows editing form<",
-    "crowi_text5": "<code>/nonexistent_page/</code> the list of sub pages"
-  },
-  "function": "Function",
-  "function_desc": "You can choose Valid/Invalid of the function",
-  "function_options": {
-    "timeline": "Timeline function",
-    "timeline_desc1": "You can show the timeline of the subpages.",
-    "timeline_desc2": "If there are many subpages, performance decreases while page loading.",
-    "timeline_desc3": "You can speed up list page display by invalidating.",
-    "tab_switch": "Save tab-switching in the browser",
-    "tab_switch_desc1": "Save edit tab and history tab switching in the browser and make it object for forward/back command of the browser.",
-    "tab_switch_desc2": "By invalidating, you can make page transition as the only object for forward/back command of the browser.",
-    "attach_title_header": "Add h1 section when create new page automatically",
-    "attach_title_header_desc": "Add page path to the first line as h1 section when create new page",
-    "recent_created__n_draft_num_desc": "Number of Recently Created Pages & Drafts Displayed",
-    "recently_created_n_draft_num_desc": "Number of recently created pages and drafts displayed on user page",
-    "stale_notification": "Display Notification on Stale Pages",
-    "stale_notification_desc": "Displays the notification to pages more than 1 year since the last update."
-  },
-  "code_highlight": "Code Highlight",
-  "nocdn_desc": "This function is disabled when the environment variable <code>NO_CDN=true</code>.<br>Github style has been forcibly applied.",
-  "custom_title": "Custom Title",
-  "custom_title_detail": "You can customize <code>&lt;title&gt;</code> tag.<br><code>&#123;&#123;sitename&#125;&#125;</code> will be automatically replaced with the app name, and <code>&#123;&#123;page&#125;&#125;</code> will be replaced with the page name/path.",
-  "custom_header": "Custom HTML Header",
-  "custom_header_detail": "You can customize HTML header that applies all pages. Your custom script will be inserted in <code>&lt;header&gt;</code> but above other <code>&lt;script&gt;</code> tags.<br>Relaod page to see changes.",
-  "custom_css": "Custom CSS",
-  "write_css": "You can write CSS that is applied to whole system.",
-  "ctrl_space": "Ctrl+Space to Autocomplete",
-  "custom_script": "Custom script",
-  "write_java": "You can write Javascript that is applied to whole system.",
-  "reflect_change": "You need to reload the page to reflect the change."
-}

+ 0 - 18
resource/locales/en-US/admin/export_management.json

@@ -1,18 +0,0 @@
-{
-  "exporting_collection_list": "Exporting Collection List",
-  "exported_data_list": "Exported Archive Data List",
-  "export_collections": "Export Collections",
-  "check_all": "Check All",
-  "uncheck_all": "Uncheck All",
-  "desc_password_seed": "DO NOT FORGET to set current <code>PASSWORD_SEED</code> to your new GROWI system when restoring user data, or users will NOT be able to login with their password.<br><br><strong>HINT:</strong><br>The current <code>PASSWORD_SEED</code> will be stored in <code>meta.json</code> in exported ZIP.",
-  "create_new_archive_data": "Create New Archive Data",
-  "export": "Export",
-  "cancel": "Cancel",
-  "file": "File",
-  "growi_version": "Growi Version",
-  "collections": "Collections",
-  "exported_at": "Exported At",
-  "export_menu": "Export Menu",
-  "download": "Download",
-  "delete": "Delete"
-}

+ 0 - 58
resource/locales/en-US/admin/importer_management.json

@@ -1,58 +0,0 @@
-{
-  "beta_warning": "This function is Beta.",
-  "import_from": "Import from {{from}}",
-  "import_growi_archive": "Import GROWI Archive",
-  "growi_settings": {
-    "overwrite_documents": "Imported documents will overwrite existing documents",
-    "growi_archive_file": "GROWI Archive File",
-    "uploaded_data": "Uploaded Data",
-    "extracted_file": "Extracted File",
-    "collection": "Collection",
-    "upload": "Upload",
-    "discard": "Discard Uploaded Data",
-    "errors": {
-      "at_least_one": "Select one or more collections.",
-      "page_and_revision": "'Pages' and 'Revisions' must be imported both.",
-      "depends": "'{{target}}' must be selected when '{{condition}}' is selected."
-    },
-    "configuration": {
-      "pages": {
-        "overwrite_author": {
-          "label": "Overwrite page's author with the current user",
-          "desc": "Recommended <span class=\"text-danger\">NOT</span> to check this when users will also be restored."
-        },
-        "set_public_to_page": {
-          "label": "Set 'Public' to the pages that is '{{from}}'",
-          "desc": "Make sure that this configuration makes all <b>'{{from}}'</b> pages readable from <span class=\"text-danger\">ANY users</span>."
-        },
-        "initialize_meta_datas": {
-          "label": "Initialize page's like, read users and comment count",
-          "desc": "Recommended <span class=\"text-danger\">NOT</span> to check this when users will also be restored."
-        },
-        "initialize_hackmd_related_datas": {
-          "label": "Initialize HackMD related data",
-          "desc": "Recommended to check this unless there is important drafts on HackMD."
-        }
-      },
-      "revisions": {
-        "overwrite_author": {
-          "label": "Overwrite revision's author with the current user",
-          "desc": "Recommended <span class=\"text-danger\">NOT</span> to check this when users will also be restored."
-        }
-      }
-    }
-  },
-  "esa_settings": {
-    "team_name": "Team name",
-    "access_token": "Access token",
-    "test_connection": "Test connection to esa"
-  },
-  "qiita_settings": {
-    "team_name": "Team name",
-    "access_token": "Access token",
-    "test_connection": "Test connection to qiita:team"
-  },
-  "import": "Import",
-  "page_skip": "Pages with a name that already exists on GROWI are not imported",
-  "Directory_hierarchy_tag": "Directory Hierarchy Tag"
-}

+ 0 - 35
resource/locales/en-US/admin/markdown_setting.json

@@ -1,35 +0,0 @@
-{
-  "lineBreak_header": "Line Break Setting",
-  "lineBreak_desc": "You can change line break settings.",
-  "lineBreak_options": {
-    "enable_lineBreak": "Enable Line Break",
-    "enable_lineBreak_desc": "Treat line break in the text page as<code>&lt;br&gt;</code>in HTML",
-    "enable_lineBreak_for_comment": "Enable Line Break in comment",
-    "enable_lineBreak_for_comment_desc": "Treat line break in comment as<code>&lt;br&gt;</code>in HTML"
-  },
-  "presentation_header": "Presentation Setting",
-  "presentation_desc": "You can change presentation settings.",
-  "presentation_options": {
-    "page_break_setting": "Page break Setting",
-    "preset_one_separator": "Preset 1",
-    "preset_one_separator_desc": "3 Blank lines",
-    "preset_one_separator_value": "\\n\\n\\n",
-    "preset_two_separator": "Preset 2",
-    "preset_two_separator_desc": "5 Hyphens",
-    "preset_two_separator_value": "-----",
-    "custom_separator": "Custom",
-    "custom_separator_desc": "Regular Expression"
-  },
-  "xss_header": "Prevent XSS(Cross Site Scripting) Setting",
-  "xss_desc": "You can change the handling of HTML tags in markdown text.",
-  "xss_options": {
-    "enable_xss_prevention": "Enable XSS Prevention",
-    "ignore_all_tags": "Ignore All Tags",
-    "ignore_all_tags_desc": "Stripe all HTML tags and attributes",
-    "recommended_setting": "Recommended Setting",
-    "custom_whitelist": "Custom Whitelist",
-    "tag_names": "Tag names",
-    "tag_attributes": "Tag attributes",
-    "import_recommended": "Import recommended {{target}}"
-  }
-}

+ 0 - 32
resource/locales/en-US/admin/user_group_management.json

@@ -1,32 +0,0 @@
-{
-  "create_group": "Create New Group",
-  "deny_create_group": "You can't create a new group with the current settings",
-  "group_name": "Group Name",
-  "group_example": "e.g. : Group1",
-  "add_modal": {
-    "add_user": "Add a User to the Created Group",
-    "search_option": "Search Option",
-    "enable_option": "Enable {{option}}",
-    "forward_match": "forword match",
-    "partial_match": "partial match",
-    "backward_match": "backward match"
-  },
-  "group_list": "Group List",
-  "back_to_list": "Go Back to Group List",
-  "basic_info": "Basic Info",
-  "user_list": "User List",
-  "created_group": "Group was created",
-  "is_loading_data": "fetch data...",
-  "no_pages": "There are no pages the group has view permission",
-  "remove_from_group": "Remove this user",
-  "delete_modal": {
-    "header": "Delete Group",
-    "desc": "Once deleted, the deleted group and its private pages cannot be retrieved",
-    "dropdown_desc": "Choose an action for private pages",
-    "select_group": "Select a group",
-    "no_groups": "No groups to select",
-    "publish_pages": "Publish All",
-    "delete_pages": "Delete All",
-    "transfer_pages": "Transfer to another group"
-  }
-}

+ 0 - 46
resource/locales/en-US/admin/user_management.json

@@ -1,46 +0,0 @@
-{
-  "invite_users": "Invite New Users",
-  "invite_modal": {
-    "emails": "Emails",
-    "invite_thru_email": "Send Invitation Email",
-    "valid_email": "Valid email address is required",
-    "temporary_password": "The created user has a temporary password",
-    "send_new_password": "Please send the new password to the user.",
-    "send_temporary_password": "Be sure to copy the temporary password ON THIS SCREEN and send it to the user.",
-    "existing_email": "The following emails already exist"
-  },
-  "user_table": {
-    "administrator": "Administrator",
-    "edit_menu": "Edit Menu",
-    "reset_password": "Reset Password",
-    "administrator_menu": "Administrator Menu",
-    "accept": "Accept",
-    "deactivate_account": "Deactivate Account",
-    "your_own": "You cannot deactivate your own account",
-    "remove_admin_access": "Remove Admin Access",
-    "cannot_remove": "You cannot remove yourself from administrator",
-    "give_admin_access": "Give Admin Access"
-  },
-  "reset_password": "Reset Password",
-  "reset_password_modal": {
-    "password_never_seen": "The temporary password can never be retrieved after this screen is closed.",
-    "password_reset_message": "Let the user know the new password below and strongly recommend to change another one immediately.",
-    "send_new_password": "Please send the new password to the user.",
-    "target_user": "Target User",
-    "new_password": "New Password"
-  },
-  "external_account": "External Account Management",
-  "external_account_list": "External Account List",
-  "invite": "Invite",
-  "invited": "User was invited",
-  "back_to_user_management": "Back to User Management",
-  "authentication_provider": "Authentication Provider",
-  "manage": "Manage",
-  "password_setting": "Password Setting",
-  "password_setting_help": "Is password set?",
-  "set": "Yes",
-  "unset": "No",
-  "related_username": "Related user's ",
-  "cannot_invite_maximum_users": "Can not invite more than the maximum number of users.",
-  "current_users": "Current users:"
-}

+ 38 - 2
resource/locales/en-US/translation.json

@@ -305,6 +305,15 @@
       "Post": "Post"
     }
   },
+  "toaster": {
+    "update_successed": "Succeeded to update {{target}}",
+    "give_user_admin": "Succeeded to give {{username}} admin",
+    "remove_user_admin": "Succeeded to remove {{username}} admin ",
+    "activate_user_success": "Succeeded to activating {{username}}",
+    "deactivate_user_success": "Succeeded to deactivate {{username}}",
+    "remove_user_success": "Succeeded to removing {{username}} ",
+    "remove_external_user_success": "Succeeded to remove {{accountId}} "
+  },
   "template": {
     "modal_label": {
       "Create/Edit Template Page": "Create/Edit Template Page",
@@ -510,10 +519,30 @@
     }
   },
   "notification_setting": {
+    "slack_incoming_configuration": "Slack Incoming Webhooks Configuration",
+    "prioritize_webhook": "Prioritize Incoming Webhook than Slack App",
+    "prioritize_webhook_desc": "Check this option and GROWI use Incoming Webhooks even if Slack App settings are enabled.",
+    "slack_app_configuration": "Slack App Configuration",
+    "slack_app_configuration_desc": "This is the way that compatible with Crowi,<br /> but not recommended in GROWI because it is <strong>too complex</strong>.",
+    "use_instead":"Please use Slack Incoming Webhooks Configuration instead.",
+    "how_to": {
+      "header": "How to configure Incoming Webhooks?",
+      "workspace": "(At Workspace) Add a hook",
+      "workspace_desc1": "Go to <a href='https: //slack.com/services/new/incoming-webhook'>Incoming Webhooks Configuration page</a>.",
+      "workspace_desc2": "Choose the default channel to post.",
+      "workspace_desc3": "Add.",
+      "at_growi": "(At GROWI admin page) Set Webhook URL",
+      "at_growi_desc": "Input &rdquo;Webhook URL&rdquo; and submit on this page."
+    },
+    "user_trigger_notification_header": "Default Notification Settings for Patterns",
+    "pattern": "Pattern",
+    "channel": "Channel",
+    "pattern_desc": "Path name of wiki. Pattern expression with <code>*</code> can be used.",
+    "channel_desc": "Slack channel name. Without <code>#</code>.",
     "notification_list": "List of Notification Settings",
     "add_notification": "Add New",
     "trigger_path": "Trigger Path",
-    "trigger_path_help": "(expression with %s is supported)",
+    "trigger_path_help": "(expression with <code>*</code> is supported)",
     "trigger_events": "Trigger Events",
     "notify_to": "Notify To",
     "back_to_list": "Go back to list",
@@ -526,11 +555,18 @@
     "event_comment": "When someone \"COMMENTS\" on page",
     "email": {
       "ifttt_link": "Create a new IFTTT applet with Email trigger"
-    }
+    },
+    "updated_slackApp": "Succeeded to update Slack App Configuration setting",
+    "add_notification_pattern": "Add user trigger notification patterns",
+    "delete_notification_pattern": "Delete notification pattern",
+    "delete_notification_pattern_desc1": "Delete Path: {{path}}",
+    "delete_notification_pattern_desc2": "Once deleted, it cannot be recovered",
+    "toggle_notification": "Updated setting of {{path}}"
   },
   "full_text_search_management": {
     "elasticsearch_management": "Elasticsearch Management",
     "connection_status": "Connection Status",
+    "connection_status_label_unconfigured": "UNCONFIGURED",
     "connection_status_label_connected": "CONNECTED",
     "connection_status_label_disconnected": "DISCONNECTED",
     "indices_status": "Indices Status",

+ 298 - 0
resource/locales/ja/admin/admin.json

@@ -0,0 +1,298 @@
+{
+  "admin_top": {
+    "management_wiki": "Wiki管理",
+    "system_information": "システム情報",
+    "wiki_administrator": "この画面はWiki管理者のみがアクセスできる画面です。",
+    "assign_administrator": "「ユーザー管理」から「管理者にする」ボタンを使ってユーザーをWiki管理者に任命することができます。",
+    "list_of_installed_plugins": "インストールされているプラグイン一覧",
+    "package_name": "パッケージ名",
+    "specified_version": "指定バージョン",
+    "installed_version": "インストールされているバージョン"
+  },
+  "app_setting": {
+    "site_name": "サイト名",
+    "sitename_change": "ヘッダーや HTML タイトルに使用されるサイト名を変更できます。",
+    "header_content": "ここに入力した内容は、ヘッダー等に表示されます。",
+    "site_url_desc": "サイトURLを設定します。",
+    "site_url_warn": "サイトURLが設定されていないため、一部機能が動作しない状態になっています。",
+    "siteurl_help": "<code>http://</code> または <code>https://</code> から始まるサイトのURL",
+    "confidential_name": "コンフィデンシャル表示",
+    "confidential_example": "例: 社外秘",
+    "default_language": "新規ユーザーのデフォルト設定言語",
+    "file_uploading": "ファイルアップロード",
+    "enable_files_except_image": "画像以外のファイルアップロードを許可",
+    "attach_enable": "許可をしている場合、画像以外のファイルをページに添付可能になります。",
+    "update": "更新",
+    "mail_settings": "メールの設定",
+    "smtp_used": "SMTPの設定がされている場合、それが利用されます。",
+    "smtp_but_aws": "SMTP設定がなく、AWSの設定がある場合、SESでの送信を試みます。",
+    "neihter_of": "どちらの設定もない場合、メールは送信されません。",
+    "from_e-mail_address": "Fromアドレス",
+    "smtp_settings": "SMTP設定",
+    "host": "ホスト",
+    "port": "ポート",
+    "user": "ユーザー",
+    "aws_settings": "AWS設定",
+    "aws_access": "AWS にアクセスするための設定を行います。AWS の設定を完了させると、ファイルアップロード機能、プロフィール写真機能などが有効になります。",
+    "no_smtp_setting": "また、SMTP の設定が無い場合、SES を利用したメール送信が行われます。FromメールアドレスのVerify、プロダクション利用設定をする必要があります。",
+    "change_setting": "この設定を途中で変更すると、これまでにアップロードしたファイル等へのアクセスができなくなりますのでご注意下さい。",
+    "region": "リージョン",
+    "bucket_name": "バケット名",
+    "custom_endpoint": "カスタムエンドポイント",
+    "custom_endpoint_change": "MinIOなど、S3互換APIを持つ他のオブジェクトストレージサービスを使用する場合のみ、そのエンドポイントのURLを入力してください。空欄の場合は、Amazon S3を使用します。",
+    "plugin_settings": "プラグイン設定",
+    "enable_plugin_loading": "プラグインの読み込みを有効にします。",
+    "load_plugins": "プラグインを読み込む",
+    "enable": "有効",
+    "disable": "無効",
+    "use_env_var_if_empty": "データベース側の値が空の場合、環境変数 <code>{{variable}}</code> の値を利用します"
+  },
+  "markdown_setting": {
+    "lineBreak_header": "Line Break設定",
+    "lineBreak_desc": "Line Breakの設定を変更できます。",
+    "lineBreak_options": {
+      "enable_lineBreak": "Line Break を有効にする",
+      "enable_lineBreak_desc": "ページテキスト中の改行を、HTML内で<code>&lt;br&gt;</code>として扱います",
+      "enable_lineBreak_for_comment": "コメント欄で Line Break を有効にする",
+      "enable_lineBreak_for_comment_desc": "コメント中の改行を、HTML内で<code>&lt;br&gt;</code>として扱います"
+    },
+    "presentation_header": "プレゼンテーション設定",
+    "presentation_desc": "プレゼンテーションの設定を変更できます。",
+    "presentation_options": {
+      "page_break_setting": "改頁を設定する",
+      "preset_one_separator": "プリセット 1",
+      "preset_one_separator_desc": "連続した空行3行で改頁します",
+      "preset_one_separator_value": "\\n\\n\\n",
+      "preset_two_separator": "プリセット 2",
+      "preset_two_separator_desc": "連続したハイフン5つで改頁します",
+      "preset_two_separator_value": "-----",
+      "custom_separator": "カスタム",
+      "custom_separator_desc": "正規表現を設定できます"
+    },
+    "xss_header": "XSS(Cross Site Scripting)対策設定",
+    "xss_desc": "マークダウンテキスト内の HTML タグの扱いを設定し、悪意のあるプログラムからの攻撃を防ぎます",
+    "xss_options": {
+      "enable_xss_prevention": "XSSを抑制する",
+      "ignore_all_tags": "すべてのタグを抑制する",
+      "ignore_all_tags_desc": "すべてのHTMLタグと属性を使用不可にします",
+      "recommended_setting": "おすすめ設定",
+      "custom_whitelist": "カスタムホワイトリスト",
+      "tag_names": "タグ名",
+      "tag_attributes": "タグ属性",
+      "import_recommended": "{{target}} のおすすめをインポート"
+    }
+  },
+  "customize_setting": {
+    "recommended": "おすすめ",
+    "layout": "レイアウト",
+    "theme": "テーマ",
+    "layout_desc": {
+      "growi_title": "シンプル・明瞭",
+      "growi_text1": "全画面レイアウトで、余白は少なくなります。",
+      "growi_text2": "コメントはページの下部に表示されます。",
+      "growi_text3": "ページ情報は下部に表示されます。",
+      "kibela_title": "閲覧重視の構造",
+      "kibela_text1": "コンテンツが中心に表示されます。",
+      "kibela_text2": "コメントはページの下部に表示されます。",
+      "kibela_text3": "ページ情報は下部に表示されます。",
+      "crowi_title": "ビュー・コントロールの分離",
+      "crowi_text1": "サイドバーを開くと情報が表示されます。",
+      "crowi_text2": "コメントはサイドバーに表示されます。",
+      "crowi_text3": "ページ情報はサイドバーに表示されます。"
+    },
+    "behavior": "動作",
+    "behavior_desc": {
+      "growi_text1": "<code>/page</code>と<code>/page/</code>どちらのパスも同じページを表示します。",
+      "growi_text2": "<code>/nonexistent_page</code> では編集フォームを表示します",
+      "growi_text3": "<b>GROWI Enhanced Layout</b>では全てのページが配下のページリストを表示します",
+      "crowi_text1": "<code>/page</code> ではページを表示します。",
+      "crowi_text2": "<code>/page/</code> では配下のページを表示します。",
+      "crowi_text3": "<code>/page/</code>がポータルに適応している場合、ポータルページと配下のページリストを表示します。",
+      "crowi_text4": "<code>/nonexistent_page</code> では編集フォームを表示します",
+      "crowi_text5": "<code>/nonexistent_page</code> では配下のページリストを表示します。"
+    },
+    "function": "機能",
+    "function_desc": "機能の有効/無効を選択できます。",
+    "function_options": {
+      "timeline": "タイムライン機能",
+      "timeline_desc1": "配下ページのタイムラインを表示できます。",
+      "timeline_desc2": "配下ページが多い場合はページロード時のパフォーマンスが落ちます。",
+      "timeline_desc3": "無効化することでリストページの表示を高速化できます。",
+      "tab_switch": "タブ変更をブラウザ履歴に保存",
+      "tab_switch_desc1": "編集タブやヒストリータブ等の切り替えをブラウザ履歴に保存し、ブラウザの戻る/進む操作の対象にします。",
+      "tab_switch_desc2": "無効化することで、ページ遷移のみを戻る/進む操作の対象にすることができます。",
+      "attach_title_header": "新規ページ作成時の h1 セクション自動挿入",
+      "attach_title_header_desc": "新規作成したページの1行目に、ページのパスを h1 セクションとして挿入します。",
+      "recent_created__n_draft_num_desc": "最近作成したページと下書きの表示数",
+      "recently_created_n_draft_num_desc": "ホーム画面の Recently Created での、1ページの表示数を設定します。",
+      "stale_notification": "古いページに通知を表示する",
+      "stale_notification_desc": "最後の更新から1年を超えるページへの通知を表示します。"
+    },
+    "code_highlight": "コードハイライト",
+    "nocdn_desc": "この機能は、環境変数 <code>NO_CDN=true</code> の時は無効化されます。<br>GitHub スタイルが適用されています。",
+    "custom_title": "カスタム Title",
+    "custom_title_detail": "<code>&lt;title&gt;</code>タグのコンテンツをカスタマイズできます。<br><code>&#123;&#123;sitename&#125;&#125;</code>がサイト名、<code>&#123;&#123;page&#125;&#125;</code>がページ名またはページパスに置換されます。",
+    "custom_header": "カスタム HTML Header",
+    "custom_header_detail": "システム全体に適用される HTML を記述できます。<code>&lt;header&gt;</code> タグ内の他の <code>&lt;script&gt;</code> タグ読み込み前に展開されます。<br>変更の反映はページの更新が必要です。",
+    "custom_css": "カスタム CSS",
+    "write_css": " システム全体に適用されるCSSを記述できます。",
+    "ctrl_space": "Ctrl+Space でコード補完",
+    "custom_script": "カスタムスクリプト",
+    "write_java": "システム全体に適用されるJavaScriptを記述できます。",
+    "reflect_change": "変更の反映はページの更新が必要です。"
+  },
+  "export_management": {
+    "exporting_collection_list": "エクスポート中のコレクション",
+    "exported_data_list": "エクスポートされたアーカイブリスト",
+    "export_collections": "コレクションのエクスポート",
+    "check_all": "全てにチェックを付ける",
+    "uncheck_all": "全てからチェックを外す",
+    "desc_password_seed": "ユーザーデータをバックアップ/リストアする場合、現在の <code>PASSWORD_SEED</code> を新しい GROWI システムにセットすることを忘れないでください。さもなくば、ユーザーがパスワードでログインできなくなります。<br><br><strong>ヒント:</strong><br>現在の <code>PASSWORD_SEED</code> は、エクスポートされる ZIP 中の <code>meta.json</code> に保存されます。",
+    "create_new_archive_data": "アーカイブデータの新規作成",
+    "export": "エクスポート",
+    "cancel": "キャンセル",
+    "file": "ファイル名",
+    "growi_version": "Growi バージョン",
+    "collections": "コレクション",
+    "exported_at": "エクスポートされた時間",
+    "export_menu": "エクスポートメニュー",
+    "download": "ダウンロード",
+    "delete": "削除"
+  },
+  "importer_management": {
+    "beta_warning": "この機能はベータ版です",
+    "import_from": "{{from}} からインポート",
+    "import_growi_archive": "GROWI アーカイブをインポート",
+    "growi_settings": {
+      "overwrite_documents": "インポートされたドキュメントは既存のドキュメントを上書きします",
+      "growi_archive_file": "GROWI アーカイブファイル",
+      "uploaded_data": "アップロードされたデータ",
+      "extracted_file": "展開されたファイル",
+      "collection": "コレクション",
+      "upload": "アップロード",
+      "discard": "アップロードしたデータを破棄する",
+      "errors": {
+        "at_least_one": "コレクションが選択されていません",
+        "page_and_revision": "'Pages' と 'Revisions' はセットでインポートする必要があります",
+        "depends": "'{{condition}}' をインポートする場合は、'{{target}}' を一緒に選択する必要があります"
+      },
+      "configuration": {
+        "pages": {
+          "overwrite_author": {
+            "label": "ページ作成者を現在のユーザーで上書きする",
+            "desc": "users を同時に復元しない場合、このオプションは<span class=\"text-danger\">非推奨</span>です。"
+          },
+          "set_public_to_page": {
+            "label": "'{{from}}' 設定のページを '公開' 設定にする",
+            "desc": "全ての <b>'{{from}}'</b> 設定のページが<span class=\"text-danger\">全ユーザーから</span>読み取り可能になることに注意してください。"
+          },
+          "initialize_meta_datas": {
+            "label": "「いいね」「閲覧したユーザー」「コメント数」を初期化する",
+            "desc": "users を同時に復元しない場合、このオプションは<span class=\"text-danger\">非推奨</span>です。"
+          },
+          "initialize_hackmd_related_datas": {
+            "label": "HackMD 関連データを初期化する",
+            "desc": "HackMD に重要な下書きデータがない限りはこのオプションをチェックすることを推奨します。"
+          }
+        },
+        "revisions": {
+          "overwrite_author": {
+            "label": "リビジョン作成者を現在のユーザーで上書きする",
+            "desc": "users を同時に復元しない場合、このオプションは<span class=\"text-danger\">非推奨</span>です。"
+          }
+        }
+      }
+    },
+    "esa_settings": {
+      "team_name": "チーム名",
+      "access_token": "アクセストークン",
+      "test_connection": "接続テスト"
+    },
+    "qiita_settings": {
+      "team_name": "チーム名",
+      "access_token": "アクセストークン",
+      "test_connection": "接続テスト"
+    },
+    "import": "インポート",
+    "page_skip": "既に GROWI 側に同名のページが存在する場合、そのページはスキップされます",
+    "Directory_hierarchy_tag": "ディレクトリ階層タグ"
+  },
+  "user_management": {
+    "invite_users": "新規ユーザーの招待",
+    "invite_modal": {
+      "emails": "メールアドレス (複数行入力で複数人招待可能)",
+      "invite_thru_email": "招待をメールで送信",
+      "valid_email": "メールアドレスを入力してください。",
+      "temporary_password": "作成したユーザーは仮パスワードが設定されています。",
+      "send_new_password": "新規発行したパスワードを、対象ユーザーへ連絡してください。",
+      "send_temporary_password": "招待メールを送っていない場合、この画面で必ず仮パスワードをコピーし、招待者へ連絡してください。",
+      "existing_email": "以下のEmailはすでに存在しています。"
+    },
+    "user_table": {
+      "administrator": "管理者",
+      "edit_menu": "編集メニュー",
+      "reset_password": "パスワードの再発行",
+      "administrator_menu": "管理者メニュー",
+      "accept": "承認する",
+      "deactivate_account": "アカウント停止",
+      "your_own": "自分自身のアカウントを停止することはできません",
+      "remove_admin_access": "管理者から外す",
+      "cannot_remove": "自分自身を管理者から外すことはできません",
+      "give_admin_access": "管理者にする"
+    },
+    "reset_password": "パスワードのリセット",
+    "reset_password_modal": {
+      "password_never_seen": "表示されたパスワードはこの画面を閉じると二度と表示できませんのでご注意ください。",
+      "password_reset_message": "対象ユーザーに下記のパスワードを伝え、すぐに新しく別のパスワードを設定するよう伝えてください。",
+      "send_new_password": "新規発行したパスワードを、対象ユーザーへ連絡してください。",
+      "target_user": "対象ユーザー",
+      "new_password": "新しいパスワード"
+    },
+    "external_account": "外部アカウントの管理",
+    "external_account_list": "外部アカウント一覧",
+    "invite": "招待する",
+    "invited": "ユーザーを招待しました",
+    "back_to_user_management": "ユーザー管理に戻る",
+    "authentication_provider": "認証情報プロバイダ",
+    "manage": "操作",
+    "password_setting": "パスワード設定",
+    "password_setting_help": "関連付けられているユーザーがパスワードを設定しているかどうかを表示します",
+    "set": "設定済み",
+    "unset": "未設定",
+    "related_username": "関連付けられているユーザーの ",
+    "cannot_invite_maximum_users": "ユーザーが上限に達したため招待できません。",
+    "current_users": "現在のユーザー数:"
+  },
+  "user_group_management": {
+    "create_group": "新規グループの作成",
+    "deny_create_group": "新規グループの作成はできません。",
+    "group_name": "グループ名",
+    "group_example": "例: Group1",
+    "add_modal": {
+      "add_user": "グループへのユーザー追加",
+      "search_option": "検索オプション",
+      "enable_option": "{{option}}を有効にする",
+      "forward_match": "前方一致",
+      "partial_match": "部分一致",
+      "backward_match": "後方一致"
+    },
+    "group_list": "グループ一覧",
+    "back_to_list": "グループ一覧に戻る",
+    "basic_info": "基本情報",
+    "user_list": "ユーザー一覧",
+    "created_group": "グループを作成しました",
+    "is_loading_data": "データを取得中です...",
+    "no_pages": "グループが閲覧権限を保有するページはありません",
+    "remove_from_group": "グループから外す",
+    "delete_modal": {
+      "header": "グループの削除",
+      "desc": "グループ及び限定公開のページの削除を行うと元に戻すことはできませんのでご注意ください。",
+      "dropdown_desc": "削除するグループの限定公開ページの処理を選択してください",
+      "select_group": "グループを選択してください",
+      "no_groups": "グループがありません",
+      "publish_pages": "全て公開する",
+      "delete_pages": "全て削除する",
+      "transfer_pages": "全て他のグループに移譲する"
+    }
+  }
+}

+ 0 - 10
resource/locales/ja/admin/admin_top.json

@@ -1,10 +0,0 @@
-{
-  "management_wiki": "Wiki管理",
-  "system_information": "システム情報",
-  "wiki_administrator": "この画面はWiki管理者のみがアクセスできる画面です。",
-  "assign_administrator": "「ユーザー管理」から「管理者にする」ボタンを使ってユーザーをWiki管理者に任命することができます。",
-  "list_of_installed_plugins": "インストールされているプラグイン一覧",
-  "package_name": "パッケージ名",
-  "specified_version": "指定バージョン",
-  "installed_version": "インストールされているバージョン"
-}

+ 0 - 38
resource/locales/ja/admin/app_setting.json

@@ -1,38 +0,0 @@
-{
-  "site_name": "サイト名",
-  "sitename_change": "ヘッダーや HTML タイトルに使用されるサイト名を変更できます。",
-  "header_content": "ここに入力した内容は、ヘッダー等に表示されます。",
-  "site_url_desc": "サイトURLを設定します。",
-  "site_url_warn": "サイトURLが設定されていないため、一部機能が動作しない状態になっています。",
-  "siteurl_help": "<code>http://</code> または <code>https://</code> から始まるサイトのURL",
-  "confidential_name": "コンフィデンシャル表示",
-  "confidential_example": "例: 社外秘",
-  "default_language": "新規ユーザーのデフォルト設定言語",
-  "file_uploading": "ファイルアップロード",
-  "enable_files_except_image": "画像以外のファイルアップロードを許可",
-  "attach_enable": "許可をしている場合、画像以外のファイルをページに添付可能になります。",
-  "update": "更新",
-  "mail_settings": "メールの設定",
-  "smtp_used": "SMTPの設定がされている場合、それが利用されます。",
-  "smtp_but_aws": "SMTP設定がなく、AWSの設定がある場合、SESでの送信を試みます。",
-  "neihter_of": "どちらの設定もない場合、メールは送信されません。",
-  "from_e-mail_address": "Fromアドレス",
-  "smtp_settings": "SMTP設定",
-  "host": "ホスト",
-  "port": "ポート",
-  "user": "ユーザー",
-  "aws_settings": "AWS設定",
-  "aws_access": "AWS にアクセスするための設定を行います。AWS の設定を完了させると、ファイルアップロード機能、プロフィール写真機能などが有効になります。",
-  "no_smtp_setting": "また、SMTP の設定が無い場合、SES を利用したメール送信が行われます。FromメールアドレスのVerify、プロダクション利用設定をする必要があります。",
-  "change_setting": "この設定を途中で変更すると、これまでにアップロードしたファイル等へのアクセスができなくなりますのでご注意下さい。",
-  "region": "リージョン",
-  "bucket_name": "バケット名",
-  "custom_endpoint": "カスタムエンドポイント",
-  "custom_endpoint_change": "MinIOなど、S3互換APIを持つ他のオブジェクトストレージサービスを使用する場合のみ、そのエンドポイントのURLを入力してください。空欄の場合は、Amazon S3を使用します。",
-  "plugin_settings": "プラグイン設定",
-  "enable_plugin_loading": "プラグインの読み込みを有効にします。",
-  "load_plugins": "プラグインを読み込む",
-  "enable": "有効",
-  "disable": "無効",
-  "use_env_var_if_empty": "データベース側の値が空の場合、環境変数 <code>{{variable}}</code> の値を利用します"
-}

+ 0 - 59
resource/locales/ja/admin/customize_setting.json

@@ -1,59 +0,0 @@
-{
-  "recommended": "おすすめ",
-  "layout": "レイアウト",
-  "theme": "テーマ",
-  "layout_desc": {
-    "growi_title": "シンプル・明瞭",
-    "growi_text1": "全画面レイアウトで、余白は少なくなります。",
-    "growi_text2": "コメントはページの下部に表示されます。",
-    "growi_text3": "ページ情報は下部に表示されます。",
-    "kibela_title": "閲覧重視の構造",
-    "kibela_text1": "コンテンツが中心に表示されます。",
-    "kibela_text2": "コメントはページの下部に表示されます。",
-    "kibela_text3": "ページ情報は下部に表示されます。",
-    "crowi_title": "ビュー・コントロールの分離",
-    "crowi_text1": "サイドバーを開くと情報が表示されます。",
-    "crowi_text2": "コメントはサイドバーに表示されます。",
-    "crowi_text3": "ページ情報はサイドバーに表示されます。"
-  },
-  "behavior": "動作",
-  "behavior_desc": {
-    "growi_text1": "<code>/page</code>と<code>/page/</code>どちらのパスも同じページを表示します。",
-    "growi_text2": "<code>/nonexistent_page</code> では編集フォームを表示します",
-    "growi_text3": "<b>GROWI Enhanced Layout</b>では全てのページが配下のページリストを表示します",
-    "crowi_text1": "<code>/page</code> ではページを表示します。",
-    "crowi_text2": "<code>/page/</code> では配下のページを表示します。",
-    "crowi_text3": "<code>/page/</code>がポータルに適応している場合、ポータルページと配下のページリストを表示します。",
-    "crowi_text4": "<code>/nonexistent_page</code> では編集フォームを表示します",
-    "crowi_text5": "<code>/nonexistent_page</code> では配下のページリストを表示します。"
-  },
-  "function": "機能",
-  "function_desc": "機能の有効/無効を選択できます。",
-  "function_options": {
-    "timeline": "タイムライン機能",
-    "timeline_desc1": "配下ページのタイムラインを表示できます。",
-    "timeline_desc2": "配下ページが多い場合はページロード時のパフォーマンスが落ちます。",
-    "timeline_desc3": "無効化することでリストページの表示を高速化できます。",
-    "tab_switch": "タブ変更をブラウザ履歴に保存",
-    "tab_switch_desc1": "編集タブやヒストリータブ等の切り替えをブラウザ履歴に保存し、ブラウザの戻る/進む操作の対象にします。",
-    "tab_switch_desc2": "無効化することで、ページ遷移のみを戻る/進む操作の対象にすることができます。",
-    "attach_title_header": "新規ページ作成時の h1 セクション自動挿入",
-    "attach_title_header_desc": "新規作成したページの1行目に、ページのパスを h1 セクションとして挿入します。",
-    "recent_created__n_draft_num_desc": "最近作成したページと下書きの表示数",
-    "recently_created_n_draft_num_desc": "ホーム画面の Recently Created での、1ページの表示数を設定します。",
-    "stale_notification": "古いページに通知を表示する",
-    "stale_notification_desc": "最後の更新から1年を超えるページへの通知を表示します。"
-  },
-  "code_highlight": "コードハイライト",
-  "nocdn_desc": "この機能は、環境変数 <code>NO_CDN=true</code> の時は無効化されます。<br>GitHub スタイルが適用されています。",
-  "custom_title": "カスタム Title",
-  "custom_title_detail": "<code>&lt;title&gt;</code>タグのコンテンツをカスタマイズできます。<br><code>&#123;&#123;sitename&#125;&#125;</code>がサイト名、<code>&#123;&#123;page&#125;&#125;</code>がページ名またはページパスに置換されます。",
-  "custom_header": "カスタム HTML Header",
-  "custom_header_detail": "システム全体に適用される HTML を記述できます。<code>&lt;header&gt;</code> タグ内の他の <code>&lt;script&gt;</code> タグ読み込み前に展開されます。<br>変更の反映はページの更新が必要です。",
-  "custom_css": "カスタム CSS",
-  "write_css": " システム全体に適用されるCSSを記述できます。",
-  "ctrl_space": "Ctrl+Space でコード補完",
-  "custom_script": "カスタムスクリプト",
-  "write_java": "システム全体に適用されるJavaScriptを記述できます。",
-  "reflect_change": "変更の反映はページの更新が必要です。"
-}

+ 0 - 18
resource/locales/ja/admin/export_management.json

@@ -1,18 +0,0 @@
-{
-  "exporting_collection_list": "エクスポート中のコレクション",
-  "exported_data_list": "エクスポートされたアーカイブリスト",
-  "export_collections": "コレクションのエクスポート",
-  "check_all": "全てにチェックを付ける",
-  "uncheck_all": "全てからチェックを外す",
-  "desc_password_seed": "ユーザーデータをバックアップ/リストアする場合、現在の <code>PASSWORD_SEED</code> を新しい GROWI システムにセットすることを忘れないでください。さもなくば、ユーザーがパスワードでログインできなくなります。<br><br><strong>ヒント:</strong><br>現在の <code>PASSWORD_SEED</code> は、エクスポートされる ZIP 中の <code>meta.json</code> に保存されます。",
-  "create_new_archive_data": "アーカイブデータの新規作成",
-  "export": "エクスポート",
-  "cancel": "キャンセル",
-  "file": "ファイル名",
-  "growi_version": "Growi バージョン",
-  "collections": "コレクション",
-  "exported_at": "エクスポートされた時間",
-  "export_menu": "エクスポートメニュー",
-  "download": "ダウンロード",
-  "delete": "削除"
-}

+ 0 - 58
resource/locales/ja/admin/importer_management.json

@@ -1,58 +0,0 @@
-{
-  "beta_warning": "この機能はベータ版です",
-  "import_from": "{{from}} からインポート",
-  "import_growi_archive": "GROWI アーカイブをインポート",
-  "growi_settings": {
-    "overwrite_documents": "インポートされたドキュメントは既存のドキュメントを上書きします",
-    "growi_archive_file": "GROWI アーカイブファイル",
-    "uploaded_data": "アップロードされたデータ",
-    "extracted_file": "展開されたファイル",
-    "collection": "コレクション",
-    "upload": "アップロード",
-    "discard": "アップロードしたデータを破棄する",
-    "errors": {
-      "at_least_one": "コレクションが選択されていません",
-      "page_and_revision": "'Pages' と 'Revisions' はセットでインポートする必要があります",
-      "depends": "'{{condition}}' をインポートする場合は、'{{target}}' を一緒に選択する必要があります"
-    },
-    "configuration": {
-      "pages": {
-        "overwrite_author": {
-          "label": "ページ作成者を現在のユーザーで上書きする",
-          "desc": "users を同時に復元しない場合、このオプションは<span class=\"text-danger\">非推奨</span>です。"
-        },
-        "set_public_to_page": {
-          "label": "'{{from}}' 設定のページを '公開' 設定にする",
-          "desc": "全ての <b>'{{from}}'</b> 設定のページが<span class=\"text-danger\">全ユーザーから</span>読み取り可能になることに注意してください。"
-        },
-        "initialize_meta_datas": {
-          "label": "「いいね」「閲覧したユーザー」「コメント数」を初期化する",
-          "desc": "users を同時に復元しない場合、このオプションは<span class=\"text-danger\">非推奨</span>です。"
-        },
-        "initialize_hackmd_related_datas": {
-          "label": "HackMD 関連データを初期化する",
-          "desc": "HackMD に重要な下書きデータがない限りはこのオプションをチェックすることを推奨します。"
-        }
-      },
-      "revisions": {
-        "overwrite_author": {
-          "label": "リビジョン作成者を現在のユーザーで上書きする",
-          "desc": "users を同時に復元しない場合、このオプションは<span class=\"text-danger\">非推奨</span>です。"
-        }
-      }
-    }
-  },
-  "esa_settings": {
-    "team_name": "チーム名",
-    "access_token": "アクセストークン",
-    "test_connection": "接続テスト"
-  },
-  "qiita_settings": {
-    "team_name": "チーム名",
-    "access_token": "アクセストークン",
-    "test_connection": "接続テスト"
-  },
-  "import": "インポート",
-  "page_skip": "既に GROWI 側に同名のページが存在する場合、そのページはスキップされます",
-  "Directory_hierarchy_tag": "ディレクトリ階層タグ"
-}

+ 0 - 35
resource/locales/ja/admin/markdown_setting.json

@@ -1,35 +0,0 @@
-{
-  "lineBreak_header": "Line Break設定",
-  "lineBreak_desc": "Line Breakの設定を変更できます。",
-  "lineBreak_options": {
-    "enable_lineBreak": "Line Break を有効にする",
-    "enable_lineBreak_desc": "ページテキスト中の改行を、HTML内で<code>&lt;br&gt;</code>として扱います",
-    "enable_lineBreak_for_comment": "コメント欄で Line Break を有効にする",
-    "enable_lineBreak_for_comment_desc": "コメント中の改行を、HTML内で<code>&lt;br&gt;</code>として扱います"
-  },
-  "presentation_header": "プレゼンテーション設定",
-  "presentation_desc": "プレゼンテーションの設定を変更できます。",
-  "presentation_options": {
-    "page_break_setting": "改頁を設定する",
-    "preset_one_separator": "プリセット 1",
-    "preset_one_separator_desc": "連続した空行3行で改頁します",
-    "preset_one_separator_value": "\\n\\n\\n",
-    "preset_two_separator": "プリセット 2",
-    "preset_two_separator_desc": "連続したハイフン5つで改頁します",
-    "preset_two_separator_value": "-----",
-    "custom_separator": "カスタム",
-    "custom_separator_desc": "正規表現を設定できます"
-  },
-  "xss_header": "XSS(Cross Site Scripting)対策設定",
-  "xss_desc": "マークダウンテキスト内の HTML タグの扱いを設定し、悪意のあるプログラムからの攻撃を防ぎます",
-  "xss_options": {
-    "enable_xss_prevention": "XSSを抑制する",
-    "ignore_all_tags": "すべてのタグを抑制する",
-    "ignore_all_tags_desc": "すべてのHTMLタグと属性を使用不可にします",
-    "recommended_setting": "おすすめ設定",
-    "custom_whitelist": "カスタムホワイトリスト",
-    "tag_names": "タグ名",
-    "tag_attributes": "タグ属性",
-    "import_recommended": "{{target}} のおすすめをインポート"
-  }
-}

+ 0 - 32
resource/locales/ja/admin/user_group_management.json

@@ -1,32 +0,0 @@
-{
-  "create_group": "新規グループの作成",
-  "deny_create_group": "新規グループの作成はできません。",
-  "group_name": "グループ名",
-  "group_example": "例: Group1",
-  "add_modal": {
-    "add_user": "グループへのユーザー追加",
-    "search_option": "検索オプション",
-    "enable_option": "{{option}}を有効にする",
-    "forward_match": "前方一致",
-    "partial_match": "部分一致",
-    "backward_match": "後方一致"
-  },
-  "group_list": "グループ一覧",
-  "back_to_list": "グループ一覧に戻る",
-  "basic_info": "基本情報",
-  "user_list": "ユーザー一覧",
-  "created_group": "グループを作成しました",
-  "is_loading_data": "データを取得中です...",
-  "no_pages": "グループが閲覧権限を保有するページはありません",
-  "remove_from_group": "グループから外す",
-  "delete_modal": {
-    "header": "グループの削除",
-    "desc": "グループ及び限定公開のページの削除を行うと元に戻すことはできませんのでご注意ください。",
-    "dropdown_desc": "削除するグループの限定公開ページの処理を選択してください",
-    "select_group": "グループを選択してください",
-    "no_groups": "グループがありません",
-    "publish_pages": "全て公開する",
-    "delete_pages": "全て削除する",
-    "transfer_pages": "全て他のグループに移譲する"
-  }
-}

+ 0 - 46
resource/locales/ja/admin/user_management.json

@@ -1,46 +0,0 @@
-{
-  "invite_users": "新規ユーザーの招待",
-  "invite_modal": {
-    "emails": "メールアドレス (複数行入力で複数人招待可能)",
-    "invite_thru_email": "招待をメールで送信",
-    "valid_email": "メールアドレスを入力してください。",
-    "temporary_password": "作成したユーザーは仮パスワードが設定されています。",
-    "send_new_password": "新規発行したパスワードを、対象ユーザーへ連絡してください。",
-    "send_temporary_password": "招待メールを送っていない場合、この画面で必ず仮パスワードをコピーし、招待者へ連絡してください。",
-    "existing_email": "以下のEmailはすでに存在しています。"
-  },
-  "user_table": {
-    "administrator": "管理者",
-    "edit_menu": "編集メニュー",
-    "reset_password": "パスワードの再発行",
-    "administrator_menu": "管理者メニュー",
-    "accept": "承認する",
-    "deactivate_account": "アカウント停止",
-    "your_own": "自分自身のアカウントを停止することはできません",
-    "remove_admin_access": "管理者から外す",
-    "cannot_remove": "自分自身を管理者から外すことはできません",
-    "give_admin_access": "管理者にする"
-  },
-  "reset_password": "パスワードのリセット",
-  "reset_password_modal": {
-    "password_never_seen": "表示されたパスワードはこの画面を閉じると二度と表示できませんのでご注意ください。",
-    "password_reset_message": "対象ユーザーに下記のパスワードを伝え、すぐに新しく別のパスワードを設定するよう伝えてください。",
-    "send_new_password": "新規発行したパスワードを、対象ユーザーへ連絡してください。",
-    "target_user": "対象ユーザー",
-    "new_password": "新しいパスワード"
-  },
-  "external_account": "外部アカウントの管理",
-  "external_account_list": "外部アカウント一覧",
-  "invite": "招待する",
-  "invited": "ユーザーを招待しました",
-  "back_to_user_management": "ユーザー管理に戻る",
-  "authentication_provider": "認証情報プロバイダ",
-  "manage": "操作",
-  "password_setting": "パスワード設定",
-  "password_setting_help": "関連付けられているユーザーがパスワードを設定しているかどうかを表示します",
-  "set": "設定済み",
-  "unset": "未設定",
-  "related_username": "関連付けられているユーザーの ",
-  "cannot_invite_maximum_users": "ユーザーが上限に達したため招待できません。",
-  "current_users": "現在のユーザー数:"
-}

+ 38 - 2
resource/locales/ja/translation.json

@@ -303,6 +303,15 @@
       "Post": "投稿"
     }
   },
+  "toaster": {
+    "update_successed": "{{target}}を更新しました",
+    "give_user_admin": "{{username}}を管理者に設定しました",
+    "remove_user_admin": "{{username}}を管理者から外しました",
+    "activate_user_success": "{{username}}を有効化しました",
+    "deactivate_user_success": "{{username}}を無効化しました",
+    "remove_user_success": "{{username}}を削除しました",
+    "remove_external_user_success": "{{accountId}}を削除しました "
+  },
   "template": {
     "modal_label": {
       "Create/Edit Template Page": "テンプレートページの作成/編集",
@@ -493,10 +502,30 @@
     }
   },
   "notification_setting": {
+    "slack_incoming_configuration": "Slack Incoming Webhooks 設定",
+    "prioritize_webhook": "Slack アプリより Incoming Webhook を優先する",
+    "prioritize_webhook_desc": "このオプションをオンにすると、 Slack App が有効になっていても GROWI は Incoming Webhook を使用します。",
+    "slack_app_configuration": "Slack App 設定",
+    "slack_app_configuration_desc": "Crowi 互換の機能です。<br /> <strong>設定が複雑すぎる</strong>のでオススメしません。",
+    "use_instead": "代わりに Slack Incoming Webhooks 設定を使用してください。",
+    "how_to": {
+      "header": "Incoming Webhooks の設定方法",
+      "workspace": "ワークスペースで Webhook を追加します。",
+      "workspace_desc1": "<a href='https://slack.com/services/new/incoming-webhook'>Incoming Webhooks Configuration page</a> にアクセスします。",
+      "workspace_desc2": "投稿するチャンネルを選びます。",
+      "workspace_desc3": "追加します。",
+      "at_growi": "GROWI 管理画面で Webhook URL を設定します。",
+      "at_growi_desc": "このページで &rdquo;Webhook URL&rdquo; を入力して送信します。"
+    },
+    "user_trigger_notification_header": "デフォルトパターンの通知設定",
+    "pattern": "パターン",
+    "channel": "チャンネル名",
+    "pattern_desc": "Wiki のパス名。 パスには <code>*</code> を使用できます。",
+    "channel_desc": "<code>#</code> を除いた Slack チャンネル名",
     "notification_list": "通知設定の一覧",
     "add_notification": "通知設定の追加",
     "trigger_path": "トリガーパス",
-    "trigger_path_help": "(%sが使用できます)",
+    "trigger_path_help": "(<code>*</code>が使用できます)",
     "trigger_events": "トリガーイベント",
     "notify_to": "通知先",
     "back_to_list": "通知設定一覧に戻る",
@@ -509,11 +538,18 @@
     "event_comment": "コメントが投稿されたとき",
     "email": {
       "ifttt_link": "IFTTT でメールトリガの新しいアプレットを作る"
-    }
+    },
+    "updated_slackApp": "SlackApp設定を更新しました",
+    "add_notification_pattern": "通知パターンを追加しました。",
+    "delete_notification_pattern": "通知パターンを削除しました。",
+    "delete_notification_pattern_desc1": "Path: {{path}} を削除します。",
+    "delete_notification_pattern_desc2": "Once deleted, it cannot be recovered",
+    "toggle_notification": "{{path}}の通知設定を変更しました"
   },
   "full_text_search_management": {
     "elasticsearch_management": "Elasticsearch 管理",
     "connection_status": "接続の状態",
+    "connection_status_label_unconfigured": "設定されていません",
     "connection_status_label_connected": "接続されています",
     "connection_status_label_disconnected": "切断されています",
     "indices_status": "インデックスの状態",

+ 99 - 0
src/client/js/admin.jsx

@@ -0,0 +1,99 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { Provider } from 'unstated';
+import { I18nextProvider } from 'react-i18next';
+
+import loggerFactory from '@alias/logger';
+
+import AdminHome from './components/Admin/AdminHome/AdminHome';
+import UserGroupDetailPage from './components/Admin/UserGroupDetail/UserGroupDetailPage';
+import NotificationSetting from './components/Admin/Notification/NotificationSetting';
+import ManageGlobalNotification from './components/Admin/Notification/ManageGlobalNotification';
+import MarkdownSetting from './components/Admin/MarkdownSetting/MarkDownSetting';
+import UserManagement from './components/Admin/UserManagement';
+import AppSettingsPage from './components/Admin/App/AppSettingsPage';
+import ManageExternalAccount from './components/Admin/ManageExternalAccount';
+import UserGroupPage from './components/Admin/UserGroup/UserGroupPage';
+import Customize from './components/Admin/Customize/Customize';
+import ImportDataPage from './components/Admin/ImportDataPage';
+import ExportArchiveDataPage from './components/Admin/ExportArchiveDataPage';
+import FullTextSearchManagement from './components/Admin/FullTextSearchManagement';
+import AdminNavigation from './components/Admin/Common/AdminNavigation';
+
+import AdminHomeContainer from './services/AdminHomeContainer';
+import AdminCustomizeContainer from './services/AdminCustomizeContainer';
+import AdminUserGroupDetailContainer from './services/AdminUserGroupDetailContainer';
+import AdminUsersContainer from './services/AdminUsersContainer';
+import AdminAppContainer from './services/AdminAppContainer';
+import AdminMarkDownContainer from './services/AdminMarkDownContainer';
+import AdminExternalAccountsContainer from './services/AdminExternalAccountsContainer';
+import AdminNotificationContainer from './services/AdminNotificationContainer';
+
+import { appContainer, componentMappings } from './bootstrap';
+
+const logger = loggerFactory('growi:admin');
+
+const { i18n } = appContainer;
+const websocketContainer = appContainer.getContainer('WebsocketContainer');
+
+// create unstated container instance
+const adminAppContainer = new AdminAppContainer(appContainer);
+const adminHomeContainer = new AdminHomeContainer(appContainer);
+const adminCustomizeContainer = new AdminCustomizeContainer(appContainer);
+const adminUsersContainer = new AdminUsersContainer(appContainer);
+const adminExternalAccountsContainer = new AdminExternalAccountsContainer(appContainer);
+const adminNotificationContainer = new AdminNotificationContainer(appContainer);
+const adminMarkDownContainer = new AdminMarkDownContainer(appContainer);
+const adminUserGroupDetailContainer = new AdminUserGroupDetailContainer(appContainer);
+const injectableContainers = [
+  appContainer,
+  websocketContainer,
+  adminAppContainer,
+  adminHomeContainer,
+  adminCustomizeContainer,
+  adminUsersContainer,
+  adminExternalAccountsContainer,
+  adminNotificationContainer,
+  adminNotificationContainer,
+  adminMarkDownContainer,
+  adminUserGroupDetailContainer,
+];
+
+logger.info('unstated containers have been initialized');
+
+/**
+ * define components
+ *  key: id of element
+ *  value: React Element
+ */
+Object.assign(componentMappings, {
+  'admin-home': <AdminHome />,
+  'admin-app': <AppSettingsPage />,
+  'admin-markdown-setting': <MarkdownSetting />,
+  'admin-customize': <Customize />,
+  'admin-importer': <ImportDataPage />,
+  'admin-export-page': <ExportArchiveDataPage />,
+  'admin-notification-setting': <NotificationSetting />,
+  'admin-global-notification-setting': <ManageGlobalNotification />,
+  'admin-user-page': <UserManagement />,
+  'admin-external-account-setting': <ManageExternalAccount />,
+  'admin-user-group-detail': <UserGroupDetailPage />,
+  'admin-full-text-search-management': <FullTextSearchManagement />,
+  'admin-user-group-page': <UserGroupPage />,
+  'admin-navigation': <AdminNavigation />,
+});
+
+
+Object.keys(componentMappings).forEach((key) => {
+  const elem = document.getElementById(key);
+  if (elem) {
+    ReactDOM.render(
+      <I18nextProvider i18n={i18n}>
+        <Provider inject={injectableContainers}>
+          {componentMappings[key]}
+        </Provider>
+      </I18nextProvider>,
+      elem,
+    );
+  }
+});

+ 11 - 139
src/client/js/app.jsx

@@ -4,9 +4,7 @@ import { Provider } from 'unstated';
 import { I18nextProvider } from 'react-i18next';
 
 import loggerFactory from '@alias/logger';
-import Xss from '@commons/service/xss';
 
-import HeaderSearchBox from './components/HeaderSearchBox';
 import SearchPage from './components/SearchPage';
 import TagsList from './components/TagsList';
 import PageEditor from './components/PageEditor';
@@ -29,52 +27,25 @@ import BookmarkButton from './components/BookmarkButton';
 import LikeButton from './components/LikeButton';
 import PagePathAutoComplete from './components/PagePathAutoComplete';
 import RecentCreated from './components/RecentCreated/RecentCreated';
-import StaffCredit from './components/StaffCredit/StaffCredit';
 import MyDraftList from './components/MyDraftList/MyDraftList';
 import UserPictureList from './components/User/UserPictureList';
 import TableOfContents from './components/TableOfContents';
 
-import AdminHome from './components/Admin/AdminHome/AdminHome';
-import UserGroupDetailPage from './components/Admin/UserGroupDetail/UserGroupDetailPage';
-import MarkdownSetting from './components/Admin/MarkdownSetting/MarkDownSetting';
-import UserManagement from './components/Admin/UserManagement';
-import AppSettingsPage from './components/Admin/App/AppSettingsPage';
-import ManageExternalAccount from './components/Admin/ManageExternalAccount';
-import UserGroupPage from './components/Admin/UserGroup/UserGroupPage';
-import Customize from './components/Admin/Customize/Customize';
-import ImportDataPage from './components/Admin/ImportDataPage';
-import ExportArchiveDataPage from './components/Admin/ExportArchiveDataPage';
-import FullTextSearchManagement from './components/Admin/FullTextSearchManagement';
 import PersonalSettings from './components/Me/PersonalSettings';
-
-import AppContainer from './services/AppContainer';
 import PageContainer from './services/PageContainer';
 import CommentContainer from './services/CommentContainer';
 import EditorContainer from './services/EditorContainer';
 import TagContainer from './services/TagContainer';
-import AdminHomeContainer from './services/AdminHomeContainer';
-import AdminCustomizeContainer from './services/AdminCustomizeContainer';
-import UserGroupDetailContainer from './services/UserGroupDetailContainer';
-import AdminUsersContainer from './services/AdminUsersContainer';
-import AdminAppContainer from './services/AdminAppContainer';
-import WebsocketContainer from './services/WebsocketContainer';
-import AdminMarkDownContainer from './services/AdminMarkDownContainer';
-import AdminExternalAccountsContainer from './services/AdminExternalAccountsContainer';
 import PersonalContainer from './services/PersonalContainer';
 
-const logger = loggerFactory('growi:app');
+import { appContainer, componentMappings } from './bootstrap';
 
-if (!window) {
-  window = {};
-}
+const logger = loggerFactory('growi:app');
 
-// setup xss library
-const xss = new Xss();
-window.xss = xss;
+const { i18n } = appContainer;
+const websocketContainer = appContainer.getContainer('WebsocketContainer');
 
 // create unstated container instance
-const appContainer = new AppContainer();
-const websocketContainer = new WebsocketContainer(appContainer);
 const pageContainer = new PageContainer(appContainer);
 const commentContainer = new CommentContainer(appContainer);
 const editorContainer = new EditorContainer(appContainer, defaultEditorOptions, defaultPreviewOptions);
@@ -85,19 +56,12 @@ const injectableContainers = [
 
 logger.info('unstated containers have been initialized');
 
-appContainer.initPlugins();
-appContainer.injectToWindow();
-
-const i18n = appContainer.i18n;
-
 /**
  * define components
  *  key: id of element
  *  value: React Element
  */
-let componentMappings = {
-  'search-top': <HeaderSearchBox />,
-  'search-sidebar': <HeaderSearchBox crowi={appContainer} />,
+Object.assign(componentMappings, {
   'search-page': <SearchPage crowi={appContainer} />,
 
   // 'revision-history': <PageHistory pageId={pageId} />,
@@ -112,16 +76,11 @@ let componentMappings = {
 
   'user-created-list': <RecentCreated />,
   'user-draft-list': <MyDraftList />,
-
-  'admin-full-text-search-management': <FullTextSearchManagement />,
-
-  'staff-credit': <StaffCredit />,
-  'admin-importer': <ImportDataPage />,
-};
+});
 
 // additional definitions if data exists
 if (pageContainer.state.pageId != null) {
-  componentMappings = Object.assign({
+  Object.assign(componentMappings, {
     'page-editor-with-hackmd': <PageEditorByHackmd />,
     'page-comments-list': <PageComments />,
     'page-attachment': <PageAttachment />,
@@ -135,15 +94,15 @@ if (pageContainer.state.pageId != null) {
     'bookmark-button-lg': <BookmarkButton pageId={pageContainer.state.pageId} crowi={appContainer} size="lg" />,
     'rename-page-name-input': <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} />,
     'duplicate-page-name-input': <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} />,
-  }, componentMappings);
+  });
 }
 if (pageContainer.state.path != null) {
-  componentMappings = Object.assign({
+  Object.assign(componentMappings, {
     // eslint-disable-next-line quote-props
     'page': <Page />,
     'revision-path': <RevisionPath behaviorType={appContainer.config.behaviorType} pageId={pageContainer.state.pageId} pagePath={pageContainer.state.path} />,
     'tag-label': <TagLabels />,
-  }, componentMappings);
+  });
 }
 
 Object.keys(componentMappings).forEach((key) => {
@@ -160,94 +119,6 @@ Object.keys(componentMappings).forEach((key) => {
   }
 });
 
-// create unstated container instance for admin
-const adminHomeContainer = new AdminHomeContainer(appContainer);
-const adminCustomizeContainer = new AdminCustomizeContainer(appContainer);
-const adminUsersContainer = new AdminUsersContainer(appContainer);
-const adminExternalAccountsContainer = new AdminExternalAccountsContainer(appContainer);
-const adminMarkDownContainer = new AdminMarkDownContainer(appContainer);
-const adminContainers = {
-  'admin-home': adminHomeContainer,
-  'admin-customize': adminCustomizeContainer,
-  'admin-user-page': adminUsersContainer,
-  'admin-external-account-setting': adminExternalAccountsContainer,
-  'admin-markdown-setting': adminMarkDownContainer,
-  'admin-export-page': websocketContainer,
-};
-
-// render for admin
-const adminAppElem = document.getElementById('admin-app');
-if (adminAppElem != null) {
-  const adminAppContainer = new AdminAppContainer(appContainer);
-  ReactDOM.render(
-    <Provider inject={[injectableContainers, adminAppContainer]}>
-      <I18nextProvider i18n={i18n}>
-        <AppSettingsPage />
-      </I18nextProvider>
-    </Provider>,
-    adminAppElem,
-  );
-}
-
-/**
- * define components
- *  key: id of element
- *  value: React Element
- */
-const adminComponentMappings = {
-  'admin-home': <AdminHome />,
-  'admin-customize': <Customize />,
-  'admin-user-page': <UserManagement />,
-  'admin-external-account-setting': <ManageExternalAccount />,
-  'admin-markdown-setting': <MarkdownSetting />,
-  'admin-export-page': <ExportArchiveDataPage crowi={appContainer} />,
-};
-
-
-Object.keys(adminComponentMappings).forEach((key) => {
-  const adminElem = document.getElementById(key);
-  if (adminElem) {
-    ReactDOM.render(
-      <Provider inject={[injectableContainers, adminContainers[key]]}>
-        <I18nextProvider i18n={i18n}>
-          {adminComponentMappings[key]}
-        </I18nextProvider>
-      </Provider>,
-      adminElem,
-    );
-  }
-});
-
-const adminUserGroupDetailElem = document.getElementById('admin-user-group-detail');
-if (adminUserGroupDetailElem != null) {
-  const userGroupDetailContainer = new UserGroupDetailContainer(appContainer);
-  ReactDOM.render(
-    <Provider inject={[userGroupDetailContainer]}>
-      <I18nextProvider i18n={i18n}>
-        <UserGroupDetailPage />
-      </I18nextProvider>
-    </Provider>,
-    adminUserGroupDetailElem,
-  );
-}
-
-const adminUserGroupPageElem = document.getElementById('admin-user-group-page');
-if (adminUserGroupPageElem != null) {
-  const isAclEnabled = adminUserGroupPageElem.getAttribute('data-isAclEnabled') === 'true';
-
-  ReactDOM.render(
-    <Provider inject={[websocketContainer]}>
-      <I18nextProvider i18n={i18n}>
-        <UserGroupPage
-          crowi={appContainer}
-          isAclEnabled={isAclEnabled}
-        />
-      </I18nextProvider>
-    </Provider>,
-    adminUserGroupPageElem,
-  );
-}
-
 const personalSettingsElem = document.getElementById('personal-setting');
 if (personalSettingsElem != null) {
   const personalContainer = new PersonalContainer(appContainer);
@@ -263,6 +134,7 @@ if (personalSettingsElem != null) {
   );
 }
 
+
 // うわーもうー (commented by Crowi team -- 2018.03.23 Yuki Takei)
 $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', () => {
   ReactDOM.render(

+ 44 - 0
src/client/js/bootstrap.jsx

@@ -0,0 +1,44 @@
+import React from 'react';
+
+import loggerFactory from '@alias/logger';
+import Xss from '@commons/service/xss';
+
+import HeaderSearchBox from './components/HeaderSearchBox';
+import StaffCredit from './components/StaffCredit/StaffCredit';
+
+import AppContainer from './services/AppContainer';
+import WebsocketContainer from './services/WebsocketContainer';
+
+const logger = loggerFactory('growi:app');
+
+if (!window) {
+  window = {};
+}
+
+// setup xss library
+const xss = new Xss();
+window.xss = xss;
+
+// create unstated container instance
+const appContainer = new AppContainer();
+// eslint-disable-next-line no-unused-vars
+const websocketContainer = new WebsocketContainer(appContainer);
+
+logger.info('unstated containers have been initialized');
+
+appContainer.initPlugins();
+appContainer.injectToWindow();
+
+/**
+ * define components
+ *  key: id of element
+ *  value: React Element
+ */
+const componentMappings = {
+  'search-top': <HeaderSearchBox />,
+  'search-sidebar': <HeaderSearchBox crowi={appContainer} />,
+
+  'staff-credit': <StaffCredit />,
+};
+
+export { appContainer, componentMappings };

+ 4 - 4
src/client/js/components/Admin/AdminHome/AdminHome.jsx

@@ -34,21 +34,21 @@ class AdminHome extends React.Component {
     return (
       <Fragment>
         <p>
-          {t('admin_top:wiki_administrator')}
+          {t('admin:admin_top.wiki_administrator')}
           <br></br>
-          {t('admin_top:assign_administrator')}
+          {t('admin:admin_top.assign_administrator')}
         </p>
 
         <div className="row mb-5">
           <div className="col-md-12">
-            <h2 className="admin-setting-header">{t('admin_top:system_information')}</h2>
+            <h2 className="admin-setting-header">{t('admin:admin_top.system_information')}</h2>
             <SystemInfomationTable />
           </div>
         </div>
 
         <div className="row mb-5">
           <div className="col-md-12">
-            <h2 className="admin-setting-header">{t('admin_top:list_of_installed_plugins')}</h2>
+            <h2 className="admin-setting-header">{t('admin:admin_top.list_of_installed_plugins')}</h2>
             <InstalledPluginTable />
           </div>
         </div>

+ 3 - 3
src/client/js/components/Admin/AdminHome/InstalledPluginTable.jsx

@@ -15,9 +15,9 @@ class InstalledPluginTable extends React.Component {
       <table className="table table-bordered">
         <thead>
           <tr>
-            <th className="text-center">{t('admin_top:package_name')}</th>
-            <th className="text-center">{t('admin_top:specified_version')}</th>
-            <th className="text-center">{t('admin_top:installed_version')}</th>
+            <th className="text-center">{t('admin:admin_top.package_name')}</th>
+            <th className="text-center">{t('admin:admin_top.specified_version')}</th>
+            <th className="text-center">{t('admin:admin_top.installed_version')}</th>
           </tr>
         </thead>
         <tbody>

+ 19 - 19
src/client/js/components/Admin/App/AppSetting.jsx

@@ -25,7 +25,7 @@ class AppSetting extends React.Component {
 
     try {
       await adminAppContainer.updateAppSettingHandler();
-      toastSuccess(t('toaster:update_successed', { target: 'App' }));
+      toastSuccess(t('toaster.update_successed', { target: t('App Settings') }));
     }
     catch (err) {
       toastError(err);
@@ -38,36 +38,36 @@ class AppSetting extends React.Component {
 
     return (
       <React.Fragment>
-        <div className="row md-5">
-          <label className="col-xs-3 control-label">{t('app_setting:site_name')}</label>
+        <div className="row mb-5">
+          <label className="col-xs-3 control-label">{t('admin:app_setting.site_name')}</label>
           <div className="col-xs-6">
             <input
               className="form-control"
               type="text"
-              defaultValue={adminAppContainer.state.title}
+              defaultValue={adminAppContainer.state.title || ''}
               onChange={(e) => { adminAppContainer.changeTitle(e.target.value) }}
               placeholder="GROWI"
             />
-            <p className="help-block">{t('app_setting:sitename_change')}</p>
+            <p className="help-block">{t('admin:app_setting.sitename_change')}</p>
           </div>
         </div>
 
-        <div className="row md-5">
-          <label className="col-xs-3 control-label">{t('app_setting:confidential_name')}</label>
+        <div className="row mb-5">
+          <label className="col-xs-3 control-label">{t('admin:app_setting.confidential_name')}</label>
           <div className="col-xs-6">
             <input
               className="form-control"
               type="text"
-              defaultValue={adminAppContainer.state.confidential}
+              defaultValue={adminAppContainer.state.confidential || ''}
               onChange={(e) => { adminAppContainer.changeConfidential(e.target.value) }}
-              placeholder={t('app_setting:confidential_example')}
+              placeholder={t('admin:app_setting.confidential_example')}
             />
-            <p className="help-block">{t('app_setting:header_content')}</p>
+            <p className="help-block">{t('admin:app_setting.header_content')}</p>
           </div>
         </div>
 
-        <div className="row md-5">
-          <label className="col-xs-3 control-label">{t('app_setting:default_language')}</label>
+        <div className="row mb-5">
+          <label className="col-xs-3 control-label">{t('admin:app_setting.default_language')}</label>
           <div className="col-xs-6">
             <div className="radio radio-primary radio-inline">
               <input
@@ -76,7 +76,7 @@ class AppSetting extends React.Component {
                 name="globalLang"
                 value="en-US"
                 checked={adminAppContainer.state.globalLang === 'en-US'}
-                onClick={(e) => { adminAppContainer.changeGlobalLang(e.target.value) }}
+                onChange={(e) => { adminAppContainer.changeGlobalLang(e.target.value) }}
               />
               <label htmlFor="radioLangEn">{t('English')}</label>
             </div>
@@ -87,15 +87,15 @@ class AppSetting extends React.Component {
                 name="globalLang"
                 value="ja"
                 checked={adminAppContainer.state.globalLang === 'ja'}
-                onClick={(e) => { adminAppContainer.changeGlobalLang(e.target.value) }}
+                onChange={(e) => { adminAppContainer.changeGlobalLang(e.target.value) }}
               />
               <label htmlFor="radioLangJa">{t('Japanese')}</label>
             </div>
           </div>
         </div>
 
-        <div className="row md-5">
-          <label className="col-xs-3 control-label">{t('app_setting:file_uploading')}</label>
+        <div className="row mb-5">
+          <label className="col-xs-3 control-label">{t('admin:app_setting.file_uploading')}</label>
           <div className="col-xs-6">
             <div className="checkbox checkbox-info">
               <input
@@ -105,13 +105,13 @@ class AppSetting extends React.Component {
                 checked={adminAppContainer.state.fileUpload}
                 onChange={(e) => { adminAppContainer.changeFileUpload(e.target.checked) }}
               />
-              <label htmlFor="cbFileUpload">{t('app_setting:enable_files_except_image')}</label>
+              <label htmlFor="cbFileUpload">{t('admin:app_setting.enable_files_except_image')}</label>
             </div>
 
             <p className="help-block">
-              {t('app_setting:enable_files_except_image')}
+              {t('admin:app_setting.enable_files_except_image')}
               <br />
-              {t('app_setting:attach_enable')}
+              {t('admin:app_setting.attach_enable')}
             </p>
           </div>
         </div>

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

@@ -53,21 +53,21 @@ class AppSettingsPage extends React.Component {
 
         <div className="row mt-5">
           <div className="col-md-12">
-            <h2 className="admin-setting-header">{t('app_setting:mail_settings')}</h2>
+            <h2 className="admin-setting-header">{t('admin:app_setting.mail_settings')}</h2>
             <MailSetting />
           </div>
         </div>
 
         <div className="row mt-5">
           <div className="col-md-12">
-            <h2 className="admin-setting-header">{t('app_setting:aws_settings')}</h2>
+            <h2 className="admin-setting-header">{t('admin:app_setting.aws_settings')}</h2>
             <AwsSetting />
           </div>
         </div>
 
         <div className="row mt-5">
           <div className="col-md-12">
-            <h2 className="admin-setting-header">{t('app_setting:plugin_settings')}</h2>
+            <h2 className="admin-setting-header">{t('admin:app_setting.plugin_settings')}</h2>
             <PluginSetting />
           </div>
         </div>

+ 14 - 14
src/client/js/components/Admin/App/AwsSetting.jsx

@@ -25,7 +25,7 @@ class AwsSetting extends React.Component {
 
     try {
       await adminAppContainer.updateAwsSettingHandler();
-      toastSuccess(t('toaster:update_successed', { target: 'AWS' }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:app_setting.aws_settings') }));
     }
     catch (err) {
       toastError(err);
@@ -39,26 +39,26 @@ class AwsSetting extends React.Component {
     return (
       <React.Fragment>
         <p className="well">
-          {t('app_setting:aws_access')}
+          {t('admin:app_setting.aws_access')}
           <br />
-          {t('app_setting:no_smtp_setting')}
+          {t('admin:app_setting.no_smtp_setting')}
           <br />
           <br />
           <span className="text-danger">
             <i className="ti-unlink"></i>
-            {t('app_setting:change_setting')}
+            {t('admin:app_setting.change_setting')}
           </span>
         </p>
 
         <div className="row mb-5">
           <label className="col-xs-3 control-label">
-            {t('app_setting:region')}
+            {t('admin:app_setting.region')}
           </label>
           <div className="col-xs-6">
             <input
               className="form-control"
               placeholder={`${t('eg')} ap-northeast-1`}
-              defaultValue={adminAppContainer.state.region}
+              defaultValue={adminAppContainer.state.region || ''}
               onChange={(e) => {
                 adminAppContainer.changeRegion(e.target.value);
               }}
@@ -68,32 +68,32 @@ class AwsSetting extends React.Component {
 
         <div className="row mb-5">
           <label className="col-xs-3 control-label">
-            {t('app_setting:custom_endpoint')}
+            {t('admin:app_setting.custom_endpoint')}
           </label>
           <div className="col-xs-6">
             <input
               className="form-control"
               type="text"
               placeholder={`${t('eg')} http://localhost:9000`}
-              defaultValue={adminAppContainer.state.customEndpoint}
+              defaultValue={adminAppContainer.state.customEndpoint || ''}
               onChange={(e) => {
                 adminAppContainer.changeCustomEndpoint(e.target.value);
               }}
             />
-            <p className="help-block">{t('app_setting:custom_endpoint_change')}</p>
+            <p className="help-block">{t('admin:app_setting.custom_endpoint_change')}</p>
           </div>
         </div>
 
         <div className="row mb-5">
           <label className="col-xs-3 control-label">
-            {t('app_setting:bucket_name')}
+            {t('admin:app_setting.bucket_name')}
           </label>
           <div className="col-xs-6">
             <input
               className="form-control"
               type="text"
               placeholder={`${t('eg')} crowi`}
-              defaultValue={adminAppContainer.state.bucket}
+              defaultValue={adminAppContainer.state.bucket || ''}
               onChange={(e) => {
                 adminAppContainer.changeBucket(e.target.value);
               }}
@@ -109,7 +109,7 @@ class AwsSetting extends React.Component {
             <input
               className="form-control"
               type="text"
-              defaultValue={adminAppContainer.state.accessKeyId}
+              defaultValue={adminAppContainer.state.accessKeyId || ''}
               onChange={(e) => {
                 adminAppContainer.changeAccessKeyId(e.target.value);
               }}
@@ -125,9 +125,9 @@ class AwsSetting extends React.Component {
             <input
               className="form-control"
               type="text"
-              defaultValue={adminAppContainer.state.secretKey}
+              defaultValue={adminAppContainer.state.secretAccessKey || ''}
               onChange={(e) => {
-                adminAppContainer.changeSecretKey(e.target.value);
+                adminAppContainer.changeSecretAccessKey(e.target.value);
               }}
             />
           </div>

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

@@ -25,7 +25,7 @@ class MailSetting extends React.Component {
 
     try {
       await adminAppContainer.updateMailSettingHandler();
-      toastSuccess(t('toster.update_successed', { target: 'Mail' }));
+      toastSuccess(t('toster.update_successed', { target: t('admin:app_setting.mail_settings') }));
     }
     catch (err) {
       toastError(err);
@@ -38,36 +38,36 @@ class MailSetting extends React.Component {
 
     return (
       <React.Fragment>
-        <p className="well">{t('app_setting:smtp_used')} {t('app_setting:smtp_but_aws')}<br />{t('app_setting:neihter_of')}</p>
+        <p className="well">{t('admin:app_setting.smtp_used')} {t('admin:app_setting.smtp_but_aws')}<br />{t('admin:app_setting.neihter_of')}</p>
         <div className="row mb-5">
-          <label className="col-xs-3 control-label">{t('app_setting:from_e-mail_address')}</label>
+          <label className="col-xs-3 control-label">{t('admin:app_setting.from_e-mail_address')}</label>
           <div className="col-xs-6">
             <input
               className="form-control"
               type="text"
               placeholder={`${t('eg')} mail@growi.org`}
-              defaultValue={adminAppContainer.state.fromAddress}
+              defaultValue={adminAppContainer.state.fromAddress || ''}
               onChange={(e) => { adminAppContainer.changeFromAddress(e.target.value) }}
             />
           </div>
         </div>
 
         <div className="row mb-5">
-          <label className="col-xs-3 control-label">{t('app_setting:smtp_settings')}</label>
+          <label className="col-xs-3 control-label">{t('admin:app_setting.smtp_settings')}</label>
           <div className="col-xs-4">
-            <label>{t('app_setting:host')}</label>
+            <label>{t('admin:app_setting.host')}</label>
             <input
               className="form-control"
               type="text"
-              defaultValue={adminAppContainer.state.smtpHost}
+              defaultValue={adminAppContainer.state.smtpHost || ''}
               onChange={(e) => { adminAppContainer.changeSmtpHost(e.target.value) }}
             />
           </div>
           <div className="col-xs-2">
-            <label>{t('app_setting:port')}</label>
+            <label>{t('admin:app_setting.port')}</label>
             <input
               className="form-control"
-              defaultValue={adminAppContainer.state.smtpPort}
+              defaultValue={adminAppContainer.state.smtpPort || ''}
               onChange={(e) => { adminAppContainer.changeSmtpPort(e.target.value) }}
             />
           </div>
@@ -75,11 +75,11 @@ class MailSetting extends React.Component {
 
         <div className="row mb-5">
           <div className="col-xs-3 col-xs-offset-3">
-            <label>{t('app_setting:user')}</label>
+            <label>{t('admin:app_setting.user')}</label>
             <input
               className="form-control"
               type="text"
-              defaultValue={adminAppContainer.state.SmtpUser}
+              defaultValue={adminAppContainer.state.SmtpUser || ''}
               onChange={(e) => { adminAppContainer.changeSmtpUser(e.target.value) }}
             />
           </div>
@@ -88,7 +88,7 @@ class MailSetting extends React.Component {
             <input
               className="form-control"
               type="password"
-              defaultValue={adminAppContainer.state.smtpPassword}
+              defaultValue={adminAppContainer.state.smtpPassword || ''}
               onChange={(e) => { adminAppContainer.changeSmtpPassword(e.target.value) }}
             />
           </div>

+ 3 - 3
src/client/js/components/Admin/App/PluginSetting.jsx

@@ -26,7 +26,7 @@ class PluginSetting extends React.Component {
 
     try {
       await adminAppContainer.updatePluginSettingHandler();
-      toastSuccess(t('toaster:update_successed', { target: 'Plugin' }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:app_setting.plugin_settings') }));
     }
     catch (err) {
       toastError(err);
@@ -39,7 +39,7 @@ class PluginSetting extends React.Component {
 
     return (
       <React.Fragment>
-        <p className="well">{t('app_setting:enable_plugin_loading')}</p>
+        <p className="well">{t('admin:app_setting.enable_plugin_loading')}</p>
 
         <div className="row mb-5">
           <div className="col-xs-offset-3 col-xs-6 text-left">
@@ -52,7 +52,7 @@ class PluginSetting extends React.Component {
                   adminAppContainer.changeIsEnabledPlugins(e.target.checked);
                 }}
               />
-              <label htmlFor="isEnabledPlugins">{t('app_setting:load_plugins')}</label>
+              <label htmlFor="isEnabledPlugins">{t('admin:app_setting.load_plugins')}</label>
             </div>
           </div>
         </div>

+ 8 - 7
src/client/js/components/Admin/App/SiteUrlSetting.jsx

@@ -25,7 +25,7 @@ class SiteUrlSetting extends React.Component {
 
     try {
       await adminAppContainer.updateSiteUrlSettingHandler();
-      toastSuccess(t('toaster:update_successed', { target: 'URL' }));
+      toastSuccess(t('toaster.update_successed', { target: t('Site URL settings') }));
     }
     catch (err) {
       toastError(err);
@@ -38,8 +38,9 @@ class SiteUrlSetting extends React.Component {
 
     return (
       <React.Fragment>
-        <p className="well">{t('app_setting:site_url_desc')}</p>
-        {!adminAppContainer.state.isSetSiteUrl && (<p className="alert alert-danger"><i className="icon-exclamation"></i> {t('app_setting:site_url_warn')}</p>)}
+        <p className="well">{t('admin:app_setting.site_url_desc')}</p>
+        {!adminAppContainer.state.isSetSiteUrl
+          && (<p className="alert alert-danger"><i className="icon-exclamation"></i> {t('admin:app_setting.site_url_warn')}</p>)}
 
         <div className="row">
           <div className="col-md-12">
@@ -62,20 +63,20 @@ class SiteUrlSetting extends React.Component {
                         className="form-control"
                         type="text"
                         name="settingForm[app:siteUrl]"
-                        defaultValue={adminAppContainer.state.siteUrl}
+                        defaultValue={adminAppContainer.state.siteUrl || ''}
                         onChange={(e) => { adminAppContainer.changeSiteUrl(e.target.value) }}
                         placeholder="e.g. https://my.growi.org"
                       />
                       <p className="help-block">
                         {/* eslint-disable-next-line react/no-danger */}
-                        <div dangerouslySetInnerHTML={{ __html: t('app_setting:siteurl_help') }} />
+                        <span dangerouslySetInnerHTML={{ __html: t('admin:app_setting.siteurl_help') }} />
                       </p>
                     </td>
                     <td>
-                      <input className="form-control" type="text" value={adminAppContainer.state.envSiteUrl} readOnly />
+                      <input className="form-control" type="text" value={adminAppContainer.state.envSiteUrl || ''} readOnly />
                       <p className="help-block">
                         {/* eslint-disable-next-line react/no-danger */}
-                        <div dangerouslySetInnerHTML={{ __html: t('app_setting:use_env_var_if_empty', { variable: 'APP_SITE_URL' }) }} />
+                        <span dangerouslySetInnerHTML={{ __html: t('admin:app_setting.use_env_var_if_empty', { variable: 'APP_SITE_URL' }) }} />
                       </p>
                     </td>
                   </tr>

+ 59 - 0
src/client/js/components/Admin/Common/AdminNavigation.jsx

@@ -0,0 +1,59 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+import urljoin from 'url-join';
+
+const AdminNavigation = (props) => {
+  const { t } = props;
+  const pathname = window.location.pathname;
+
+  const isActiveMenu = (path) => {
+    return (pathname.startsWith(urljoin('/admin', path)));
+  };
+
+  return (
+    <ul className="nav nav-pills nav-stacked">
+      <li className={`${pathname === '/admin' && 'active'}`}>
+        <a href="/admin"><i className="icon-fw icon-home"></i> { t('Management Wiki Home') }</a>
+      </li>
+      <li className={`${isActiveMenu('/app') && 'active'}`}>
+        <a href="/admin/app"><i className="icon-fw icon-settings"></i> { t('App Settings') }</a>
+      </li>
+      <li className={`${isActiveMenu('/security') && 'active'}`}>
+        <a href="/admin/security"><i className="icon-fw icon-shield"></i> { t('security_settings') }</a>
+      </li>
+      <li className={`${isActiveMenu('/markdown') && 'active'}`}>
+        <a href="/admin/markdown"><i className="icon-fw icon-note"></i> { t('Markdown Settings') }</a>
+      </li>
+      <li className={`${isActiveMenu('/customize') && 'active'}`}>
+        <a href="/admin/customize"><i className="icon-fw icon-wrench"></i> { t('Customize') }</a>
+      </li>
+      <li className={`${isActiveMenu('/importer') && 'active'}`}>
+        <a href="/admin/importer"><i className="icon-fw icon-cloud-upload"></i> { t('Import Data') }</a>
+      </li>
+      <li className={`${isActiveMenu('/export') && 'active'}`}>
+        <a href="/admin/export"><i className="icon-fw icon-cloud-download"></i> { t('Export Archive Data') }</a>
+      </li>
+      <li className={`${(isActiveMenu('/notification') || isActiveMenu('/global-notification')) && 'active'}`}>
+        <a href="/admin/notification"><i className="icon-fw icon-bell"></i> { t('Notification Settings') }</a>
+      </li>
+      <li className={`${(isActiveMenu('/users')) && 'active'}`}>
+        <a href="/admin/users"><i className="icon-fw icon-user"></i> { t('User_Management') }</a>
+      </li>
+      <li className={`${isActiveMenu('/user-group') && 'active'}`}>
+        <a href="/admin/user-groups"><i className="icon-fw icon-people"></i> { t('UserGroup Management') }</a>
+      </li>
+      <li className={`${isActiveMenu('/search') && 'active'}`}>
+        <a href="/admin/search"><i className="icon-fw icon-magnifier"></i> { t('Full Text Search Management') }</a>
+      </li>
+    </ul>
+  );
+};
+
+
+AdminNavigation.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+
+};
+
+export default withTranslation()(AdminNavigation);

+ 1 - 0
src/client/js/components/Admin/CustomCssEditor.jsx

@@ -21,6 +21,7 @@ export default class CustomCssEditor extends React.Component {
       <CodeMirror
         value={this.props.value}
         autoFocus
+        detach
         options={{
           mode: 'css',
           lineNumbers: true,

+ 1 - 0
src/client/js/components/Admin/CustomHeaderEditor.jsx

@@ -19,6 +19,7 @@ export default class CustomHeaderEditor extends React.Component {
       <CodeMirror
         value={this.props.value}
         autoFocus
+        detach
         options={{
           mode: 'htmlmixed',
           lineNumbers: true,

+ 1 - 0
src/client/js/components/Admin/CustomScriptEditor.jsx

@@ -21,6 +21,7 @@ export default class CustomScriptEditor extends React.Component {
       <CodeMirror
         value={this.props.value}
         autoFocus
+        detach
         options={{
           mode: 'javascript',
           lineNumbers: true,

+ 13 - 6
src/client/js/components/Admin/Customize/Customize.jsx

@@ -3,8 +3,6 @@ import React, { Fragment } from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import loggerFactory from '@alias/logger';
-
 import AppContainer from '../../../services/AppContainer';
 import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 
@@ -20,25 +18,34 @@ import CustomizeScriptSetting from './CustomizeScriptSetting';
 import CustomizeHeaderSetting from './CustomizeHeaderSetting';
 import CustomizeTitle from './CustomizeTitle';
 
-const logger = loggerFactory('growi:Customize');
-
 class Customize extends React.Component {
 
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      isRetrieving: true,
+    };
+
+  }
+
   async componentDidMount() {
     const { adminCustomizeContainer } = this.props;
 
     try {
       await adminCustomizeContainer.retrieveCustomizeData();
+      this.setState({ isRetrieving: false });
     }
     catch (err) {
       toastError(err);
-      adminCustomizeContainer.setState({ retrieveError: err });
-      logger.error(err);
     }
 
   }
 
   render() {
+    if (this.state.isRetrieving) {
+      return null;
+    }
 
     return (
       <Fragment>

+ 11 - 15
src/client/js/components/Admin/Customize/CustomizeBehaviorSetting.jsx

@@ -3,8 +3,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import loggerFactory from '@alias/logger';
-
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
@@ -14,7 +12,6 @@ import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import CustomizeBehaviorOption from './CustomizeBehaviorOption';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
-const logger = loggerFactory('growi:Customize');
 
 class CustomizeBehaviorSetting extends React.Component {
 
@@ -29,11 +26,10 @@ class CustomizeBehaviorSetting extends React.Component {
 
     try {
       await adminCustomizeContainer.updateCustomizeBehavior();
-      toastSuccess(t('toaster:update_successed', { target: 'Behavior' }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.behavior') }));
     }
     catch (err) {
       toastError(err);
-      logger.error(err);
     }
   }
 
@@ -42,19 +38,19 @@ class CustomizeBehaviorSetting extends React.Component {
 
     return (
       <React.Fragment>
-        <h2 className="admin-setting-header">{t('customize_setting:behavior')}</h2>
+        <h2 className="admin-setting-header">{t('admin:customize_setting.behavior')}</h2>
         <div className="row">
           <div className="col-xs-6">
             <CustomizeBehaviorOption
               behaviorType="growi"
               isSelected={adminCustomizeContainer.state.currentBehavior === 'growi'}
               onSelected={() => adminCustomizeContainer.switchBehaviorType('growi')}
-              labelHtml={`GROWI Simplified Behavior <small class="text-success">${t('customize_setting:recommended')}</small>`}
+              labelHtml={`GROWI Simplified Behavior <small class="text-success">${t('admin:customize_setting.recommended')}</small>`}
             >
               <ul>
-                <li><span dangerouslySetInnerHTML={{ __html: t('customize_setting:behavior_desc.growi_text1') }} /></li>
-                <li><span dangerouslySetInnerHTML={{ __html: t('customize_setting:behavior_desc.growi_text2') }} /></li>
-                <li><span dangerouslySetInnerHTML={{ __html: t('customize_setting:behavior_desc.growi_text3') }} /></li>
+                <li><span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.behavior_desc.growi_text1') }} /></li>
+                <li><span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.behavior_desc.growi_text2') }} /></li>
+                <li><span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.behavior_desc.growi_text3') }} /></li>
               </ul>
             </CustomizeBehaviorOption>
           </div>
@@ -67,13 +63,13 @@ class CustomizeBehaviorSetting extends React.Component {
               labelHtml="Crowi Classic Behavior"
             >
               <ul>
-                <li><span dangerouslySetInnerHTML={{ __html: t('customize_setting:behavior_desc.crowi_text1') }} /></li>
-                <li><span dangerouslySetInnerHTML={{ __html: t('customize_setting:behavior_desc.crowi_text2') }} /></li>
+                <li><span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.behavior_desc.crowi_text1') }} /></li>
+                <li><span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.behavior_desc.crowi_text2') }} /></li>
                 <ul>
-                  <li><span dangerouslySetInnerHTML={{ __html: t('customize_setting:behavior_desc.crowi_text3') }} /></li>
+                  <li><span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.behavior_desc.crowi_text3') }} /></li>
                 </ul>
-                <li><span dangerouslySetInnerHTML={{ __html: t('customize_setting:behavior_desc.crowi_text4') }} /></li>
-                <li><span dangerouslySetInnerHTML={{ __html: t('customize_setting:behavior_desc.crowi_text5') }} /></li>
+                <li><span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.behavior_desc.crowi_text4') }} /></li>
+                <li><span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.behavior_desc.crowi_text5') }} /></li>
               </ul>
             </CustomizeBehaviorOption>
           </div>

+ 6 - 21
src/client/js/components/Admin/Customize/CustomizeCssSetting.jsx

@@ -2,8 +2,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import loggerFactory from '@alias/logger';
-
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
@@ -13,35 +11,23 @@ import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import CustomCssEditor from '../CustomCssEditor';
 
-const logger = loggerFactory('growi:Customize');
-
 class CustomizeCssSetting extends React.Component {
 
   constructor(props) {
     super(props);
 
-    this.state = {
-      editorInputValue: '',
-    };
-
     this.onClickSubmit = this.onClickSubmit.bind(this);
   }
 
-  componentDidMount() {
-    const { customizeCss } = this.props.appContainer.getConfig();
-    this.setState({ editorInputValue: customizeCss || '' });
-  }
-
   async onClickSubmit() {
     const { t, adminCustomizeContainer } = this.props;
 
     try {
       await adminCustomizeContainer.updateCustomizeCss();
-      toastSuccess(t('toaster:update_successed', { target: 'CustomCss' }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.custom_css') }));
     }
     catch (err) {
       toastError(err);
-      logger.error(err);
     }
   }
 
@@ -50,23 +36,22 @@ class CustomizeCssSetting extends React.Component {
 
     return (
       <React.Fragment>
-        <h2 className="admin-setting-header">{t('customize_setting:custom_css')}</h2>
+        <h2 className="admin-setting-header">{t('admin:customize_setting.custom_css')}</h2>
         <p className="well">
-          {t('customize_setting:write_css')}<br />
-          {t('customize_setting:reflect_change')}
+          {t('admin:customize_setting.write_css')}<br />
+          {t('admin:customize_setting.reflect_change')}
         </p>
         <div className="form-group">
           <div className="col-xs-12">
             <CustomCssEditor
-              // The value passed must be immutable
-              value={this.state.editorInputValue}
+              value={adminCustomizeContainer.state.currentCustomizeCss || ''}
               onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeCss(inputValue) }}
             />
           </div>
           <div className="col-xs-12">
             <p className="help-block text-right">
               <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
-              {t('customize_setting:ctrl_space')}
+              {t('admin:customize_setting.ctrl_space')}
             </p>
           </div>
         </div>

+ 16 - 21
src/client/js/components/Admin/Customize/CustomizeFunctionSetting.jsx

@@ -2,8 +2,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import loggerFactory from '@alias/logger';
-
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
@@ -13,8 +11,6 @@ import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import CustomizeFunctionOption from './CustomizeFunctionOption';
 
-const logger = loggerFactory('growi:importer');
-
 class CustomizeBehaviorSetting extends React.Component {
 
   constructor(props) {
@@ -28,11 +24,10 @@ class CustomizeBehaviorSetting extends React.Component {
 
     try {
       await adminCustomizeContainer.updateCustomizeFunction();
-      toastSuccess(t('toaster:update_successed', { target: 'Function' }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.function') }));
     }
     catch (err) {
       toastError(err);
-      logger.error(err);
     }
   }
 
@@ -41,21 +36,21 @@ class CustomizeBehaviorSetting extends React.Component {
 
     return (
       <React.Fragment>
-        <h2 className="admin-setting-header">{t('customize_setting:function')}</h2>
-        <p className="well">{t('customize_setting:function_desc')}</p>
+        <h2 className="admin-setting-header">{t('admin:customize_setting.function')}</h2>
+        <p className="well">{t('admin:customize_setting.function_desc')}</p>
 
         <div className="form-group row">
           <div className="col-xs-offset-3 col-xs-6 text-left">
             <CustomizeFunctionOption
               optionId="isEnabledTimeline"
-              label={t('customize_setting:function_options.timeline')}
+              label={t('admin:customize_setting.function_options.timeline')}
               isChecked={adminCustomizeContainer.state.isEnabledTimeline}
               onChecked={() => { adminCustomizeContainer.switchEnableTimeline() }}
             >
               <p className="help-block">
-                {t('customize_setting:function_options.timeline_desc1')}<br />
-                {t('customize_setting:function_options.timeline_desc2')}<br />
-                {t('customize_setting:function_options.timeline_desc3')}
+                {t('admin:customize_setting.function_options.timeline_desc1')}<br />
+                {t('admin:customize_setting.function_options.timeline_desc2')}<br />
+                {t('admin:customize_setting.function_options.timeline_desc3')}
               </p>
             </CustomizeFunctionOption>
           </div>
@@ -65,13 +60,13 @@ class CustomizeBehaviorSetting extends React.Component {
           <div className="col-xs-offset-3 col-xs-6 text-left">
             <CustomizeFunctionOption
               optionId="isSavedStatesOfTabChanges"
-              label={t('customize_setting:function_options.tab_switch')}
+              label={t('admin:customize_setting.function_options.tab_switch')}
               isChecked={adminCustomizeContainer.state.isSavedStatesOfTabChanges}
               onChecked={() => { adminCustomizeContainer.switchSavedStatesOfTabChanges() }}
             >
               <p className="help-block">
-                {t('customize_setting:function_options.tab_switch_desc1')}<br />
-                {t('customize_setting:function_options.tab_switch_desc2')}
+                {t('admin:customize_setting.function_options.tab_switch_desc1')}<br />
+                {t('admin:customize_setting.function_options.tab_switch_desc2')}
               </p>
             </CustomizeFunctionOption>
           </div>
@@ -81,12 +76,12 @@ class CustomizeBehaviorSetting extends React.Component {
           <div className="col-xs-offset-3 col-xs-6 text-left">
             <CustomizeFunctionOption
               optionId="isEnabledAttachTitleHeader"
-              label={t('customize_setting:function_options.attach_title_header')}
+              label={t('admin:customize_setting.function_options.attach_title_header')}
               isChecked={adminCustomizeContainer.state.isEnabledAttachTitleHeader}
               onChecked={() => { adminCustomizeContainer.switchEnabledAttachTitleHeader() }}
             >
               <p className="help-block">
-                {t('customize_setting:function_options.attach_title_header_desc')}
+                {t('admin:customize_setting.function_options.attach_title_header_desc')}
               </p>
             </CustomizeFunctionOption>
           </div>
@@ -95,7 +90,7 @@ class CustomizeBehaviorSetting extends React.Component {
         <div className="form-group row">
           <div className="col-xs-offset-3 col-xs-6 text-left">
             <div className="my-0 btn-group">
-              <label>{t('customize_setting:function_options.recent_created__n_draft_num_desc')}</label>
+              <label>{t('admin:customize_setting.function_options.recent_created__n_draft_num_desc')}</label>
               <div className="dropdown">
                 <button className="btn btn-default dropdown-toggle w-100" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                   <span className="pull-left">{adminCustomizeContainer.state.currentRecentCreatedLimit}</span>
@@ -117,7 +112,7 @@ class CustomizeBehaviorSetting extends React.Component {
                 </ul>
               </div>
               <p className="help-block">
-                {t('customize_setting:function_options.recently_created_n_draft_num_desc')}
+                {t('admin:customize_setting.function_options.recently_created_n_draft_num_desc')}
               </p>
             </div>
           </div>
@@ -127,12 +122,12 @@ class CustomizeBehaviorSetting extends React.Component {
           <div className="col-xs-offset-3 col-xs-6 text-left">
             <CustomizeFunctionOption
               optionId="isEnabledStaleNotification"
-              label={t('customize_setting:function_options.stale_notification')}
+              label={t('admin:customize_setting.function_options.stale_notification')}
               isChecked={adminCustomizeContainer.state.isEnabledStaleNotification}
               onChecked={() => { adminCustomizeContainer.switchEnableStaleNotification() }}
             >
               <p className="help-block">
-                {t('customize_setting:function_options.stale_notification_desc')}
+                {t('admin:customize_setting.function_options.stale_notification_desc')}
               </p>
             </CustomizeFunctionOption>
           </div>

+ 5 - 20
src/client/js/components/Admin/Customize/CustomizeHeaderSetting.jsx

@@ -2,8 +2,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import loggerFactory from '@alias/logger';
-
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
@@ -13,35 +11,23 @@ import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import CustomHeaderEditor from '../CustomHeaderEditor';
 
-const logger = loggerFactory('growi:Customize');
-
 class CustomizeHeaderSetting extends React.Component {
 
   constructor(props) {
     super(props);
 
-    this.state = {
-      editorInputValue: '',
-    };
-
     this.onClickSubmit = this.onClickSubmit.bind(this);
   }
 
-  componentDidMount() {
-    const { customizeHeader } = this.props.appContainer.getConfig();
-    this.setState({ editorInputValue: customizeHeader || '' });
-  }
-
   async onClickSubmit() {
     const { t, adminCustomizeContainer } = this.props;
 
     try {
       await adminCustomizeContainer.updateCustomizeHeader();
-      toastSuccess(t('toaster:update_successed', { target: 'CustomHeader' }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.custom_header') }));
     }
     catch (err) {
       toastError(err);
-      logger.error(err);
     }
   }
 
@@ -50,12 +36,12 @@ class CustomizeHeaderSetting extends React.Component {
 
     return (
       <React.Fragment>
-        <h2 className="admin-setting-header">{t('customize_setting:custom_header')}</h2>
+        <h2 className="admin-setting-header">{t('admin:customize_setting.custom_header')}</h2>
 
         <p
           className="well"
           // eslint-disable-next-line react/no-danger
-          dangerouslySetInnerHTML={{ __html: t('customize_setting:custom_header_detail') }}
+          dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.custom_header_detail') }}
         />
 
         <div className="help-block">
@@ -68,15 +54,14 @@ class CustomizeHeaderSetting extends React.Component {
 
         <div className="col-xs-12">
           <CustomHeaderEditor
-            // The value passed must be immutable
-            value={this.state.editorInputValue}
+            value={adminCustomizeContainer.state.currentCustomizeHeader || ''}
             onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeHeader(inputValue) }}
           />
         </div>
         <div className="col-xs-12">
           <p className="help-block text-right">
             <i className="fa fa-fw fa-keyboard-o" aria-hidden="true"></i>
-            {t('customize_setting:ctrl_space')}
+            {t('admin:customize_setting.ctrl_space')}
           </p>
         </div>
 

+ 25 - 29
src/client/js/components/Admin/Customize/CustomizeHighlightSetting.jsx

@@ -3,8 +3,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import loggerFactory from '@alias/logger';
-
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
@@ -13,8 +11,6 @@ import AppContainer from '../../../services/AppContainer';
 import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
-const logger = loggerFactory('growi:customizeHighlight');
-
 class CustomizeHighlightSetting extends React.Component {
 
   constructor(props) {
@@ -28,29 +24,33 @@ class CustomizeHighlightSetting extends React.Component {
 
     try {
       await adminCustomizeContainer.updateHighlightJsStyle();
-      toastSuccess(t('toaster:update_successed', { target: 'Highlight' }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.code_highlight') }));
     }
     catch (err) {
       toastError(err);
-      logger.error(err);
     }
   }
 
-  getDemoFunction() {
-    return `function $initHighlight(block, cls) {
-    try {
+  renderHljsDemo() {
+    const { adminCustomizeContainer } = this.props;
 
-      if (cls.search(/\bno\-highlight\b/) !== -1) {
-        return \`\${process(block, true, 0x0F)} class="\${cls}"\`;
-      }
-    }
-    catch (e) {
-      /* handle exception */
-    }
-    for (let i = 0 / 2; i < classes.length; i++) {
-      if (checkCondition(classes[i]) === undefined) { console.log('undefined') }
-    }
-  };`;
+    /* eslint-disable max-len */
+    const html = `<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">MersenneTwister</span>(<span class="hljs-params">seed</span>) </span>{
+  <span class="hljs-keyword">if</span> (<span class="hljs-built_in">arguments</span>.length == <span class="hljs-number">0</span>) {
+    seed = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().getTime();
+  }
+
+  <span class="hljs-keyword">this</span>._mt = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Array</span>(<span class="hljs-number">624</span>);
+  <span class="hljs-keyword">this</span>.setSeed(seed);
+}</span>`;
+    /* eslint-enable max-len */
+
+    return (
+      <pre className={`hljs ${!adminCustomizeContainer.state.isHighlightJsStyleBorderEnabled && 'hljs-no-border'}`}>
+        {/* eslint-disable-next-line react/no-danger */}
+        <code dangerouslySetInnerHTML={{ __html: html }}></code>
+      </pre>
+    );
   }
 
   render() {
@@ -65,19 +65,19 @@ class CustomizeHighlightSetting extends React.Component {
 
       menuItem.push(
         <li key={styleId} role="presentation" type="button" onClick={() => adminCustomizeContainer.switchHighlightJsStyle(styleId, styleName, isBorderEnable)}>
-          <a role="menuitem">{styleName}</a>
+          <a role="button">{styleName}</a>
         </li>,
       );
     });
 
     return (
       <React.Fragment>
-        <h2 className="admin-setting-header">{t('customize_setting:code_highlight')}</h2>
+        <h2 className="admin-setting-header">{t('admin:customize_setting.code_highlight')}</h2>
 
         <div className="form-group row">
           <div className="col-xs-offset-3 col-xs-6 text-left">
             <div className="my-0 btn-group">
-              <label>{t('customize_setting:theme')}</label>
+              <label>{t('admin:customize_setting.theme')}</label>
               <div className="dropdown">
                 <button className="btn btn-default dropdown-toggle w-100" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                   <span className="pull-left">{adminCustomizeContainer.state.currentHighlightJsStyleName}</span>
@@ -91,7 +91,7 @@ class CustomizeHighlightSetting extends React.Component {
                 </ul>
               </div>
               {/* eslint-disable-next-line react/no-danger */}
-              <p className="help-block text-warning"><span dangerouslySetInnerHTML={{ __html: t('customize_setting:nocdn_desc') }} /></p>
+              <p className="help-block text-warning"><span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.nocdn_desc') }} /></p>
             </div>
           </div>
         </div>
@@ -115,11 +115,7 @@ class CustomizeHighlightSetting extends React.Component {
         <div className="help-block">
           <label>Examples:</label>
           <div className="wiki">
-            <pre className={`hljs ${!adminCustomizeContainer.state.isHighlightJsStyleBorderEnabled && 'hljs-no-border'}`}>
-              <code className="highlightjs-demo">
-                {this.getDemoFunction()}
-              </code>
-            </pre>
+            {this.renderHljsDemo()}
           </div>
         </div>
 

+ 13 - 13
src/client/js/components/Admin/Customize/CustomizeLayoutOptions.jsx

@@ -20,13 +20,13 @@ class CustomizeLayoutOptions extends React.Component {
             layoutType="crowi-plus"
             isSelected={adminCustomizeContainer.state.currentLayout === 'growi'}
             onSelected={() => adminCustomizeContainer.switchLayoutType('growi')}
-            labelHtml={`GROWI Enhanced Layout <small class="text-success">${t('customize_setting:recommended')}</small>`}
+            labelHtml={`GROWI Enhanced Layout <small class="text-success">${t('admin:customize_setting.recommended')}</small>`}
           >
-            <h4>{t('customize_setting:layout_desc.growi_title')}</h4>
+            <h4>{t('admin:customize_setting.layout_desc.growi_title')}</h4>
             <ul>
-              <li>{t('customize_setting:layout_desc.growi_text1')}</li>
-              <li>{t('customize_setting:layout_desc.growi_text2')}</li>
-              <li>{t('customize_setting:layout_desc.growi_text3')}</li>
+              <li>{t('admin:customize_setting.layout_desc.growi_text1')}</li>
+              <li>{t('admin:customize_setting.layout_desc.growi_text2')}</li>
+              <li>{t('admin:customize_setting.layout_desc.growi_text3')}</li>
             </ul>
           </CustomizeLayoutOption>
         </div>
@@ -38,11 +38,11 @@ class CustomizeLayoutOptions extends React.Component {
             onSelected={() => adminCustomizeContainer.switchLayoutType('kibela')}
             labelHtml="Kibela Like Layout"
           >
-            <h4>{t('customize_setting:layout_desc.kibela_title')}</h4>
+            <h4>{t('admin:customize_setting.layout_desc.kibela_title')}</h4>
             <ul>
-              <li>{t('customize_setting:layout_desc.kibela_text1')}</li>
-              <li>{t('customize_setting:layout_desc.kibela_text2')}</li>
-              <li>{t('customize_setting:layout_desc.kibela_text3')}</li>
+              <li>{t('admin:customize_setting.layout_desc.kibela_text1')}</li>
+              <li>{t('admin:customize_setting.layout_desc.kibela_text2')}</li>
+              <li>{t('admin:customize_setting.layout_desc.kibela_text3')}</li>
             </ul>
           </CustomizeLayoutOption>
         </div>
@@ -54,11 +54,11 @@ class CustomizeLayoutOptions extends React.Component {
             onSelected={() => adminCustomizeContainer.switchLayoutType('crowi')}
             labelHtml="Crowi Classic Layout"
           >
-            <h4>{t('customize_setting:layout_desc.crowi_title')}</h4>
+            <h4>{t('admin:customize_setting.layout_desc.crowi_title')}</h4>
             <ul>
-              <li>{t('customize_setting:layout_desc.crowi_text1')}</li>
-              <li>{t('customize_setting:layout_desc.crowi_text2')}</li>
-              <li>{t('customize_setting:layout_desc.crowi_text3')}</li>
+              <li>{t('admin:customize_setting.layout_desc.crowi_text1')}</li>
+              <li>{t('admin:customize_setting.layout_desc.crowi_text2')}</li>
+              <li>{t('admin:customize_setting.layout_desc.crowi_text3')}</li>
             </ul>
           </CustomizeLayoutOption>
         </div>

+ 3 - 9
src/client/js/components/Admin/Customize/CustomizeLayoutSetting.jsx

@@ -2,8 +2,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import loggerFactory from '@alias/logger';
-
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
@@ -14,9 +12,6 @@ import CustomizeThemeOptions from './CustomizeThemeOptions';
 import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
-const logger = loggerFactory('growi:importer');
-
-
 class CustomizeLayoutSetting extends React.Component {
 
   constructor(props) {
@@ -30,11 +25,10 @@ class CustomizeLayoutSetting extends React.Component {
 
     try {
       await adminCustomizeContainer.updateCustomizeLayoutAndTheme();
-      toastSuccess(t('toaster:update_successed', { target: 'Layout' }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.layout') }));
     }
     catch (err) {
       toastError(err);
-      logger.error(err);
     }
   }
 
@@ -54,9 +48,9 @@ class CustomizeLayoutSetting extends React.Component {
 
     return (
       <React.Fragment>
-        <h2 className="admin-setting-header">{t('customize_setting:layout')}</h2>
+        <h2 className="admin-setting-header">{t('admin:customize_setting.layout')}</h2>
         <CustomizeLayoutOptions />
-        <h2 className="admin-setting-header">{t('customize_setting:theme')}</h2>
+        <h2 className="admin-setting-header">{t('admin:customize_setting.theme')}</h2>
         {this.renderDevAlert()}
         <CustomizeThemeOptions />
         <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />

+ 6 - 21
src/client/js/components/Admin/Customize/CustomizeScriptSetting.jsx

@@ -2,8 +2,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import loggerFactory from '@alias/logger';
-
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
@@ -13,35 +11,23 @@ import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import CustomScriptEditor from '../CustomScriptEditor';
 
-const logger = loggerFactory('growi:customizeScript');
-
 class CustomizeScriptSetting extends React.Component {
 
   constructor(props) {
     super(props);
 
-    this.state = {
-      editorInputValue: '',
-    };
-
     this.onClickSubmit = this.onClickSubmit.bind(this);
   }
 
-  componentDidMount() {
-    const { customizeScript } = this.props.appContainer.getConfig();
-    this.setState({ editorInputValue: customizeScript || '' });
-  }
-
   async onClickSubmit() {
     const { t, adminCustomizeContainer } = this.props;
 
     try {
       await adminCustomizeContainer.updateCustomizeScript();
-      toastSuccess(t('toaster:update_successed', { target: 'CustomScript' }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.custom_script') }));
     }
     catch (err) {
       toastError(err);
-      logger.error(err);
     }
   }
 
@@ -58,10 +44,10 @@ class CustomizeScriptSetting extends React.Component {
 
     return (
       <React.Fragment>
-        <h2 className="admin-setting-header">{t('customize_setting:custom_script')}</h2>
+        <h2 className="admin-setting-header">{t('admin:customize_setting.custom_script')}</h2>
         <p className="well">
-          {t('customize_setting:write_java')}<br />
-          {t('customize_setting:reflect_change')}
+          {t('admin:customize_setting.write_java')}<br />
+          {t('admin:customize_setting.reflect_change')}
         </p>
 
         <div className="help-block">
@@ -89,15 +75,14 @@ class CustomizeScriptSetting extends React.Component {
         <div className="form-group">
           <div className="col-xs-12">
             <CustomScriptEditor
-              // The value passed must be immutable
-              value={this.state.editorInputValue}
+              value={adminCustomizeContainer.state.currentCustomizeScript || ''}
               onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeScript(inputValue) }}
             />
           </div>
           <div className="col-xs-12">
             <p className="help-block text-right">
               <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
-              {t('customize_setting:ctrl_space')}
+              {t('admin:customize_setting.ctrl_space')}
             </p>
           </div>
         </div>

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

@@ -2,16 +2,12 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import loggerFactory from '@alias/logger';
-
 import AppContainer from '../../../services/AppContainer';
 import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
-const logger = loggerFactory('growi:Customize');
-
 class CustomizeTitle extends React.Component {
 
   constructor(props) {
@@ -25,11 +21,10 @@ class CustomizeTitle extends React.Component {
 
     try {
       await adminCustomizeContainer.updateCustomizeTitle();
-      toastSuccess(t('toaster:update_successed', { target: 'CustomTitle' }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.custom_title') }));
     }
     catch (err) {
       toastError(err);
-      logger.error(err);
     }
   }
 
@@ -39,11 +34,11 @@ class CustomizeTitle extends React.Component {
 
     return (
       <React.Fragment>
-        <h2 className="admin-setting-header">{t('customize_setting:custom_title')}</h2>
+        <h2 className="admin-setting-header">{t('admin:customize_setting.custom_title')}</h2>
         <p
           className="well"
-          // eslint-disable-next-line react/no-danger, max-len
-          dangerouslySetInnerHTML={{ __html: t('customize_setting:custom_title_detail') }}
+          // eslint-disable-next-line react/no-danger
+          dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.custom_title_detail') }}
         />
         {/* TODO i18n */}
         <div className="help-block">

+ 15 - 3
src/client/js/components/Admin/ElasticsearchManagement/ElasticsearchManagement.jsx

@@ -18,6 +18,7 @@ class ElasticsearchManagement extends React.Component {
     super(props);
 
     this.state = {
+      isConfigured: null,
       isConnected: null,
       isRebuildingProcessing: false,
       isRebuildingCompleted: false,
@@ -68,6 +69,7 @@ class ElasticsearchManagement extends React.Component {
       const { info } = await appContainer.apiv3Get('/search/indices');
 
       this.setState({
+        isConfigured: true,
         isConnected: true,
 
         indicesData: info.indices,
@@ -75,9 +77,17 @@ class ElasticsearchManagement extends React.Component {
         isNormalized: info.isNormalized,
       });
     }
-    catch (e) {
+    catch (errors) {
       this.setState({ isConnected: false });
-      toastError(e);
+
+      // evaluate whether configured or not
+      for (const error of errors) {
+        if (error.code === 'search-service-unconfigured') {
+          this.setState({ isConfigured: false });
+        }
+      }
+
+      toastError(errors);
     }
   }
 
@@ -130,7 +140,7 @@ class ElasticsearchManagement extends React.Component {
   render() {
     const { t } = this.props;
     const {
-      isConnected, isRebuildingProcessing, isRebuildingCompleted,
+      isConfigured, isConnected, isRebuildingProcessing, isRebuildingCompleted,
       isNormalized, indicesData, aliasesData,
     } = this.state;
 
@@ -139,6 +149,7 @@ class ElasticsearchManagement extends React.Component {
         <div className="row">
           <div className="col-xs-12">
             <StatusTable
+              isConfigured={isConfigured}
               isConnected={isConnected}
               isNormalized={isNormalized}
               indicesData={indicesData}
@@ -154,6 +165,7 @@ class ElasticsearchManagement extends React.Component {
           <label className="col-xs-3 control-label">{ t('full_text_search_management.reconnect') }</label>
           <div className="col-xs-6">
             <ReconnectControls
+              isConfigured={isConfigured}
               isConnected={isConnected}
               onReconnectingRequested={this.reconnect}
             />

+ 3 - 2
src/client/js/components/Admin/ElasticsearchManagement/ReconnectControls.jsx

@@ -7,9 +7,9 @@ import { createSubscribedElement } from '../../UnstatedUtils';
 class ReconnectControls extends React.PureComponent {
 
   render() {
-    const { t, isConnected } = this.props;
+    const { t, isConfigured, isConnected } = this.props;
 
-    const isEnabled = (isConnected != null) && !isConnected;
+    const isEnabled = (isConfigured == null || isConfigured === true) && isConnected === false;
 
     return (
       <>
@@ -41,6 +41,7 @@ const ReconnectControlsWrapper = (props) => {
 ReconnectControls.propTypes = {
   t: PropTypes.func.isRequired, // i18next
 
+  isConfigured: PropTypes.bool,
   isConnected: PropTypes.bool,
   onReconnectingRequested: PropTypes.func.isRequired,
 };

+ 6 - 2
src/client/js/components/Admin/ElasticsearchManagement/StatusTable.jsx

@@ -95,11 +95,14 @@ class StatusTable extends React.PureComponent {
 
   render() {
     const { t } = this.props;
-    const { isConnected, isNormalized } = this.props;
+    const { isConfigured, isConnected, isNormalized } = this.props;
 
 
     let connectionStatusLabel = <span className="label label-default">――</span>;
-    if (isConnected != null) {
+    if (isConfigured != null && !isConfigured) {
+      connectionStatusLabel = <span className="label label-default">{ t('full_text_search_management.connection_status_label_unconfigured') }</span>;
+    }
+    else if (isConnected != null) {
       connectionStatusLabel = isConnected
         ? <span className="label label-success">{ t('full_text_search_management.connection_status_label_connected') }</span>
         : <span className="label label-danger">{ t('full_text_search_management.connection_status_label_disconnected') }</span>;
@@ -146,6 +149,7 @@ const StatusTableWrapper = (props) => {
 StatusTable.propTypes = {
   t: PropTypes.func.isRequired, // i18next
 
+  isConfigured: PropTypes.bool,
   isConnected: PropTypes.bool,
   isNormalized: PropTypes.bool,
   indicesData: PropTypes.object,

+ 4 - 6
src/client/js/components/Admin/ExportArchiveData/ArchiveFilesTable.jsx

@@ -5,24 +5,22 @@ import { format } from 'date-fns';
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
-// import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 import ArchiveFilesTableMenu from './ArchiveFilesTableMenu';
 
 class ArchiveFilesTable extends React.Component {
 
   render() {
-    // eslint-disable-next-line no-unused-vars
     const { t } = this.props;
 
     return (
       <table className="table table-bordered">
         <thead>
           <tr>
-            <th>{t('export_management:file')}</th>
-            <th>{t('export_management:growi_version')}</th>
-            <th>{t('export_management:collections')}</th>
-            <th>{t('export_management:exported_at')}</th>
+            <th>{t('admin:export_management.file')}</th>
+            <th>{t('admin:export_management.growi_version')}</th>
+            <th>{t('admin:export_management.collections')}</th>
+            <th>{t('admin:export_management.exported_at')}</th>
             <th></th>
           </tr>
         </thead>

+ 3 - 3
src/client/js/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.jsx

@@ -17,15 +17,15 @@ class ArchiveFilesTableMenu extends React.Component {
           <i className="icon-settings"></i> <span className="caret"></span>
         </button>
         <ul className="dropdown-menu" role="menu">
-          <li className="dropdown-header">{t('export_management:export_menu')}</li>
+          <li className="dropdown-header">{t('admin:export_management.export_menu')}</li>
           <li>
             <a type="button" href={`/admin/export/${this.props.fileName}`}>
-              <i className="icon-cloud-download" /> {t('export_management:download')}
+              <i className="icon-cloud-download" /> {t('admin:export_management.download')}
             </a>
           </li>
           <li>
             <a type="button" role="button" onClick={() => this.props.onZipFileStatRemove(this.props.fileName)}>
-              <span className="text-danger"><i className="icon-trash" /> {t('export_management:delete')}</span>
+              <span className="text-danger"><i className="icon-trash" /> {t('admin:export_management.delete')}</span>
             </a>
           </li>
         </ul>

+ 6 - 6
src/client/js/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx

@@ -117,7 +117,7 @@ class SelectCollectionsModal extends React.Component {
       return <></>;
     }
 
-    const html = this.props.t('export_management:desc_password_seed');
+    const html = this.props.t('admin:export_management.desc_password_seed');
 
     // eslint-disable-next-line react/no-danger
     return <div className="well well-sm" dangerouslySetInnerHTML={{ __html: html }}></div>;
@@ -172,7 +172,7 @@ class SelectCollectionsModal extends React.Component {
     return (
       <Modal show={this.props.isOpen} onHide={this.props.onClose}>
         <Modal.Header closeButton>
-          <Modal.Title>{t('export_management:export_collections')}</Modal.Title>
+          <Modal.Title>{t('admin:export_management.export_collections')}</Modal.Title>
         </Modal.Header>
 
         <form onSubmit={this.export}>
@@ -180,10 +180,10 @@ class SelectCollectionsModal extends React.Component {
             <div className="row">
               <div className="col-sm-12">
                 <button type="button" className="btn btn-sm btn-default mr-2" onClick={this.checkAll}>
-                  <i className="fa fa-check-square-o"></i> {t('export_management:check_all')}
+                  <i className="fa fa-check-square-o"></i> {t('admin:export_management.check_all')}
                 </button>
                 <button type="button" className="btn btn-sm btn-default mr-2" onClick={this.uncheckAll}>
-                  <i className="fa fa-square-o"></i> {t('export_management:uncheck_all')}
+                  <i className="fa fa-square-o"></i> {t('admin:export_management.uncheck_all')}
                 </button>
               </div>
             </div>
@@ -215,8 +215,8 @@ class SelectCollectionsModal extends React.Component {
           </Modal.Body>
 
           <Modal.Footer>
-            <button type="button" className="btn btn-sm btn-default" onClick={this.props.onClose}>{t('export_management:cancel')}</button>
-            <button type="submit" className="btn btn-sm btn-primary" disabled={!this.validateForm()}>{t('export_management:export')}</button>
+            <button type="button" className="btn btn-sm btn-default" onClick={this.props.onClose}>{t('admin:export_management.cancel')}</button>
+            <button type="submit" className="btn btn-sm btn-primary" disabled={!this.validateForm()}>{t('admin:export_management.export')}</button>
           </Modal.Footer>
         </form>
       </Modal>

+ 3 - 3
src/client/js/components/Admin/ExportArchiveDataPage.jsx

@@ -214,19 +214,19 @@ class ExportArchiveDataPage extends React.Component {
         <h2>{t('Export Archive Data')}</h2>
 
         <button type="button" className="btn btn-default" disabled={isExporting} onClick={this.openExportModal}>
-          {t('export_management:create_new_archive_data')}
+          {t('admin:export_management.create_new_archive_data')}
         </button>
 
         { showExportingData && (
           <div className="mt-5">
-            <h3>{t('export_management:exporting_collection_list')}</h3>
+            <h3>{t('admin:export_management.exporting_collection_list')}</h3>
             { this.renderProgressBarsForCollections() }
             { this.renderProgressBarForZipping() }
           </div>
         ) }
 
         <div className="mt-5">
-          <h3>{t('export_management:exported_data_list')}</h3>
+          <h3>{t('admin:export_management.exported_data_list')}</h3>
           <ArchiveFilesTable
             zipFileStats={this.state.zipFileStats}
             onZipFileStatRemove={this.onZipFileStatRemove}

+ 2 - 2
src/client/js/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx

@@ -57,7 +57,7 @@ class ImportCollectionConfigurationModal extends React.Component {
     const { t } = this.props;
     const { option } = this.state;
 
-    const translationBase = 'importer_management:growi_settings.configuration.pages';
+    const translationBase = 'admin:importer_management.growi_settings.configuration.pages';
 
     /* eslint-disable react/no-unescaped-entities */
     return (
@@ -149,7 +149,7 @@ class ImportCollectionConfigurationModal extends React.Component {
     const { t } = this.props;
     const { option } = this.state;
 
-    const translationBase = 'importer_management:growi_settings.configuration.revisions';
+    const translationBase = 'admin:importer_management.growi_settings.configuration.revisions';
 
     /* eslint-disable react/no-unescaped-entities */
     return (

+ 10 - 10
src/client/js/components/Admin/ImportData/GrowiArchive/ImportForm.jsx

@@ -222,7 +222,7 @@ class ImportForm extends React.Component {
     const { warnForOtherGroups, selectedCollections } = this.state;
 
     if (selectedCollections.size === 0) {
-      warnForOtherGroups.push(t('importer_management:growi_settings.errors.at_least_one'));
+      warnForOtherGroups.push(t('admin:importer_management.growi_settings.errors.at_least_one'));
     }
 
     this.setState({ warnForOtherGroups });
@@ -238,7 +238,7 @@ class ImportForm extends React.Component {
 
     // MUST be included both or neither when importing
     if (pageRelatedCollectionsLength !== 0 && pageRelatedCollectionsLength !== 2) {
-      warnForPageGroups.push(t('importer_management:growi_settings.errors.page_and_revision'));
+      warnForPageGroups.push(t('admin:importer_management.growi_settings.errors.page_and_revision'));
     }
 
     this.setState({ warnForPageGroups });
@@ -251,7 +251,7 @@ class ImportForm extends React.Component {
     // MUST include also 'users' if 'externalaccounts' is selected
     if (selectedCollections.has('externalaccounts')) {
       if (!selectedCollections.has('users')) {
-        warnForUserGroups.push(t('importer_management:growi_settings.errors.depends', { target: 'Users', condition: 'Externalaccounts' }));
+        warnForUserGroups.push(t('admin:importer_management.growi_settings.errors.depends', { target: 'Users', condition: 'Externalaccounts' }));
       }
     }
 
@@ -265,7 +265,7 @@ class ImportForm extends React.Component {
     // MUST include also 'users' if 'usergroups' is selected
     if (selectedCollections.has('usergroups')) {
       if (!selectedCollections.has('users')) {
-        warnForUserGroups.push(t('importer_management:growi_settings.errors.depends', { target: 'Users', condition: 'Usergroups' }));
+        warnForUserGroups.push(t('admin:importer_management.growi_settings.errors.depends', { target: 'Users', condition: 'Usergroups' }));
       }
     }
 
@@ -279,7 +279,7 @@ class ImportForm extends React.Component {
     // MUST include also 'usergroups' if 'usergrouprelations' is selected
     if (selectedCollections.has('usergrouprelations')) {
       if (!selectedCollections.has('usergroups')) {
-        warnForUserGroups.push(t('importer_management:growi_settings.errors.depends', { target: 'Usergroups', condition: 'Usergrouprelations' }));
+        warnForUserGroups.push(t('admin:importer_management.growi_settings.errors.depends', { target: 'Usergroups', condition: 'Usergrouprelations' }));
       }
     }
 
@@ -454,27 +454,27 @@ class ImportForm extends React.Component {
         <form className="form-inline">
           <div className="form-group">
             <button type="button" className="btn btn-sm btn-default mr-2" onClick={this.checkAll}>
-              <i className="fa fa-check-square-o"></i> {t('export_management:check_all')}
+              <i className="fa fa-check-square-o"></i> {t('admin:export_management.check_all')}
             </button>
           </div>
           <div className="form-group">
             <button type="button" className="btn btn-sm btn-default mr-2" onClick={this.uncheckAll}>
-              <i className="fa fa-square-o"></i> {t('export_management:uncheck_all')}
+              <i className="fa fa-square-o"></i> {t('admin:export_management.uncheck_all')}
             </button>
           </div>
         </form>
 
-        {this.renderGroups(GROUPS_PAGE, 'Page', warnForPageGroups, { wellContent: t('importer_management:growi_settings.overwrite_documents') })}
+        {this.renderGroups(GROUPS_PAGE, 'Page', warnForPageGroups, { wellContent: t('admin:importer_management.growi_settings.overwrite_documents') })}
         {this.renderGroups(GROUPS_USER, 'User', warnForUserGroups)}
         {this.renderGroups(GROUPS_CONFIG, 'Config', warnForConfigGroups)}
         {this.renderOthers()}
 
         <div className="mt-4 text-center">
           <button type="button" className="btn btn-default mx-1" onClick={this.props.onDiscard}>
-            {t('importer_management:growi_settings.discard')}
+            {t('admin:importer_management.growi_settings.discard')}
           </button>
           <button type="button" className="btn btn-primary mx-1" onClick={this.import} disabled={!canImport || isImporting}>
-            {t('importer_management:import')}
+            {t('admin:importer_management.import')}
           </button>
         </div>
 

+ 2 - 2
src/client/js/components/Admin/ImportData/GrowiArchive/UploadForm.jsx

@@ -51,7 +51,7 @@ class UploadForm extends React.Component {
       <form className="form-horizontal" onSubmit={this.uploadZipFile}>
         <fieldset>
           <div className="form-group">
-            <label htmlFor="file" className="col-xs-3 control-label">{t('importer_management:growi_settings.growi_archive_file')}</label>
+            <label htmlFor="file" className="col-xs-3 control-label">{t('admin:importer_management.growi_settings.growi_archive_file')}</label>
             <div className="col-xs-6">
               <input
                 type="file"
@@ -66,7 +66,7 @@ class UploadForm extends React.Component {
           <div className="form-group">
             <div className="col-xs-offset-3 col-xs-6">
               <button type="submit" className="btn btn-primary" disabled={!this.validateForm()}>
-                {t('importer_management:growi_settings.upload')}
+                {t('admin:importer_management.growi_settings.upload')}
               </button>
             </div>
           </div>

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

@@ -83,7 +83,7 @@ class GrowiArchiveSection extends React.Component {
 
     return (
       <Fragment>
-        <h2>{t('importer_management:import_growi_archive')}</h2>
+        <h2>{t('admin:importer_management.import_growi_archive')}</h2>
 
         {this.state.fileName != null ? (
           <div className="px-4">

+ 13 - 13
src/client/js/components/Admin/ImportDataPage.jsx

@@ -143,7 +143,7 @@ class ImportDataPage extends React.Component {
           role="form"
         >
           <fieldset>
-            <legend>{t('importer_management:import_from', { from: 'esa.io' })}</legend>
+            <legend>{t('admin:importer_management.import_from', { from: 'esa.io' })}</legend>
             <table className="table table-bordered table-mapping">
               <thead>
                 <tr>
@@ -173,7 +173,7 @@ class ImportDataPage extends React.Component {
 
             <div className="well well-sm mb-0 small">
               <ul>
-                <li>{t('importer_management:page_skip')}</li>
+                <li>{t('admin:importer_management.page_skip')}</li>
               </ul>
             </div>
 
@@ -183,7 +183,7 @@ class ImportDataPage extends React.Component {
 
             <div className="form-group">
               <label htmlFor="settingForm[importer:esa:team_name]" className="col-xs-3 control-label">
-                {t('importer_management:esa_settings.team_name')}
+                {t('admin:importer_management.esa_settings.team_name')}
               </label>
               <div className="col-xs-6">
                 <input className="form-control" type="text" name="esaTeamName" value={esaTeamName} onChange={this.handleInputValue} />
@@ -193,7 +193,7 @@ class ImportDataPage extends React.Component {
 
             <div className="form-group">
               <label htmlFor="settingForm[importer:esa:access_token]" className="col-xs-3 control-label">
-                {t('importer_management:esa_settings.access_token')}
+                {t('admin:importer_management.esa_settings.access_token')}
               </label>
               <div className="col-xs-6">
                 <input className="form-control" type="password" name="esaAccessToken" value={esaAccessToken} onChange={this.handleInputValue} />
@@ -208,7 +208,7 @@ class ImportDataPage extends React.Component {
                   className="btn btn-primary btn-esa"
                   name="Esa"
                   onClick={this.esaHandleSubmit}
-                  value={t('importer_management:import')}
+                  value={t('admin:importer_management.import')}
                 />
                 <input type="button" className="btn btn-secondary" onClick={this.esaHandleSubmitUpdate} value={t('Update')} />
                 <span className="col-xs-offset-1">
@@ -218,7 +218,7 @@ class ImportDataPage extends React.Component {
                     id="importFromEsa"
                     className="btn btn-default btn-esa"
                     onClick={this.esaHandleSubmitTest}
-                    value={t('importer_management:esa_settings.test_connection')}
+                    value={t('admin:importer_management.esa_settings.test_connection')}
                   />
                 </span>
 
@@ -233,7 +233,7 @@ class ImportDataPage extends React.Component {
           role="form"
         >
           <fieldset>
-            <legend>{t('importer_management:import_from', { from: 'Qiita:Team' })}</legend>
+            <legend>{t('admin:importer_management.import_from', { from: 'Qiita:Team' })}</legend>
             <table className="table table-bordered table-mapping">
               <thead>
                 <tr>
@@ -254,7 +254,7 @@ class ImportDataPage extends React.Component {
                   <th>-</th>
                 </tr>
                 <tr>
-                  <th>{t('importer_management:Directory_hierarchy_tag')}</th>
+                  <th>{t('admin:importer_management.Directory_hierarchy_tag')}</th>
                   <th></th>
                   <th>(TBD)</th>
                 </tr>
@@ -267,7 +267,7 @@ class ImportDataPage extends React.Component {
             </table>
             <div className="well well-sm mb-0 small">
               <ul>
-                <li>{t('importer_management:page_skip')}</li>
+                <li>{t('admin:importer_management.page_skip')}</li>
               </ul>
             </div>
 
@@ -276,7 +276,7 @@ class ImportDataPage extends React.Component {
             </div>
             <div className="form-group">
               <label htmlFor="settingForm[importer:qiita:team_name]" className="col-xs-3 control-label">
-                {t('importer_management:qiita_settings.team_name')}
+                {t('admin:importer_management.qiita_settings.team_name')}
               </label>
               <div className="col-xs-6">
                 <input className="form-control" type="text" name="qiitaTeamName" value={qiitaTeamName} onChange={this.handleInputValue} />
@@ -285,7 +285,7 @@ class ImportDataPage extends React.Component {
 
             <div className="form-group">
               <label htmlFor="settingForm[importer:qiita:access_token]" className="col-xs-3 control-label">
-                {t('importer_management:qiita_settings.access_token')}
+                {t('admin:importer_management.qiita_settings.access_token')}
               </label>
               <div className="col-xs-6">
                 <input className="form-control" type="password" name="qiitaAccessToken" value={qiitaAccessToken} onChange={this.handleInputValue} />
@@ -301,7 +301,7 @@ class ImportDataPage extends React.Component {
                   className="btn btn-primary btn-qiita"
                   name="Qiita"
                   onClick={this.qiitaHandleSubmit}
-                  value={t('importer_management:import')}
+                  value={t('admin:importer_management.import')}
                 />
                 <input type="button" className="btn btn-secondary" onClick={this.qiitaHandleSubmitUpdate} value={t('Update')} />
                 <span className="col-xs-offset-1">
@@ -311,7 +311,7 @@ class ImportDataPage extends React.Component {
                     id="importFromQiita"
                     className="btn btn-default btn-qiita"
                     onClick={this.qiitaHandleSubmitTest}
-                    value={t('importer_management:qiita_settings.test_connection')}
+                    value={t('admin:importer_management.qiita_settings.test_connection')}
                   />
                 </span>
 

+ 2 - 2
src/client/js/components/Admin/ManageExternalAccount.jsx

@@ -50,11 +50,11 @@ class ManageExternalAccount extends React.Component {
         <p>
           <a className="btn btn-default" href="/admin/users">
             <i className="icon-fw ti-arrow-left" aria-hidden="true"></i>
-            {t('user_management:back_to_user_management')}
+            {t('admin:user_management.back_to_user_management')}
           </a>
         </p>
 
-        <h2>{t('user_management:external_account_list')}</h2>
+        <h2>{t('admin:user_management.external_account_list')}</h2>
 
         {pager}
         <ExternalAccountTable />

+ 5 - 5
src/client/js/components/Admin/MarkdownSetting/LineBreakForm.jsx

@@ -26,7 +26,7 @@ class LineBreakForm extends React.Component {
 
     try {
       await this.props.adminMarkDownContainer.updateLineBreakSetting();
-      toastSuccess(t('toaster:update_successed', { target: 'Line Break' }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:markdown_setting.lineBreak_header') }));
     }
     catch (err) {
       toastError(err);
@@ -38,7 +38,7 @@ class LineBreakForm extends React.Component {
     const { t, adminMarkDownContainer } = this.props;
     const { isEnabledLinebreaks } = adminMarkDownContainer.state;
 
-    const helpLineBreak = { __html: t('markdown_setting:lineBreak_options.enable_lineBreak_desc') };
+    const helpLineBreak = { __html: t('admin:markdown_setting.lineBreak_options.enable_lineBreak_desc') };
 
     return (
       <div className="form-group row">
@@ -51,7 +51,7 @@ class LineBreakForm extends React.Component {
               onChange={() => { adminMarkDownContainer.setState({ isEnabledLinebreaks: !isEnabledLinebreaks }) }}
             />
             <label htmlFor="isEnabledLinebreaks">
-              {t('markdown_setting:lineBreak_options.enable_lineBreak')}
+              {t('admin:markdown_setting.lineBreak_options.enable_lineBreak')}
             </label>
           </div>
           <p className="help-block" dangerouslySetInnerHTML={helpLineBreak} />
@@ -64,7 +64,7 @@ class LineBreakForm extends React.Component {
     const { t, adminMarkDownContainer } = this.props;
     const { isEnabledLinebreaksInComments } = adminMarkDownContainer.state;
 
-    const helpLineBreakInComment = { __html: t('markdown_setting:lineBreak_options.enable_lineBreak_for_comment_desc') };
+    const helpLineBreakInComment = { __html: t('admin:markdown_setting.lineBreak_options.enable_lineBreak_for_comment_desc') };
 
     return (
       <div className="form-group row">
@@ -77,7 +77,7 @@ class LineBreakForm extends React.Component {
               onChange={() => { adminMarkDownContainer.setState({ isEnabledLinebreaksInComments: !isEnabledLinebreaksInComments }) }}
             />
             <label htmlFor="isEnabledLinebreaksInComments">
-              {t('markdown_setting:lineBreak_options.enable_lineBreak')}
+              {t('admin:markdown_setting.lineBreak_options.enable_lineBreak')}
             </label>
           </div>
           <p className="help-block" dangerouslySetInnerHTML={helpLineBreakInComment} />

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

@@ -38,22 +38,22 @@ class MarkdownSetting extends React.Component {
       <React.Fragment>
         {/* Line Break Setting */}
         <div className="row mb-5">
-          <h2 className="border-bottom">{t('markdown_setting:lineBreak_header')}</h2>
-          <p className="well">{t('markdown_setting:lineBreak_desc')}</p>
+          <h2 className="border-bottom">{t('admin:markdown_setting.lineBreak_header')}</h2>
+          <p className="well">{t('admin:markdown_setting.lineBreak_desc')}</p>
           <LineBreakForm />
         </div>
 
         {/* Presentation Setting */}
         <div className="row mb-5">
-          <h2 className="border-bottom">{t('markdown_setting:presentation_header')}</h2>
-          <p className="well">{t('markdown_setting:presentation_desc')}</p>
+          <h2 className="border-bottom">{t('admin:markdown_setting.presentation_header')}</h2>
+          <p className="well">{t('admin:markdown_setting.presentation_desc')}</p>
           <PresentationForm />
         </div>
 
         {/* XSS Setting */}
         <div className="row mb-5">
-          <h2 className="border-bottom">{t('markdown_setting:xss_header')}</h2>
-          <p className="well">{t('markdown_setting:xss_desc')}</p>
+          <h2 className="border-bottom">{t('admin:markdown_setting.xss_header')}</h2>
+          <p className="well">{t('admin:markdown_setting.xss_desc')}</p>
           <XssForm />
         </div>
       </React.Fragment>

+ 10 - 10
src/client/js/components/Admin/MarkdownSetting/PresentationForm.jsx

@@ -24,7 +24,7 @@ class PresentationForm extends React.Component {
 
     try {
       await this.props.adminMarkDownContainer.updatePresentationSetting();
-      toastSuccess(t('toaster:update_successed', { target: 'Presentation' }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:markdown_setting.presentation_header') }));
     }
     catch (err) {
       toastError(err);
@@ -41,7 +41,7 @@ class PresentationForm extends React.Component {
       <fieldset className="form-group row my-2">
 
         <label className="col-xs-3 control-label text-right">
-          {t('markdown_setting:presentation_options.page_break_setting')}
+          {t('admin:markdown_setting.presentation_options.page_break_setting')}
         </label>
 
         <div className="col-xs-3 radio radio-primary">
@@ -52,10 +52,10 @@ class PresentationForm extends React.Component {
             onChange={() => adminMarkDownContainer.switchPageBreakSeparator(1)}
           />
           <label htmlFor="pageBreakOption1">
-            <p className="font-weight-bold">{t('markdown_setting:presentation_options.preset_one_separator')}</p>
+            <p className="font-weight-bold">{t('admin:markdown_setting.presentation_options.preset_one_separator')}</p>
             <div className="mt-3">
-              {t('markdown_setting:presentation_options.preset_one_separator_desc')}
-              <pre><code>{t('markdown_setting:presentation_options.preset_one_separator_value')}</code></pre>
+              {t('admin:markdown_setting.presentation_options.preset_one_separator_desc')}
+              <pre><code>{t('admin:markdown_setting.presentation_options.preset_one_separator_value')}</code></pre>
             </div>
           </label>
         </div>
@@ -68,10 +68,10 @@ class PresentationForm extends React.Component {
             onChange={() => adminMarkDownContainer.switchPageBreakSeparator(2)}
           />
           <label htmlFor="pageBreakOption2">
-            <p className="font-weight-bold">{t('markdown_setting:presentation_options.preset_two_separator')}</p>
+            <p className="font-weight-bold">{t('admin:markdown_setting.presentation_options.preset_two_separator')}</p>
             <div className="mt-3">
-              {t('markdown_setting:presentation_options.preset_two_separator_desc')}
-              <pre><code>{t('markdown_setting:presentation_options.preset_two_separator_value')}</code></pre>
+              {t('admin:markdown_setting.presentation_options.preset_two_separator_desc')}
+              <pre><code>{t('admin:markdown_setting.presentation_options.preset_two_separator_value')}</code></pre>
             </div>
           </label>
         </div>
@@ -84,9 +84,9 @@ class PresentationForm extends React.Component {
             onChange={() => adminMarkDownContainer.switchPageBreakSeparator(3)}
           />
           <label htmlFor="pageBreakOption3">
-            <p className="font-weight-bold">{t('markdown_setting:presentation_options.custom_separator')}</p>
+            <p className="font-weight-bold">{t('admin:markdown_setting.presentation_options.custom_separator')}</p>
             <div className="mt-3">
-              {t('markdown_setting:presentation_options.custom_separator_desc')}
+              {t('admin:markdown_setting.presentation_options.custom_separator_desc')}
               <input
                 className="form-control"
                 defaultValue={pageBreakCustomSeparator}

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

@@ -37,9 +37,9 @@ class WhiteListInput extends React.Component {
       <>
         <div className="m-t-15">
           <div className="d-flex justify-content-between">
-            {t('markdown_setting:xss_options.tag_names')}
+            {t('admin:markdown_setting.xss_options.tag_names')}
             <p id="btn-import-tags" className="btn btn-xs btn-primary" onClick={this.onClickRecommendTagButton}>
-              {t('markdown_setting:xss_options.import_recommended', { target: 'Tags' })}
+              {t('admin:markdown_setting.xss_options.import_recommended', { target: 'Tags' })}
             </p>
           </div>
           <textarea
@@ -54,9 +54,9 @@ class WhiteListInput extends React.Component {
         </div>
         <div className="m-t-15">
           <div className="d-flex justify-content-between">
-            {t('markdown_setting:xss_options.tag_attributes')}
+            {t('admin:markdown_setting.xss_options.tag_attributes')}
             <p id="btn-import-tags" className="btn btn-xs btn-primary" onClick={this.onClickRecommendAttrButton}>
-              {t('markdown_setting:xss_options.import_recommended', { target: 'Attrs' })}
+              {t('admin:markdown_setting.xss_options.import_recommended', { target: 'Attrs' })}
             </p>
           </div>
           <textarea

+ 8 - 8
src/client/js/components/Admin/MarkdownSetting/XssForm.jsx

@@ -27,7 +27,7 @@ class XssForm extends React.Component {
 
     try {
       await this.props.adminMarkDownContainer.updateXssSetting();
-      toastSuccess(t('toaster:update_successed', { target: 'XSS' }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:markdown_setting.xss_desc') }));
     }
     catch (err) {
       toastError(err);
@@ -50,9 +50,9 @@ class XssForm extends React.Component {
             onChange={() => { adminMarkDownContainer.setState({ xssOption: 1 }) }}
           />
           <label htmlFor="xssOption1">
-            <p className="font-weight-bold">{t('markdown_setting:xss_options.ignore_all_tags')}</p>
+            <p className="font-weight-bold">{t('admin:markdown_setting.xss_options.ignore_all_tags')}</p>
             <div className="m-t-15">
-              {t('markdown_setting:xss_options.ignore_all_tags_desc')}
+              {t('admin:markdown_setting.xss_options.ignore_all_tags_desc')}
             </div>
           </label>
         </div>
@@ -66,10 +66,10 @@ class XssForm extends React.Component {
             onChange={() => { adminMarkDownContainer.setState({ xssOption: 2 }) }}
           />
           <label htmlFor="xssOption2">
-            <p className="font-weight-bold">{t('markdown_setting:xss_options.recommended_setting')}</p>
+            <p className="font-weight-bold">{t('admin:markdown_setting.xss_options.recommended_setting')}</p>
             <div className="m-t-15">
               <div className="d-flex justify-content-between">
-                {t('markdown_setting:xss_options.tag_names')}
+                {t('admin:markdown_setting.xss_options.tag_names')}
               </div>
               <textarea
                 className="form-control xss-list"
@@ -82,7 +82,7 @@ class XssForm extends React.Component {
             </div>
             <div className="m-t-15">
               <div className="d-flex justify-content-between">
-                {t('markdown_setting:xss_options.tag_attributes')}
+                {t('admin:markdown_setting.xss_options.tag_attributes')}
               </div>
               <textarea
                 className="form-control xss-list"
@@ -105,7 +105,7 @@ class XssForm extends React.Component {
             onChange={() => { adminMarkDownContainer.setState({ xssOption: 3 }) }}
           />
           <label htmlFor="xssOption3">
-            <p className="font-weight-bold">{t('markdown_setting:xss_options.custom_whitelist')}</p>
+            <p className="font-weight-bold">{t('admin:markdown_setting.xss_options.custom_whitelist')}</p>
             <WhiteListInput />
           </label>
         </div>
@@ -132,7 +132,7 @@ class XssForm extends React.Component {
                   onChange={adminMarkDownContainer.switchEnableXss}
                 />
                 <label htmlFor="XssEnable">
-                  {t('markdown_setting:xss_options.enable_xss_prevention')}
+                  {t('admin:markdown_setting.xss_options.enable_xss_prevention')}
                 </label>
               </div>
             </div>

+ 60 - 0
src/client/js/components/Admin/Notification/GlobalNotification.jsx

@@ -0,0 +1,60 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+
+import AppContainer from '../../../services/AppContainer';
+import AdminNotificationContainer from '../../../services/AdminNotificationContainer';
+import GlobalNotificationList from './GlobalNotificationList';
+
+class GlobalNotification extends React.Component {
+
+  render() {
+    const { t, adminNotificationContainer } = this.props;
+    const { globalNotifications } = adminNotificationContainer.state;
+    return (
+      <React.Fragment>
+
+        <a href="/admin/global-notification/new">
+          <p className="btn btn-default">{t('notification_setting.add_notification')}</p>
+        </a>
+
+        <h2 className="border-bottom mb-5">{t('notification_setting.notification_list')}</h2>
+
+        <table className="table table-bordered">
+          <thead>
+            <tr>
+              <th>ON/OFF</th>
+              {/* eslint-disable-next-line react/no-danger */}
+              <th>{t('notification_setting.trigger_path')} <span dangerouslySetInnerHTML={{ __html: t('notification_setting.trigger_path_help') }} /></th>
+              <th>{t('notification_setting.trigger_events')}</th>
+              <th>{t('notification_setting.notify_to')}</th>
+              <th></th>
+            </tr>
+          </thead>
+          {globalNotifications.length !== 0 && (
+            <tbody className="admin-notif-list">
+              <GlobalNotificationList />
+            </tbody>
+          )}
+        </table>
+
+      </React.Fragment>
+    );
+  }
+
+}
+
+const GlobalNotificationWrapper = (props) => {
+  return createSubscribedElement(GlobalNotification, props, [AppContainer, AdminNotificationContainer]);
+};
+
+GlobalNotification.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
+
+};
+
+export default withTranslation()(GlobalNotificationWrapper);

+ 175 - 0
src/client/js/components/Admin/Notification/GlobalNotificationList.jsx

@@ -0,0 +1,175 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+import urljoin from 'url-join';
+import loggerFactory from '@alias/logger';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+import AppContainer from '../../../services/AppContainer';
+import AdminNotificationContainer from '../../../services/AdminNotificationContainer';
+import NotificationDeleteModal from './NotificationDeleteModal';
+
+const logger = loggerFactory('growi:GolobalNotificationList');
+
+class GlobalNotificationList extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      isConfirmationModalOpen: false,
+      notificationForConfiguration: null,
+    };
+
+    this.openConfirmationModal = this.openConfirmationModal.bind(this);
+    this.closeConfirmationModal = this.closeConfirmationModal.bind(this);
+    this.onClickSubmit = this.onClickSubmit.bind(this);
+  }
+
+  async toggleIsEnabled(notification) {
+    const { t } = this.props;
+    const isEnabled = !notification.isEnabled;
+    try {
+      await this.props.appContainer.apiv3.put(`/notification-setting/global-notification/${notification._id}/enabled`, {
+        isEnabled,
+      });
+      toastSuccess(t('notification_setting.toggle_notification', { path: notification.triggerPath }));
+      await this.props.adminNotificationContainer.retrieveNotificationData();
+    }
+    catch (err) {
+      toastError(err);
+      logger.error(err);
+    }
+  }
+
+  openConfirmationModal(notification) {
+    this.setState({ isConfirmationModalOpen: true, notificationForConfiguration: notification });
+  }
+
+  closeConfirmationModal() {
+    this.setState({ isConfirmationModalOpen: false, notificationForConfiguration: null });
+  }
+
+  async onClickSubmit() {
+    const { t, adminNotificationContainer } = this.props;
+
+    try {
+      const deletedNotificaton = await adminNotificationContainer.deleteGlobalNotificationPattern(this.state.notificationForConfiguration._id);
+      toastSuccess(t('notification_setting.delete_notification_pattern', { path: deletedNotificaton.triggerPath }));
+    }
+    catch (err) {
+      toastError(err);
+      logger.error(err);
+    }
+    this.setState({ isConfirmationModalOpen: false });
+  }
+
+  render() {
+    const { t, adminNotificationContainer } = this.props;
+    const { globalNotifications } = adminNotificationContainer.state;
+
+    return (
+      <React.Fragment>
+        {globalNotifications.map((notification) => {
+          return (
+            <tr key={notification._id}>
+              <td className="align-middle td-abs-center">
+                <input
+                  id="isNotificationEnabled"
+                  type="checkbox"
+                  defaultChecked={notification.isEnabled}
+                  onClick={e => this.toggleIsEnabled(notification)}
+                />
+              </td>
+              <td>
+                {notification.triggerPath}
+              </td>
+              <td>
+                {notification.triggerEvents.includes('pageCreate') && (
+                  <span className="label label-success" data-toggle="tooltip" data-placement="top" title="Page Create">
+                    <i className="icon-doc"></i> CREATE
+                  </span>
+                )}
+                {notification.triggerEvents.includes('pageEdit') && (
+                  <span className="label label-warning" data-toggle="tooltip" data-placement="top" title="Page Edit">
+                    <i className="icon-pencil"></i> EDIT
+                  </span>
+                )}
+                {notification.triggerEvents.includes('pageMove') && (
+                  <span className="label label-warning" data-toggle="tooltip" data-placement="top" title="Page Move">
+                    <i className="icon-action-redo"></i> MOVE
+                  </span>
+                )}
+                {notification.triggerEvents.includes('pageDelete') && (
+                  <span className="label label-danger" data-toggle="tooltip" data-placement="top" title="Page Delte">
+                    <i className="icon-fire"></i> DELETE
+                  </span>
+                )}
+                {notification.triggerEvents.includes('pageLike') && (
+                  <span className="label label-info" data-toggle="tooltip" data-placement="top" title="Page Like">
+                    <i className="icon-like"></i> LIKE
+                  </span>
+                )}
+                {notification.triggerEvents.includes('comment') && (
+                  <span className="label label-default" data-toggle="tooltip" data-placement="top" title="New Comment">
+                    <i className="icon-fw icon-bubble"></i> POST
+                  </span>
+                )}
+              </td>
+              <td>
+                {notification.__t === 'mail'
+                  && <span data-toggle="tooltip" data-placement="top" title="Email"><i className="ti-email"></i> {notification.toEmail}</span>}
+                {notification.__t === 'slack'
+                  && <span data-toggle="tooltip" data-placement="top" title="Slack"><i className="fa fa-slack"></i> {notification.slackChannels}</span>}
+              </td>
+              <td className="td-abs-center">
+                <div className="btn-group admin-group-menu">
+                  <button type="button" className="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown">
+                    <i className="icon-settings"></i> <span className="caret"></span>
+                  </button>
+                  <ul className="dropdown-menu" role="menu">
+                    <li>
+                      <a href={urljoin('/admin/global-notification/', notification._id)}>
+                        <i className="icon-fw icon-note"></i> {t('Edit')}
+                      </a>
+                    </li>
+                    <li onClick={() => this.openConfirmationModal(notification)}>
+                      <a>
+                        <i className="icon-fw icon-fire text-danger"></i> {t('Delete')}
+                      </a>
+                    </li>
+                  </ul>
+                </div>
+              </td>
+            </tr>
+          );
+        })}
+        {this.state.notificationForConfiguration != null && (
+          <NotificationDeleteModal
+            isOpen={this.state.isConfirmationModalOpen}
+            onClose={this.closeConfirmationModal}
+            onClickSubmit={this.onClickSubmit}
+            notificationForConfiguration={this.state.notificationForConfiguration}
+          />
+        )}
+      </React.Fragment>
+    );
+
+  }
+
+}
+
+const GlobalNotificationListWrapper = (props) => {
+  return createSubscribedElement(GlobalNotificationList, props, [AppContainer, AdminNotificationContainer]);
+};
+
+GlobalNotificationList.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
+
+};
+
+export default withTranslation()(GlobalNotificationListWrapper);

+ 279 - 0
src/client/js/components/Admin/Notification/ManageGlobalNotification.jsx

@@ -0,0 +1,279 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+import urljoin from 'url-join';
+
+import loggerFactory from '@alias/logger';
+
+import { toastError } from '../../../util/apiNotification';
+
+import TriggerEventCheckBox from './TriggerEventCheckBox';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+import AppContainer from '../../../services/AppContainer';
+import { createSubscribedElement } from '../../UnstatedUtils';
+
+const logger = loggerFactory('growi:manageGlobalNotification');
+
+class ManageGlobalNotification extends React.Component {
+
+  constructor() {
+    super();
+
+    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 });
+  }
+
+  onChangeNotifyToType(notifyToType) {
+    this.setState({ notifyToType });
+  }
+
+  onChangeEmailToSend(inputValue) {
+    this.setState({ emailToSend: inputValue });
+  }
+
+  onChangeSlackChannelToSend(inputValue) {
+    this.setState({ slackChannelToSend: inputValue });
+  }
+
+  onChangeTriggerEvents(triggerEvent) {
+    const { triggerEvents } = this.state;
+
+    if (triggerEvents.has(triggerEvent)) {
+      triggerEvents.delete(triggerEvent);
+      this.setState({ triggerEvents });
+    }
+    else {
+      triggerEvents.add(triggerEvent);
+      this.setState({ triggerEvents });
+    }
+  }
+
+  async submitHandler() {
+
+    const requestParams = {
+      triggerPath: this.state.triggerPath,
+      notifyToType: this.state.notifyToType,
+      toEmail: this.state.emailToSend,
+      slackChannels: this.state.slackChannelToSend,
+      triggerEvents: [...this.state.triggerEvents],
+    };
+
+    try {
+      if (this.state.globalNotificationId != null) {
+        await this.props.appContainer.apiv3.put(`/notification-setting/global-notification/${this.state.globalNotificationId}`, requestParams);
+      }
+      else {
+        await this.props.appContainer.apiv3.post('/notification-setting/global-notification', requestParams);
+      }
+      window.location.href = urljoin(window.location.origin, '/admin/notification#global-notification');
+    }
+    catch (err) {
+      toastError(err);
+      logger.error(err);
+    }
+  }
+
+
+  render() {
+    const { t } = this.props;
+    return (
+      <React.Fragment>
+
+        <a href="/admin/notification#global-notification" className="btn btn-default">
+          <i className="icon-fw ti-arrow-left" aria-hidden="true"></i>
+          {t('notification_setting.back_to_list')}
+        </a>
+
+        <div className="row">
+          <div className="m-t-20 form-box col-md-12">
+            <h2 className="border-bottom mb-5">{t('notification_setting.notification_detail')}</h2>
+          </div>
+
+          <div className="col-sm-4">
+            <div className="form-group">
+              <h3 htmlFor="triggerPath">{t('notification_setting.trigger_path')}
+                {/* eslint-disable-next-line react/no-danger */}
+                <small dangerouslySetInnerHTML={{ __html: t('notification_setting.trigger_path_help', '<code>*</code>') }} />
+                <input
+                  className="form-control"
+                  type="text"
+                  name="triggerPath"
+                  value={this.state.triggerPath}
+                  onChange={(e) => { this.onChangeTriggerPath(e.target.value) }}
+                  required
+                />
+              </h3>
+            </div>
+
+            <div className="form-group form-inline">
+              <h3>{t('notification_setting.notify_to')}</h3>
+              <div className="radio radio-primary">
+                <input
+                  type="radio"
+                  id="mail"
+                  name="notifyToType"
+                  value="mail"
+                  checked={this.state.notifyToType === 'mail'}
+                  onChange={() => { this.onChangeNotifyToType('mail') }}
+                />
+                <label htmlFor="mail">
+                  <p className="font-weight-bold">Email</p>
+                </label>
+              </div>
+              <div className="radio radio-primary">
+                <input
+                  type="radio"
+                  id="slack"
+                  name="notifyToType"
+                  value="slack"
+                  checked={this.state.notifyToType === 'slack'}
+                  onChange={() => { this.onChangeNotifyToType('slack') }}
+                />
+                <label htmlFor="slack">
+                  <p className="font-weight-bold">Slack</p>
+                </label>
+              </div>
+            </div>
+
+            {this.state.notifyToType === 'mail'
+              ? (
+                <div className="form-group notify-to-option" id="mail-input">
+                  <input
+                    className="form-control"
+                    type="text"
+                    name="toEmail"
+                    placeholder="Email"
+                    value={this.state.emailToSend}
+                    onChange={(e) => { this.onChangeEmailToSend(e.target.value) }}
+                  />
+                  <p className="help">
+                    <b>Hint: </b>
+                    <a href="https://ifttt.com/create" target="blank">{t('notification_setting.email.ifttt_link')}
+                      <i className="icon-share-alt" />
+                    </a>
+                  </p>
+                </div>
+              )
+              : (
+                <div className="form-group notify-to-option" id="slack-input">
+                  <input
+                    className="form-control"
+                    type="text"
+                    name="notificationGlobal[slackChannels]"
+                    placeholder="Slack Channel"
+                    value={this.state.slackChannelToSend}
+                    onChange={(e) => { this.onChangeSlackChannelToSend(e.target.value) }}
+                  />
+                </div>
+              )}
+
+          </div>
+
+
+          <div className="col-sm-offset-1 col-sm-5">
+            <div className="form-group">
+              <h3>{t('notification_setting.trigger_events')}</h3>
+              <TriggerEventCheckBox
+                event="pageCreate"
+                checked={this.state.triggerEvents.has('pageCreate')}
+                onChange={() => this.onChangeTriggerEvents('pageCreate')}
+              >
+                <span className="label label-success">
+                  <i className="icon-doc"></i> CREATE
+                </span>
+              </TriggerEventCheckBox>
+              <TriggerEventCheckBox
+                event="pageEdit"
+                checked={this.state.triggerEvents.has('pageEdit')}
+                onChange={() => this.onChangeTriggerEvents('pageEdit')}
+              >
+                <span className="label label-warning">
+                  <i className="icon-pencil"></i>EDIT
+                </span>
+              </TriggerEventCheckBox>
+              <TriggerEventCheckBox
+                event="pageMove"
+                checked={this.state.triggerEvents.has('pageMove')}
+                onChange={() => this.onChangeTriggerEvents('pageMove')}
+              >
+                <span className="label label-warning">
+                  <i className="icon-action-redo"></i>MOVE
+                </span>
+              </TriggerEventCheckBox>
+              <TriggerEventCheckBox
+                event="pageDelete"
+                checked={this.state.triggerEvents.has('pageDelete')}
+                onChange={() => this.onChangeTriggerEvents('pageDelete')}
+              >
+                <span className="label label-danger">
+                  <i className="icon-fire"></i>DELETE
+                </span>
+              </TriggerEventCheckBox>
+              <TriggerEventCheckBox
+                event="pageLike"
+                checked={this.state.triggerEvents.has('pageLike')}
+                onChange={() => this.onChangeTriggerEvents('pageLike')}
+              >
+                <span className="label label-info">
+                  <i className="icon-like"></i>LIKE
+                </span>
+              </TriggerEventCheckBox>
+              <TriggerEventCheckBox
+                event="comment"
+                checked={this.state.triggerEvents.has('comment')}
+                onChange={() => this.onChangeTriggerEvents('comment')}
+              >
+                <span className="label label-default">
+                  <i className="icon-bubble"></i>POST
+                </span>
+              </TriggerEventCheckBox>
+
+            </div>
+          </div>
+
+          <AdminUpdateButtonRow
+            onClick={this.submitHandler}
+            disabled={this.state.retrieveError != null}
+          />
+
+        </div>
+
+      </React.Fragment>
+
+    );
+  }
+
+}
+
+const ManageGlobalNotificationWrapper = (props) => {
+  return createSubscribedElement(ManageGlobalNotification, props, [AppContainer]);
+};
+
+ManageGlobalNotification.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
+};
+
+export default withTranslation()(ManageGlobalNotificationWrapper);

+ 48 - 0
src/client/js/components/Admin/Notification/NotificationDeleteModal.jsx

@@ -0,0 +1,48 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import Modal from 'react-bootstrap/es/Modal';
+
+class NotificationDeleteModal extends React.PureComponent {
+
+  render() {
+    const { t, notificationForConfiguration } = this.props;
+    return (
+      <Modal show={this.props.isOpen} onHide={this.props.onClose}>
+        <Modal.Header className="modal-header" closeButton>
+          <Modal.Title>
+            <div className="modal-header bg-danger">
+              <i className="icon icon-fire"></i> Delete Global Notification Setting
+            </div>
+          </Modal.Title>
+        </Modal.Header>
+        <Modal.Body>
+          <p>
+            {t('notification_setting.delete_notification_pattern_desc1', { path: notificationForConfiguration.triggerPath })}
+          </p>
+          <span className="text-danger">
+            {t('notification_setting.delete_notification_pattern_desc2')}
+          </span>
+        </Modal.Body>
+        <Modal.Footer className="text-right">
+          <button type="button" className="btn btn-sm btn-danger" onClick={this.props.onClickSubmit}>
+            <i className="icon icon-fire"></i> {t('Delete')}
+          </button>
+        </Modal.Footer>
+      </Modal>
+    );
+  }
+
+}
+
+NotificationDeleteModal.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+
+  isOpen: PropTypes.bool.isRequired,
+  onClose: PropTypes.func.isRequired,
+  onClickSubmit: PropTypes.func.isRequired,
+  notificationForConfiguration: PropTypes.object.isRequired,
+};
+
+export default withTranslation()(NotificationDeleteModal);

+ 80 - 0
src/client/js/components/Admin/Notification/NotificationSetting.jsx

@@ -0,0 +1,80 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import loggerFactory from '@alias/logger';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastError } from '../../../util/apiNotification';
+
+import AppContainer from '../../../services/AppContainer';
+import AdminNotificationContainer from '../../../services/AdminNotificationContainer';
+
+import SlackAppConfiguration from './SlackAppConfiguration';
+import UserTriggerNotification from './UserTriggerNotification';
+import GlobalNotification from './GlobalNotification';
+
+const logger = loggerFactory('growi:NotificationSetting');
+
+class NotificationSetting extends React.Component {
+
+  async componentDidMount() {
+    const { adminNotificationContainer } = this.props;
+
+    try {
+      await adminNotificationContainer.retrieveNotificationData();
+    }
+    catch (err) {
+      toastError(err);
+      adminNotificationContainer.setState({ retrieveError: err });
+      logger.error(err);
+    }
+
+  }
+
+  render() {
+
+    return (
+      <React.Fragment>
+        <div className="notification-settings">
+          <ul className="nav nav-tabs" role="tablist">
+            <li className="active">
+              <a href="#slack-configuration" data-toggle="tab" role="tab"><i className="icon-settings"></i> Slack Configuration</a>
+            </li>
+            <li>
+              <a href="#user-trigger-notification" data-toggle="tab" role="tab"><i className="icon-settings"></i> User Trigger Notification</a>
+            </li>
+            <li>
+              <a href="#global-notification" data-toggle="tab" role="tab"><i className="icon-settings"></i> Global Notification</a>
+            </li>
+          </ul>
+          <div className="tab-content m-t-15">
+            <div id="slack-configuration" className="tab-pane active" role="tabpanel">
+              <SlackAppConfiguration />
+            </div>
+            <div id="user-trigger-notification" className="tab-pane" role="tabpanel">
+              <UserTriggerNotification />
+            </div>
+            <div id="global-notification" className="tab-pane" role="tabpanel">
+              <GlobalNotification />
+            </div>
+          </div>
+        </div>
+      </React.Fragment>
+    );
+  }
+
+}
+
+const NotificationSettingWrapper = (props) => {
+  return createSubscribedElement(NotificationSetting, props, [AppContainer, AdminNotificationContainer]);
+};
+
+NotificationSetting.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
+
+};
+
+export default withTranslation()(NotificationSettingWrapper);

+ 184 - 0
src/client/js/components/Admin/Notification/SlackAppConfiguration.jsx

@@ -0,0 +1,184 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import loggerFactory from '@alias/logger';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+import AppContainer from '../../../services/AppContainer';
+import AdminNotificationContainer from '../../../services/AdminNotificationContainer';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+
+const logger = loggerFactory('growi:slackAppConfiguration');
+
+class SlackAppConfiguration extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.onClickSubmit = this.onClickSubmit.bind(this);
+  }
+
+  async onClickSubmit() {
+    const { t, adminNotificationContainer } = this.props;
+
+    try {
+      await adminNotificationContainer.updateSlackAppConfiguration();
+      toastSuccess(t('notification_setting.updated_slackApp'));
+    }
+    catch (err) {
+      toastError(err);
+      logger.error(err);
+    }
+  }
+
+  render() {
+    const { t, adminNotificationContainer } = this.props;
+
+    return (
+      <React.Fragment>
+        <div className="row mb-5">
+          <div className="col-xs-6 text-left">
+            <div className="my-0 btn-group">
+              <div className="dropdown">
+                <button className="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                  <span className="pull-left">Slack {adminNotificationContainer.state.selectSlackOption} </span>
+                  <span className="bs-caret pull-right">
+                    <span className="caret" />
+                  </span>
+                </button>
+                {/* TODO adjust dropdown after BS4 */}
+                <ul className="dropdown-menu" role="menu">
+                  <li type="button" onClick={() => adminNotificationContainer.switchSlackOption('Incoming Webhooks')}>
+                    <a role="menuitem">Slack Incoming Webhooks</a>
+                  </li>
+                  <li type="button" onClick={() => adminNotificationContainer.switchSlackOption('App')}>
+                    <a role="menuitem">Slack App</a>
+                  </li>
+                </ul>
+              </div>
+            </div>
+          </div>
+        </div>
+        {adminNotificationContainer.state.selectSlackOption === 'Incoming Webhooks' ? (
+          <React.Fragment>
+            <h2 className="border-bottom mb-5">{t('notification_setting.slack_incoming_configuration')}</h2>
+
+            <div className="row mb-5">
+              <label className="col-xs-3 text-right">Webhook URL</label>
+              <div className="col-xs-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  defaultValue={adminNotificationContainer.state.webhookUrl}
+                  onChange={e => adminNotificationContainer.changeWebhookUrl(e.target.value)}
+                />
+              </div>
+            </div>
+
+            <div className="row mb-5">
+              <div className="col-xs-offset-3 col-xs-6 text-left">
+                <div className="checkbox checkbox-success">
+                  <input
+                    id="cbPrioritizeIWH"
+                    type="checkbox"
+                    checked={adminNotificationContainer.state.isIncomingWebhookPrioritized}
+                    onChange={() => { adminNotificationContainer.switchIsIncomingWebhookPrioritized() }}
+                  />
+                  <label htmlFor="cbPrioritizeIWH">
+                    {t('notification_setting.prioritize_webhook')}
+                  </label>
+                </div>
+                <p className="help-block">
+                  {t('notification_setting.prioritize_webhook_desc')}
+                </p>
+              </div>
+            </div>
+          </React.Fragment>
+        )
+          : (
+            <React.Fragment>
+              <h2 className="border-bottom mb-5">{t('notification_setting.slack_app_configuration')}</h2>
+
+              <div className="well">
+                <i className="icon-fw icon-exclamation text-danger"></i><span className="text-danger">NOT RECOMMENDED</span>
+                <br /><br />
+                {/* eslint-disable-next-line react/no-danger */}
+                <span dangerouslySetInnerHTML={{ __html: t('notification_setting.slack_app_configuration_desc') }} />
+                <br /><br />
+                <a
+                  href="#slack-incoming-webhooks"
+                  data-toggle="tab"
+                  onClick={() => adminNotificationContainer.switchSlackOption('Incoming Webhooks')}
+                >
+                  {t('notification_setting.use_instead')}
+                </a>{' '}
+              </div>
+
+              <div className="row mb-5">
+                <label className="col-xs-3 text-right">OAuth Access Token</label>
+                <div className="col-xs-6">
+                  <input
+                    className="form-control"
+                    type="text"
+                    defaultValue={adminNotificationContainer.state.slackToken}
+                    onChange={e => adminNotificationContainer.changeSlackToken(e.target.value)}
+                  />
+                </div>
+              </div>
+
+            </React.Fragment>
+          )
+        }
+
+        <AdminUpdateButtonRow
+          onClick={this.onClickSubmit}
+          disabled={adminNotificationContainer.state.retrieveError != null}
+        />
+
+        <hr />
+
+        <h3>
+          <i className="icon-question" aria-hidden="true"></i>{' '}
+          <a href="#collapseHelpForIwh" data-toggle="collapse">{t('notification_setting.how_to.header')}</a>
+        </h3>
+
+        <ol id="collapseHelpForIwh" className="collapse">
+          <li>
+            {t('notification_setting.how_to.workspace')}
+            <ol>
+              {/* eslint-disable-next-line react/no-danger */}
+              <li dangerouslySetInnerHTML={{ __html:  t('notification_setting.how_to.workspace_desc1') }} />
+              <li>{t('notification_setting.how_to.workspace_desc2')}</li>
+              <li>{t('notification_setting.how_to.workspace_desc3')}</li>
+            </ol>
+          </li>
+          <li>
+            {t('notification_setting.how_to.at_growi')}
+            <ol>
+              {/* eslint-disable-next-line react/no-danger */}
+              <li dangerouslySetInnerHTML={{ __html: t('notification_setting.how_to.at_growi_desc') }} />
+            </ol>
+          </li>
+        </ol>
+
+      </React.Fragment>
+    );
+  }
+
+}
+
+const SlackAppConfigurationWrapper = (props) => {
+  return createSubscribedElement(SlackAppConfiguration, props, [AppContainer, AdminNotificationContainer]);
+};
+
+SlackAppConfiguration.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
+
+};
+
+export default withTranslation()(SlackAppConfigurationWrapper);

+ 36 - 0
src/client/js/components/Admin/Notification/TriggerEventCheckBox.jsx

@@ -0,0 +1,36 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+const TriggerEventCheckBox = (props) => {
+  const { t } = props;
+
+  return (
+    <div className="checkbox checkbox-inverse">
+      <input
+        type="checkbox"
+        id={`trigger-event-${props.event}`}
+        value={props.event}
+        checked={props.checked}
+        onChange={props.onChange}
+      />
+      <label htmlFor={`trigger-event-${props.event}`}>
+        {props.children}{' '}
+        {t(`notification_setting.event_${props.event}`)}
+      </label>
+    </div>
+  );
+};
+
+
+TriggerEventCheckBox.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+
+  checked: PropTypes.bool.isRequired,
+  onChange: PropTypes.func.isRequired,
+  event: PropTypes.string.isRequired,
+  children: PropTypes.object.isRequired,
+};
+
+
+export default withTranslation()(TriggerEventCheckBox);

+ 49 - 0
src/client/js/components/Admin/Notification/UserNotificationRow.jsx

@@ -0,0 +1,49 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+
+import AppContainer from '../../../services/AppContainer';
+import AdminNotificationContainer from '../../../services/AdminNotificationContainer';
+
+
+class UserNotificationRow extends React.PureComponent {
+
+  render() {
+    const { t, notification } = this.props;
+    return (
+      <React.Fragment>
+        <tr className="admin-notif-row" key={notification._id}>
+          <td>
+            {notification.pathPattern}
+          </td>
+          <td>
+            {notification.channel}
+          </td>
+          <td>
+            <button type="submit" className="btn btn-default" onClick={() => { this.props.onClickDeleteBtn(notification._id) }}>{t('Delete')}</button>
+          </td>
+        </tr>
+      </React.Fragment>
+    );
+
+  }
+
+}
+
+
+const UserNotificationRowWrapper = (props) => {
+  return createSubscribedElement(UserNotificationRow, props, [AppContainer, AdminNotificationContainer]);
+};
+
+UserNotificationRow.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
+
+  notification: PropTypes.object.isRequired,
+  onClickDeleteBtn: PropTypes.func.isRequired,
+};
+
+export default withTranslation()(UserNotificationRowWrapper);

+ 151 - 0
src/client/js/components/Admin/Notification/UserTriggerNotification.jsx

@@ -0,0 +1,151 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import loggerFactory from '@alias/logger';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+import AppContainer from '../../../services/AppContainer';
+import AdminNotificationContainer from '../../../services/AdminNotificationContainer';
+import UserNotificationRow from './UserNotificationRow';
+
+const logger = loggerFactory('growi:slackAppConfiguration');
+
+class UserTriggerNotification extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      pathPattern: '',
+      channel: '',
+    };
+
+    this.changePathPattern = this.changePathPattern.bind(this);
+    this.changeChannel = this.changeChannel.bind(this);
+    this.validateForm = this.validateForm.bind(this);
+    this.onClickSubmit = this.onClickSubmit.bind(this);
+    this.onClickDeleteBtn = this.onClickDeleteBtn.bind(this);
+
+  }
+
+  /**
+   * Change pathPattern
+   */
+  changePathPattern(pathPattern) {
+    this.setState({ pathPattern });
+  }
+
+  /**
+   * Change channel
+   */
+  changeChannel(channel) {
+    this.setState({ channel });
+  }
+
+  validateForm() {
+    return this.state.pathPattern !== '' && this.state.channel !== '';
+  }
+
+  async onClickSubmit() {
+    const { t, adminNotificationContainer } = this.props;
+
+    try {
+      await adminNotificationContainer.addNotificationPattern(this.state.pathPattern, this.state.channel);
+      toastSuccess(t('notification_setting.add_notification_pattern'));
+      this.setState({ pathPattern: '', channel: '' });
+    }
+    catch (err) {
+      toastError(err);
+      logger.error(err);
+    }
+  }
+
+  async onClickDeleteBtn(notificationIdForDelete) {
+    const { t, adminNotificationContainer } = this.props;
+
+    try {
+      const deletedNotificaton = await adminNotificationContainer.deleteUserTriggerNotificationPattern(notificationIdForDelete);
+      toastSuccess(t('notification_setting.delete_notification_pattern', { path: deletedNotificaton.pathPattern }));
+    }
+    catch (err) {
+      toastError(err);
+      logger.error(err);
+    }
+  }
+
+  render() {
+    const { t, adminNotificationContainer } = this.props;
+
+    return (
+      <React.Fragment>
+        <h2 className="border-bottom mb-5">{t('notification_setting.user_trigger_notification_header')}</h2>
+
+        <table className="table table-bordered">
+          <thead>
+            <tr>
+              <th>{t('notification_setting.pattern')}</th>
+              <th>{t('notification_setting.channel')}</th>
+              <th />
+            </tr>
+          </thead>
+          <tbody className="admin-notif-list">
+            <tr>
+              <td>
+                <input
+                  className="form-control"
+                  type="text"
+                  name="pathPattern"
+                  value={this.state.pathPattern}
+                  placeholder="e.g. /projects/xxx/MTG/*"
+                  onChange={(e) => { this.changePathPattern(e.target.value) }}
+                />
+                {/* eslint-disable-next-line react/no-danger */}
+                <p className="help-block" dangerouslySetInnerHTML={{ __html: t('notification_setting.pattern_desc') }} />
+              </td>
+
+              <td>
+                <input
+                  className="form-control form-inline"
+                  type="text"
+                  name="channel"
+                  value={this.state.channel}
+                  placeholder="e.g. project-xxx"
+                  onChange={(e) => { this.changeChannel(e.target.value) }}
+                />
+                {/* eslint-disable-next-line react/no-danger */}
+                <p className="help-block" dangerouslySetInnerHTML={{ __html: t('notification_setting.channel_desc') }} />
+
+              </td>
+              <td>
+                <button type="button" className="btn btn-primary" disabled={!this.validateForm()} onClick={this.onClickSubmit}>{t('add')}</button>
+              </td>
+            </tr>
+            {adminNotificationContainer.state.userNotifications.map((notification) => {
+              return <UserNotificationRow notification={notification} onClickDeleteBtn={this.onClickDeleteBtn} key={notification._id} />;
+            })
+            }
+          </tbody>
+        </table>
+      </React.Fragment>
+    );
+  }
+
+
+}
+
+
+const UserTriggerNotificationWrapper = (props) => {
+  return createSubscribedElement(UserTriggerNotification, props, [AppContainer, AdminNotificationContainer]);
+};
+
+UserTriggerNotification.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
+
+};
+
+export default withTranslation()(UserTriggerNotificationWrapper);

+ 5 - 5
src/client/js/components/Admin/UserGroup/UserGroupCreateForm.jsx

@@ -71,23 +71,23 @@ class UserGroupCreateForm extends React.Component {
           {this.props.isAclEnabled
             ? (
               <button type="button" data-toggle="collapse" className="btn btn-default" href="#createGroupForm">
-                {t('user_group_management:create_group')}
+                {t('admin:user_group_management.create_group')}
               </button>
             )
             : (
-              t('user_group_management:deny_create_group')
+              t('admin:user_group_management.deny_create_group')
             )
           }
         </p>
         <form onSubmit={this.handleSubmit}>
           <div id="createGroupForm" className="collapse">
             <div className="form-group">
-              <label htmlFor="name">{t('user_group_management:group_name')}</label>
+              <label htmlFor="name">{t('admin:user_group_management.group_name')}</label>
               <textarea
                 id="name"
                 name="name"
                 className="form-control"
-                placeholder={t('user_group_management:group_example')}
+                placeholder={t('admin:user_group_management.group_example')}
                 value={this.state.name}
                 onChange={this.handleChange}
               >
@@ -113,7 +113,7 @@ UserGroupCreateForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
-  isAclEnabled: PropTypes.bool,
+  isAclEnabled: PropTypes.bool.isRequired,
   onCreate: PropTypes.func.isRequired,
 };
 

+ 9 - 8
src/client/js/components/Admin/UserGroup/UserGroupDeleteModal.jsx

@@ -34,21 +34,21 @@ class UserGroupDeleteModal extends React.Component {
         actionForPages: this.actionForPages.public,
         iconClass: 'icon-people',
         styleClass: '',
-        label: t('user_group_management:delete_modal.publish_pages'),
+        label: t('admin:user_group_management.delete_modal.publish_pages'),
       },
       {
         id: 2,
         actionForPages: this.actionForPages.delete,
         iconClass: 'icon-trash',
         styleClass: 'text-danger',
-        label: t('user_group_management:delete_modal.delete_pages'),
+        label: t('admin:user_group_management.delete_modal.delete_pages'),
       },
       {
         id: 3,
         actionForPages: this.actionForPages.transfer,
         iconClass: 'icon-options',
         styleClass: '',
-        label: t('user_group_management:delete_modal.transfer_pages'),
+        label: t('admin:user_group_management.delete_modal.transfer_pages'),
       },
     ];
 
@@ -111,7 +111,7 @@ class UserGroupDeleteModal extends React.Component {
         value={this.state.actionName}
         onChange={this.handleActionChange}
       >
-        <option value="" disabled>{t('user_group_management:delete_modal.dropdown_desc')}</option>
+        <option value="" disabled>{t('admin:user_group_management.delete_modal.dropdown_desc')}</option>
         {optoins}
       </select>
     );
@@ -129,7 +129,8 @@ class UserGroupDeleteModal extends React.Component {
       return <option key={group._id} value={group._id} data-content={dataContent}>{this.xss.process(group.name)}</option>;
     });
 
-    const defaultOptionText = groups.length === 0 ? t('user_group_management:delete_modal.no_groups') : t('user_group_management:delete_modal.select_group');
+    const defaultOptionText = groups.length === 0 ? t('admin:user_group_management.delete_modal.no_groups')
+      : t('admin:user_group_management.delete_modal.select_group');
 
     return (
       <select
@@ -164,15 +165,15 @@ class UserGroupDeleteModal extends React.Component {
       <Modal show={this.props.isShow} onHide={this.onHide}>
         <Modal.Header className="modal-header bg-danger" closeButton>
           <Modal.Title>
-            <i className="icon icon-fire"></i> {t('user_group_management:delete_modal.header')}
+            <i className="icon icon-fire"></i> {t('admin:user_group_management.delete_modal.header')}
           </Modal.Title>
         </Modal.Header>
         <Modal.Body>
           <div>
-            <span className="font-weight-bold">{t('user_group_management:group_name')}</span> : &quot;{this.props.deleteUserGroup.name}&quot;
+            <span className="font-weight-bold">{t('admin:user_group_management.group_name')}</span> : &quot;{this.props.deleteUserGroup.name}&quot;
           </div>
           <div className="text-danger mt-5">
-            {t('user_group_management:delete_modal.desc')}
+            {t('admin:user_group_management.delete_modal.desc')}
           </div>
         </Modal.Body>
         <Modal.Footer>

+ 4 - 4
src/client/js/components/Admin/UserGroup/UserGroupPage.jsx

@@ -138,15 +138,17 @@ class UserGroupPage extends React.Component {
   }
 
   render() {
+    const { isAclEnabled } = this.props.appContainer.config;
+
     return (
       <Fragment>
         <UserGroupCreateForm
-          isAclEnabled={this.props.isAclEnabled}
+          isAclEnabled={isAclEnabled}
           onCreate={this.addUserGroup}
         />
         <UserGroupTable
           userGroups={this.state.userGroups}
-          isAclEnabled={this.props.isAclEnabled}
+          isAclEnabled={isAclEnabled}
           onDelete={this.showDeleteModal}
           userGroupRelations={this.state.userGroupRelations}
         />
@@ -179,8 +181,6 @@ const UserGroupPageWrapper = (props) => {
 
 UserGroupPage.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
-  isAclEnabled: PropTypes.bool,
 };
 
 export default UserGroupPageWrapper;

+ 3 - 3
src/client/js/components/Admin/UserGroup/UserGroupTable.jsx

@@ -43,7 +43,7 @@ class UserGroupTable extends React.Component {
 
     return (
       <Fragment>
-        <h2>{t('user_group_management:group_list')}</h2>
+        <h2>{t('admin:user_group_management.group_list')}</h2>
 
         <table className="table table-bordered table-user-list">
           <thead>
@@ -89,7 +89,7 @@ class UserGroupTable extends React.Component {
                             </li>
 
                             <li>
-                              <a href="#" onClick={this.onDelete} data-user-group-id={group._id}>
+                              <a role="button" onClick={this.onDelete} data-user-group-id={group._id}>
                                 <i className="icon-fw icon-fire text-danger"></i> {t('Delete')}
                               </a>
                             </li>
@@ -127,7 +127,7 @@ UserGroupTable.propTypes = {
 
   userGroups: PropTypes.arrayOf(PropTypes.object).isRequired,
   userGroupRelations: PropTypes.object.isRequired,
-  isAclEnabled: PropTypes.bool,
+  isAclEnabled: PropTypes.bool.isRequired,
   onDelete: PropTypes.func.isRequired,
 };
 

+ 1 - 1
src/client/js/components/Admin/UserGroupDetail/CheckBoxForSerchUserOption.jsx

@@ -17,7 +17,7 @@ class CheckBoxForSerchUserOption extends React.Component {
           onChange={this.props.onChange}
         />
         <label className="text-capitalize form-check-label ml-3" htmlFor={`isAlso${option}Searched`}>
-          {t('user_group_management:add_modal.enable_option', { option })}
+          {t('admin:user_group_management.add_modal.enable_option', { option })}
         </label>
       </div>
     );

+ 1 - 1
src/client/js/components/Admin/UserGroupDetail/RadioButtonForSerchUserOption.jsx

@@ -17,7 +17,7 @@ class RadioButtonForSerchUserOption extends React.Component {
           onChange={this.props.onChange}
         />
         <label className="text-capitalize form-check-label ml-3" htmlFor={`${searchType}Match`}>
-          {t(`user_group_management:add_modal.${searchType}_match`)}
+          {t(`admin:user_group_management.add_modal.${searchType}_match`)}
         </label>
       </div>
     );

+ 2 - 2
src/client/js/components/Admin/UserGroupDetail/UserGroupDetailPage.jsx

@@ -18,12 +18,12 @@ class UserGroupDetailPage extends React.Component {
       <div>
         <a href="/admin/user-groups" className="btn btn-default">
           <i className="icon-fw ti-arrow-left" aria-hidden="true"></i>
-          {t('user_group_management:back_to_list')}
+          {t('admin:user_group_management.back_to_list')}
         </a>
         <div className="m-t-20 form-box">
           <UserGroupEditForm />
         </div>
-        <legend className="m-t-20">{t('user_group_management:user_list')}</legend>
+        <legend className="m-t-20">{t('admin:user_group_management.user_list')}</legend>
         <UserGroupUserTable />
         <UserGroupUserModal />
         <legend className="m-t-20">{t('Page')}</legend>

+ 12 - 9
src/client/js/components/Admin/UserGroupDetail/UserGroupEditForm.jsx

@@ -5,7 +5,7 @@ import dateFnsFormat from 'date-fns/format';
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
-import UserGroupDetailContainer from '../../../services/UserGroupDetailContainer';
+import AdminUserGroupDetailContainer from '../../../services/AdminUserGroupDetailContainer';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 class UserGroupEditForm extends React.Component {
@@ -13,9 +13,12 @@ class UserGroupEditForm extends React.Component {
   constructor(props) {
     super(props);
 
+    const { adminUserGroupDetailContainer } = props;
+    const { userGroup } = adminUserGroupDetailContainer.state;
+
     this.state = {
-      name: props.userGroupDetailContainer.state.userGroup.name,
-      nameCache: props.userGroupDetailContainer.state.userGroup.name, // cache for name. update every submit
+      name: userGroup.name,
+      nameCache: userGroup.name, // cache for name. update every submit
     };
 
     this.xss = window.xss;
@@ -35,7 +38,7 @@ class UserGroupEditForm extends React.Component {
     e.preventDefault();
 
     try {
-      const res = await this.props.userGroupDetailContainer.updateUserGroup({
+      const res = await this.props.adminUserGroupDetailContainer.updateUserGroup({
         name: this.state.name,
       });
 
@@ -55,12 +58,12 @@ class UserGroupEditForm extends React.Component {
   }
 
   render() {
-    const { t, userGroupDetailContainer } = this.props;
+    const { t, adminUserGroupDetailContainer } = this.props;
 
     return (
       <form className="form-horizontal" onSubmit={this.handleSubmit}>
         <fieldset>
-          <legend>{t('user_group_management:basic_info')}</legend>
+          <legend>{t('admin:user_group_management.basic_info')}</legend>
           <div className="form-group">
             <label htmlFor="name" className="col-sm-2 control-label">{t('Name')}</label>
             <div className="col-sm-4">
@@ -73,7 +76,7 @@ class UserGroupEditForm extends React.Component {
               <input
                 type="text"
                 className="form-control"
-                value={dateFnsFormat(new Date(userGroupDetailContainer.state.userGroup.createdAt), 'yyyy-MM-dd')}
+                value={dateFnsFormat(new Date(adminUserGroupDetailContainer.state.userGroup.createdAt), 'yyyy-MM-dd')}
                 disabled
               />
             </div>
@@ -93,14 +96,14 @@ class UserGroupEditForm extends React.Component {
 UserGroupEditForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  userGroupDetailContainer: PropTypes.instanceOf(UserGroupDetailContainer).isRequired,
+  adminUserGroupDetailContainer: PropTypes.instanceOf(AdminUserGroupDetailContainer).isRequired,
 };
 
 /**
  * Wrapper component for using unstated
  */
 const UserGroupEditFormWrapper = (props) => {
-  return createSubscribedElement(UserGroupEditForm, props, [AppContainer, UserGroupDetailContainer]);
+  return createSubscribedElement(UserGroupEditForm, props, [AppContainer, AdminUserGroupDetailContainer]);
 };
 
 export default withTranslation()(UserGroupEditFormWrapper);

+ 6 - 6
src/client/js/components/Admin/UserGroupDetail/UserGroupPageList.jsx

@@ -6,7 +6,7 @@ import Page from '../../PageList/Page';
 import PaginationWrapper from '../../PaginationWrapper';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
-import UserGroupDetailContainer from '../../../services/UserGroupDetailContainer';
+import AdminUserGroupDetailContainer from '../../../services/AdminUserGroupDetailContainer';
 import { toastError } from '../../../util/apiNotification';
 
 class UserGroupPageList extends React.Component {
@@ -33,7 +33,7 @@ class UserGroupPageList extends React.Component {
     const offset = (pageNum - 1) * limit;
 
     try {
-      const res = await this.props.appContainer.apiv3.get(`/user-groups/${this.props.userGroupDetailContainer.state.userGroup._id}/pages`, {
+      const res = await this.props.appContainer.apiv3.get(`/user-groups/${this.props.adminUserGroupDetailContainer.state.userGroup._id}/pages`, {
         limit,
         offset,
       });
@@ -51,14 +51,14 @@ class UserGroupPageList extends React.Component {
   }
 
   render() {
-    const { t, userGroupDetailContainer } = this.props;
+    const { t, adminUserGroupDetailContainer } = this.props;
 
     return (
       <Fragment>
         <ul className="page-list-ul page-list-ul-flat">
           {this.state.currentPages.map((page) => { return <Page key={page._id} page={page} /> })}
         </ul>
-        {userGroupDetailContainer.state.relatedPages.length === 0 ? <p>{t('user_group_management:no_pages')}</p> : null}
+        {adminUserGroupDetailContainer.state.relatedPages.length === 0 ? <p>{t('admin:user_group_management.no_pages')}</p> : null}
         <PaginationWrapper
           activePage={this.state.activePage}
           changePage={this.handlePageChange}
@@ -74,14 +74,14 @@ class UserGroupPageList extends React.Component {
 UserGroupPageList.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  userGroupDetailContainer: PropTypes.instanceOf(UserGroupDetailContainer).isRequired,
+  adminUserGroupDetailContainer: PropTypes.instanceOf(AdminUserGroupDetailContainer).isRequired,
 };
 
 /**
  * Wrapper component for using unstated
  */
 const UserGroupPageListWrapper = (props) => {
-  return createSubscribedElement(UserGroupPageList, props, [AppContainer, UserGroupDetailContainer]);
+  return createSubscribedElement(UserGroupPageList, props, [AppContainer, AdminUserGroupDetailContainer]);
 };
 
 export default withTranslation()(UserGroupPageListWrapper);

+ 15 - 10
src/client/js/components/Admin/UserGroupDetail/UserGroupUserFormByInput.jsx

@@ -6,7 +6,7 @@ import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 import { debounce } from 'throttle-debounce';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
-import UserGroupDetailContainer from '../../../services/UserGroupDetailContainer';
+import AdminUserGroupDetailContainer from '../../../services/AdminUserGroupDetailContainer';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import UserPicture from '../../User/UserPicture';
 
@@ -36,16 +36,19 @@ class UserGroupUserFormByInput extends React.Component {
   }
 
   async addUserBySubmit() {
+    const { adminUserGroupDetailContainer } = this.props;
+    const { userGroup } = adminUserGroupDetailContainer.state;
+
     if (this.state.inputUser.length === 0) { return }
     const userName = this.state.inputUser[0].username;
 
     try {
-      await this.props.userGroupDetailContainer.addUserByUsername(userName);
-      toastSuccess(`Added "${this.xss.process(userName)}" to "${this.xss.process(this.props.userGroupDetailContainer.state.userGroup.name)}"`);
+      await adminUserGroupDetailContainer.addUserByUsername(userName);
+      toastSuccess(`Added "${this.xss.process(userName)}" to "${this.xss.process(userGroup.name)}"`);
       this.setState({ inputUser: '' });
     }
     catch (err) {
-      toastError(new Error(`Unable to add "${this.xss.process(userName)}" to "${this.xss.process(this.props.userGroupDetailContainer.state.userGroup.name)}"`));
+      toastError(new Error(`Unable to add "${this.xss.process(userName)}" to "${this.xss.process(userGroup.name)}"`));
     }
   }
 
@@ -54,8 +57,10 @@ class UserGroupUserFormByInput extends React.Component {
   }
 
   async searhApplicableUsers() {
+    const { adminUserGroupDetailContainer } = this.props;
+
     try {
-      const users = await this.props.userGroupDetailContainer.fetchApplicableUsers(this.state.keyword);
+      const users = await adminUserGroupDetailContainer.fetchApplicableUsers(this.state.keyword);
       this.setState({ applicableUsers: users, isLoading: false });
     }
     catch (err) {
@@ -89,14 +94,14 @@ class UserGroupUserFormByInput extends React.Component {
   }
 
   renderMenuItemChildren(option) {
-    const { userGroupDetailContainer } = this.props;
+    const { adminUserGroupDetailContainer } = this.props;
     const user = option;
     return (
       <React.Fragment>
         <UserPicture user={user} size="sm" withoutLink />
         <strong className="ml-2">{user.username}</strong>
-        {userGroupDetailContainer.state.isAlsoNameSearched && <span className="ml-2">{user.name}</span>}
-        {userGroupDetailContainer.state.isAlsoMailSearched && <span className="ml-2">{user.email}</span>}
+        {adminUserGroupDetailContainer.state.isAlsoNameSearched && <span className="ml-2">{user.name}</span>}
+        {adminUserGroupDetailContainer.state.isAlsoMailSearched && <span className="ml-2">{user.email}</span>}
       </React.Fragment>
     );
   }
@@ -151,14 +156,14 @@ class UserGroupUserFormByInput extends React.Component {
 UserGroupUserFormByInput.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  userGroupDetailContainer: PropTypes.instanceOf(UserGroupDetailContainer).isRequired,
+  adminUserGroupDetailContainer: PropTypes.instanceOf(AdminUserGroupDetailContainer).isRequired,
 };
 
 /**
  * Wrapper component for using unstated
  */
 const UserGroupUserFormByInputWrapper = (props) => {
-  return createSubscribedElement(UserGroupUserFormByInput, props, [AppContainer, UserGroupDetailContainer]);
+  return createSubscribedElement(UserGroupUserFormByInput, props, [AppContainer, AdminUserGroupDetailContainer]);
 };
 
 export default withTranslation()(UserGroupUserFormByInputWrapper);

+ 17 - 17
src/client/js/components/Admin/UserGroupDetail/UserGroupUserModal.jsx

@@ -6,39 +6,39 @@ import Modal from 'react-bootstrap/es/Modal';
 import UserGroupUserFormByInput from './UserGroupUserFormByInput';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
-import UserGroupDetailContainer from '../../../services/UserGroupDetailContainer';
+import AdminUserGroupDetailContainer from '../../../services/AdminUserGroupDetailContainer';
 import RadioButtonForSerchUserOption from './RadioButtonForSerchUserOption';
 import CheckBoxForSerchUserOption from './CheckBoxForSerchUserOption';
 
 class UserGroupUserModal extends React.Component {
 
   render() {
-    const { t, userGroupDetailContainer } = this.props;
+    const { t, adminUserGroupDetailContainer } = this.props;
 
     return (
-      <Modal show={userGroupDetailContainer.state.isUserGroupUserModalOpen} onHide={userGroupDetailContainer.closeUserGroupUserModal}>
+      <Modal show={adminUserGroupDetailContainer.state.isUserGroupUserModalOpen} onHide={adminUserGroupDetailContainer.closeUserGroupUserModal}>
         <Modal.Header closeButton>
-          <Modal.Title>{t('user_group_management:add_modal.add_user')}</Modal.Title>
+          <Modal.Title>{t('admin:user_group_management.add_modal.add_user')}</Modal.Title>
         </Modal.Header>
         <Modal.Body>
           <div className="p-3">
             <UserGroupUserFormByInput />
           </div>
-          <h2 className="border-bottom">{t('user_group_management:add_modal.search_option')}</h2>
+          <h2 className="border-bottom">{t('admin:user_group_management.add_modal.search_option')}</h2>
           <div className="row mt-4">
             <div className="col-xs-6">
               <div className="mb-5">
                 <CheckBoxForSerchUserOption
                   option="Mail"
-                  checked={userGroupDetailContainer.state.isAlsoMailSearched}
-                  onChange={userGroupDetailContainer.switchIsAlsoMailSearched}
+                  checked={adminUserGroupDetailContainer.state.isAlsoMailSearched}
+                  onChange={adminUserGroupDetailContainer.switchIsAlsoMailSearched}
                 />
               </div>
               <div className="mb-5">
                 <CheckBoxForSerchUserOption
                   option="Name"
-                  checked={userGroupDetailContainer.state.isAlsoNameSearched}
-                  onChange={userGroupDetailContainer.switchIsAlsoNameSearched}
+                  checked={adminUserGroupDetailContainer.state.isAlsoNameSearched}
+                  onChange={adminUserGroupDetailContainer.switchIsAlsoNameSearched}
                 />
               </div>
             </div>
@@ -46,22 +46,22 @@ class UserGroupUserModal extends React.Component {
               <div className="mb-5">
                 <RadioButtonForSerchUserOption
                   searchType="forward"
-                  checked={userGroupDetailContainer.state.searchType === 'forward'}
-                  onChange={() => { userGroupDetailContainer.switchSearchType('forward') }}
+                  checked={adminUserGroupDetailContainer.state.searchType === 'forward'}
+                  onChange={() => { adminUserGroupDetailContainer.switchSearchType('forward') }}
                 />
               </div>
               <div className="mb-5">
                 <RadioButtonForSerchUserOption
                   searchType="partial"
-                  checked={userGroupDetailContainer.state.searchType === 'partial'}
-                  onChange={() => { userGroupDetailContainer.switchSearchType('partial') }}
+                  checked={adminUserGroupDetailContainer.state.searchType === 'partial'}
+                  onChange={() => { adminUserGroupDetailContainer.switchSearchType('partial') }}
                 />
               </div>
               <div className="mb-5">
                 <RadioButtonForSerchUserOption
                   searchType="backward"
-                  checked={userGroupDetailContainer.state.searchType === 'backword'}
-                  onChange={() => { userGroupDetailContainer.switchSearchType('backword') }}
+                  checked={adminUserGroupDetailContainer.state.searchType === 'backword'}
+                  onChange={() => { adminUserGroupDetailContainer.switchSearchType('backword') }}
                 />
               </div>
             </div>
@@ -76,14 +76,14 @@ class UserGroupUserModal extends React.Component {
 UserGroupUserModal.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  userGroupDetailContainer: PropTypes.instanceOf(UserGroupDetailContainer).isRequired,
+  adminUserGroupDetailContainer: PropTypes.instanceOf(AdminUserGroupDetailContainer).isRequired,
 };
 
 /**
  * Wrapper component for using unstated
  */
 const UserGroupUserModalWrapper = (props) => {
-  return createSubscribedElement(UserGroupUserModal, props, [AppContainer, UserGroupDetailContainer]);
+  return createSubscribedElement(UserGroupUserModal, props, [AppContainer, AdminUserGroupDetailContainer]);
 };
 
 export default withTranslation()(UserGroupUserModalWrapper);

+ 10 - 10
src/client/js/components/Admin/UserGroupDetail/UserGroupUserTable.jsx

@@ -6,7 +6,7 @@ import dateFnsFormat from 'date-fns/format';
 import UserPicture from '../../User/UserPicture';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
-import UserGroupDetailContainer from '../../../services/UserGroupDetailContainer';
+import AdminUserGroupDetailContainer from '../../../services/AdminUserGroupDetailContainer';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 class UserGroupUserTable extends React.Component {
@@ -21,17 +21,17 @@ class UserGroupUserTable extends React.Component {
 
   async removeUser(username) {
     try {
-      await this.props.userGroupDetailContainer.removeUserByUsername(username);
-      toastSuccess(`Removed "${this.xss.process(username)}" from "${this.xss.process(this.props.userGroupDetailContainer.state.userGroup.name)}"`);
+      await this.props.adminUserGroupDetailContainer.removeUserByUsername(username);
+      toastSuccess(`Removed "${this.xss.process(username)}" from "${this.xss.process(this.props.adminUserGroupDetailContainer.state.userGroup.name)}"`);
     }
     catch (err) {
       // eslint-disable-next-line max-len
-      toastError(new Error(`Unable to remove "${this.xss.process(username)}" from "${this.xss.process(this.props.userGroupDetailContainer.state.userGroup.name)}"`));
+      toastError(new Error(`Unable to remove "${this.xss.process(username)}" from "${this.xss.process(this.props.adminUserGroupDetailContainer.state.userGroup.name)}"`));
     }
   }
 
   render() {
-    const { t, userGroupDetailContainer } = this.props;
+    const { t, adminUserGroupDetailContainer } = this.props;
 
     return (
       <table className="table table-bordered table-user-list">
@@ -48,7 +48,7 @@ class UserGroupUserTable extends React.Component {
           </tr>
         </thead>
         <tbody>
-          {userGroupDetailContainer.state.userGroupRelations.map((sRelation) => {
+          {adminUserGroupDetailContainer.state.userGroupRelations.map((sRelation) => {
             const { relatedUser } = sRelation;
 
             return (
@@ -70,7 +70,7 @@ class UserGroupUserTable extends React.Component {
                     <ul className="dropdown-menu" role="menu">
                       <li>
                         <a onClick={() => { return this.removeUser(relatedUser.username) }}>
-                          <i className="icon-fw icon-user-unfollow"></i> {t('user_group_management:remove_from_group')}
+                          <i className="icon-fw icon-user-unfollow"></i> {t('admin:user_group_management.remove_from_group')}
                         </a>
                       </li>
                     </ul>
@@ -83,7 +83,7 @@ class UserGroupUserTable extends React.Component {
           <tr>
             <td></td>
             <td className="text-center">
-              <button className="btn btn-default" type="button" onClick={userGroupDetailContainer.openUserGroupUserModal}>
+              <button className="btn btn-default" type="button" onClick={adminUserGroupDetailContainer.openUserGroupUserModal}>
                 <i className="ti-plus"></i>
               </button>
             </td>
@@ -103,14 +103,14 @@ class UserGroupUserTable extends React.Component {
 UserGroupUserTable.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  userGroupDetailContainer: PropTypes.instanceOf(UserGroupDetailContainer).isRequired,
+  adminUserGroupDetailContainer: PropTypes.instanceOf(AdminUserGroupDetailContainer).isRequired,
 };
 
 /**
  * Wrapper component for using unstated
  */
 const UserGroupUserTableWrapper = (props) => {
-  return createSubscribedElement(UserGroupUserTable, props, [AppContainer, UserGroupDetailContainer]);
+  return createSubscribedElement(UserGroupUserTable, props, [AppContainer, AdminUserGroupDetailContainer]);
 };
 
 export default withTranslation()(UserGroupUserTableWrapper);

+ 1 - 1
src/client/js/components/Admin/UserManagement.jsx

@@ -57,7 +57,7 @@ class UserManagement extends React.Component {
           <InviteUserControl />
           <a className="btn btn-default btn-outline ml-2" href="/admin/users/external-accounts">
             <i className="icon-user-follow" aria-hidden="true"></i>
-            {t('user_management:external_account')}
+            {t('admin:user_management.external_account')}
           </a>
         </p>
 

+ 9 - 9
src/client/js/components/Admin/Users/ExternalAccountTable.jsx

@@ -26,7 +26,7 @@ class ExternalAccountTable extends React.Component {
 
     try {
       const accountId = await this.props.adminExternalAccountsContainer.removeExternalAccountById(externalAccountId);
-      toastSuccess(t('toaster:remove_external_user_success', { accountId }));
+      toastSuccess(t('toaster.remove_external_user_success', { accountId }));
     }
     catch (err) {
       toastError(err);
@@ -41,11 +41,11 @@ class ExternalAccountTable extends React.Component {
         <table className="table table-bordered table-user-list">
           <thead>
             <tr>
-              <th width="120px">{t('user_management:authentication_provider')}</th>
+              <th width="120px">{t('admin:user_management.authentication_provider')}</th>
               <th><code>accountId</code></th>
-              <th>{t('user_management:related_username')}<code>username</code></th>
+              <th>{t('admin:user_management.related_username')}<code>username</code></th>
               <th>
-                {t('user_management:password_setting')}
+                {t('admin:user_management.password_setting')}
                 <div
                   className="text-muted"
                   data-toggle="popover"
@@ -55,7 +55,7 @@ class ExternalAccountTable extends React.Component {
                   role="button"
                   data-animation="false"
                   data-html="true"
-                  data-content={t('user_management:password_setting_help')}
+                  data-content={t('admin:user_management.password_setting_help')}
                 >
                   <small>
                     <i className="icon-question" aria-hidden="true"></i>
@@ -81,12 +81,12 @@ class ExternalAccountTable extends React.Component {
                     {ea.user.password
                       ? (
                         <span className="label label-info">
-                          {t('user_management:set')}
+                          {t('admin:user_management.set')}
                         </span>
                       )
                       : (
                         <span className="label label-warning">
-                          {t('user_management:unset')}
+                          {t('admin:user_management.unset')}
                         </span>
                       )
                     }
@@ -98,9 +98,9 @@ class ExternalAccountTable extends React.Component {
                         <i className="icon-settings"></i> <span className="caret"></span>
                       </button>
                       <ul className="dropdown-menu" role="menu">
-                        <li className="dropdown-header">{t('user_management:user_table.edit_menu')}</li>
+                        <li className="dropdown-header">{t('admin:user_management.user_table.edit_menu')}</li>
                         <li>
-                          <a onClick={() => { return this.removeExtenalAccount(ea._id) }}>
+                          <a role="button" onClick={() => { return this.removeExtenalAccount(ea._id) }}>
                             <i className="icon-fw icon-fire text-danger"></i> {t('Delete')}
                           </a>
                         </li>

+ 3 - 3
src/client/js/components/Admin/Users/GiveAdminButton.jsx

@@ -20,7 +20,7 @@ class GiveAdminButton extends React.Component {
 
     try {
       const username = await this.props.adminUsersContainer.giveUserAdmin(this.props.user._id);
-      toastSuccess(t('toaster:give_user_admin', { username }));
+      toastSuccess(t('toaster.give_user_admin', { username }));
     }
     catch (err) {
       toastError(err);
@@ -31,8 +31,8 @@ class GiveAdminButton extends React.Component {
     const { t } = this.props;
 
     return (
-      <a className="px-4" onClick={() => { this.onClickGiveAdminBtn() }}>
-        <i className="icon-fw icon-user-following"></i> {t('user_management:user_table.give_admin_access')}
+      <a role="button" className="px-4" onClick={() => { this.onClickGiveAdminBtn() }}>
+        <i className="icon-fw icon-user-following"></i> {t('admin:user_management.user_table.give_admin_access')}
       </a>
     );
   }

+ 1 - 1
src/client/js/components/Admin/Users/InviteUserControl.jsx

@@ -15,7 +15,7 @@ class InviteUserControl extends React.Component {
     return (
       <Fragment>
         <button type="button" className="btn btn-default" onClick={adminUsersContainer.toggleUserInviteModal}>
-          {t('user_management:invite_users')}
+          {t('admin:user_management.invite_users')}
         </button>
         <UserInviteModal />
       </Fragment>

+ 8 - 8
src/client/js/components/Admin/Users/PasswordResetModal.jsx

@@ -41,12 +41,12 @@ class PasswordResetModal extends React.Component {
 
     return (
       <div>
-        <p className="alert alert-danger">{t('user_management:reset_password_modal.password_reset_message')}</p>
+        <p className="alert alert-danger">{t('admin:user_management.reset_password_modal.password_reset_message')}</p>
         <p>
-          {t('user_management:reset_password_modal.target_user')}: <code>{user.email}</code>
+          {t('admin:user_management.reset_password_modal.target_user')}: <code>{user.email}</code>
         </p>
         <p>
-          {t('user_management:reset_password_modal.new_password')}: <code>{this.state.temporaryPassword}</code>
+          {t('admin:user_management.reset_password_modal.new_password')}: <code>{this.state.temporaryPassword}</code>
         </p>
       </div>
     );
@@ -59,14 +59,14 @@ class PasswordResetModal extends React.Component {
     return (
       <div>
         <p>
-          {t('user_management:reset_password_modal.password_never_seen')}<br />
-          <span className="text-danger">{t('user_management:reset_password_modal.send_new_password')}</span>
+          {t('admin:user_management.reset_password_modal.password_never_seen')}<br />
+          <span className="text-danger">{t('admin:user_management.reset_password_modal.send_new_password')}</span>
         </p>
         <p>
-          {t('user_management:reset_password_modal.target_user')}: <code>{user.email}</code>
+          {t('admin:user_management.reset_password_modal.target_user')}: <code>{user.email}</code>
         </p>
         <button type="submit" className="btn btn-primary" onClick={this.resetPassword}>
-          {t('user_management:reset_password')}
+          {t('admin:user_management.reset_password')}
         </button>
       </div>
     );
@@ -88,7 +88,7 @@ class PasswordResetModal extends React.Component {
       <Modal show={adminUsersContainer.state.isPasswordResetModalShown} onHide={adminUsersContainer.hidePasswordResetModal}>
         <Modal.Header className="modal-header" closeButton>
           <Modal.Title>
-            {t('user_management:reset_password')}
+            {t('admin:user_management.reset_password')}
           </Modal.Title>
         </Modal.Header>
         <Modal.Body>

+ 5 - 5
src/client/js/components/Admin/Users/RemoveAdminButton.jsx

@@ -20,7 +20,7 @@ class RemoveAdminButton extends React.Component {
 
     try {
       const username = await this.props.adminUsersContainer.removeUserAdmin(this.props.user._id);
-      toastSuccess(t('toaster:remove_user_admin', { username }));
+      toastSuccess(t('toaster.remove_user_admin', { username }));
     }
     catch (err) {
       toastError(err);
@@ -32,8 +32,8 @@ class RemoveAdminButton extends React.Component {
     const { t } = this.props;
 
     return (
-      <a className="px-4" onClick={() => { this.onClickRemoveAdminBtn() }}>
-        <i className="icon-fw icon-user-unfollow"></i> {t('user_management:user_table.remove_admin_access')}
+      <a role="button" className="px-4" onClick={() => { this.onClickRemoveAdminBtn() }}>
+        <i className="icon-fw icon-user-unfollow"></i> {t('admin:user_management.user_table.remove_admin_access')}
       </a>
     );
   }
@@ -43,8 +43,8 @@ class RemoveAdminButton extends React.Component {
 
     return (
       <div className="px-4">
-        <i className="icon-fw icon-user-unfollow mb-2"></i>{t('user_management:user_table.remove_admin_access')}
-        <p className="alert alert-danger">{t('user_management:user_table.cannot_remove')}</p>
+        <i className="icon-fw icon-user-unfollow mb-2"></i>{t('admin:user_management.user_table.remove_admin_access')}
+        <p className="alert alert-danger">{t('admin:user_management.user_table.cannot_remove')}</p>
       </div>
     );
   }

+ 2 - 2
src/client/js/components/Admin/Users/StatusActivateButton.jsx

@@ -20,7 +20,7 @@ class StatusActivateButton extends React.Component {
 
     try {
       const username = await this.props.adminUsersContainer.activateUser(this.props.user._id);
-      toastSuccess(t('toaster:activate_user_success', { username }));
+      toastSuccess(t('toaster.activate_user_success', { username }));
     }
     catch (err) {
       toastError(err);
@@ -32,7 +32,7 @@ class StatusActivateButton extends React.Component {
 
     return (
       <a className="px-4" onClick={() => { this.onClickAcceptBtn() }}>
-        <i className="icon-fw icon-user-following"></i> {t('user_management:user_table.accept')}
+        <i className="icon-fw icon-user-following"></i> {t('admin:user_management.user_table.accept')}
       </a>
     );
   }

+ 4 - 4
src/client/js/components/Admin/Users/StatusSuspendedButton.jsx

@@ -20,7 +20,7 @@ class StatusSuspendedButton extends React.Component {
 
     try {
       const username = await this.props.adminUsersContainer.deactivateUser(this.props.user._id);
-      toastSuccess(t('toaster:deactivate_user_success', { username }));
+      toastSuccess(t('toaster.deactivate_user_success', { username }));
     }
     catch (err) {
       toastError(err);
@@ -32,7 +32,7 @@ class StatusSuspendedButton extends React.Component {
 
     return (
       <a className="px-4" onClick={() => { this.onClickDeactiveBtn() }}>
-        <i className="icon-fw icon-ban"></i> {t('user_management:user_table.deactivate_account')}
+        <i className="icon-fw icon-ban"></i> {t('admin:user_management.user_table.deactivate_account')}
       </a>
     );
   }
@@ -42,8 +42,8 @@ class StatusSuspendedButton extends React.Component {
 
     return (
       <div className="px-4">
-        <i className="icon-fw icon-ban mb-2"></i>{t('user_management:user_table.deactivate_account')}
-        <p className="alert alert-danger">{t('user_management:user_table.your_own')}</p>
+        <i className="icon-fw icon-ban mb-2"></i>{t('admin:user_management.user_table.deactivate_account')}
+        <p className="alert alert-danger">{t('admin:user_management.user_table.your_own')}</p>
       </div>
     );
   }

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor