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

Merge pull request #547 from weseek/feat/mail-notification-combine

Feat/mail notification combine
Sou Mizobuchi 7 лет назад
Родитель
Сommit
eda6432ab7

+ 17 - 0
lib/crowi/index.js

@@ -37,6 +37,7 @@ function Crowi(rootdir, env) {
   this.mailer = {};
   this.interceptorManager = {};
   this.passportService = null;
+  this.globalNotificationService = null;
   this.xss = new Xss();
 
   this.tokens = null;
@@ -90,6 +91,8 @@ Crowi.prototype.init = function() {
       return self.setupSlack();
     }).then(function() {
       return self.setupCsrf();
+    }).then(function() {
+      return self.setUpGlobalNotification();
     });
 };
 
@@ -246,6 +249,10 @@ Crowi.prototype.getInterceptorManager = function() {
   return this.interceptorManager;
 };
 
+Crowi.prototype.getGlobalNotificationService = function() {
+  return this.globalNotificationService;
+};
+
 Crowi.prototype.setupPassport = function() {
   const config = this.getConfig();
   const Config = this.model('Config');
@@ -452,4 +459,14 @@ Crowi.prototype.require = function(modulePath) {
   return require(modulePath);
 };
 
+/**
+ * setup GlobalNotificationService
+ */
+Crowi.prototype.setUpGlobalNotification = function() {
+  const GlobalNotificationService = require('../service/global-notification');
+  if (this.globalNotificationService == null) {
+    this.globalNotificationService = new GlobalNotificationService(this);
+  }
+};
+
 module.exports = Crowi;

+ 19 - 0
lib/form/admin/notificationGlobal.js

@@ -0,0 +1,19 @@
+'use strict';
+
+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(),
+);
+

+ 1 - 0
lib/form/index.js

@@ -36,5 +36,6 @@ module.exports = {
     slackIwhSetting: require('./admin/slackIwhSetting'),
     slackSetting: require('./admin/slackSetting'),
     userGroupCreate: require('./admin/userGroupCreate'),
+    notificationGlobal: require('./admin/notificationGlobal'),
   },
 };

+ 9 - 0
lib/locales/en-US/notifications/comment.txt

@@ -0,0 +1,9 @@
+{{ username }} commented on {{ path }}.
+
+----------------------
+
+{{ comment }}
+
+----------------------
+
+Growi: {{ appTitle }}

+ 5 - 0
lib/locales/en-US/notifications/pageCreate.txt

@@ -0,0 +1,5 @@
+{{ username }} created a new page under {{ path }}.
+
+----------------------
+
+Growi: {{ appTitle }}

+ 5 - 0
lib/locales/en-US/notifications/pageDelete.txt

@@ -0,0 +1,5 @@
+{{ username }} deleted the page  {{ path }}.
+
+----------------------
+
+Growi: {{ appTitle }}

+ 5 - 0
lib/locales/en-US/notifications/pageEdit.txt

@@ -0,0 +1,5 @@
+{{ username }} edited the page {{ path }}.
+
+----------------------
+
+Growi: {{ appTitle }}

+ 5 - 0
lib/locales/en-US/notifications/pageLike.txt

@@ -0,0 +1,5 @@
+{{ username }} liked the page {{ path }}.
+
+----------------------
+
+Growi: {{ appTitle }}

+ 5 - 0
lib/locales/en-US/notifications/pageMove.txt

@@ -0,0 +1,5 @@
+{{ username }} renamed the page {{ oldPath }} to {{ newPath }}.
+
+----------------------
+
+Growi: {{ appTitle }}

+ 6 - 91
lib/models/GlobalNotificationSetting.js

@@ -1,95 +1,10 @@
-const debug = require('debug')('growi:models:GlobalNotificationSetting');
 const mongoose = require('mongoose');
-
-/**
- * parent schema in this model
- */
-const notificationSchema = new mongoose.Schema({
-  isEnabled: { type: Boolean, required: true, default: true },
-  triggerPath: { type: String, required: true },
-  triggerEvents: { type: [String] },
-});
-
-/**
- * create child schemas inherited from parentSchema
- * all child schemas are stored in globalnotificationsettings collection
- * @link{http://url.com module_name}
- * @param {object} parentSchema
- * @param {string} modelName
- * @param {string} discriminatorKey
- */
-const createChildSchemas = (parentSchema, modelName, discriminatorKey) => {
-  const Notification = mongoose.model(modelName, parentSchema);
-  const mailNotification = Notification.discriminator('mail', new mongoose.Schema({
-    toEmail: String,
-  }, {discriminatorKey: discriminatorKey}));
-
-  const slackNotification = Notification.discriminator('slack', new mongoose.Schema({
-    slackChannels: String,
-  }, {discriminatorKey: discriminatorKey}));
-
-  return {
-    Mail: mailNotification,
-    Slack: slackNotification,
-  };
-};
-
-
-/**
- * GlobalNotificationSetting Class
- * @class GlobalNotificationSetting
- */
-class GlobalNotificationSetting {
-
-  constructor(crowi) {
-    this.crowi = crowi;
-  }
-
-  /**
-   * enable notification setting
-   * @param {string} id
-   */
-  static enable(id) {
-    // return new Promise((resolve, reject) => {
-      // save
-      // return resolve(Notification)
-    //}
-  }
-
-  /**
-   * disable notification setting
-   * @param {string} id
-   */
-  static disable(id) {
-    // return new Promise((resolve, reject) => {
-      // save
-      // return resolve(Notification)
-    //}
-  }
-
-  /**
-   * find a list of notification settings by path and a list of events
-   * @param {string} path
-   * @param {string} event
-   * @param {boolean} enabled
-   */
-  static findSettingByPathAndEvent(path, event, enabled) {
-    // return new Promise((resolve, reject) => {
-      // if(enabled == null) {
-      //   find all
-      // }
-      // else {
-      //   find only enabled/disabled
-      // }
-      // sort by path in mongoDB
-
-      // return resolve([Notification])
-    //}
-  }
-}
+const GlobalNotificationSetting = require('./GlobalNotificationSetting/index');
+const GlobalNotificationSettingClass = GlobalNotificationSetting.class;
+const GlobalNotificationSettingSchema = GlobalNotificationSetting.schema;
 
 module.exports = function(crowi) {
-  GlobalNotificationSetting.crowi = crowi;
-  notificationSchema.loadClass(GlobalNotificationSetting);
-  return createChildSchemas(notificationSchema, 'GlobalNotificationSetting', 'type');
+  GlobalNotificationSettingClass.crowi = crowi;
+  GlobalNotificationSettingSchema.loadClass(GlobalNotificationSettingClass);
+  return mongoose.model('GlobalNotificationSetting', GlobalNotificationSettingSchema);
 };

