Răsfoiți Sursa

Merge pull request #6492 from weseek/support/global-notification-next-admin

support: global notification next admin
cao 3 ani în urmă
părinte
comite
81f09dc987

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

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

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

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

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

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

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

@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React from 'react';
 
 import { isClient, objectIdUtils } from '@growi/core';
 import {
@@ -52,6 +52,7 @@ const CustomizeSettingContents = dynamic(() => import('../../components/Admin/Cu
 const DataImportPageContents = dynamic(() => import('../../components/Admin/ImportData/ImportDataPageContents'), { ssr: false });
 const ExportArchiveDataPage = dynamic(() => import('../../components/Admin/ExportArchiveDataPage'), { ssr: false });
 const NotificationSetting = dynamic(() => import('../../components/Admin/Notification/NotificationSetting'), { ssr: false });
+const ManageGlobalNotification = dynamic(() => import('../../components/Admin/Notification/ManageGlobalNotification'), { ssr: false });
 const SlackIntegration = dynamic(() => import('../../components/Admin/SlackIntegration/SlackIntegration'), { ssr: false });
 const LegacySlackIntegration = dynamic(() => import('../../components/Admin/LegacySlackIntegration/LegacySlackIntegration'), { ssr: false });
 const UserManagement = dynamic(() => import('../../components/Admin/UserManagement'), { ssr: false });
@@ -140,8 +141,10 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
       component: <NotificationSetting />,
     },
     'global-notification': {
-      title: '',
-      component: <>global-notification</>,
+      new: {
+        title: t('external_notification.external_notification'),
+        component: <ManageGlobalNotification />,
+      },
     },
     'slack-integration': {
       title: t('slack_integration.slack_integration'),