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

Merge branch 'master' into fix/console-error-for-app-setting

# Conflicts:
#	src/client/js/components/Admin/App/AppSetting.jsx
#	src/client/js/components/Admin/App/SiteUrlSetting.jsx
itizawa 6 лет назад
Родитель
Сommit
14f39e4b71
99 измененных файлов с 2647 добавлено и 1675 удалено
  1. 298 0
      resource/locales/en-US/admin/admin.json
  2. 0 10
      resource/locales/en-US/admin/admin_top.json
  3. 0 38
      resource/locales/en-US/admin/app_setting.json
  4. 0 59
      resource/locales/en-US/admin/customize_setting.json
  5. 0 18
      resource/locales/en-US/admin/export_management.json
  6. 0 58
      resource/locales/en-US/admin/importer_management.json
  7. 0 35
      resource/locales/en-US/admin/markdown_setting.json
  8. 0 32
      resource/locales/en-US/admin/user_group_management.json
  9. 0 46
      resource/locales/en-US/admin/user_management.json
  10. 0 9
      resource/locales/en-US/common/toaster.json
  11. 37 2
      resource/locales/en-US/translation.json
  12. 298 0
      resource/locales/ja/admin/admin.json
  13. 0 10
      resource/locales/ja/admin/admin_top.json
  14. 0 38
      resource/locales/ja/admin/app_setting.json
  15. 0 59
      resource/locales/ja/admin/customize_setting.json
  16. 0 18
      resource/locales/ja/admin/export_management.json
  17. 0 58
      resource/locales/ja/admin/importer_management.json
  18. 0 35
      resource/locales/ja/admin/markdown_setting.json
  19. 0 32
      resource/locales/ja/admin/user_group_management.json
  20. 0 46
      resource/locales/ja/admin/user_management.json
  21. 0 9
      resource/locales/ja/common/toaster.json
  22. 37 2
      resource/locales/ja/translation.json
  23. 8 0
      src/client/js/app.jsx
  24. 4 4
      src/client/js/components/Admin/AdminHome/AdminHome.jsx
  25. 3 3
      src/client/js/components/Admin/AdminHome/InstalledPluginTable.jsx
  26. 11 11
      src/client/js/components/Admin/App/AppSetting.jsx
  27. 3 3
      src/client/js/components/Admin/App/AppSettingsPage.jsx
  28. 8 8
      src/client/js/components/Admin/App/AwsSetting.jsx
  29. 7 7
      src/client/js/components/Admin/App/MailSetting.jsx
  30. 3 3
      src/client/js/components/Admin/App/PluginSetting.jsx
  31. 6 5
      src/client/js/components/Admin/App/SiteUrlSetting.jsx
  32. 11 11
      src/client/js/components/Admin/Customize/CustomizeBehaviorSetting.jsx
  33. 5 5
      src/client/js/components/Admin/Customize/CustomizeCssSetting.jsx
  34. 16 16
      src/client/js/components/Admin/Customize/CustomizeFunctionSetting.jsx
  35. 4 4
      src/client/js/components/Admin/Customize/CustomizeHeaderSetting.jsx
  36. 4 4
      src/client/js/components/Admin/Customize/CustomizeHighlightSetting.jsx
  37. 13 13
      src/client/js/components/Admin/Customize/CustomizeLayoutOptions.jsx
  38. 3 3
      src/client/js/components/Admin/Customize/CustomizeLayoutSetting.jsx
  39. 5 5
      src/client/js/components/Admin/Customize/CustomizeScriptSetting.jsx
  40. 3 3
      src/client/js/components/Admin/Customize/CustomizeTitle.jsx
  41. 4 6
      src/client/js/components/Admin/ExportArchiveData/ArchiveFilesTable.jsx
  42. 3 3
      src/client/js/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.jsx
  43. 6 6
      src/client/js/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx
  44. 3 3
      src/client/js/components/Admin/ExportArchiveDataPage.jsx
  45. 2 2
      src/client/js/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx
  46. 10 10
      src/client/js/components/Admin/ImportData/GrowiArchive/ImportForm.jsx
  47. 2 2
      src/client/js/components/Admin/ImportData/GrowiArchive/UploadForm.jsx
  48. 1 1
      src/client/js/components/Admin/ImportData/GrowiArchiveSection.jsx
  49. 13 13
      src/client/js/components/Admin/ImportDataPage.jsx
  50. 2 2
      src/client/js/components/Admin/ManageExternalAccount.jsx
  51. 5 5
      src/client/js/components/Admin/MarkdownSetting/LineBreakForm.jsx
  52. 6 6
      src/client/js/components/Admin/MarkdownSetting/MarkDownSetting.jsx
  53. 10 10
      src/client/js/components/Admin/MarkdownSetting/PresentationForm.jsx
  54. 4 4
      src/client/js/components/Admin/MarkdownSetting/WhiteListInput.jsx
  55. 8 8
      src/client/js/components/Admin/MarkdownSetting/XssForm.jsx
  56. 60 0
      src/client/js/components/Admin/Notification/GlobalNotification.jsx
  57. 175 0
      src/client/js/components/Admin/Notification/GlobalNotificationList.jsx
  58. 279 0
      src/client/js/components/Admin/Notification/ManageGlobalNotification.jsx
  59. 48 0
      src/client/js/components/Admin/Notification/NotificationDeleteModal.jsx
  60. 80 0
      src/client/js/components/Admin/Notification/NotificationSetting.jsx
  61. 184 0
      src/client/js/components/Admin/Notification/SlackAppConfiguration.jsx
  62. 36 0
      src/client/js/components/Admin/Notification/TriggerEventCheckBox.jsx
  63. 49 0
      src/client/js/components/Admin/Notification/UserNotificationRow.jsx
  64. 151 0
      src/client/js/components/Admin/Notification/UserTriggerNotification.jsx
  65. 4 4
      src/client/js/components/Admin/UserGroup/UserGroupCreateForm.jsx
  66. 9 8
      src/client/js/components/Admin/UserGroup/UserGroupDeleteModal.jsx
  67. 1 1
      src/client/js/components/Admin/UserGroup/UserGroupTable.jsx
  68. 1 1
      src/client/js/components/Admin/UserGroupDetail/CheckBoxForSerchUserOption.jsx
  69. 1 1
      src/client/js/components/Admin/UserGroupDetail/RadioButtonForSerchUserOption.jsx
  70. 2 2
      src/client/js/components/Admin/UserGroupDetail/UserGroupDetailPage.jsx
  71. 1 1
      src/client/js/components/Admin/UserGroupDetail/UserGroupEditForm.jsx
  72. 1 1
      src/client/js/components/Admin/UserGroupDetail/UserGroupPageList.jsx
  73. 2 2
      src/client/js/components/Admin/UserGroupDetail/UserGroupUserModal.jsx
  74. 1 1
      src/client/js/components/Admin/UserGroupDetail/UserGroupUserTable.jsx
  75. 1 1
      src/client/js/components/Admin/UserManagement.jsx
  76. 8 8
      src/client/js/components/Admin/Users/ExternalAccountTable.jsx
  77. 2 2
      src/client/js/components/Admin/Users/GiveAdminButton.jsx
  78. 1 1
      src/client/js/components/Admin/Users/InviteUserControl.jsx
  79. 8 8
      src/client/js/components/Admin/Users/PasswordResetModal.jsx
  80. 4 4
      src/client/js/components/Admin/Users/RemoveAdminButton.jsx
  81. 2 2
      src/client/js/components/Admin/Users/StatusActivateButton.jsx
  82. 4 4
      src/client/js/components/Admin/Users/StatusSuspendedButton.jsx
  83. 8 8
      src/client/js/components/Admin/Users/UserInviteModal.jsx
  84. 3 3
      src/client/js/components/Admin/Users/UserMenu.jsx
  85. 1 1
      src/client/js/components/Admin/Users/UserRemoveButton.jsx
  86. 1 1
      src/client/js/components/Admin/Users/UserTable.jsx
  87. 137 0
      src/client/js/services/AdminNotificationContainer.js
  88. 0 17
      src/server/form/admin/notificationGlobal.js
  89. 0 8
      src/server/form/admin/slackIwhSetting.js
  90. 0 7
      src/server/form/admin/slackSetting.js
  91. 0 3
      src/server/form/index.js
  92. 4 218
      src/server/routes/admin.js
  93. 2 0
      src/server/routes/apiv3/index.js
  94. 1 2
      src/server/routes/apiv3/markdown-setting.js
  95. 503 0
      src/server/routes/apiv3/notification-setting.js
  96. 0 8
      src/server/routes/index.js
  97. 5 133
      src/server/views/admin/global-notification-detail.html
  98. 0 124
      src/server/views/admin/global-notification.html
  99. 1 287
      src/server/views/admin/notification.html

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

+ 0 - 9
resource/locales/en-US/common/toaster.json

@@ -1,9 +0,0 @@
-{
-  "update_successed": "Succeeded to update {{target}} setting",
-  "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}} "
-}

+ 37 - 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,7 +555,13 @@
     "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",

+ 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": "現在のユーザー数:"
-}

+ 0 - 9
resource/locales/ja/common/toaster.json

@@ -1,9 +0,0 @@
-{
-  "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}}を削除しました "
-}

+ 37 - 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,7 +538,13 @@
     "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 管理",

+ 8 - 0
src/client/js/app.jsx

@@ -36,6 +36,8 @@ import TableOfContents from './components/TableOfContents';
 
 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';
@@ -59,6 +61,7 @@ import AdminAppContainer from './services/AdminAppContainer';
 import WebsocketContainer from './services/WebsocketContainer';
 import AdminMarkDownContainer from './services/AdminMarkDownContainer';
 import AdminExternalAccountsContainer from './services/AdminExternalAccountsContainer';