+ 16 - 0
lib/models/GlobalNotificationSetting/GlobalNotificationMailSetting.js

@@ -0,0 +1,16 @@
+const mongoose = require('mongoose');
+const GlobalNotificationSetting = require('./index');
+const GlobalNotificationSettingClass = GlobalNotificationSetting.class;
+const GlobalNotificationSettingSchema = GlobalNotificationSetting.schema;
+
+module.exports = function(crowi) {
+  GlobalNotificationSettingClass.crowi = crowi;
+  GlobalNotificationSettingSchema.loadClass(GlobalNotificationSettingClass);
+
+  const GlobalNotificationSettingModel = mongoose.model('GlobalNotificationSetting', GlobalNotificationSettingSchema);
+  const GlobalNotificationMailSettingModel = GlobalNotificationSettingModel.discriminator('mail', new mongoose.Schema({
+    toEmail: String,
+  }, {discriminatorKey: 'type'}));
+
+  return GlobalNotificationMailSettingModel;
+};

+ 16 - 0
lib/models/GlobalNotificationSetting/GlobalNotificationSlackSetting.js

@@ -0,0 +1,16 @@
+const mongoose = require('mongoose');
+const GlobalNotificationSetting = require('./index');
+const GlobalNotificationSettingClass = GlobalNotificationSetting.class;
+const GlobalNotificationSettingSchema = GlobalNotificationSetting.schema;
+
+module.exports = function(crowi) {
+  GlobalNotificationSettingClass.crowi = crowi;
+  GlobalNotificationSettingSchema.loadClass(GlobalNotificationSettingClass);
+
+  const GlobalNotificationSettingModel = mongoose.model('GlobalNotificationSetting', GlobalNotificationSettingSchema);
+  const GlobalNotificationSlackSettingModel = GlobalNotificationSettingModel.discriminator('slack', new mongoose.Schema({
+    slackChannels: String,
+  }, {discriminatorKey: 'type'}));
+
+  return GlobalNotificationSlackSettingModel;
+};

+ 113 - 0
lib/models/GlobalNotificationSetting/index.js

@@ -0,0 +1,113 @@
+const mongoose = require('mongoose');
+
+/**
+ * parent schema for GlobalNotificationSetting model
+ */
+const globalNotificationSettingSchema = new mongoose.Schema({
+  isEnabled: { type: Boolean, required: true, default: true },
+  triggerPath: { type: String, required: true },
+  triggerEvents: { type: [String] },
+});
+
+
+/**
+ * GlobalNotificationSetting Class
+ * @class GlobalNotificationSetting
+ */
+class GlobalNotificationSetting {
+
+  constructor(crowi) {
+    this.crowi = crowi;
+  }
+
+  /**
+   * enable notification setting
+   * @param {string} id
+   */
+  static async enable(id) {
+    const setting = await this.findOne({_id: id});
+
+    setting.isEnabled = true;
+    setting.save();
+
+    return setting;
+  }
+
+  /**
+   * disable notification setting
+   * @param {string} id
+   */
+  static async disable(id) {
+    const setting = await this.findOne({_id: id});
+
+    setting.isEnabled = false;
+    setting.save();
+
+    return setting;
+  }
+
+  /**
+   * find all notification settings
+   */
+  static async findAll() {
+    const settings = await this.find().sort({ triggerPath: 1 });
+
+    return settings;
+  }
+
+  /**
+   * find a list of notification settings by path and a list of events
+   * @param {string} path
+   * @param {string} event
+   */
+  static async findSettingByPathAndEvent(path, event) {
+    const pathsToMatch = generatePathsToMatch(path);
+
+    const settings = await this.find({
+      triggerPath: {$in: pathsToMatch},
+      triggerEvents: event,
+      isEnabled: true
+    })
+    .sort({ triggerPath: 1 });
+
+    return settings;
+  }
+}
+
+
+// move this to util
+// remove this from models/page
+const cutOffLastSlash = path => {
+  const lastSlash = path.lastIndexOf('/');
+  return path.substr(0, lastSlash);
+};
+
+const generatePathsOnTree = (path, pathList) => {
+  pathList.push(path);
+
+  if (path === '') {
+    return pathList;
+  }
+
+  const newPath = cutOffLastSlash(path);
+
+  return generatePathsOnTree(newPath, pathList);
+};
+
+const generatePathsToMatch = (originalPath) => {
+  const pathList = generatePathsOnTree(originalPath, []);
+  return pathList.map(path => {
+    if (path !== originalPath) {
+      return path + '/*';
+    }
+    else {
+      return path;
+    }
+  });
+};
+
+
+module.exports = {
+  class: GlobalNotificationSetting,
+  schema: globalNotificationSettingSchema,
+};

