ソースを参照

Merge pull request #1766 from weseek/feat/add-global-notification-setting-for-master-merge

Feat/add global notification setting for master merge
Yuki Takei 6 年 前
コミット
bbed48ce9e

+ 4 - 0
resource/locales/ja/translation.json

@@ -554,6 +554,10 @@
     "channel": "チャンネル名",
     "channel": "チャンネル名",
     "pattern_desc": "Wiki のパス名。 パスには <code>*</code> を使用できます。",
     "pattern_desc": "Wiki のパス名。 パスには <code>*</code> を使用できます。",
     "channel_desc": "<code>#</code> を除いた Slack チャンネル名",
     "channel_desc": "<code>#</code> を除いた Slack チャンネル名",
+    "valid_page": "通知の有効 / 無効",
+    "link_notification_help": "<strong>linkを知っている人のみ閲覧できるページ</strong>は常に通知されません。",
+    "just_me_notification_help": "<strong>'自分のみ'に閲覧制限をしているページ</strong>に変更を加えた際に通知する",
+    "group_notification_help": "<strong>'特定グループにのみ'に閲覧制限をしているページ</strong>に変更を加えた際に通知する",
     "notification_list": "通知設定の一覧",
     "notification_list": "通知設定の一覧",
     "add_notification": "通知設定の追加",
     "add_notification": "通知設定の追加",
     "trigger_path": "トリガーパス",
     "trigger_path": "トリガーパス",

+ 84 - 4
src/client/js/components/Admin/Notification/GlobalNotification.jsx

@@ -2,25 +2,105 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
+import loggerFactory from '@alias/logger';
+
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
 import AdminNotificationContainer from '../../../services/AdminNotificationContainer';
 import AdminNotificationContainer from '../../../services/AdminNotificationContainer';
 import GlobalNotificationList from './GlobalNotificationList';
 import GlobalNotificationList from './GlobalNotificationList';
 
 
