Răsfoiți Sursa

Merge pull request #1583 from weseek/reactify-admin/notification-setting

Reactify admin/notification setting
Yuki Takei 6 ani în urmă
părinte
comite
6062218aee
25 a modificat fișierele cu 1779 adăugiri și 811 ștergeri
  1. 28 2
      resource/locales/en-US/translation.json
  2. 28 2
      resource/locales/ja/translation.json
  3. 8 0
      src/client/js/app.jsx
  4. 60 0
      src/client/js/components/Admin/Notification/GlobalNotification.jsx
  5. 175 0
      src/client/js/components/Admin/Notification/GlobalNotificationList.jsx
  6. 279 0
      src/client/js/components/Admin/Notification/ManageGlobalNotification.jsx
  7. 48 0
      src/client/js/components/Admin/Notification/NotificationDeleteModal.jsx
  8. 80 0
      src/client/js/components/Admin/Notification/NotificationSetting.jsx
  9. 184 0
      src/client/js/components/Admin/Notification/SlackAppConfiguration.jsx
  10. 36 0
      src/client/js/components/Admin/Notification/TriggerEventCheckBox.jsx
  11. 49 0
      src/client/js/components/Admin/Notification/UserNotificationRow.jsx
  12. 151 0
      src/client/js/components/Admin/Notification/UserTriggerNotification.jsx
  13. 137 0
      src/client/js/services/AdminNotificationContainer.js
  14. 0 17
      src/server/form/admin/notificationGlobal.js
  15. 0 8
      src/server/form/admin/slackIwhSetting.js
  16. 0 7
      src/server/form/admin/slackSetting.js
  17. 0 3
      src/server/form/index.js
  18. 4 218
      src/server/routes/admin.js
  19. 2 0
      src/server/routes/apiv3/index.js
  20. 1 2
      src/server/routes/apiv3/markdown-setting.js
  21. 503 0
      src/server/routes/apiv3/notification-setting.js
  22. 0 8
      src/server/routes/index.js
  23. 5 133
      src/server/views/admin/global-notification-detail.html
  24. 0 124
      src/server/views/admin/global-notification.html
  25. 1 287
      src/server/views/admin/notification.html

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

@@ -519,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",
@@ -535,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",

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

@@ -502,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": "通知設定一覧に戻る",
@@ -518,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} />,
 };

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

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