+import AdminNotificationContainer from './services/AdminNotificationContainer';
 
 const logger = loggerFactory('growi:app');
 
@@ -163,12 +166,15 @@ 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 adminContainers = {
   'admin-home': adminHomeContainer,
   'admin-customize': adminCustomizeContainer,
   'admin-user-page': adminUsersContainer,
   'admin-external-account-setting': adminExternalAccountsContainer,
+  'admin-notification-setting': adminNotificationContainer,
+  'admin-global-notification-setting': adminNotificationContainer,
   'admin-markdown-setting': adminMarkDownContainer,
   'admin-export-page': websocketContainer,
 };
@@ -197,6 +203,8 @@ const adminComponentMappings = {
   'admin-customize': <Customize />,
   'admin-user-page': <UserManagement />,
   'admin-external-account-setting': <ManageExternalAccount />,
+  'admin-notification-setting': <NotificationSetting />,
+  'admin-global-notification-setting': <ManageGlobalNotification />,
   'admin-markdown-setting': <MarkdownSetting />,
   'admin-export-page': <ExportArchiveDataPage crowi={appContainer} />,
 };

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

+ 11 - 11
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);
@@ -39,7 +39,7 @@ class AppSetting extends React.Component {
     return (
       <React.Fragment>
         <div className="row mb-5">
-          <label className="col-xs-3 control-label">{t('app_setting:site_name')}</label>
+          <label className="col-xs-3 control-label">{t('admin:app_setting.site_name')}</label>
           <div className="col-xs-6">
             <input
               className="form-control"
@@ -48,26 +48,26 @@ class AppSetting extends React.Component {
               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 mb-5">
-          <label className="col-xs-3 control-label">{t('app_setting:confidential_name')}</label>
+          <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}
               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 mb-5">
-          <label className="col-xs-3 control-label">{t('app_setting:default_language')}</label>
+          <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
@@ -95,7 +95,7 @@ class AppSetting extends React.Component {
         </div>
 
         <div className="row mb-5">
-          <label className="col-xs-3 control-label">{t('app_setting:file_uploading')}</label>
+          <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>

+ 8 - 8
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,20 +39,20 @@ 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
@@ -68,7 +68,7 @@ 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
@@ -80,13 +80,13 @@ class AwsSetting extends React.Component {
                 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

+ 7 - 7
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,9 +38,9 @@ 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"
@@ -53,9 +53,9 @@ class MailSetting extends React.Component {
         </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"
@@ -64,7 +64,7 @@ class MailSetting extends React.Component {
             />
           </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}
@@ -75,7 +75,7 @@ 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"

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

+ 6 - 5
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">
@@ -68,14 +69,14 @@ class SiteUrlSetting extends React.Component {
                       />
                       <p className="help-block">
                         {/* eslint-disable-next-line react/no-danger */}
-                        <span 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 />
                       <p className="help-block">
                         {/* eslint-disable-next-line react/no-danger */}
-                        <span 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>

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

@@ -29,7 +29,7 @@ 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);
@@ -42,19 +42,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 +67,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>

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

@@ -37,7 +37,7 @@ class CustomizeCssSetting extends React.Component {
 
     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);
@@ -50,10 +50,10 @@ 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">
@@ -66,7 +66,7 @@ class CustomizeCssSetting extends React.Component {
           <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 - 16
src/client/js/components/Admin/Customize/CustomizeFunctionSetting.jsx

@@ -28,7 +28,7 @@ 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);
@@ -41,21 +41,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 +65,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 +81,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 +95,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 +117,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 +127,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>

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

@@ -37,7 +37,7 @@ class CustomizeHeaderSetting extends React.Component {
 
     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);
@@ -50,12 +50,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">
@@ -76,7 +76,7 @@ class CustomizeHeaderSetting extends React.Component {
         <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>
 

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

@@ -28,7 +28,7 @@ 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);
@@ -72,12 +72,12 @@ class CustomizeHighlightSetting extends React.Component {
 
     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>

+ 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 - 3
src/client/js/components/Admin/Customize/CustomizeLayoutSetting.jsx

@@ -30,7 +30,7 @@ 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);
@@ -54,9 +54,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} />

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

@@ -37,7 +37,7 @@ class CustomizeScriptSetting extends React.Component {
 
     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);
@@ -58,10 +58,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">
@@ -97,7 +97,7 @@ class CustomizeScriptSetting extends React.Component {
           <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>

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

@@ -25,7 +25,7 @@ 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);
@@ -39,11 +39,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') }}
+          dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.custom_title_detail') }}
         />
         {/* TODO i18n */}
         <div className="help-block">

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

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

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

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

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

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

@@ -60,7 +60,7 @@ class UserGroupEditForm extends React.Component {
     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">

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

@@ -58,7 +58,7 @@ class UserGroupPageList extends React.Component {
         <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}
+        {userGroupDetailContainer.state.relatedPages.length === 0 ? <p>{t('admin:user_group_management.no_pages')}</p> : null}
         <PaginationWrapper
           activePage={this.state.activePage}
           changePage={this.handlePageChange}

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

@@ -18,13 +18,13 @@ class UserGroupUserModal extends React.Component {
     return (
       <Modal show={userGroupDetailContainer.state.isUserGroupUserModalOpen} onHide={userGroupDetailContainer.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">

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

@@ -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>

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

+ 8 - 8
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,7 +98,7 @@ 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) }}>
                             <i className="icon-fw icon-fire text-danger"></i> {t('Delete')}

+ 2 - 2
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);
@@ -32,7 +32,7 @@ class GiveAdminButton extends React.Component {
 
     return (
       <a className="px-4" onClick={() => { this.onClickGiveAdminBtn() }}>
-        <i className="icon-fw icon-user-following"></i> {t('user_management:user_table.give_admin_access')}
+        <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>

+ 4 - 4
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);
@@ -33,7 +33,7 @@ class RemoveAdminButton extends React.Component {
 
     return (
       <a className="px-4" onClick={() => { this.onClickRemoveAdminBtn() }}>
-        <i className="icon-fw icon-user-unfollow"></i> {t('user_management:user_table.remove_admin_access')}
+        <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>
     );
   }

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

@@ -44,7 +44,7 @@ class UserInviteModal extends React.Component {
 
     return (
       <>
-        <label> {t('user_management:invite_modal.emails')}</label>
+        <label> {t('admin:user_management.invite_modal.emails')}</label>
         <textarea
           className="form-control"
           placeholder="e.g. user@growi.org"
@@ -52,7 +52,7 @@ class UserInviteModal extends React.Component {
           value={this.state.emailInputValue}
           onChange={this.handleInput}
         />
-        {!this.validEmail() && <p className="m-2 text-danger">{t('user_management:invite_modal.valid_email')}</p>}
+        {!this.validEmail() && <p className="m-2 text-danger">{t('admin:user_management.invite_modal.valid_email')}</p>}
       </>
     );
   }
@@ -63,8 +63,8 @@ class UserInviteModal extends React.Component {
 
     return (
       <>
-        <p>{t('user_management:invite_modal.temporary_password')}</p>
-        <p>{t('user_management:invite_modal.send_new_password')}</p>
+        <p>{t('admin:user_management.invite_modal.temporary_password')}</p>
+        <p>{t('admin:user_management.invite_modal.send_new_password')}</p>
         {invitedEmailList.createdUserList.length > 0 && this.renderCreatedEmail(invitedEmailList.createdUserList)}
         {invitedEmailList.existingEmailList.length > 0 && this.renderExistingEmail(invitedEmailList.existingEmailList)}
       </>
@@ -79,7 +79,7 @@ class UserInviteModal extends React.Component {
         <div className="checkbox checkbox-success text-left" onChange={this.handleCheckBox} style={{ flex: 1 }}>
           <input type="checkbox" id="sendEmail" className="form-check-input" name="sendEmail" defaultChecked={this.state.sendEmail} />
           <label htmlFor="sendEmail">
-            {t('user_management:invite_modal.invite_thru_email')}
+            {t('admin:user_management.invite_modal.invite_thru_email')}
           </label>
         </div>
         <div>
@@ -105,7 +105,7 @@ class UserInviteModal extends React.Component {
     return (
       <>
         <label className="mr-3 text-left text-danger" style={{ flex: 1 }}>
-          {t('user_management:invite_modal.send_temporary_password')}
+          {t('admin:user_management.invite_modal.send_temporary_password')}
         </label>
         <Button
           bsStyle="primary"
@@ -138,7 +138,7 @@ class UserInviteModal extends React.Component {
 
     return (
       <>
-        <p className="text-warning">{t('user_management:existing_email')}</p>
+        <p className="text-warning">{t('admin:user_management.existing_email')}</p>
         <ul>
           {emailList.map((user) => {
             return (
@@ -188,7 +188,7 @@ class UserInviteModal extends React.Component {
       <Modal show={adminUsersContainer.state.isUserInviteModalShown} onHide={this.onToggleModal}>
         <Modal.Header className="modal-header" closeButton>
           <Modal.Title>
-            {t('user_management:invite_users')}
+            {t('admin:user_management.invite_users')}
           </Modal.Title>
         </Modal.Header>
         <Modal.Body>

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

@@ -33,10 +33,10 @@ class UserMenu extends React.Component {
 
     return (
       <Fragment>
-        <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 onClick={this.onPasswordResetClicked}>
           <a>
-            <i className="icon-fw icon-key"></i>{t('user_management:user_table.reset_password')}
+            <i className="icon-fw icon-key"></i>{t('admin:user_management.user_table.reset_password')}
           </a>
         </li>
       </Fragment>
@@ -65,7 +65,7 @@ class UserMenu extends React.Component {
     return (
       <Fragment>
         <li className="divider pl-0"></li>
-        <li className="dropdown-header">{t('user_management:user_table.administrator_menu')}</li>
+        <li className="dropdown-header">{t('admin:user_management.user_table.administrator_menu')}</li>
         <li>
           {user.admin === true && <RemoveAdminButton user={user} />}
           {user.admin === false && <GiveAdminButton user={user} />}

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

@@ -21,7 +21,7 @@ class UserRemoveButton extends React.Component {
     try {
       await this.props.adminUsersContainer.removeUser(this.props.user._id);
       const { username } = this.props.user;
-      toastSuccess(t('toaster:remove_user_success', { username }));
+      toastSuccess(t('toaster.remove_user_success', { username }));
     }
     catch (err) {
       toastError(err);

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

@@ -85,7 +85,7 @@ class UserTable extends React.Component {
                 <tr key={user._id}>
                   <td>
                     <UserPicture user={user} className="picture img-circle" />
-                    {user.admin && <span className="label label-inverse label-admin ml-2">{t('user_management:user_table.administrator')}</span>}
+                    {user.admin && <span className="label label-inverse label-admin ml-2">{t('admin:user_management.user_table.administrator')}</span>}
                   </td>
                   <td>{this.getUserStatusLabel(user.status)}</td>
                   <td>

+ 137 - 0
src/client/js/services/AdminNotificationContainer.js

@@ -0,0 +1,137 @@
+import { Container } from 'unstated';
+
+import loggerFactory from '@alias/logger';
+
+import { toastError } from '../util/apiNotification';
+
+const logger = loggerFactory('growi:services:AdminNotificationContainer');
+
+/**
+ * Service container for admin Notification setting page (NotificationSetting.jsx)
+ * @extends {Container} unstated Container
+ */
+export default class AdminNotificationContainer extends Container {
+
+  constructor(appContainer) {
+    super();
+
+    this.appContainer = appContainer;
+
+    this.state = {
+      retrieveError: null,
+      selectSlackOption: 'Incoming Webhooks',
+      webhookUrl: '',
+      isIncomingWebhookPrioritized: false,
+      slackToken: '',
+      userNotifications: [],
+      globalNotifications: [],
+    };
+
+  }
+
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'AdminNotificationContainer';
+  }
+
+  /**
+   * Retrieve notificationData
+   */
+  async retrieveNotificationData() {
+    try {
+      const response = await this.appContainer.apiv3.get('/notification-setting/');
+      const { notificationParams } = response.data;
+
+      this.setState({
+        webhookUrl: notificationParams.webhookUrl || '',
+        isIncomingWebhookPrioritized: notificationParams.isIncomingWebhookPrioritized || false,
+        slackToken: notificationParams.slackToken || '',
+        userNotifications: notificationParams.userNotifications || [],
+        globalNotifications: notificationParams.globalNotifications || [],
+      });
+
+    }
+    catch (err) {
+      logger.error(err);
+      toastError(new Error('Failed to fetch data'));
+    }
+  }
+
+  /**
+   * Switch slackOption
+   */
+  switchSlackOption(slackOption) {
+    this.setState({ selectSlackOption: slackOption });
+  }
+
+  /**
+   * Change webhookUrl
+   */
+  changeWebhookUrl(webhookUrl) {
+    this.setState({ webhookUrl });
+  }
+
+  /**
+   * Switch incomingWebhookPrioritized
+   */
+  switchIsIncomingWebhookPrioritized() {
+    this.setState({ isIncomingWebhookPrioritized: !this.state.isIncomingWebhookPrioritized });
+  }
+
+  /**
+   * Change slackToken
+   */
+  changeSlackToken(slackToken) {
+    this.setState({ slackToken });
+  }
+
+  /**
+   * Update slackAppConfiguration
+   * @memberOf SlackAppConfiguration
+   */
+  async updateSlackAppConfiguration() {
+    const response = await this.appContainer.apiv3.put('/notification-setting/slack-configuration', {
+      webhookUrl: this.state.webhookUrl,
+      isIncomingWebhookPrioritized: this.state.isIncomingWebhookPrioritized,
+      slackToken: this.state.slackToken,
+    });
+
+    return response;
+  }
+
+  /**
+   * Add notificationPattern
+   * @memberOf SlackAppConfiguration
+   */
+  async addNotificationPattern(pathPattern, channel) {
+    const response = await this.appContainer.apiv3.post('/notification-setting/user-notification', {
+      pathPattern,
+      channel,
+    });
+
+    this.setState({ userNotifications: response.data.responseParams.userNotifications });
+  }
+
+  /**
+   * Delete user trigger notification pattern
+   */
+  async deleteUserTriggerNotificationPattern(notificatiionId) {
+    const response = await this.appContainer.apiv3.delete(`/notification-setting/user-notification/${notificatiionId}`);
+    const deletedNotificaton = response.data;
+    await this.retrieveNotificationData();
+    return deletedNotificaton;
+  }
+
+  /**
+   * Delete global notification pattern
+   */
+  async deleteGlobalNotificationPattern(notificatiionId) {
+    const response = await this.appContainer.apiv3.delete(`/notification-setting/global-notification/${notificatiionId}`);
+    const deletedNotificaton = response.data;
+    await this.retrieveNotificationData();
+    return deletedNotificaton;
+  }
+
+}

+ 0 - 17
src/server/form/admin/notificationGlobal.js

@@ -1,17 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('notificationGlobal[id]').trim(),
-  field('notificationGlobal[triggerPath]').trim(),
-  field('notificationGlobal[notifyToType]').trim(),
-  field('notificationGlobal[toEmail]').trim(),
-  field('notificationGlobal[slackChannels]').trim(),
-  field('notificationGlobal[triggerEvent:pageCreate]').trim(),
-  field('notificationGlobal[triggerEvent:pageEdit]').trim(),
-  field('notificationGlobal[triggerEvent:pageDelete]').trim(),
-  field('notificationGlobal[triggerEvent:pageMove]').trim(),
-  field('notificationGlobal[triggerEvent:pageLike]').trim(),
-  field('notificationGlobal[triggerEvent:comment]').trim(),
-);

+ 0 - 8
src/server/form/admin/slackIwhSetting.js

@@ -1,8 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('slackIwhSetting[slack:incomingWebhookUrl]', 'Webhook URL'),
-  field('slackIwhSetting[slack:isIncomingWebhookPrioritized]', 'Prioritize Incoming Webhook than Slack App ').trim().toBooleanStrict(),
-);

+ 0 - 7
src/server/form/admin/slackSetting.js

@@ -1,7 +0,0 @@
-const form = require('express-form');
-
-const field = form.field;
-
-module.exports = form(
-  field('slackSetting[slack:token]', 'token'),
-);

+ 0 - 3
src/server/form/index.js

@@ -20,9 +20,6 @@ module.exports = {
     securityPassportGitHub: require('./admin/securityPassportGitHub'),
     securityPassportTwitter: require('./admin/securityPassportTwitter'),
     securityPassportOidc: require('./admin/securityPassportOidc'),
-    slackIwhSetting: require('./admin/slackIwhSetting'),
-    slackSetting: require('./admin/slackSetting'),
     userGroupCreate: require('./admin/userGroupCreate'),
-    notificationGlobal: require('./admin/notificationGlobal'),
   },
 };

+ 4 - 218
src/server/routes/admin.js

@@ -9,8 +9,6 @@ module.exports = function(crowi, app) {
   const UserGroup = models.UserGroup;
   const UserGroupRelation = models.UserGroupRelation;
   const GlobalNotificationSetting = models.GlobalNotificationSetting;
-  const GlobalNotificationMailSetting = models.GlobalNotificationMailSetting;
-  const GlobalNotificationSlackSetting = models.GlobalNotificationSlackSetting; // eslint-disable-line no-unused-vars
 
   const {
     configManager,
@@ -165,49 +163,8 @@ module.exports = function(crowi, app) {
   // app.get('/admin/notification'               , admin.notification.index);
   actions.notification = {};
   actions.notification.index = async(req, res) => {
-    const UpdatePost = crowi.model('UpdatePost');
-    let slackSetting = configManager.getConfigByPrefix('notification', 'slack:');
-    const hasSlackIwhUrl = !!configManager.getConfig('notification', 'slack:incomingWebhookUrl');
-    const hasSlackToken = !!configManager.getConfig('notification', 'slack:token');
 
-    if (!hasSlackIwhUrl) {
-      slackSetting['slack:incomingWebhookUrl'] = '';
-    }
-
-    if (req.session.slackSetting) {
-      slackSetting = req.session.slackSetting;
-      req.session.slackSetting = null;
-    }
-
-    const globalNotifications = await GlobalNotificationSetting.findAll();
-    const userNotifications = await UpdatePost.findAll();
-
-    return res.render('admin/notification', {
-      userNotifications,
-      slackSetting,
-      hasSlackIwhUrl,
-      hasSlackToken,
-      globalNotifications,
-    });
-  };
-
-  // app.post('/admin/notification/slackSetting' , admin.notification.slackauth);
-  actions.notification.slackSetting = async function(req, res) {
-    const slackSetting = req.form.slackSetting;
-
-    if (req.form.isValid) {
-      await configManager.updateConfigsInTheSameNamespace('notification', slackSetting);
-      req.flash('successMessage', ['Successfully Updated!']);
-
-      // Re-setup
-      crowi.setupSlack().then(() => {
-      });
-    }
-    else {
-      req.flash('errorMessage', req.form.errors);
-    }
-
-    return res.redirect('/admin/notification');
+    return res.render('admin/notification');
   };
 
   // app.get('/admin/notification/slackAuth'     , admin.notification.slackauth);
@@ -240,25 +197,6 @@ module.exports = function(crowi, app) {
       });
   };
 
-  // app.post('/admin/notification/slackIwhSetting' , admin.notification.slackIwhSetting);
-  actions.notification.slackIwhSetting = async function(req, res) {
-    const slackIwhSetting = req.form.slackIwhSetting;
-
-    if (req.form.isValid) {
-      await configManager.updateConfigsInTheSameNamespace('notification', slackIwhSetting);
-      req.flash('successMessage', ['Successfully Updated!']);
-
-      // Re-setup
-      crowi.setupSlack().then(() => {
-        return res.redirect('/admin/notification#slack-incoming-webhooks');
-      });
-    }
-    else {
-      req.flash('errorMessage', req.form.errors);
-      return res.redirect('/admin/notification#slack-incoming-webhooks');
-    }
-  };
-
   // app.post('/admin/notification/slackSetting/disconnect' , admin.notification.disconnectFromSlack);
   actions.notification.disconnectFromSlack = async function(req, res) {
     await configManager.updateConfigsInTheSameNamespace('notification', { 'slack:token': '' });
@@ -270,112 +208,18 @@ module.exports = function(crowi, app) {
   actions.globalNotification = {};
   actions.globalNotification.detail = async(req, res) => {
     const notificationSettingId = req.params.id;
-    const renderVars = {};
+    let globalNotification;
 
     if (notificationSettingId) {
       try {
-        renderVars.setting = await GlobalNotificationSetting.findOne({ _id: notificationSettingId });
+        globalNotification = await GlobalNotificationSetting.findOne({ _id: notificationSettingId });
       }
       catch (err) {
         logger.error(`Error in finding a global notification setting with {_id: ${notificationSettingId}}`);
       }
     }
 
-    return res.render('admin/global-notification-detail', renderVars);
-  };
-
-  actions.globalNotification.create = (req, res) => {
-    const form = req.form.notificationGlobal;
-    let setting;
-
-    switch (form.notifyToType) {
-      case GlobalNotificationSetting.TYPE.MAIL:
-        setting = new GlobalNotificationMailSetting(crowi);
-        setting.toEmail = form.toEmail;
-        break;
-      case GlobalNotificationSetting.TYPE.SLACK:
-        setting = new GlobalNotificationSlackSetting(crowi);
-        setting.slackChannels = form.slackChannels;
-        break;
-      default:
-        logger.error('GlobalNotificationSetting Type Error: undefined type');
-        req.flash('errorMessage', 'Error occurred in creating a new global notification setting: undefined notification type');
-        return res.redirect('/admin/notification#global-notification');
-    }
-
-    setting.triggerPath = form.triggerPath;
-    setting.triggerEvents = getNotificationEvents(form);
-    setting.save();
-
-    return res.redirect('/admin/notification#global-notification');
-  };
-
-  actions.globalNotification.update = async(req, res) => {
-    const form = req.form.notificationGlobal;
-
-    const models = {
-      [GlobalNotificationSetting.TYPE.MAIL]: GlobalNotificationMailSetting,
-      [GlobalNotificationSetting.TYPE.SLACK]: GlobalNotificationSlackSetting,
-    };
-
-    let setting = await GlobalNotificationSetting.findOne({ _id: form.id });
-    setting = setting.toObject();
-
-    // when switching from one type to another,
-    // remove toEmail from slack setting and slackChannels from mail setting
-    if (setting.__t !== form.notifyToType) {
-      setting = models[setting.__t].hydrate(setting);
-      setting.toEmail = undefined;
-      setting.slackChannels = undefined;
-      await setting.save();
-      setting = setting.toObject();
-    }
-
-    switch (form.notifyToType) {
-      case GlobalNotificationSetting.TYPE.MAIL:
-        setting = GlobalNotificationMailSetting.hydrate(setting);
-        setting.toEmail = form.toEmail;
-        break;
-      case GlobalNotificationSetting.TYPE.SLACK:
-        setting = GlobalNotificationSlackSetting.hydrate(setting);
-        setting.slackChannels = form.slackChannels;
-        break;
-      default:
-        logger.error('GlobalNotificationSetting Type Error: undefined type');
-        req.flash('errorMessage', 'Error occurred in updating the global notification setting: undefined notification type');
-        return res.redirect('/admin/notification#global-notification');
-    }
-
-    setting.__t = form.notifyToType;
-    setting.triggerPath = form.triggerPath;
-    setting.triggerEvents = getNotificationEvents(form);
-    await setting.save();
-
-    return res.redirect('/admin/notification#global-notification');
-  };
-
-  actions.globalNotification.remove = async(req, res) => {
-    const id = req.params.id;
-
-    try {
-      await GlobalNotificationSetting.findOneAndRemove({ _id: id });
-      return res.redirect('/admin/notification#global-notification');
-    }
-    catch (err) {
-      req.flash('errorMessage', 'Error in deleting global notification setting');
-      return res.redirect('/admin/notification#global-notification');
-    }
-  };
-
-  const getNotificationEvents = (form) => {
-    const triggerEvents = [];
-    const triggerEventKeys = Object.keys(form).filter((key) => { return key.match(/^triggerEvent/) });
-    triggerEventKeys.forEach((key) => {
-      if (form[key]) {
-        triggerEvents.push(form[key]);
-      }
-    });
-    return triggerEvents;
+    return res.render('admin/global-notification-detail', { globalNotification });
   };
 
   actions.search = {};
@@ -803,45 +647,6 @@ module.exports = function(crowi, app) {
     return res.json({ status: true });
   };
 
-
-  // app.post('/_api/admin/notifications.add'    , admin.api.notificationAdd);
-  actions.api.notificationAdd = function(req, res) {
-    const UpdatePost = crowi.model('UpdatePost');
-    const pathPattern = req.body.pathPattern;
-    const channel = req.body.channel;
-
-    debug('notification.add', pathPattern, channel);
-    UpdatePost.create(pathPattern, channel, req.user)
-      .then((doc) => {
-        debug('Successfully save updatePost', doc);
-
-        // fixme: うーん
-        doc.creator = doc.creator._id.toString();
-        return res.json(ApiResponse.success({ updatePost: doc }));
-      })
-      .catch((err) => {
-        debug('Failed to save updatePost', err);
-        return res.json(ApiResponse.error());
-      });
-  };
-
-  // app.post('/_api/admin/notifications.remove' , admin.api.notificationRemove);
-  actions.api.notificationRemove = function(req, res) {
-    const UpdatePost = crowi.model('UpdatePost');
-    const id = req.body.id;
-
-    UpdatePost.remove(id)
-      .then(() => {
-        debug('Successfully remove updatePost');
-
-        return res.json(ApiResponse.success({}));
-      })
-      .catch((err) => {
-        debug('Failed to remove updatePost', err);
-        return res.json(ApiResponse.error());
-      });
-  };
-
   // app.get('/_api/admin/users.search' , admin.api.userSearch);
   actions.api.usersSearch = function(req, res) {
     const User = crowi.model('User');
@@ -859,25 +664,6 @@ module.exports = function(crowi, app) {
       });
   };
 
-  actions.api.toggleIsEnabledForGlobalNotification = async(req, res) => {
-    const id = req.query.id;
-    const isEnabled = (req.query.isEnabled === 'true');
-
-    try {
-      if (isEnabled) {
-        await GlobalNotificationSetting.enable(id);
-      }
-      else {
-        await GlobalNotificationSetting.disable(id);
-      }
-
-      return res.json(ApiResponse.success());
-    }
-    catch (err) {
-      return res.json(ApiResponse.error());
-    }
-  };
-
   /**
    * save esa settings, update config cache, and response json
    *

+ 2 - 0
src/server/routes/apiv3/index.js

@@ -21,6 +21,8 @@ module.exports = (crowi) => {
 
   router.use('/customize-setting', require('./customize-setting')(crowi));
 
+  router.use('/notification-setting', require('./notification-setting')(crowi));
+
   router.use('/users', require('./users')(crowi));
 
   router.use('/user-groups', require('./user-group')(crowi));

+ 1 - 2
src/server/routes/apiv3/markdown-setting.js

@@ -1,7 +1,6 @@
 const loggerFactory = require('@alias/logger');
 
-// eslint-disable-next-line no-unused-vars
-const logger = loggerFactory('growi:routes:apiv3:user-group');
+const logger = loggerFactory('growi:routes:apiv3:markdown-setting');
 
 const express = require('express');
 

+ 503 - 0
src/server/routes/apiv3/notification-setting.js

@@ -0,0 +1,503 @@
+const loggerFactory = require('@alias/logger');
+
+// eslint-disable-next-line no-unused-vars
+const logger = loggerFactory('growi:routes:apiv3:notification-setting');
+
+const express = require('express');
+
+const router = express.Router();
+
+const { body } = require('express-validator/check');
+
+const ErrorV3 = require('../../models/vo/error-apiv3');
+
+const validator = {
+  slackConfiguration: [
+    body('webhookUrl').isString().trim(),
+    body('isIncomingWebhookPrioritized').isBoolean(),
+    body('slackToken').isString().trim(),
+  ],
+  userNotification: [
+    body('pathPattern').isString().trim(),
+    body('channel').isString().trim(),
+  ],
+  globalNotification: [
+    body('triggerPath').isString().trim().not()
+      .isEmpty(),
+    body('notifyToType').isString().trim().isIn(['mail', 'slack']),
+    body('toEmail').trim().custom((value, { req }) => {
+      return (req.body.notifyToType === 'mail') ? (!!value && value.match(/.+@.+\..+/)) : true;
+    }),
+    body('slackChannels').trim().custom((value, { req }) => {
+      return (req.body.notifyToType === 'slack') ? !!value : true;
+    }),
+  ],
+};
+
+/**
+ * @swagger
+ *  tags:
+ *    name: NotificationSetting
+ */
+
+/**
+ * @swagger
+ *
+ *  components:
+ *    schemas:
+ *      SlackConfigurationParams:
+ *        type: object
+ *        properties:
+ *          webhookUrl:
+ *            type: string
+ *            description: incoming webhooks url
+ *          isIncomingWebhookPrioritized:
+ *            type: boolean
+ *            description: use incoming webhooks even if Slack App settings are enabled
+ *          slackToken:
+ *            type: string
+ *            description: OAuth access token
+ *      UserNotificationParams:
+ *        type: object
+ *        properties:
+ *          pathPattern:
+ *            type: string
+ *            description: path name of wiki
+ *          channel:
+ *            type: string
+ *            description: slack channel name without '#'
+ *      GlobalNotificationParams:
+ *        type: object
+ *        properties:
+ *          notifyToType:
+ *            type: string
+ *            description: What is type for notify
+ *          toEmail:
+ *            type: string
+ *            description: email for notify
+ *          slackChannels:
+ *            type: string
+ *            description: channels for notify
+ *          triggerPath:
+ *            type: string
+ *            description: trigger path for notify
+ *          triggerEvents:
+ *            type: array
+ *            items:
+ *              type: string
+ *              description: trigger events for notify
+ */
+module.exports = (crowi) => {
+  const loginRequiredStrictly = require('../../middleware/login-required')(crowi);
+  const adminRequired = require('../../middleware/admin-required')(crowi);
+  const csrf = require('../../middleware/csrf')(crowi);
+
+  const UpdatePost = crowi.model('UpdatePost');
+  const GlobalNotificationSetting = crowi.model('GlobalNotificationSetting');
+
+  const { ApiV3FormValidator } = crowi.middlewares;
+
+  const GlobalNotificationMailSetting = crowi.models.GlobalNotificationMailSetting;
+  const GlobalNotificationSlackSetting = crowi.models.GlobalNotificationSlackSetting;
+
+  /**
+   * @swagger
+   *
+   *    /notification-setting/:
+   *      get:
+   *        tags: [NotificationSetting]
+   *        description: Get notification paramators
+   *        responses:
+   *          200:
+   *            description: params of notification
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    notificationParams:
+   *                      type: object
+   *                      description: notification params
+   */
+  router.get('/', loginRequiredStrictly, adminRequired, async(req, res) => {
+
+    const notificationParams = {
+      webhookUrl: await crowi.configManager.getConfig('notification', 'slack:incomingWebhookUrl'),
+      isIncomingWebhookPrioritized: await crowi.configManager.getConfig('notification', 'slack:isIncomingWebhookPrioritized'),
+      slackToken: await crowi.configManager.getConfig('notification', 'slack:token'),
+      userNotifications: await UpdatePost.findAll(),
+      globalNotifications: await GlobalNotificationSetting.findAll(),
+    };
+    return res.apiv3({ notificationParams });
+  });
+
+  /**
+   * @swagger
+   *
+   *    /notification-setting/slack-configuration:
+   *      put:
+   *        tags: [NotificationSetting]
+   *        description: Update slack configuration setting
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/SlackConfigurationParams'
+   *        responses:
+   *          200:
+   *            description: Succeeded to update slack configuration setting
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  $ref: '#/components/schemas/SlackConfigurationParams'
+   */
+  router.put('/slack-configuration', loginRequiredStrictly, adminRequired, csrf, validator.slackConfiguration, ApiV3FormValidator, async(req, res) => {
+
+    const requestParams = {
+      'slack:incomingWebhookUrl': req.body.webhookUrl,
+      'slack:isIncomingWebhookPrioritized': req.body.isIncomingWebhookPrioritized,
+      'slack:token': req.body.slackToken,
+    };
+
+    try {
+      await crowi.configManager.updateConfigsInTheSameNamespace('notification', requestParams);
+      const responseParams = {
+        webhookUrl: await crowi.configManager.getConfig('notification', 'slack:incomingWebhookUrl'),
+        isIncomingWebhookPrioritized: await crowi.configManager.getConfig('notification', 'slack:isIncomingWebhookPrioritized'),
+        slackToken: await crowi.configManager.getConfig('notification', 'slack:token'),
+      };
+      await crowi.setupSlack();
+      return res.apiv3({ responseParams });
+    }
+    catch (err) {
+      const msg = 'Error occurred in updating slack configuration';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'update-slackConfiguration-failed'));
+    }
+
+  });
+
+  /**
+  * @swagger
+  *
+  *    /notification-setting/user-notification:
+  *      post:
+  *        tags: [NotificationSetting]
+  *        description: add user notification setting
+  *        requestBody:
+  *          required: true
+  *          content:
+  *            application/json:
+  *              schema:
+  *                $ref: '#/components/schemas/UserNotificationParams'
+  *        responses:
+  *          200:
+  *            description: Succeeded to add user notification setting
+  *            content:
+  *              application/json:
+  *                schema:
+  *                  properties:
+  *                    createdUser:
+  *                      type: object
+  *                      description: user who set notification
+  *                    userNotifications:
+  *                      type: object
+  *                      description: user trigger notifications for updated
+  */
+  router.post('/user-notification', loginRequiredStrictly, adminRequired, csrf, validator.userNotification, ApiV3FormValidator, async(req, res) => {
+    const { pathPattern, channel } = req.body;
+    const UpdatePost = crowi.model('UpdatePost');
+
+    try {
+      logger.info('notification.add', pathPattern, channel);
+      const responseParams = {
+        createdUser: await UpdatePost.create(pathPattern, channel, req.user),
+        userNotifications: await UpdatePost.findAll(),
+      };
+      return res.apiv3({ responseParams });
+    }
+    catch (err) {
+      const msg = 'Error occurred in updating user notification';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'update-userNotification-failed'));
+    }
+
+  });
+
+  /**
+   * @swagger
+   *
+   *    /notification-setting/user-notification/{id}:
+   *      delete:
+   *        tags: [NotificationSetting]
+   *        description: delete user trigger notification pattern
+   *        parameters:
+   *          - name: id
+   *            in: path
+   *            required: true
+   *            description: id of user trigger notification
+   *            schema:
+   *              type: string
+   *        responses:
+   *          200:
+   *            description: Succeeded to delete user trigger notification pattern
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    deletedNotificaton:
+   *                      type: object
+   *                      description: deleted notification
+   */
+  router.delete('/user-notification/:id', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
+    const { id } = req.params;
+
+    try {
+      const deletedNotificaton = await UpdatePost.remove(id);
+      return res.apiv3(deletedNotificaton);
+    }
+    catch (err) {
+      const msg = 'Error occurred in delete user trigger notification';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'delete-userTriggerNotification-failed'));
+    }
+
+
+  });
+
+  /**
+   * @swagger
+   *
+   *    /notification-setting/global-notification:
+   *      post:
+   *        tags: [NotificationSetting]
+   *        description: add global notification
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/GlobalNotificationParams'
+   *        responses:
+   *          200:
+   *            description: Succeeded to add global notification
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    createdNotification:
+   *                      type: object
+   *                      description: notification param created
+   */
+  router.post('/global-notification', loginRequiredStrictly, adminRequired, csrf, validator.globalNotification, ApiV3FormValidator, async(req, res) => {
+
+    const {
+      notifyToType, toEmail, slackChannels, triggerPath, triggerEvents,
+    } = req.body;
+
+    let notification;
+
+    if (notifyToType === GlobalNotificationSetting.TYPE.MAIL) {
+      notification = new GlobalNotificationMailSetting(crowi);
+      notification.toEmail = toEmail;
+    }
+    if (notifyToType === GlobalNotificationSetting.TYPE.SLACK) {
+      notification = new GlobalNotificationSlackSetting(crowi);
+      notification.slackChannels = slackChannels;
+    }
+
+    notification.triggerPath = triggerPath;
+    notification.triggerEvents = triggerEvents || [];
+
+    try {
+      const createdNotification = await notification.save();
+      return res.apiv3({ createdNotification });
+    }
+    catch (err) {
+      const msg = 'Error occurred in updating global notification';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'post-globalNotification-failed'));
+    }
+
+  });
+
+  /**
+   * @swagger
+   *
+   *    /notification-setting/global-notification/{id}:
+   *      put:
+   *        tags: [NotificationSetting]
+   *        description: update global notification
+   *        parameters:
+   *          - name: id
+   *            in: path
+   *            required: true
+   *            description: global notification id for updated
+   *            schema:
+   *              type: string
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/GlobalNotificationParams'
+   *        responses:
+   *          200:
+   *            description: Succeeded to update global notification
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    createdNotification:
+   *                      type: object
+   *                      description: notification param updated
+   */
+  router.put('/global-notification/:id', loginRequiredStrictly, adminRequired, csrf, validator.globalNotification, ApiV3FormValidator, async(req, res) => {
+    const { id } = req.params;
+    const {
+      notifyToType, toEmail, slackChannels, triggerPath, triggerEvents,
+    } = req.body;
+
+    const models = {
+      [GlobalNotificationSetting.TYPE.MAIL]: GlobalNotificationMailSetting,
+      [GlobalNotificationSetting.TYPE.SLACK]: GlobalNotificationSlackSetting,
+    };
+
+    try {
+      let setting = await GlobalNotificationSetting.findOne({ _id: id });
+      setting = setting.toObject();
+
+      // when switching from one type to another,
+      // remove toEmail from slack setting and slackChannels from mail setting
+      if (setting.__t !== notifyToType) {
+        setting = models[setting.__t].hydrate(setting);
+        setting.toEmail = undefined;
+        setting.slackChannels = undefined;
+        await setting.save();
+        setting = setting.toObject();
+      }
+
+      if (notifyToType === GlobalNotificationSetting.TYPE.MAIL) {
+        setting = GlobalNotificationMailSetting.hydrate(setting);
+        setting.toEmail = toEmail;
+      }
+      if (notifyToType === GlobalNotificationSetting.TYPE.SLACK) {
+        setting = GlobalNotificationSlackSetting.hydrate(setting);
+        setting.slackChannels = slackChannels;
+      }
+
+      setting.__t = notifyToType;
+      setting.triggerPath = triggerPath;
+      setting.triggerEvents = triggerEvents || [];
+
+      const createdNotification = await setting.save();
+      return res.apiv3({ createdNotification });
+    }
+    catch (err) {
+      const msg = 'Error occurred in updating global notification';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'post-globalNotification-failed'));
+    }
+
+  });
+
+  /**
+   * @swagger
+   *
+   *    /notification-setting/global-notification/{id}/enabled:
+   *      put:
+   *        tags: [NotificationSetting]
+   *        description: toggle enabled global notification
+   *        parameters:
+   *          - name: id
+   *            in: path
+   *            required: true
+   *            description: notification id for updated
+   *            schema:
+   *              type: string
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                properties:
+   *                  isEnabled:
+   *                    type: boolean
+   *                    description: is notification enabled
+   *        responses:
+   *          200:
+   *            description: Succeeded to delete global notification pattern
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    deletedNotificaton:
+   *                      type: object
+   *                      description: notification id for updated
+   */
+  router.put('/global-notification/:id/enabled', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
+    const { id } = req.params;
+    const { isEnabled } = req.body;
+
+    try {
+      if (isEnabled) {
+        await GlobalNotificationSetting.enable(id);
+      }
+      else {
+        await GlobalNotificationSetting.disable(id);
+      }
+
+      return res.apiv3({ id });
+
+    }
+    catch (err) {
+      const msg = 'Error occurred in toggle of global notification';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'toggle-globalNotification-failed'));
+    }
+
+  });
+
+  /**
+  * @swagger
+  *
+  *    /notification-setting/global-notification/{id}:
+  *      delete:
+  *        tags: [NotificationSetting]
+  *        description: delete global notification pattern
+  *        parameters:
+  *          - name: id
+  *            in: path
+  *            required: true
+  *            description: id of global notification
+  *            schema:
+  *              type: string
+  *        responses:
+  *          200:
+  *            description: Succeeded to delete global notification pattern
+  *            content:
+  *              application/json:
+  *                schema:
+  *                  properties:
+  *                    deletedNotificaton:
+  *                      type: object
+  *                      description: deleted notification
+  */
+  router.delete('/global-notification/:id', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
+    const { id } = req.params;
+
+    try {
+      const deletedNotificaton = await GlobalNotificationSetting.findOneAndRemove({ _id: id });
+      return res.apiv3(deletedNotificaton);
+    }
+    catch (err) {
+      const msg = 'Error occurred in delete global notification';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'delete-globalNotification-failed'));
+    }
+
+
+  });
+
+  return router;
+};

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

@@ -96,19 +96,11 @@ module.exports = function(crowi, app) {
 
   // notification admin
   app.get('/admin/notification'              , loginRequiredStrictly , adminRequired , admin.notification.index);
-  app.post('/admin/notification/slackIwhSetting', loginRequiredStrictly , adminRequired , csrf, form.admin.slackIwhSetting, admin.notification.slackIwhSetting);
-  app.post('/admin/notification/slackSetting', loginRequiredStrictly , adminRequired , csrf, form.admin.slackSetting, admin.notification.slackSetting);
   app.get('/admin/notification/slackAuth'    , loginRequiredStrictly , adminRequired , admin.notification.slackAuth);
   app.get('/admin/notification/slackSetting/disconnect', loginRequiredStrictly , adminRequired , admin.notification.disconnectFromSlack);
-  app.post('/_api/admin/notification.add'    , loginRequiredStrictly , adminRequired , csrf, admin.api.notificationAdd);
-  app.post('/_api/admin/notification.remove' , loginRequiredStrictly , adminRequired , csrf, admin.api.notificationRemove);
   app.get('/_api/admin/users.search'         , loginRequiredStrictly , adminRequired , admin.api.usersSearch);
   app.get('/admin/global-notification/new'   , loginRequiredStrictly , adminRequired , admin.globalNotification.detail);
   app.get('/admin/global-notification/:id'   , loginRequiredStrictly , adminRequired , admin.globalNotification.detail);
-  app.post('/admin/global-notification/new'  , loginRequiredStrictly , adminRequired , form.admin.notificationGlobal, admin.globalNotification.create);
-  app.post('/_api/admin/global-notification/toggleIsEnabled', loginRequiredStrictly , adminRequired , admin.api.toggleIsEnabledForGlobalNotification);
-  app.post('/admin/global-notification/:id/update', loginRequiredStrictly , adminRequired , form.admin.notificationGlobal, admin.globalNotification.update);
-  app.post('/admin/global-notification/:id/remove', loginRequiredStrictly , adminRequired , admin.globalNotification.remove);
 
   app.get('/admin/users'                , loginRequiredStrictly , adminRequired , admin.user.index);
   app.post('/admin/user/:id/removeCompletely' , loginRequiredStrictly , adminRequired , csrf, admin.user.removeCompletely);

+ 5 - 133
src/server/views/admin/global-notification-detail.html

@@ -30,140 +30,12 @@
     <div class="col-md-3">
       {% include './widget/menu.html' with {current: 'notification'} %}
     </div>
-
-    <div class="col-md-9">
-      <a href="/admin/notification#global-notification" class="btn btn-default">
-        <i class="icon-fw ti-arrow-left" aria-hidden="true"></i>
-        {{ t('notification_setting.back_to_list') }}
-      </a>
-
-      {% if setting %}
-        {% set actionPath = '/admin/global-notification/' + setting.id + '/update' %}
-      {% else %}
-        {% set actionPath = '/admin/global-notification/new' %}
-      {% endif %}
-
-      <div class="row">
-        <div class="m-t-20 form-box col-md-12">
-          <legend>{{ t('notification_setting.notification_detail') }}</legend>
-
-          <form action="{{ actionPath }}" method="post" class="form-horizontal" role="form">
-            <fieldset class="col-sm-4">
-              <div class="form-group">
-                <h3 for="triggerPath">{{ t('notification_setting.trigger_path') }} <small>{{ t('notification_setting.trigger_path_help', '<code>*</code>') }}</small></h3>
-                <input class="form-control" type="text" name="notificationGlobal[triggerPath]" value="{{ setting.triggerPath || '' }}" required>
-              </div>
-
-              <div class="form-group form-inline">
-                <h3>{{ t('notification_setting.notify_to') }}</h3>
-                <div class="radio radio-primary">
-                  <input type="radio" id="mail" name="notificationGlobal[notifyToType]" value="mail" {% if setting.__t == 'mail' %}checked{% endif %}>
-                  <label for="mail">
-                    <p class="font-weight-bold">Email</p>
-                  </label>
-                </div>
-                <div class="radio radio-primary">
-                  <input type="radio" id="slack" name="notificationGlobal[notifyToType]" value="slack" {% if setting.__t == 'slack' %}checked{% endif %}>
-                  <label for="slack">
-                    <p class="font-weight-bold">Slack</p>
-                  </label>
-                </div>
-              </div>
-
-              <div class="form-group notify-to-option {% if setting.__t != 'mail' %}d-none{% endif %}" id="mail-input">
-                <input class="form-control" type="text" name="notificationGlobal[toEmail]" placeholder="Email" value="{{ setting.toEmail || '' }}">
-                <p class="help">
-                  <b>Hint: </b>
-                  <a href="https://ifttt.com/create" target="_blank">{{ t('notification_setting.email.ifttt_link') }} <i class="icon-share-alt"></i></a>
-                </p>
-              </div>
-
-              <div class="form-group notify-to-option {% if setting.__t != 'slack' %}d-none{% endif %}" id="slack-input">
-                <input class="form-control" type="text" name="notificationGlobal[slackChannels]" placeholder="Slack Channel" value="{{ setting.slackChannels || '' }}">
-              </div>
-            </fieldset>
-
-            <fieldset class="col-sm-offset-1 col-sm-5">
-              <div class="form-group">
-                <h3>{{ t('notification_setting.trigger_events') }}</h3>
-                <div class="checkbox checkbox-inverse">
-                  <input type="checkbox" id="trigger-event-pageCreate" name="notificationGlobal[triggerEvent:pageCreate]" value="pageCreate"
-                    {% if setting && (setting.triggerEvents.indexOf('pageCreate') != -1) %}checked{% endif %} />
-                  <label for="trigger-event-pageCreate">
-                    <span class="label label-success"><i class="icon-doc"></i> CREATE</span> - {{ t('notification_setting.event_pageCreate') }}
-                  </label>
-                </div>
-                <div class="checkbox checkbox-inverse">
-                  <input type="checkbox" id="trigger-event-pageEdit" name="notificationGlobal[triggerEvent:pageEdit]" value="pageEdit"
-                    {% if setting && (setting.triggerEvents.indexOf('pageEdit') != -1) %}checked{% endif %} />
-                  <label for="trigger-event-pageEdit">
-                    <span class="label label-warning"><i class="icon-pencil"></i> EDIT</span> - {{ t('notification_setting.event_pageEdit') }}
-                  </label>
-                </div>
-                <div class="checkbox checkbox-inverse">
-                  <input type="checkbox" id="trigger-event-pageMove" name="notificationGlobal[triggerEvent:pageMove]" value="pageMove"
-                    {% if setting && (setting.triggerEvents.indexOf('pageMove') != -1) %}checked{% endif %} />
-                  <label for="trigger-event-pageMove">
-                    <span class="label label-warning"><i class="icon-action-redo"></i> MOVE</span> - {{ t('notification_setting.event_pageMove') }}
-                  </label>
-                </div>
-                <div class="checkbox checkbox-inverse">
-                  <input type="checkbox" id="trigger-event-pageDelete" name="notificationGlobal[triggerEvent:pageDelete]" value="pageDelete"
-                    {% if setting && (setting.triggerEvents.indexOf('pageDelete') != -1) %}checked{% endif %} />
-                  <label for="trigger-event-pageDelete">
-                    <span class="label label-danger"><i class="icon-fire"></i> DELETE</span> - {{ t('notification_setting.event_pageDelete') }}
-                  </label>
-                </div>
-                <div class="checkbox checkbox-inverse">
-                    <input type="checkbox" id="trigger-event-pageLike" name="notificationGlobal[triggerEvent:pageLike]" value="pageLike"
-                      {% if setting && (setting.triggerEvents.indexOf('pageLike') != -1) %}checked{% endif %} />
-                    <label for="trigger-event-pageLike">
-                      <span class="label label-info"><i class="icon-like"></i> LIKE</span> - {{ t('notification_setting.event_pageLike') }}
-                    </label>
-                  </div>
-                <div class="checkbox checkbox-inverse">
-                  <input type="checkbox" id="trigger-event-comment" name="notificationGlobal[triggerEvent:comment]" value="comment"
-                    {% if setting && (setting.triggerEvents.indexOf('comment') != -1) %}checked{% endif %} />
-                  <label for="trigger-event-comment">
-                    <span class="label label-default"><i class="icon-fw icon-bubble"></i> POST</span> - {{ t('notification_setting.event_comment') }}
-                  </label>
-                </div>
-              </div>
-            </fieldset>
-
-            <div class="col-sm-offset-5 col-sm-12 m-t-20">
-              <input type="hidden" name="notificationGlobal[id]" value="{{ setting.id }}">
-              <input type="hidden" name="_csrf" value="{{ csrf() }}">
-              <button type="submit" class="btn btn-primary">
-                {% if setting %}
-                  {{ t('Update') }}
-                {% else %}
-                  {{ t('Create') }}
-                {% endif %}
-              </button>
-            </div>
-          </form>
-        </div>
-      </div>
-
+    <div class="col-md-9" id="admin-global-notification-setting"
+      data-global-notification="{{ globalNotification|json }}">
     </div>
   </div>
-</div>
-
-<script>
-  $('input[name="notificationGlobal[notifyToType]"]').change(function() {
-    var val = $(this).val();
-    $('.notify-to-option').addClass('d-none');
-    $('#' + val + '-input').removeClass('d-none');
-  });
-
-  $('button#global-notificatin-delete').submit(function() {
-    alert(123)
-  });
-</script>
-{% endblock content_main %}
-
-{% block content_footer %}
-{% endblock content_footer %}
 
+  {% endblock content_main %}
 
+  {% block content_footer %}
+  {% endblock content_footer %}

+ 0 - 124
src/server/views/admin/global-notification.html

@@ -1,124 +0,0 @@
-<a href="/admin/global-notification/new">
-  <p class="btn btn-default">{{ t('notification_setting.add_notification') }}</p>
-</a>
-<h2>{{ t('notification_setting.notification_list') }}</h2>
-
-{% set tags = {
-  pageCreate: '<span class="label label-success" data-toggle="tooltip" data-placement="top" title="Page Create"><i class="icon-doc"></i> CREATE</span>',
-  pageEdit: '<span class="label label-warning" data-toggle="tooltip" data-placement="top" title="Page Edit"><i class="icon-pencil"></i> EDIT</span>',
-  pageMove: '<span class="label label-warning" data-toggle="tooltip" data-placement="top" title="Page Move"><i class="icon-action-redo"></i> MOVE</span>',
-  pageDelete: '<span class="label label-danger" data-toggle="tooltip" data-placement="top" title="Page Delte"><i class="icon-fire"></i> DELETE</span>',
-  pageLike: '<span class="label label-info" data-toggle="tooltip" data-placement="top" title="Page Like"><i class="icon-like"></i> LIKE</span>',
-  comment: '<span class="label label-default" data-toggle="tooltip" data-placement="top" title="New Comment"><i class="icon-fw icon-bubble"></i> POST</span>'
-} %}
-
-<table class="table table-bordered">
-  <thead>
-    <th>ON/OFF</th>
-    <th>{{ t('notification_setting.trigger_path') }} {{ t('notification_setting.trigger_path_help', '<code>*</code>') }}</th>
-    <th>{{ t('notification_setting.trigger_events') }}</th>
-    <th>{{ t('notification_setting.notify_to') }}</th>
-    <th></th>
-  </thead>
-  <tbody class="admin-notif-list">
-    {% for globalNotif in globalNotifications %}
-    {% set detailPageUrl = '/admin/global-notification/' + globalNotif.id %}
-    <tr>
-      <td class="align-middle td-abs-center">
-        <input type="checkbox" class="js-switch" data-size="small" data-id="{{ globalNotif._id.toString() }}" {% if globalNotif.isEnabled %}checked{% endif %} />
-      </td>
-      <td>
-        {{ globalNotif.triggerPath }}
-      </td>
-      <td style="max-width: 200px;">
-        {% for event in globalNotif.triggerEvents %}
-          {{ tags[event] | safe }}
-        {% endfor %}
-      </td>
-      <td>
-        {% if globalNotif.__t == 'mail' %}<span data-toggle="tooltip" data-placement="top" title="Email"><i class="ti-email"></i> {{ globalNotif.toEmail }}</span>
-        {% elseif globalNotif.__t == 'slack' %}<span data-toggle="tooltip" data-placement="top" title="Slack"><i class="fa fa-slack"></i> {{ globalNotif.slackChannels }}</span>
-        {% endif %}
-      </td>
-      <td class="td-abs-center">
-        <div class="btn-group admin-group-menu">
-          <button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown">
-            <i class="icon-settings"></i> <span class="caret"></span>
-          </button>
-          <ul class="dropdown-menu" role="menu">
-            <li>
-              <a href="{{ detailPageUrl }}">
-                <i class="icon-fw icon-note"></i> {{ t('Edit') }}
-              </a>
-            </li>
-
-            <li class="btn-delete">
-              <a href="#"
-                  data-setting-id="{{ globalNotif.id }}"
-                  data-target="#admin-delete-global-notification"
-                  data-toggle="modal">
-                <i class="icon-fw icon-fire text-danger"></i> {{ t('Delete') }}
-              </a>
-            </li>
-
-          </ul>
-        </div>
-      </td>
-    </tr>
-    {% endfor %}
-  </tbody>
-</table>
-
-<div class="modal fade" id="admin-delete-global-notification">
-    <div class="modal-dialog">
-      <div class="modal-content">
-        <div class="modal-header bg-danger">
-          <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-          <div class="modal-title">
-            <i class="icon icon-fire"></i> Delete Global Notification Setting
-          </div>
-        </div>
-
-        <div class="modal-body">
-          <span class="text-danger">
-            削除すると元に戻すことはできませんのでご注意ください。
-          </span>
-        </div>
-        <div class="modal-footer">
-          <form action="#" method="post" id="admin-global-notification-setting-delete" class="text-right">
-            <input type="hidden" name="setting-id" value="">
-            <input type="hidden" name="_csrf" value="{{ csrf() }}">
-            <button type="submit" value="" class="btn btn-sm btn-danger">
-              <i class="icon icon-fire"></i> 削除
-            </button>
-          </form>
-        </div>
-
-      </div>
-      <!-- /.modal-content -->
-    </div>
-    <!-- /.modal-dialog -->
-  </div>
-
-<script>
-  $(".btn-delete").on("click", function(event) {
-    var id = $(event.currentTarget).find("a").data("setting-id");
-    $("#admin-global-notification-setting-delete").attr("action", "/admin/global-notification/" + id + "/remove");
-  });
-
-  $(".js-switch").on("change", function(event) {
-    var id = event.currentTarget.dataset.id;
-    var isEnabled = event.currentTarget.checked;
-    $.post('/_api/admin/global-notification/toggleIsEnabled?id=' + id + '&isEnabled=' + isEnabled, function(res) {
-      if (res.ok) {
-        // do nothing
-      }
-      else {
-        $('.admin-notification > .row > .col-md-9').prepend(
-          '<div class=\"alert alert-danger\">Error occurred in deleting global notifcation setting.</div>'
-        );
-        location.reload();
-      }
-    });
-  });
-</script>

+ 1 - 287
src/server/views/admin/notification.html

@@ -16,296 +16,10 @@
     <div class="col-md-3">
       {% include './widget/menu.html' with {current: 'notification'} %}
     </div>
-    <div class="col-md-9">
-
-      {% set smessage = req.flash('successMessage') %}
-      {% if smessage.length %}
-      <div class="alert alert-success">
-        {% for e in smessage %}
-          {{ e }}<br>
-        {% endfor %}
-      </div>
-      {% endif %}
-
-      {% set emessage = req.flash('errorMessage') %}
-      {% if emessage.length %}
-      <div class="alert alert-danger">
-        {% for e in emessage %}
-        {{ e }}<br>
-        {% endfor %}
-      </div>
-      {% endif %}
-
-      <ul class="nav nav-tabs" role="tablist">
-        <li role="tab" class="active">
-          <a href="#slack-configuration" data-toggle="tab" role="tab"><i class="icon-settings"></i> Slack Configuration</a>
-        </li>
-        <li role="tab">
-          <a href="#user-trigger-notification" data-toggle="tab" role="tab"><i class="icon-settings"></i> User Trigger Notification</a>
-        </li>
-        <li role="tab">
-          <a href="#global-notification" data-toggle="tab" role="tab"><i class="icon-settings"></i> Global Notification</a>
-        </li>
-      </ul>
-
-      <div class="tab-content m-t-15">
-        <div id="slack-configuration" class="tab-pane active" role="tabpanel">
-
-          <select class="selectpicker" id="selectSlackOption" data-width="auto">
-            <option value="1">Slack Incoming Webhooks</option>
-            <option value="2">Slack App</option>
-          </select><!-- /.select-tab -->
-
-          <div class="tab-content m-t-15">
-
-            <div id="slack-incoming-webhooks" class="tab-pane active" role="tabpanel">
-
-              <form action="/admin/notification/slackIwhSetting" method="post" class="form-horizontal" id="appSettingForm" role="form">
-                <fieldset>
-                  <legend>Slack Incoming Webhooks Configuration</legend>
-
-                  <div class="form-group">
-                    <label for="slackIwhSetting[slack:incomingWebhookUrl]" class="col-xs-3 control-label">Webhook URL</label>
-                    <div class="col-xs-9">
-                      <input class="form-control" type="text" name="slackIwhSetting[slack:incomingWebhookUrl]" value="{{ slackSetting['slack:incomingWebhookUrl'] }}">
-                    </div>
-                  </div>
-
-                  <div class="form-group">
-                    <label for="slackIwhSetting[slack:isIncomingWebhookPrioritized]" class="col-xs-3 control-label"></label>
-                    <div class="col-xs-9">
-                      <div class="checkbox checkbox-info">
-                        <input type="checkbox" id ="cbPrioritizeIWH" name="slackIwhSetting[slack:isIncomingWebhookPrioritized]" value="1"
-                         {% if slackSetting['slack:isIncomingWebhookPrioritized'] %}checked{% endif %}>
-                        <label for="cbPrioritizeIWH">
-                         Prioritize Incoming Webhook than Slack App
-                        </label>
-                      </div>
-                      <p class="help-block">Check this option and GROWI use Incoming Webhooks even if Slack App settings are enabled.</p>
-                    </div>
-                  </div>
-
-                  <div class="form-group">
-                    <div class="col-xs-offset-3 col-xs-6">
-                      <button type="submit" class="btn btn-primary">Save</button>
-                    </div>
-                  </div>
-                </fieldset>
-                <input type="hidden" name="_csrf" value="{{ csrf() }}">
-              </form>
-
-              <hr>
-              <h3>
-                <i class="icon-question" aria-hidden="true"></i>
-                <a href="#collapseHelpForIwh" data-toggle="collapse">How to configure Incoming Webhooks?</a>
-              </h3>
-
-              <ol id="collapseHelpForIwh" class="collapse">
-                <li>
-                 (At Workspace) Add a hook
-                  <ol>
-                    <li>Go to <a href="https://slack.com/services/new/incoming-webhook">Incoming Webhooks Configuration page</a>.</li>
-                    <li>Choose the default channel to post.</li>
-                    <li>Add.</li>
-                  </ol>
-                </li>
-                <li>
-                (At GROWI admin page) Set Webhook URL
-                  <ol>
-                    <li>Input "Webhook URL" and submit on this page.</li>
-                  </ol>
-                </li>
-              </ol>
-
-            </div><!-- /#slack-incoming-webhooks -->
-
-            <div id="slack-app" class="tab-pane" role="tabpanel" >
-
-              <form action="/admin/notification/slackSetting" method="post" class="form-horizontal" id="appSettingForm" role="form">
-                <fieldset>
-                  <legend>Slack App Configuration</legend>
-
-                  <p class="well">
-                    <i class="icon-fw icon-exclamation text-danger"></i><span class="text-danger">NOT RECOMMENDED</span>
-                    <br><br>
-                    This is the way that compatible with Crowi,<br>
-                    but not recommended in GROWI because it is <strong>too complex</strong>.
-                    <br><br>
-                    Please use <a href="#slack-incoming-webhooks" data-toggle="tab" onclick="activateSlackIwh()">Slack incomming webhooks Configuration</a> instead.
-                  </p>
-
-                  <div class="form-group">
-                    <label for="slackSetting[slack:token]" class="col-xs-3 control-label">OAuth Access Token</label>
-                    <div class="col-xs-6">
-                      <input class="form-control" type="text" name="slackSetting[slack:token]" value="{{ slackSetting['slack:token'] || '' }}">
-                    </div>
-                  </div>
-
-                  <div class="form-group">
-                    <div class="col-xs-offset-3 col-xs-6">
-                      <button type="submit" class="btn btn-primary">Save</button>
-                    </div>
-                  </div>
-                </fieldset>
-                <input type="hidden" name="_csrf" value="{{ csrf() }}">
-              </form>
-
-              <hr>
-              <h3>
-                <i class="icon-question" aria-hidden="true"></i>
-                <a href="#collapseHelpForApp" data-toggle="collapse">How to configure Slack App?</a>
-              </h3>
-
-              <ol id="collapseHelpForApp" class="collapse">
-                <li>
-                  Register Slack App
-                  <ol>
-                    <li>
-                     Create App from <a href="https://api.slack.com/applications/new">this link</a>, and fill the form out as below:
-                      <dl class="dl-horizontal">
-                        <dt>App Name</dt> <dd><code>growi</code> </dd>
-                        <dt>Development Slack Workspace</dt> <dd>Select the workspace you want to notify to.</dd>
-                      </dl>
-                    </li>
-                    <li><strong>Save</strong> it.</li>
-                  </ol>
-                </li>
-                <li>
-                  Set Permission Scopes to the App
-                  <ol>
-                    <li>Go to "OAuth &amp; Permissions" page.</li>
-                    <li>Add "Send messages as GROWI"(<code>chat:write:bot</code>).</li>
-                    <li>Don't forget to <strong>save</strong>.</li>
-                  </ol>
-                </li>
-                <li>
-                  Create a bot user
-                  <ol>
-                    <li>Go to "Bot Users" page and add.</li>
-                  </ol>
-                </li>
-                <li>
-                  Install the app
-                  <ol>
-                    <li>Go to "Install App to Your Workspace" page and install.</li>
-                    <li>Go to "OAuth &amp; Permissions" page and copy <code>OAuth Access Token</code>.</li>
-                  </ol>
-                </li>
-                <li>
-                  (At this page) Set OAuth Access Token
-                  <ol>
-                    <li>Input "OAuth Access Token".</li>
-                    <li>Don't forget to <strong>save</strong>.</li>
-                  </ol>
-                </li>
-              </ol>
-
-            </div><!-- /#slack-app -->
-
-          </div><!-- /.tab-content -->
-        </div>
-
-        <div id="user-trigger-notification" class="tab-pane" role="tabpanel">
-          <h4>Default Notification Settings for Patterns</h4>
-
-          <table class="table table-bordered">
-            <thead>
-              <th>Pattern</th>
-              <th>Channel</th>
-              <th>Operation</th>
-            </thead>
-            <tbody class="admin-notif-list">
-              <form id="slackNotificationForm">
-              <tr>
-                <td>
-                  <input class="form-control" type="text" name="pathPattern" value="" placeholder="e.g. /projects/xxx/MTG/*">
-                  <p class="help-block">
-                    Path name of wiki. Pattern expression with <code>*</code> can be used.
-                  </p>
-                </td>
-                <td>
-                  <input class="form-control form-inline" type="text" name="channel" value="" placeholder="e.g. project-xxx">
-                  <p class="help-block">
-                    Slack channel name. Without <code>#</code>.
-                  </p>
-                </td>
-                <td>
-                  <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                  <input type="submit" value="Add" class="btn btn-primary">
-                </td>
-              </tr>
-              </form>
-
-              {% for userNotif in userNotifications %}
-              <tr class="admin-notif-row" data-updatepost-id="{{ userNotif._id.toString() }}">
-                <td>
-                  {{ userNotif.pathPattern }}
-                </td>
-                <td>
-                  {{ userNotif.channel }}
-                </td>
-                <td>
-                  <form class="admin-remove-updatepost">
-                    <input type="hidden" name="id" value="{{ userNotif._id.toString() }}">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                    <input type="submit" value="Delete" class="btn btn-default">
-                  </form>
-                </td>
-              </tr>
-              {% endfor %}
-            </tbody>
-          </table>
-        </div><!-- /#user-trigger-notification -->
-
-        <div id="global-notification" class="tab-pane" role="tabpanel" >
-          {% include './global-notification.html' %}
-        </div><!-- /#global-notification -->
-
-      </div><!-- /.tab-content -->
-
-    </div>
+    <div class="col-md-9" id="admin-notification-setting"></div>
   </div>
-
-  <script>
-    function activateTab(tab){
-      $('.nav-tabs a[href="#' + tab + '"]').tab('show');
-    };
-
-    function activateSlackIwh() {
-      $("#selectSlackOption").selectpicker('val', '1');
-      $("#slack-app").removeClass('active');
-      $("#slack-incoming-webhooks").addClass('active');
-    }
-
-    function activateSlackApp() {
-      $("#selectSlackOption").selectpicker('val', '2');
-      $("#slack-incoming-webhooks").removeClass('active');
-      $("#slack-app").addClass('active');
-    }
-
-    window.addEventListener('load', function(e) {
-      // hash on page
-      if (location.hash) {
-        if (location.hash == '#global-notification') {
-          activateTab('global-notification');
-        }
-      }
-    });
-
-    $("#selectSlackOption").on('change', function() {
-      if (this.value === "1") {
-        activateSlackIwh();
-      }
-      else if (this.value === "2") {
-        activateSlackApp();
-      }
-    });
-  </script>
 </div>
 {% endblock content_main %}
 
 {% block content_footer %}
 {% endblock content_footer %}
-
-
-