+ 3 - 1
lib/models/index.js

@@ -12,5 +12,7 @@ module.exports = {
   Comment: require('./comment'),
   Attachment: require('./attachment'),
   UpdatePost: require('./updatePost'),
-  GlobalNotification: require('./GlobalNotificationSetting'),
+  GlobalNotificationSetting: require('./GlobalNotificationSetting'),
+  GlobalNotificationMailSetting: require('./GlobalNotificationSetting/GlobalNotificationMailSetting'),
+  GlobalNotificationSlackSetting: require('./GlobalNotificationSetting/GlobalNotificationSlackSetting'),
 };

+ 1 - 1
lib/models/page.js

@@ -1047,7 +1047,7 @@ module.exports = function(crowi) {
     var newRevision = await Revision.prepareRevision(pageData, body, user);
 
     const revision = await Page.pushRevision(pageData, newRevision, user);
-    const savedPage = await Page.findPageByPath(revision.path).populate('revision');
+    const savedPage = await Page.findPageByPath(revision.path).populate('revision').populate('creator');
     if (grant != null) {
       const grantData = await Page.updateGrant(savedPage, grant, user, grantUserGroupId);
       debug('Page grant update:', grantData);

+ 128 - 15
lib/routes/admin.js

@@ -2,6 +2,7 @@ module.exports = function(crowi, app) {
   'use strict';
 
   var debug = require('debug')('growi:routes:admin')
+    , logger = require('@alias/logger')('growi:routes:admin')
     , fs = require('fs')
     , models = crowi.models
     , Page = models.Page
@@ -11,6 +12,9 @@ module.exports = function(crowi, app) {
     , UserGroup = models.UserGroup
     , UserGroupRelation = models.UserGroupRelation
     , Config = models.Config
+    , GlobalNotificationSetting = models.GlobalNotificationSetting
+    , GlobalNotificationMailSetting = models.GlobalNotificationMailSetting
+    , GlobalNotificationSlackSetting = models.GlobalNotificationSlackSetting
     , PluginUtils = require('../plugins/plugin-utils')
     , pluginUtils = new PluginUtils()
     , ApiResponse = require('../util/apiResponse')
@@ -187,13 +191,12 @@ module.exports = function(crowi, app) {
 
   // app.get('/admin/notification'               , admin.notification.index);
   actions.notification = {};
-  actions.notification.index = function(req, res) {
-    var config = crowi.getConfig();
-    var UpdatePost = crowi.model('UpdatePost');
-    var slackSetting = Config.setupCofigFormData('notification', config);
-    var hasSlackIwhUrl = Config.hasSlackIwhUrl(config);
-    var hasSlackToken = Config.hasSlackToken(config);
-    var slack = crowi.slack;
+  actions.notification.index = async(req, res) => {
+    const config = crowi.getConfig();
+    const UpdatePost = crowi.model('UpdatePost');
+    let slackSetting = Config.setupCofigFormData('notification', config);
+    const hasSlackIwhUrl = Config.hasSlackIwhUrl(config);
+    const hasSlackToken = Config.hasSlackToken(config);
 
     if (!Config.hasSlackIwhUrl(req.config)) {
       slackSetting['slack:incomingWebhookUrl'] = '';
@@ -204,14 +207,15 @@ module.exports = function(crowi, app) {
       req.session.slackSetting = null;
     }
 
-    UpdatePost.findAll()
-    .then(function(settings) {
-      return res.render('admin/notification', {
-        settings,
-        slackSetting,
-        hasSlackIwhUrl,
-        hasSlackToken,
-      });
+    const globalNotifications = await GlobalNotificationSetting.findAll();
+    const userNotifications = await UpdatePost.findAll();
+
+    return res.render('admin/notification', {
+      userNotifications,
+      slackSetting,
+      hasSlackIwhUrl,
+      hasSlackToken,
+      globalNotifications,
     });
   };
 
@@ -309,6 +313,84 @@ module.exports = function(crowi, app) {
     });
   };
 
+  actions.globalNotification = {};
+  actions.globalNotification.detail = async(req, res) => {
+    const notificationSettingId = req.params.id;
+    let renderVars = {};
+
+    if (notificationSettingId) {
+      try {
+        renderVars.setting = 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 'mail':
+        setting = new GlobalNotificationMailSetting(crowi);
+        setting.toEmail = form.toEmail;
+        break;
+      // case '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 setting = await GlobalNotificationSetting.findOne({_id: form.id});
+
+    switch (form.notifyToType) {
+      case 'mail':
+        setting.toEmail = form.toEmail;
+        break;
+      // case 'slack':
+      //   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.triggerPath = form.triggerPath;
+    setting.triggerEvents = getNotificationEvents(form);
+    setting.save();
+
+    return res.redirect('/admin/notification#global-notification');
+  };
+
+  const getNotificationEvents = (form) => {
+    let triggerEvents = [];
+    const triggerEventKeys = Object.keys(form).filter(key => key.match(/^triggerEvent/));
+    triggerEventKeys.forEach(key => {
+      if (form[key]) {
+        triggerEvents.push(form[key]);
+      }
+    });
+    return triggerEvents;
+  };
+
   actions.search.buildIndex = function(req, res) {
     var search = crowi.getSearcher();
     if (!search) {
@@ -1070,6 +1152,37 @@ 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.disable(id);
+      }
+      else {
+        await GlobalNotificationSetting.enable(id);
+      }
+
+      return res.json(ApiResponse.success());
+    }
+    catch (err) {
+      return res.json(ApiResponse.error());
+    }
+  };
+
+  actions.api.removeGlobalNotification = async(req, res) => {
+    const id = req.query.id;
+
+    try {
+      await GlobalNotificationSetting.findOneAndRemove({_id: id});
+      return res.json(ApiResponse.success());
+    }
+    catch (err) {
+      return res.json(ApiResponse.error());
+    }
+  };
+
   /**
    * save settings, update config cache, and response json
    *

+ 6 - 4
lib/routes/comment.js

@@ -7,7 +7,7 @@ module.exports = function(crowi, app) {
     , User = crowi.model('User')
     , Page = crowi.model('Page')
     , ApiResponse = require('../util/apiResponse')
-    , notification = require('../service/global-notification')
+    , globalNotificationService = crowi.getGlobalNotificationService()
     , actions = {}
     , api = {};
 
@@ -80,10 +80,14 @@ module.exports = function(crowi, app) {
 
     res.json(ApiResponse.success({comment: createdComment}));
 
+    const path = page.path;
+
+    // global notification
+    globalNotificationService.notifyComment(createdComment, path);
+
     // slack notification
     if (slackNotificationForm.isSlackEnabled) {
       const user = await User.findUserByUsername(req.user.username);
-      const path = page.path;
       const channels = slackNotificationForm.slackChannels;
 
       if (channels) {
@@ -101,8 +105,6 @@ module.exports = function(crowi, app) {
           });
       }
     }
-    // NOTIFICATION: send comment notification here
-    // notification.sendCommentNotification(comment, path);
   };
 
   /**

+ 6 - 0
lib/routes/index.js

@@ -106,6 +106,12 @@ module.exports = function(crowi, app) {
   app.post('/_api/admin/notification.add'    , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.api.notificationAdd);
   app.post('/_api/admin/notification.remove' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.api.notificationRemove);
   app.get('/_api/admin/users.search'         , loginRequired(crowi, app) , middleware.adminRequired() , admin.api.usersSearch);
+  app.get('/admin/global-notification/detail', loginRequired(crowi, app) , middleware.adminRequired() , admin.globalNotification.detail);
+  app.get('/admin/global-notification/detail/:id', loginRequired(crowi, app) , middleware.adminRequired() , admin.globalNotification.detail);
+  app.post('/admin/global-notification/create', loginRequired(crowi, app) , middleware.adminRequired() , form.admin.notificationGlobal, admin.globalNotification.create);
+  app.post('/_api/admin/global-notification/toggleIsEnabled', loginRequired(crowi, app) , middleware.adminRequired() , admin.api.toggleIsEnabledForGlobalNotification);
+  app.post('/admin/global-notification/update', loginRequired(crowi, app) , middleware.adminRequired() , form.admin.notificationGlobal, admin.globalNotification.update);
+  app.post('/admin/global-notification/remove', loginRequired(crowi, app) , middleware.adminRequired() , admin.api.removeGlobalNotification);
 
   app.get('/admin/users'                , loginRequired(crowi, app) , middleware.adminRequired() , admin.user.index);
   app.post('/admin/user/invite'         , form.admin.userInvite ,  loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.user.invite);

+ 64 - 55
lib/routes/page.js

@@ -16,7 +16,7 @@ module.exports = function(crowi, app) {
     , pagePathUtil = require('../util/pagePathUtil')
     , swig = require('swig-templates')
     , getToday = require('../util/getToday')
-    , notification = require('../service/global-notification')
+    , globalNotificationService = crowi.getGlobalNotificationService()
 
     , actions = {};
 
@@ -704,20 +704,22 @@ module.exports = function(crowi, app) {
 
       if (data) {
         previousRevision = data.revision;
-        return Page.updatePage(data, body, req.user, { grant, grantUserGroupId });
-        // .then(() => {
-        //   // NOTIFICATION: send page edit notification here
-        //   notification.sendPageEditNotification(page);
-        // })
+        return Page.updatePage(data, body, req.user, { grant, grantUserGroupId })
+          .then((page) => {
+            // global notification
+            globalNotificationService.notifyPageEdit(page);
+            return page;
+          });
       }
       else {
         // new page
         updateOrCreate = 'create';
-        return Page.create(path, body, req.user, { grant, grantUserGroupId });
-        // .then((page) => {
-        //   // NOTIFICATION: send page create notification here
-        //   notification.sendPageCreateNotification(page);
-        // })
+        return Page.create(path, body, req.user, { grant, grantUserGroupId })
+          .then((page) => {
+            // global notification
+            globalNotificationService.notifyPageCreate(page);
+            return page;
+          });
       }
     }).then(function(data) {
       // data is a saved page data with revision.
@@ -999,17 +1001,20 @@ module.exports = function(crowi, app) {
     Page.findPageByIdAndGrantedUser(id, req.user)
     .then(function(pageData) {
       return pageData.like(req.user);
-    }).then(function(data) {
-      var result = {page: data};
-      return res.json(ApiResponse.success(result));
-    }).catch(function(err) {
+    })
+    .then(function(page) {
+      var result = {page: page};
+      res.json(ApiResponse.success(result));
+      return page;
+    })
+    .then((page) => {
+      // global notification
+      return globalNotificationService.notifyPageLike(page, req.user);
+    })
+    .catch(function(err) {
       debug('Like failed', err);
       return res.json(ApiResponse.error({}));
     });
-    // .then(() => {
-    //   // NOTIFICATION: send page like notification here
-    //   notification.sendPageLikeNotification(page);
-    // })
   };
 
   /**
@@ -1081,44 +1086,47 @@ module.exports = function(crowi, app) {
     const isRecursively = (req.body.recursively !== undefined);
 
     Page.findPageByIdAndGrantedUser(pageId, req.user)
-    .then(function(pageData) {
-      debug('Delete page', pageData._id, pageData.path);
+      .then(function(pageData) {
+        debug('Delete page', pageData._id, pageData.path);
 
-      if (isCompletely) {
-        if (isRecursively) {
-          return Page.completelyDeletePageRecursively(pageData, req.user);
-        }
-        else {
-          return Page.completelyDeletePage(pageData, req.user);
+        if (isCompletely) {
+          if (isRecursively) {
+            return Page.completelyDeletePageRecursively(pageData, req.user);
+          }
+          else {
+            return Page.completelyDeletePage(pageData, req.user);
+          }
         }
-      }
 
-      // else
+        // else
 
-      if (!pageData.isUpdatable(previousRevision)) {
-        throw new Error('Someone could update this page, so couldn\'t delete.');
-      }
+        if (!pageData.isUpdatable(previousRevision)) {
+          throw new Error('Someone could update this page, so couldn\'t delete.');
+        }
 
-      if (isRecursively) {
-        return Page.deletePageRecursively(pageData, req.user);
-      }
-      else {
-        return Page.deletePage(pageData, req.user);
-      }
-    }).then(function(data) {
-      debug('Page deleted', data.path);
-      var result = {};
-      result.page = data;
+        if (isRecursively) {
+          return Page.deletePageRecursively(pageData, req.user);
+        }
+        else {
+          return Page.deletePage(pageData, req.user);
+        }
+      })
+      .then(function(data) {
+        debug('Page deleted', data.path);
+        var result = {};
+        result.page = data;
 
-      return res.json(ApiResponse.success(result));
-    }).catch(function(err) {
-      debug('Error occured while get setting', err, err.stack);
-      return res.json(ApiResponse.error('Failed to delete page.'));
-    });
-    // .then(() => {
-    //   // NOTIFICATION: send page delete notification here
-    //   notification.sendPageDeleteNotification(page);
-    // })
+        res.json(ApiResponse.success(result));
+        return data;
+      })
+      .then((page) => {
+        // global notification
+        return globalNotificationService.notifyPageDelete(page);
+      })
+      .catch(function(err) {
+        debug('Error occured while get setting', err, err.stack);
+        return res.json(ApiResponse.error('Failed to delete page.'));
+      });
   };
 
   /**
@@ -1209,14 +1217,15 @@ module.exports = function(crowi, app) {
 
         return res.json(ApiResponse.success(result));
       })
+      .then(() => {
+        // global notification
+        globalNotificationService.notifyPageMove(page, req.body.path, req.user);
+      })
       .catch(function(err) {
         return res.json(ApiResponse.error('Failed to update page.'));
       });
-      // .then(() => {
-      //   // NOTIFICATION: send page move notification here
-      //   notification.sendPageMoveNotification(page);
-      // })
     });
+
   };
 
   /**

+ 62 - 34
lib/service/global-notification.js

@@ -1,20 +1,21 @@
 const debug = require('debug')('growi:service:GlobalNotification');
-const path = require('path');
-const Notification = require('../models/GlobalNotificationSetting');
-const mailer = require('../util/mailer');
-
 /**
  * the service class of GlobalNotificationSetting
  */
-class GlobalNotification {
+class GlobalNotificationService {
 
   constructor(crowi) {
     this.crowi = crowi;
     this.config = crowi.getConfig();
+    this.mailer = crowi.getMailer();
+    this.GlobalNotification = crowi.model('GlobalNotificationSetting');
+    this.User = crowi.model('User');
+    this.Config = crowi.model('Config');
+    this.appTitle = this.Config.appTitle(this.config);
   }
 
   notifyByMail(notification, mailOption) {
-    mailer.send(Object.assign(mailOption, {to: notification.toEmail}));
+    this.mailer.send(Object.assign(mailOption, {to: notification.toEmail}));
   }
 
   notifyBySlack(notification, slackOption) {
@@ -37,14 +38,18 @@ class GlobalNotification {
    * @memberof GlobalNotification
    * @param {obejct} page
    */
-  notifyPageCreate(page) {
-    const notifications = Notification.findSettingByPathAndEvent(page.path, 'pageCreate');
+  async notifyPageCreate(page) {
+    const notifications = await this.GlobalNotification.Parent.findSettingByPathAndEvent(page.path, 'pageCreate');
     const lang = 'en-US'; //FIXME
     const option = {
       mail: {
         subject: `#pageCreate - ${page.creator.username} created ${page.path}`,
-        template: path.join(this.crowi.localeDir, lang, 'notifications/pageCreate.txt'),
-        vars: {}
+        template: `../../locales/${lang}/notifications/pageCreate.txt`,
+        vars: {
+          appTitle: this.appTitle,
+          path: page.path,
+          username: page.creator.username,
+        }
       },
       slack: {},
     };
@@ -57,14 +62,18 @@ class GlobalNotification {
    * @memberof GlobalNotification
    * @param {obejct} page
    */
-  notifyPageEdit(page) {
-    const notifications = Notification.findSettingByPathAndEvent(page.path, 'pageEdit');
+  async notifyPageEdit(page) {
+    const notifications = await this.GlobalNotification.Parent.findSettingByPathAndEvent(page.path, 'pageEdit');
     const lang = 'en-US'; //FIXME
     const option = {
       mail: {
         subject: `#pageEdit - ${page.creator.username} edited ${page.path}`,
-        template: path.join(this.crowi.localeDir, lang, 'notifications/pageEdit.txt'),
-        vars: {}
+        template: `../../locales/${lang}/notifications/pageEdit.txt`,
+        vars: {
+          appTitle: this.appTitle,
+          path: page.path,
+          username: page.creator.username,
+        }
       },
       slack: {},
     };
@@ -77,14 +86,18 @@ class GlobalNotification {
    * @memberof GlobalNotification
    * @param {obejct} page
    */
-  notifyPageDelete(page) {
-    const notifications = Notification.findSettingByPathAndEvent(page.path, 'pageDelete');
+  async notifyPageDelete(page) {
+    const notifications = await this.GlobalNotification.Parent.findSettingByPathAndEvent(page.path, 'pageDelete');
     const lang = 'en-US'; //FIXME
     const option = {
       mail: {
         subject: `#pageDelete - ${page.creator.username} deleted ${page.path}`,  //FIXME
-        template: path.join(this.crowi.localeDir, lang, 'notifications/pageDelete.txt'),
-        vars: {}
+        template: `../../locales/${lang}/notifications/pageDelete.txt`,
+        vars: {
+          appTitle: this.appTitle,
+          path: page.path,
+          username: page.creator.username,
+        }
       },
       slack: {},
     };
@@ -97,14 +110,19 @@ class GlobalNotification {
    * @memberof GlobalNotification
    * @param {obejct} page
    */
-  notifyPageMove(page) {
-    const notifications = Notification.findSettingByPathAndEvent(page.path, 'pageMove');
+  async notifyPageMove(page, oldPagePath, user) {
+    const notifications = await this.GlobalNotification.Parent.findSettingByPathAndEvent(page.path, 'pageMove');
     const lang = 'en-US'; //FIXME
     const option = {
       mail: {
-        subject: `#pageMove - ${page.creator.username} moved ${page.path} to ${page.path}`, //FIXME
-        template: path.join(this.crowi.localeDir, lang, 'notifications/pageMove.txt'),
-        vars: {}
+        subject: `#pageMove - ${user.username} moved ${page.path} to ${page.path}`, //FIXME
+        template: `../../locales/${lang}/notifications/pageMove.txt`,
+        vars: {
+          appTitle: this.appTitle,
+          oldPath: oldPagePath,
+          newPath: page.path,
+          username: user.username,
+        }
       },
       slack: {},
     };
@@ -117,14 +135,18 @@ class GlobalNotification {
    * @memberof GlobalNotification
    * @param {obejct} page
    */
-  notifyPageLike(page) {
-    const notifications = Notification.findSettingByPathAndEvent(page.path, 'pageLike');
+  async notifyPageLike(page, user) {
+    const notifications = await this.GlobalNotification.Parent.findSettingByPathAndEvent(page.path, 'pageLike');
     const lang = 'en-US'; //FIXME
     const option = {
       mail: {
-        subject: `#pageLike - ${page.creator.username} liked ${page.path}`,
-        template: path.join(this.crowi.localeDir, lang, 'notifications/pageLike.txt'),
-        vars: {}
+        subject: `#pageLike - ${user.username} liked ${page.path}`,
+        template: `../../locales/${lang}/notifications/pageLike.txt`,
+        vars: {
+          appTitle: this.appTitle,
+          path: page.path,
+          username: page.creator.username,
+        }
       },
       slack: {},
     };
@@ -138,14 +160,20 @@ class GlobalNotification {
    * @param {obejct} page
    * @param {obejct} comment
    */
-  notifyComment(comment, path) {
-    const notifications = Notification.findSettingByPathAndEvent(path, 'comment');
+  async notifyComment(comment, path) {
+    const notifications = await this.GlobalNotification.Parent.findSettingByPathAndEvent(path, 'comment');
     const lang = 'en-US'; //FIXME
+    const user = await this.User.findOne({_id: comment.creator});
     const option = {
       mail: {
-        subject: `#comment - ${comment.creator.username} commented on ${path}`,
-        template: path.join(this.crowi.localeDir, lang, 'notifications/comment.txt'),
-        vars: {}
+        subject: `#comment - ${user.username} commented on ${path}`,
+        template: `../../locales/${lang}/notifications/comment.txt`,
+        vars: {
+          appTitle: this.appTitle,
+          path: path,
+          username: user.username,
+          comment: comment.comment,
+        }
       },
       slack: {},
     };
@@ -154,4 +182,4 @@ class GlobalNotification {
   }
 }
 
-module.exports = GlobalNotification;
+module.exports = GlobalNotificationService;

+ 157 - 0
lib/views/admin/global-notification-detail.html

@@ -0,0 +1,157 @@
+{% extends '../layout/admin.html' %}
+
+{% block html_title %}{{ customTitle(t('Notification settings')) }}{% endblock %}
+
+{% block content_header %}
+<div class="header-wrap">
+  <header id="page-header">
+    <h1 class="title" id="">{{ t('Notification settings') }}</h1>
+  </header>
+</div>
+{% endblock %}
+
+{% block content_main %}
+<div class="content-main">
+  {% set smessage = req.flash('successMessage') %}
+  {% if smessage.length %}
+  <div class="alert alert-success">
+    {{ smessage }}
+  </div>
+  {% endif %}
+
+  {% set emessage = req.flash('errorMessage') %}
+  {% if emessage.length %}
+  <div class="alert alert-danger">
+    {{ emessage }}
+  </div>
+  {% endif %}
+
+  <div class="row">
+    <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>
+        通知設定一覧に戻る
+      </a>
+
+      {% if setting %}
+        {% set actionPath = '/admin/global-notification/update' %}
+      {% else %}
+        {% set actionPath = '/admin/global-notification/create' %}
+      {% endif %}
+      <div class="m-t-20 form-box col-md-11">
+        <form action="{{ actionPath }}" method="post" class="form-horizontal" role="form">
+          <legend>通知設定詳細</legend>
+
+          <fieldset class="col-sm-offset-1 col-sm-4">
+            <div class="form-group">
+              <label for="triggerPath" class="control-label">トリガーパス</label><br />
+              <input class="form-control" type="text" name="notificationGlobal[triggerPath]" value="{{ setting.triggerPath || '' }}" required>
+            </div>
+
+            <div class="form-group">
+              <label for="notificationGlobal[notifyToType]"class="control-label">通知先</label><br />
+              <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">
+              <label for="notificationGlobal[toEmail]"class="control-label">Email</label><br />
+              <input class="form-control" type="text" name="notificationGlobal[toEmail]" value="{{ setting.toEmail || '' }}">
+            </div>
+
+            <!-- <div class="form-group notify-to-option {% if setting.__t != 'slack' %}d-none{% endif %}" id="slack-input">
+              <label for="notificationGlobal[slackChannels]"class="control-label">Slack Channels</label><br />
+              <input class="form-control" type="text" name="notificationGlobal[slackChannels]" value="{{ setting.slackChannels || '' }}">
+            </div> -->
+          </fieldset>
+
+          <fieldset class="col-sm-offset-1 col-sm-4">
+            <div class="form-group">
+              <label for="triggerEvent"class="control-label">トリガーイベント</label><br />
+              <div class="checkbox checkbox-info">
+                <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-info"><i class="icon-doc"></i> CREATE</span> - When New Page is Created
+                </label>
+              </div>
+              <div class="checkbox checkbox-info">
+                <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-info"><i class="icon-doc"></i> EDIT</span> - When Page is Edited
+                </label>
+              </div>
+              <div class="checkbox checkbox-info">
+                <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-info"><i class="icon-doc"></i> DELETE</span> - When is Deleted
+                </label>
+              </div>
+              <div class="checkbox checkbox-info">
+                <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-info"><i class="icon-doc"></i> MOVE</span> - When Page is Moved (Renamed)
+                </label>
+              </div>
+              <div class="checkbox checkbox-info">
+                  <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-doc"></i> LIKE</span> - When Someone Likes Page
+                  </label>
+                </div>
+              <div class="checkbox checkbox-info">
+                <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-info"><i class="icon-fw icon-bubbles"></i> POST</span> - When Someone Comments on Page
+                </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">{{ t('Update') }}</button>
+          </div>
+        </form>
+      </div>
+    </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 %}
+
+

+ 115 - 0
lib/views/admin/global-notification.html

@@ -0,0 +1,115 @@
+<a href="/admin/global-notification/detail">
+  <p class="btn btn-default">通知設定の追加</p>
+</a>
+<h2>通知設定一覧</h2>
+
+{% set tags = {
+  pageCreate: '<span class="label label-info" data-toggle="tooltip" data-placement="top" title="Page Create"><i class="icon-doc"></i> CREATE</span>',
+  pageEdit: '<span class="label label-info" data-toggle="tooltip" data-placement="top" title="Page Edit"><i class="icon-doc"></i> EDIT</span>',
+  pageDelete: '<span class="label label-info" data-toggle="tooltip" data-placement="top" title="Page Delte"><i class="icon-doc"></i> DELETE</span>',
+  pageMove: '<span class="label label-info" data-toggle="tooltip" data-placement="top" title="Page Move"><i class="icon-doc"></i> MOVE</span>',
+  pageLike: '<span class="label label-info" data-toggle="tooltip" data-placement="top" title="Page Like"><i class="icon-doc"></i> LIKE</span>',
+  comment: '<span class="label label-info" data-toggle="tooltip" data-placement="top" title="New Comment"><i class="icon-fw icon-bubbles"></i> POST</span>'
+} %}
+
+<table class="table table-bordered">
+  <thead>
+    <th>ON/OFF</th>
+    <th>Trigger Path (expression with <code>*</code> is supported)</th>
+    <th>Trigger Events</th>
+    <th>Notify To</th>
+    <th>Action</th>
+  </thead>
+  <tbody class="admin-notif-list">
+    {% set detailPageUrl = '/admin/global-notification/detail' %}
+    {% for globalNotif in globalNotifications %}
+    <tr class="clickable-row" data-href="{{ detailPageUrl }}" data-updatepost-id="{{ globalNotif._id.toString() }}">
+      <td class="unclickable align-middle">
+        <label class="switch">
+          <input type="checkbox" class="isEnabledToggle" {% if globalNotif.isEnabled %}checked{% endif %}>
+          <span class="slider round"></span>
+        </label>
+      </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="unclickable">
+        <p class="btn btn-danger btn-delete">{{ t('Delete') }}</p>
+      </td>
+    </tr>
+    {% endfor %}
+  </tbody>
+</table>
+
+<script>
+  $(".clickable-row > :not('.unclickable')").click(function(event) {
+    var $target = $(event.currentTarget).parent();
+    window.location = $target.data("href") + "/" + $target.data("updatepost-id");
+  });
+
+  $(".unclickable > .btn-delete").click(function(event) {
+    var $targetRow = $(event.currentTarget).closest("tr");
+    var id = $targetRow.data("updatepost-id");
+    $.post('/admin/global-notification/remove?id=' + id, function(res) {
+      if (res.ok) {
+        $targetRow.closest('tr').remove();
+        $('.admin-notification > .row > .col-md-9').prepend(
+          '<div class=\"alert alert-success\">Successfully Deleted</div>'
+        );
+        $message = $('.admin-notification > .row > .col-md-9 > .alert.alert-success');
+        setTimeout(function()
+            {
+              $message.fadeOut({
+                complete: function() {
+                  $message.remove();
+                }
+              });
+            }, 2000);
+      }
+      else {
+        $('.admin-notification > .row > .col-md-9').prepend(
+          '<div class=\"alert alert-danger\">Error occurred in deleting global notifcation setting.</div>'
+        );
+        location.reload();
+      }
+    });
+  });
+
+  $(".isEnabledToggle").on("change", function(event) {
+    var $targetRow = $(event.currentTarget).closest("tr");
+    var id = $targetRow.data("updatepost-id");
+    var isEnabled = !$targetRow.find(".isEnabledToggle").is(':checked');
+    $.post('/_api/admin/global-notification/toggleIsEnabled?id=' + id + '&isEnabled=' + isEnabled, function(res) {
+      if (res.ok) {
+        $('.admin-notification > .row > .col-md-9').prepend(
+          '<div class=\"alert alert-success\">Successfully Upated</div>'
+        );
+        $message = $('.admin-notification > .row > .col-md-9 > .alert.alert-success');
+        setTimeout(function()
+            {
+              $message.fadeOut({
+                complete: function() {
+                  $message.remove();
+                }
+              });
+            }, 2000);
+      }
+      else {
+        $('.admin-notification > .row > .col-md-9').prepend(
+          '<div class=\"alert alert-danger\">Error occurred in deleting global notifcation setting.</div>'
+        );
+        location.reload();
+      }
+    });
+  });
+</script>

+ 7 - 7
lib/views/admin/notification.html

@@ -11,7 +11,7 @@
 {% endblock %}
 
 {% block content_main %}
-<div class="content-main">
+<div class="content-main admin-notification">
   <div class="row">
     <div class="col-md-3">
       {% include './widget/menu.html' with {current: 'notification'} %}
@@ -229,17 +229,17 @@
               </tr>
               </form>
 
-              {% for notif in settings %}
-              <tr class="admin-notif-row" data-updatepost-id="{{ notif._id.toString() }}">
+              {% for userNotif in userNotifications %}
+              <tr class="admin-notif-row" data-updatepost-id="{{ userNotif._id.toString() }}">
                 <td>
-                  {{ notif.pathPattern }}
+                  {{ userNotif.pathPattern }}
                 </td>
                 <td>
-                  {{ notif.channel }}
+                  {{ userNotif.channel }}
                 </td>
                 <td>
                   <form class="admin-remove-updatepost">
-                    <input type="hidden" name="id" value="{{ notif._id.toString() }}">
+                    <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>
@@ -251,7 +251,7 @@
         </div><!-- /#user-trigger-notification -->
 
         <div id="global-notification" class="tab-pane" role="tabpanel" >
-          <p class="alert alert-info">not implemented now</p>
+          {% include './global-notification.html' %}
         </div><!-- /#global-notification -->
 
       </div><!-- /.tab-content -->

+ 3 - 0
resource/styles/agile-admin/inverse/colors/_apply-colors-dark.scss

@@ -161,6 +161,9 @@ legend {
       border-color: darken($themecolor,15%);
     }
   }
+  .clickable-row:hover {
+    background-color: lighten($bodycolor, 10%);;
+  }
 }
 
 /*

+ 3 - 0
resource/styles/agile-admin/inverse/colors/_apply-colors-light.scss

@@ -77,6 +77,9 @@
       border-color: lighten($themecolor,20%);
     }
   }
+  .clickable-row:hover {
+    background-color: darken($bodycolor, 10%);
+  }
 }
 
 /*

+ 18 - 0
resource/styles/agile-admin/inverse/colors/_apply-colors.scss

@@ -250,6 +250,24 @@ legend {
   }
 }
 
+/*
+ * Form Slider
+ */
+.admin-page {
+  span.slider {
+    background-color: #ccc;
+    &:before {
+      background-color: white;
+    }
+  }
+  input:checked+.slider {
+    background-color: #007bff;
+  }
+  input:focus+.slider {
+    box-shadow: 0 0 1px #007bff;
+  }
+}
+
 
 /*
  * Crowi sidebar

+ 72 - 0
resource/styles/scss/_admin.scss

@@ -42,6 +42,78 @@
     }
   }
 
+  .admin-notification {
+    .clickable-row {
+      .unclickable {
+        text-align: center;
+        vertical-align: middle;
+        width: 50px;
+        label.switch {
+          margin: 0;
+        }
+      }
+      :not(.unclickable) {
+        cursor: pointer;
+      }
+    }
+
+    /* slider checkbox */
+
+    /*
+    * Form Slider
+    */
+    .switch {
+      position: relative;
+      display: inline-block;
+      width: 30px;
+      height: 17px;
+    }
+
+    /* Hide default HTML checkbox */
+
+    .switch input {
+      display: none;
+    }
+
+    /* The slider */
+    .slider {
+      position: absolute;
+      cursor: pointer;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      -webkit-transition: .4s;
+      transition: .4s;
+    }
+
+    .slider:before {
+      position: absolute;
+      content: "";
+      height: 13px;
+      width: 13px;
+      left: 2px;
+      bottom: 2px;
+      -webkit-transition: .4s;
+      transition: .4s;
+    }
+
+    input:checked+.slider:before {
+      -webkit-transform: translateX(13px);
+      -ms-transform: translateX(13px);
+      transform: translateX(13px);
+    }
+
+    /* Rounded sliders */
+    .slider.round {
+      border-radius: 34px;
+    }
+
+    .slider.round:before {
+      border-radius: 50%;
+    }
+  }
+
   // Toggle Twitter Bootstrap button class when active
   // https://jsfiddle.net/ms040m01/3/
   @mixin active-color($color, $bg-color, $border-color) {