+const logger = loggerFactory('growi:GlobalNotification');
+
 class GlobalNotification extends React.Component {
 class GlobalNotification extends React.Component {
 
 
+  constructor() {
+    super();
+
+    this.onClickSubmit = this.onClickSubmit.bind(this);
+  }
+
+  async onClickSubmit() {
+    const { t, adminNotificationContainer } = this.props;
+
+    try {
+      await adminNotificationContainer.updateGlobalNotificationForPages();
+      toastSuccess(t('toaster.update_successed', { target: t('Notification Settings') }));
+    }
+    catch (err) {
+      toastError(err);
+      logger.error(err);
+    }
+  }
+
   render() {
   render() {
     const { t, adminNotificationContainer } = this.props;
     const { t, adminNotificationContainer } = this.props;
     const { globalNotifications } = adminNotificationContainer.state;
     const { globalNotifications } = adminNotificationContainer.state;
     return (
     return (
       <React.Fragment>
       <React.Fragment>
 
 
-        <a href="/admin/global-notification/new">
-          <p className="btn btn-default">{t('notification_setting.add_notification')}</p>
-        </a>
+        <h2 className="border-bottom">{t('notification_setting.valid_page')}</h2>
+
+        <p className="well">
+          {/* eslint-disable-next-line react/no-danger */}
+          <span dangerouslySetInnerHTML={{ __html: t('notification_setting.link_notification_help') }} />
+        </p>
+
+
+        <div className="row mb-4">
+          <div className="col-md-8 col-md-offset-2">
+            <div className="checkbox checkbox-success">
+              <input
+                id="isNotificationForOwnerPageEnabled"
+                type="checkbox"
+                checked={adminNotificationContainer.state.isNotificationForOwnerPageEnabled || false}
+                onChange={() => { adminNotificationContainer.switchIsNotificationForOwnerPageEnabled() }}
+              />
+              <label htmlFor="isNotificationForOwnerPageEnabled">
+                {/* eslint-disable-next-line react/no-danger */}
+                <span dangerouslySetInnerHTML={{ __html: t('notification_setting.just_me_notification_help') }} />
+
+              </label>
+            </div>
+          </div>
+        </div>
+
+
+        <div className="row mb-4">
+          <div className="col-md-8 col-md-offset-2">
+            <div className="checkbox checkbox-success">
+              <input
+                id="isNotificationForGroupPageEnabled"
+                type="checkbox"
+                checked={adminNotificationContainer.state.isNotificationForGroupPageEnabled || false}
+                onChange={() => { adminNotificationContainer.switchIsNotificationForGroupPageEnabled() }}
+              />
+              <label htmlFor="isNotificationForGroupPageEnabled">
+                {/* eslint-disable-next-line react/no-danger */}
+                <span dangerouslySetInnerHTML={{ __html: t('notification_setting.group_notification_help') }} />
+              </label>
+            </div>
+          </div>
+        </div>
+
+        <div className="row my-3">
+          <div className="col-xs-offset-4 col-xs-5">
+            <button
+              type="button"
+              className="btn btn-primary"
+              onClick={this.onClickSubmit}
+              disabled={adminNotificationContainer.state.retrieveError}
+            >{t('Update')}
+            </button>
+          </div>
+        </div>
 
 
-        <h2 className="border-bottom mb-5">{t('notification_setting.notification_list')}</h2>
+        <h2 className="border-bottom mb-5">{t('notification_setting.notification_list')}
+          <a href="/admin/global-notification/new">
+            <p className="btn btn-default pull-right">{t('notification_setting.add_notification')}</p>
+          </a>
+        </h2>
 
 
         <table className="table table-bordered">
         <table className="table table-bordered">
           <thead>
           <thead>

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

@@ -72,7 +72,7 @@ class SlackAppConfiguration extends React.Component {
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
-                  defaultValue={adminNotificationContainer.state.webhookUrl}
+                  defaultValue={adminNotificationContainer.state.webhookUrl || ''}
                   onChange={e => adminNotificationContainer.changeWebhookUrl(e.target.value)}
                   onChange={e => adminNotificationContainer.changeWebhookUrl(e.target.value)}
                 />
                 />
               </div>
               </div>
@@ -84,7 +84,7 @@ class SlackAppConfiguration extends React.Component {
                   <input
                   <input
                     id="cbPrioritizeIWH"
                     id="cbPrioritizeIWH"
                     type="checkbox"
                     type="checkbox"
-                    checked={adminNotificationContainer.state.isIncomingWebhookPrioritized}
+                    checked={adminNotificationContainer.state.isIncomingWebhookPrioritized || false}
                     onChange={() => { adminNotificationContainer.switchIsIncomingWebhookPrioritized() }}
                     onChange={() => { adminNotificationContainer.switchIsIncomingWebhookPrioritized() }}
                   />
                   />
                   <label htmlFor="cbPrioritizeIWH">
                   <label htmlFor="cbPrioritizeIWH">
@@ -123,7 +123,7 @@ class SlackAppConfiguration extends React.Component {
                   <input
                   <input
                     className="form-control"
                     className="form-control"
                     type="text"
                     type="text"
-                    defaultValue={adminNotificationContainer.state.slackToken}
+                    defaultValue={adminNotificationContainer.state.slackToken || ''}
                     onChange={e => adminNotificationContainer.changeSlackToken(e.target.value)}
                     onChange={e => adminNotificationContainer.changeSlackToken(e.target.value)}
                   />
                   />
                 </div>
                 </div>

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

@@ -78,6 +78,7 @@ class UserTriggerNotification extends React.Component {
 
 
   render() {
   render() {
     const { t, adminNotificationContainer } = this.props;
     const { t, adminNotificationContainer } = this.props;
+    const userNotifications = adminNotificationContainer.state.userNotifications || [];
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
@@ -123,10 +124,9 @@ class UserTriggerNotification extends React.Component {
                 <button type="button" className="btn btn-primary" disabled={!this.validateForm()} onClick={this.onClickSubmit}>{t('add')}</button>
                 <button type="button" className="btn btn-primary" disabled={!this.validateForm()} onClick={this.onClickSubmit}>{t('add')}</button>
               </td>
               </td>
             </tr>
             </tr>
-            {adminNotificationContainer.state.userNotifications.map((notification) => {
+            {userNotifications.length > 0 && userNotifications.map((notification) => {
               return <UserNotificationRow notification={notification} onClickDeleteBtn={this.onClickDeleteBtn} key={notification._id} />;
               return <UserNotificationRow notification={notification} onClickDeleteBtn={this.onClickDeleteBtn} key={notification._id} />;
-            })
-            }
+            })}
           </tbody>
           </tbody>
         </table>
         </table>
       </React.Fragment>
       </React.Fragment>

+ 36 - 5
src/client/js/services/AdminNotificationContainer.js

@@ -24,6 +24,8 @@ export default class AdminNotificationContainer extends Container {
       isIncomingWebhookPrioritized: false,
       isIncomingWebhookPrioritized: false,
       slackToken: '',
       slackToken: '',
       userNotifications: [],
       userNotifications: [],
+      isNotificationForOwnerPageEnabled: false,
+      isNotificationForGroupPageEnabled: false,
       globalNotifications: [],
       globalNotifications: [],
     };
     };
 
 
@@ -45,11 +47,13 @@ export default class AdminNotificationContainer extends Container {
       const { notificationParams } = response.data;
       const { notificationParams } = response.data;
 
 
       this.setState({
       this.setState({
-        webhookUrl: notificationParams.webhookUrl || '',
-        isIncomingWebhookPrioritized: notificationParams.isIncomingWebhookPrioritized || false,
-        slackToken: notificationParams.slackToken || '',
-        userNotifications: notificationParams.userNotifications || [],
-        globalNotifications: notificationParams.globalNotifications || [],
+        webhookUrl: notificationParams.webhookUrl,
+        isIncomingWebhookPrioritized: notificationParams.isIncomingWebhookPrioritized,
+        slackToken: notificationParams.slackToken,
+        userNotifications: notificationParams.userNotifications,
+        isNotificationForOwnerPageEnabled: notificationParams.isNotificationForOwnerPageEnabled,
+        isNotificationForGroupPageEnabled: notificationParams.isNotificationForGroupPageEnabled,
+        globalNotifications: notificationParams.globalNotifications,
       });
       });
 
 
     }
     }
@@ -124,6 +128,33 @@ export default class AdminNotificationContainer extends Container {
     return deletedNotificaton;
     return deletedNotificaton;
   }
   }
 
 
+  /**
+   * Switch isNotificationForOwnerPageEnabled
+   */
+  switchIsNotificationForOwnerPageEnabled() {
+    this.setState({ isNotificationForOwnerPageEnabled: !this.state.isNotificationForOwnerPageEnabled });
+  }
+
+  /**
+   * Switch isNotificationForGroupPageEnabled
+   */
+  switchIsNotificationForGroupPageEnabled() {
+    this.setState({ isNotificationForGroupPageEnabled: !this.state.isNotificationForGroupPageEnabled });
+  }
+
+  /**
+   * Update globalNotificationForPages
+   * @memberOf SlackAppConfiguration
+   */
+  async updateGlobalNotificationForPages() {
+    const response = await this.appContainer.apiv3.put('/notification-setting/notify-for-page-grant/', {
+      isNotificationForOwnerPageEnabled: this.state.isNotificationForOwnerPageEnabled,
+      isNotificationForGroupPageEnabled: this.state.isNotificationForGroupPageEnabled,
+    });
+
+    return response;
+  }
+
   /**
   /**
    * Delete global notification pattern
    * Delete global notification pattern
    */
    */

+ 3 - 0
src/server/models/config.js

@@ -117,6 +117,9 @@ module.exports = function(crowi) {
       'customize:isEnabledStaleNotification': false,
       'customize:isEnabledStaleNotification': false,
       'customize:isAllReplyShown': false,
       'customize:isAllReplyShown': false,
 
 
+      'notification:owner-page:isEnabled': false,
+      'notification:group-page:isEnabled': false,
+
       'importer:esa:team_name': undefined,
       'importer:esa:team_name': undefined,
       'importer:esa:access_token': undefined,
       'importer:esa:access_token': undefined,
       'importer:qiita:team_name': undefined,
       'importer:qiita:team_name': undefined,

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

@@ -10,6 +10,7 @@ const router = express.Router();
 const { body } = require('express-validator/check');
 const { body } = require('express-validator/check');
 
 
 const ErrorV3 = require('../../models/vo/error-apiv3');
 const ErrorV3 = require('../../models/vo/error-apiv3');
+const removeNullPropertyFromObject = require('../../../lib/util/removeNullPropertyFromObject');
 
 
 const validator = {
 const validator = {
   slackConfiguration: [
   slackConfiguration: [
@@ -32,6 +33,10 @@ const validator = {
       return (req.body.notifyToType === 'slack') ? !!value : true;
       return (req.body.notifyToType === 'slack') ? !!value : true;
     }),
     }),
   ],
   ],
+  notifyForPageGrant: [
+    body('isNotificationForOwnerPageEnabled').isBoolean(),
+    body('isNotificationForGroupPageEnabled').isBoolean(),
+  ],
 };
 };
 
 
 /**
 /**
@@ -66,6 +71,15 @@ const validator = {
  *          channel:
  *          channel:
  *            type: string
  *            type: string
  *            description: slack channel name without '#'
  *            description: slack channel name without '#'
+ *      NotifyForPageGrant:
+ *        type: object
+ *        properties:
+ *          isNotificationForOwnerPageEnabled:
+ *            type: string
+ *            description: Whether to notify on owner page
+ *          isNotificationForGroupPageEnabled:
+ *            type: string
+ *            description: Whether to notify on group page
  *      GlobalNotificationParams:
  *      GlobalNotificationParams:
  *        type: object
  *        type: object
  *        properties:
  *        properties:
@@ -125,6 +139,8 @@ module.exports = (crowi) => {
       isIncomingWebhookPrioritized: await crowi.configManager.getConfig('notification', 'slack:isIncomingWebhookPrioritized'),
       isIncomingWebhookPrioritized: await crowi.configManager.getConfig('notification', 'slack:isIncomingWebhookPrioritized'),
       slackToken: await crowi.configManager.getConfig('notification', 'slack:token'),
       slackToken: await crowi.configManager.getConfig('notification', 'slack:token'),
       userNotifications: await UpdatePost.findAll(),
       userNotifications: await UpdatePost.findAll(),
+      isNotificationForOwnerPageEnabled: await crowi.configManager.getConfig('notification', 'notification:owner-page:isEnabled'),
+      isNotificationForGroupPageEnabled: await crowi.configManager.getConfig('notification', 'notification:group-page:isEnabled'),
       globalNotifications: await GlobalNotificationSetting.findAll(),
       globalNotifications: await GlobalNotificationSetting.findAll(),
     };
     };
     return res.apiv3({ notificationParams });
     return res.apiv3({ notificationParams });
@@ -401,6 +417,51 @@ module.exports = (crowi) => {
 
 
   });
   });
 
 
+
+  /**
+   * @swagger
+   *
+   *    /notification-setting/notify-for-page-grant:
+   *      put:
+   *        tags: [NotificationSetting]
+   *        description: Update settings for notify for page grant
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/NotifyForPageGrant'
+   *        responses:
+   *          200:
+   *            description: Succeeded to settings for notify for page grant
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  $ref: '#/components/schemas/NotifyForPageGrant'
+   */
+  router.put('/notify-for-page-grant', loginRequiredStrictly, adminRequired, csrf, validator.notifyForPageGrant, ApiV3FormValidator, async(req, res) => {
+
+    let requestParams = {
+      'notification:owner-page:isEnabled': req.body.isNotificationForOwnerPageEnabled,
+      'notification:group-page:isEnabled': req.body.isNotificationForGroupPageEnabled,
+    };
+
+    requestParams = removeNullPropertyFromObject(requestParams);
+
+    try {
+      await crowi.configManager.updateConfigsInTheSameNamespace('notification', requestParams);
+      const responseParams = {
+        isNotificationForOwnerPageEnabled: await crowi.configManager.getConfig('notification', 'notification:owner-page:isEnabled'),
+        isNotificationForGroupPageEnabled: await crowi.configManager.getConfig('notification', 'notification:group-page:isEnabled'),
+      };
+      return res.apiv3({ responseParams });
+    }
+    catch (err) {
+      const msg = 'Error occurred in updating notify for page grant';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'update-notify-for-page-grant-failed'));
+    }
+  });
   /**
   /**
    * @swagger
    * @swagger
    *
    *

+ 9 - 3
src/server/routes/comment.js

@@ -256,9 +256,15 @@ module.exports = function(crowi, app) {
     const path = page.path;
     const path = page.path;
 
 
     // global notification
     // global notification
-    globalNotificationService.fire(GlobalNotificationSetting.EVENT.COMMENT, path, req.user, {
-      comment: createdComment,
-    });
+    try {
+      await globalNotificationService.fire(GlobalNotificationSetting.EVENT.COMMENT, page, req.user, {
+        comment: createdComment,
+      });
+    }
+    catch (err) {
+      logger.error('Comment notification failed', err);
+    }
+
 
 
     // slack notification
     // slack notification
     if (slackNotificationForm.isSlackEnabled) {
     if (slackNotificationForm.isSlackEnabled) {

+ 22 - 12
src/server/routes/page.js

@@ -833,10 +833,10 @@ module.exports = function(crowi, app) {
 
 
     // global notification
     // global notification
     try {
     try {
-      await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_CREATE, createdPage.path, req.user);
+      await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_CREATE, createdPage, req.user);
     }
     }
     catch (err) {
     catch (err) {
-      logger.error(err);
+      logger.error('Create notification failed', err);
     }
     }
 
 
     // user notification
     // user notification
@@ -961,10 +961,10 @@ module.exports = function(crowi, app) {
 
 
     // global notification
     // global notification
     try {
     try {
-      await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_EDIT, page.path, req.user);
+      await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_EDIT, page, req.user);
     }
     }
     catch (err) {
     catch (err) {
-      logger.error(err);
+      logger.error('Edit notification failed', err);
     }
     }
 
 
     // user notification
     // user notification
@@ -1299,10 +1299,10 @@ module.exports = function(crowi, app) {
 
 
     try {
     try {
       // global notification
       // global notification
-      await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_LIKE, page.path, req.user);
+      await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_LIKE, page, req.user);
     }
     }
     catch (err) {
     catch (err) {
-      logger.error('Like failed', err);
+      logger.error('Like notification failed', err);
     }
     }
   };
   };
 
 
@@ -1499,8 +1499,13 @@ module.exports = function(crowi, app) {
 
 
     res.json(ApiResponse.success(result));
     res.json(ApiResponse.success(result));
 
 
-    // global notification
-    await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_DELETE, page.path, req.user);
+    try {
+      // global notification
+      await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_DELETE, page, req.user);
+    }
+    catch (err) {
+      logger.error('Delete notification failed', err);
+    }
   };
   };
 
 
   /**
   /**
@@ -1652,10 +1657,15 @@ module.exports = function(crowi, app) {
 
 
     res.json(ApiResponse.success(result));
     res.json(ApiResponse.success(result));
 
 
-    // global notification
-    globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_MOVE, page.path, req.user, {
-      oldPath: req.body.path,
-    });
+    try {
+      // global notification
+      await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_MOVE, page, req.user, {
+        oldPath: req.body.path,
+      });
+    }
+    catch (err) {
+      logger.error('Move notification failed', err);
+    }
 
 
     return page;
     return page;
   };
   };

+ 37 - 5
src/server/service/global-notification/index.js

@@ -13,32 +13,64 @@ class GlobalNotificationService {
 
 
     this.gloabalNotificationMail = new GloabalNotificationMail(crowi);
     this.gloabalNotificationMail = new GloabalNotificationMail(crowi);
     this.gloabalNotificationSlack = new GloabalNotificationSlack(crowi);
     this.gloabalNotificationSlack = new GloabalNotificationSlack(crowi);
+
+    this.Page = this.crowi.model('Page');
+
   }
   }
 
 
+
   /**
   /**
    * fire global notification
    * fire global notification
    *
    *
    * @memberof GlobalNotificationService
    * @memberof GlobalNotificationService
    *
    *
    * @param {string} event event name triggered
    * @param {string} event event name triggered
-   * @param {string} path path triggered the event
+   * @param {string} page page triggered the event
    * @param {User} triggeredBy user who triggered the event
    * @param {User} triggeredBy user who triggered the event
    * @param {object} vars event specific vars
    * @param {object} vars event specific vars
    */
    */
-  async fire(event, path, triggeredBy, vars = {}) {
+  async fire(event, page, triggeredBy, vars = {}) {
     logger.debug(`global notficatoin event ${event} was triggered`);
     logger.debug(`global notficatoin event ${event} was triggered`);
 
 
     // validation
     // validation
-    if (event == null || path == null || triggeredBy == null) {
+    if (event == null || page.path == null || triggeredBy == null) {
       throw new Error(`invalid vars supplied to GlobalNotificationSlackService.generateOption for event ${event}`);
       throw new Error(`invalid vars supplied to GlobalNotificationSlackService.generateOption for event ${event}`);
     }
     }
 
 
+    if (!this.isSendNotification(page.grant)) {
+      logger.info('this page does not send notifications');
+      return;
+    }
+
     await Promise.all([
     await Promise.all([
-      this.gloabalNotificationMail.fire(event, path, triggeredBy, vars),
-      this.gloabalNotificationSlack.fire(event, path, triggeredBy, vars),
+      this.gloabalNotificationMail.fire(event, page.path, triggeredBy, vars),
+      this.gloabalNotificationSlack.fire(event, page.path, triggeredBy, vars),
     ]);
     ]);
   }
   }
 
 
+  /**
+   * fire global notification
+   *
+   * @memberof GlobalNotificationService
+   *
+   * @param {number} grant page grant
+   * @return {boolean} isSendNotification
+   */
+  isSendNotification(grant) {
+    switch (grant) {
+      case this.Page.GRANT_PUBLIC:
+        return true;
+      case this.Page.GRANT_RESTRICTED:
+        return false;
+      case this.Page.GRANT_SPECIFIED:
+        return false;
+      case this.Page.GRANT_OWNER:
+        return (this.crowi.configManager.getConfig('notification', 'notification:owner-page:isEnabled') || false);
+      case this.Page.GRANT_USER_GROUP:
+        return (this.crowi.configManager.getConfig('notification', 'notification:group-page:isEnabled') || false);
+    }
+  }
+
 }
 }
 
 
 module.exports = GlobalNotificationService;
 module.exports = GlobalNotificationService;