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

Merge pull request #529 from weseek/feat/post-comment-notification

Feat/post comment notification
Sou Mizobuchi 7 лет назад
Родитель
Сommit
a5e4a5f52c

+ 4 - 1
lib/form/comment.js

@@ -8,5 +8,8 @@ module.exports = form(
   field('commentForm.revision_id').trim().required(),
   field('commentForm.comment').trim().required(),
   field('commentForm.comment_position').trim().toInt(),
-  field('commentForm.is_markdown').trim().toBooleanStrict()
+  field('commentForm.is_markdown').trim().toBooleanStrict(),
+
+  field('slackNotificationForm.isSlackEnabled').trim().toBooleanStrict().required(),
+  field('slackNotificationForm.slackChannels').trim(),
 );

+ 1 - 0
lib/models/config.js

@@ -567,6 +567,7 @@ module.exports = function(crowi) {
       attrWhiteList: Config.attrWhiteList(config),
       highlightJsStyleBorder: Config.highlightJsStyleBorder(config),
       isSavedStatesOfTabChanges: Config.isSavedStatesOfTabChanges(config),
+      hasSlackConfig: Config.hasSlackConfig(config),
       env: {
         PLANTUML_URI: env.PLANTUML_URI || null,
         BLOCKDIAG_URI: env.BLOCKDIAG_URI || null,

+ 33 - 8
lib/routes/comment.js

@@ -2,7 +2,9 @@ module.exports = function(crowi, app) {
   'use strict';
 
   const debug = require('debug')('growi:routs:comment')
+    , logger = require('@alias/logger')('growi:routes:comment')
     , Comment = crowi.model('Comment')
+    , User = crowi.model('User')
     , Page = crowi.model('Page')
     , ApiResponse = require('../util/apiResponse')
     , actions = {}
@@ -50,18 +52,19 @@ module.exports = function(crowi, app) {
    * @apiParam {Number} comment_position=-1 Line number of the comment
    */
   api.add = async function(req, res) {
-    const form = req.form.commentForm;
+    const commentForm = req.form.commentForm;
+    const slackNotificationForm = req.form.slackNotificationForm;
 
     if (!req.form.isValid) {
       // return res.json(ApiResponse.error('Invalid comment.'));
       return res.json(ApiResponse.error('コメントを入力してください。'));
     }
 
-    const pageId = form.page_id;
-    const revisionId = form.revision_id;
-    const comment = form.comment;
-    const position = form.comment_position || -1;
-    const isMarkdown = form.is_markdown;
+    const pageId = commentForm.page_id;
+    const revisionId = commentForm.revision_id;
+    const comment = commentForm.comment;
+    const position = commentForm.comment_position || -1;
+    const isMarkdown = commentForm.is_markdown;
 
     const createdComment = await Comment.create(pageId, req.user._id, revisionId, comment, position, isMarkdown)
       .catch(function(err) {
@@ -69,12 +72,34 @@ module.exports = function(crowi, app) {
       });
 
     // update page
-    await Page.findOneAndUpdate({ _id: pageId }, {
+    const page = await Page.findOneAndUpdate({ _id: pageId }, {
       lastUpdateUser: req.user,
       updatedAt: new Date()
     });
 
-    return res.json(ApiResponse.success({comment: createdComment}));
+    res.json(ApiResponse.success({comment: createdComment}));
+
+    // slack notification
+    if (slackNotificationForm.isSlackEnabled) {
+      const user = await User.findUserByUsername(req.user.username);
+      const path = page.path;
+      const channels = slackNotificationForm.slackChannels;
+
+      if (channels) {
+        page.updateSlackChannel(channels).catch(err => {
+          logger.error('Error occured in updating slack channels: ', err);
+        });
+
+        const promises = channels.split(',').map(function(chan) {
+          return crowi.slack.postComment(createdComment, user, chan, path);
+        });
+
+        Promise.all(promises)
+          .catch(err => {
+            logger.error('Error occured in sending slack notification: ', err);
+          });
+      }
+    }
   };
 
   /**

+ 38 - 3
lib/routes/page.js

@@ -2,6 +2,7 @@ module.exports = function(crowi, app) {
   'use strict';
 
   var debug = require('debug')('growi:routes:page')
+    , logger = require('@alias/logger')('growi:routes:page')
     , Page = crowi.model('Page')
     , User = crowi.model('User')
     , Config   = crowi.model('Config')
@@ -10,6 +11,7 @@ module.exports = function(crowi, app) {
     , Bookmark = crowi.model('Bookmark')
     , UserGroupRelation = crowi.model('UserGroupRelation')
     , PageGroupRelation = crowi.model('PageGroupRelation')
+    , UpdatePost = crowi.model('UpdatePost')
     , ApiResponse = require('../util/apiResponse')
     , interceptorManager = crowi.getInterceptorManager()
     , pagePathUtil = require('../util/pagePathUtil')
@@ -254,6 +256,7 @@ module.exports = function(crowi, app) {
       tree: [],
       pageRelatedGroup: null,
       template: null,
+      slack: '',
     };
 
     var pageTeamplate = 'customlayout-selector/page';
@@ -289,6 +292,12 @@ module.exports = function(crowi, app) {
             renderVars.pageRelatedGroup = pageGroupRelation.relatedGroup;
           }
         })
+        .then(() => {
+          return getSlackChannels(page);
+        })
+        .then((channels) => {
+          renderVars.slack = channels;
+        })
         .then(function() {
           var userPage = isUserPage(page.path);
           var userData = null;
@@ -367,6 +376,17 @@ module.exports = function(crowi, app) {
     });
   };
 
+  const getSlackChannels = async page => {
+    if (page.extended.slack) {
+      return page.extended.slack;
+    }
+    else {
+      const data = await UpdatePost.findSettingsByPath(page.path);
+      const channels = data.map(e => e.channel).join(', ');
+      return channels;
+    }
+  };
+
   const replacePlaceholders = (template, req) => {
     const definitions = {
       pagepath: getPathFromRequest(req),
@@ -475,6 +495,7 @@ module.exports = function(crowi, app) {
       page: pageData,
       revision: pageData.revision || {},
       author: pageData.revision.author || false,
+      slack: '',
     };
     var userPage = isUserPage(pageData.path);
     var userData = null;
@@ -491,6 +512,12 @@ module.exports = function(crowi, app) {
         renderVars.pageRelatedGroup = pageGroupRelation.relatedGroup;
       }
     })
+    .then(() => {
+      return getSlackChannels(pageData);
+    })
+    .then(channels => {
+      renderVars.slack = channels;
+    })
     .then(function() {
       if (userPage) {
         return User.findUserByUsername(User.getUsernameByPath(pageData.path))
@@ -664,11 +691,19 @@ module.exports = function(crowi, app) {
       // TODO: move to events
       if (notify.slack) {
         if (notify.slack.on && notify.slack.channel) {
-          data.updateSlackChannel(notify.slack.channel).then(function() {}).catch(function() {});
+          data.updateSlackChannel(notify.slack.channel)
+          .catch(err => {
+            logger.error('Error occured in updating slack channels: ', err);
+          });
 
           if (crowi.slack) {
-            notify.slack.channel.split(',').map(function(chan) {
-              crowi.slack.post(pageData, req.user, chan, updateOrCreate, previousRevision);
+            const promises = notify.slack.channel.split(',').map(function(chan) {
+              return crowi.slack.postPage(pageData, req.user, chan, updateOrCreate, previousRevision);
+            });
+
+            Promise.all(promises)
+            .catch(err => {
+              logger.error('Error occured in sending slack notification: ', err);
             });
           }
         }

+ 115 - 53
lib/util/slack.js

@@ -11,19 +11,37 @@ module.exports = function(crowi) {
     Slack = require('slack-node'),
     slack = {};
 
-  const postWithIwh = function(messageObj, callback) {
-    const client = new Slack();
-    client.setWebhook(config.notification['slack:incomingWebhookUrl']);
-    client.webhook(messageObj, callback);
+  const postWithIwh = function(messageObj) {
+    return new Promise((resolve, reject) => {
+      const client = new Slack();
+      client.setWebhook(config.notification['slack:incomingWebhookUrl']);
+      client.webhook(messageObj, function(err, res) {
+        if (err) {
+          debug('Post error', err, res);
+          debug('Sent data to slack is:', messageObj);
+          return reject(err);
+        }
+        resolve(res);
+      });
+    });
   };
 
-  const postWithWebApi = function(messageObj, callback) {
-    const client = new Slack(config.notification['slack:token']);
-    // stringify attachments
-    if (messageObj.attachments != null) {
-      messageObj.attachments = JSON.stringify(messageObj.attachments);
-    }
-    client.api('chat.postMessage', messageObj, callback);
+  const postWithWebApi = function(messageObj) {
+    return new Promise((resolve, reject) => {
+      const client = new Slack(config.notification['slack:token']);
+      // stringify attachments
+      if (messageObj.attachments != null) {
+        messageObj.attachments = JSON.stringify(messageObj.attachments);
+      }
+      client.api('chat.postMessage', messageObj, function(err, res) {
+        if (err) {
+          debug('Post error', err, res);
+          debug('Sent data to slack is:', messageObj);
+          return reject(err);
+        }
+        resolve(res);
+      });
+    });
   };
 
   const convertMarkdownToMrkdwn = function(body) {
@@ -80,9 +98,23 @@ module.exports = function(crowi) {
     return diffText;
   };
 
-  const prepareSlackMessage = function(page, user, channel, updateType, previousRevision) {
-    var url = config.crowi['app:url'] || '';
-    var body = page.revision.body;
+  const prepareAttachmentTextForComment = function(comment) {
+    let body = comment.comment;
+    if (body.length > 2000) {
+      body = body.substr(0, 2000) + '...';
+    }
+
+    if (comment.isMarkdown) {
+      return convertMarkdownToMrkdwn(body);
+    }
+    else {
+      return body;
+    }
+  };
+
+  const prepareSlackMessageForPage = function(page, user, channel, updateType, previousRevision) {
+    const url = config.crowi['app:url'] || '';
+    let body = page.revision.body;
 
     if (updateType == 'create') {
       body = prepareAttachmentTextForCreate(page, user);
@@ -91,7 +123,7 @@ module.exports = function(crowi) {
       body = prepareAttachmentTextForUpdate(page, user, previousRevision);
     }
 
-    var attachment = {
+    const attachment = {
       color: '#263a3c',
       author_name: '@' + user.username,
       author_link: url + '/user/' + user.username,
@@ -105,17 +137,43 @@ module.exports = function(crowi) {
       attachment.author_icon = user.image;
     }
 
-    var message = {
+    const message = {
       channel: '#' + channel,
       username: Config.appTitle(config),
-      text: getSlackMessageText(page.path, user, updateType),
+      text: getSlackMessageTextForPage(page.path, user, updateType),
       attachments: [attachment],
     };
 
     return message;
   };
 
-  const getSlackMessageText = function(path, user, updateType) {
+  const prepareSlackMessageForComment = function(comment, user, channel, path) {
+    const url = config.crowi['app:url'] || '';
+    const body = prepareAttachmentTextForComment(comment);
+
+    const attachment = {
+      color: '#263a3c',
+      author_name: '@' + user.username,
+      author_link: url + '/user/' + user.username,
+      author_icon: user.image,
+      text: body,
+      mrkdwn_in: ['text'],
+    };
+    if (user.image) {
+      attachment.author_icon = user.image;
+    }
+
+    const message = {
+      channel: '#' + channel,
+      username: Config.appTitle(config),
+      text: getSlackMessageTextForComment(path, user),
+      attachments: [attachment],
+    };
+
+    return message;
+  };
+
+  const getSlackMessageTextForPage = function(path, user, updateType) {
     let text;
     const url = config.crowi['app:url'] || '';
 
@@ -130,46 +188,50 @@ module.exports = function(crowi) {
     return text;
   };
 
+  const getSlackMessageTextForComment = function(path, user) {
+    const url = config.crowi['app:url'] || '';
+    const pageUrl = `<${url}${path}|${path}>`;
+    const text = `:speech_balloon: ${user.username} commented on ${pageUrl}`;
+
+    return text;
+  };
+
   // slack.post = function (channel, message, opts) {
-  slack.post = (page, user, channel, updateType, previousRevision) => {
-    const messageObj = prepareSlackMessage(page, user, channel, updateType, previousRevision);
+  slack.postPage = (page, user, channel, updateType, previousRevision) => {
+    const messageObj = prepareSlackMessageForPage(page, user, channel, updateType, previousRevision);
 
-    return new Promise((resolve, reject) => {
-      // define callback function for Promise
-      const callback = function(err, res) {
-        if (err) {
-          debug('Post error', err, res);
-          debug('Sent data to slack is:', messageObj);
-          return reject(err);
-        }
-        resolve(res);
-      };
+    return slackPost(messageObj);
+  };
 
-      // when incoming Webhooks is prioritized
-      if (Config.isIncomingWebhookPrioritized(config)) {
-        if (Config.hasSlackIwhUrl(config)) {
-          debug('posting message with IncomingWebhook');
-          postWithIwh(messageObj, callback);
-        }
-        else if (Config.hasSlackToken(config)) {
-          debug('posting message with Web API');
-          postWithWebApi(messageObj, callback);
-        }
+  slack.postComment = (comment, user, channel, path) => {
+    const messageObj = prepareSlackMessageForComment(comment, user, channel, path);
+
+    return slackPost(messageObj);
+  };
+
+  const slackPost = (messageObj) => {
+    // when incoming Webhooks is prioritized
+    if (Config.isIncomingWebhookPrioritized(config)) {
+      if (Config.hasSlackIwhUrl(config)) {
+        debug('posting message with IncomingWebhook');
+        return postWithIwh(messageObj);
       }
-      // else
-      else {
-        if (Config.hasSlackToken(config)) {
-          debug('posting message with Web API');
-          postWithWebApi(messageObj, callback);
-        }
-        else if (Config.hasSlackIwhUrl(config)) {
-          debug('posting message with IncomingWebhook');
-          postWithIwh(messageObj, callback);
-        }
+      else if (Config.hasSlackToken(config)) {
+        debug('posting message with Web API');
+        return postWithWebApi(messageObj);
       }
-
-      resolve();
-    });
+    }
+    // else
+    else {
+      if (Config.hasSlackToken(config)) {
+        debug('posting message with Web API');
+        return postWithWebApi(messageObj);
+      }
+      else if (Config.hasSlackIwhUrl(config)) {
+        debug('posting message with IncomingWebhook');
+        return postWithIwh(messageObj);
+      }
+    }
   };
 
   return slack;

+ 0 - 8
lib/util/swigFunctions.js

@@ -172,14 +172,6 @@ module.exports = function(crowi, app, req, locals) {
     return Config.isEnabledTimeline(config);
   };
 
-  locals.slackConfigured = function() {
-    let config = crowi.getConfig();
-    if (Config.hasSlackToken(config) || Config.hasSlackIwhUrl(config)) {
-      return true;
-    }
-    return false;
-  };
-
   locals.isUploadable = function() {
     let config = crowi.getConfig();
     return Config.isUploadable(config);

+ 2 - 20
lib/views/_form.html

@@ -27,27 +27,9 @@
       <div id="page-editor-options-selector"></div>
     </div>
 
-    <div class="form-inline page-form-setting d-flex align-items-center" id="page-form-setting" data-slack-configured="{{ slackConfigured() }}">
-      {% if slackConfigured() %}
-      <span class="input-group input-group-sm input-group-slack extended-setting m-r-5">
-        <div class="input-group-addon">
-          <img id="slack-mark-white" src="/images/icons/slack/mark-monochrome_white.svg" width="18" height="18">
-          <img id="slack-mark-black" src="/images/icons/slack/mark-monochrome_black.svg" width="18" height="18">
-          <input class="" type="checkbox" name="pageForm[notify][slack][on]" value="1">
-        </div>
-        <input class="form-control" type="text" name="pageForm[notify][slack][channel]" value="{{ page.extended.slack|default('') }}" placeholder="slack-channel-name"
-          id="page-form-slack-channel"
-          data-toggle="popover"
-          title="Slack通知"
-          data-content="通知するにはチェックを入れてください。カンマ区切りで複数チャンネルに通知することができます。"
-          data-trigger="focus"
-          data-placement="top"
-        >
-      </span>
-      {% endif %}
-
+    <div class="form-inline d-flex align-items-center" id="page-form-setting">
+      <div id="editor-slack-notification"></div>
       <div id="page-grant-selector"></div>
-
       <input type="hidden" id="page-grant" name="pageForm[grant]" value="{{ page.grant }}">
       <input type="hidden" id="grant-group" name="pageForm[grantUserGroupId]" value="{{ pageRelatedGroup._id.toString() }}">
       <input type="hidden" id="edit-form-csrf" name="_csrf" value="{{ csrf() }}">

+ 1 - 0
lib/views/widget/page_content.html

@@ -6,6 +6,7 @@
   data-page-revision-id="{% if revision %}{{ revision._id.toString() }}{% endif %}"
   data-page-revision-created="{% if revision %}{{ revision.createdAt|datetz('U') }}{% endif %}"
   data-page-is-seen="{% if page and page.isSeenUser(user) %}1{% else %}0{% endif %}"
+  data-slack-channels="{{ slack|default('') }}"
   >
 
   {% include 'page_alerts.html' %}

+ 25 - 1
resource/js/app.js

@@ -21,6 +21,7 @@ import PageListSearch   from './components/PageListSearch';
 import PageHistory      from './components/PageHistory';
 import PageComments     from './components/PageComments';
 import CommentForm from './components/PageComment/CommentForm';
+import SlackNotification from './components/SlackNotification';
 import PageAttachment   from './components/PageAttachment';
 import SeenUserList     from './components/SeenUserList';
 import RevisionPath     from './components/Page/RevisionPath';
@@ -53,11 +54,13 @@ let pagePath;
 let pageContent = '';
 let markdown = '';
 let pageGrant = null;
+let slackChannels = '';
 if (mainContent !== null) {
   pageId = mainContent.getAttribute('data-page-id');
   pageRevisionId = mainContent.getAttribute('data-page-revision-id');
   pageRevisionCreatedAt = +mainContent.getAttribute('data-page-revision-created');
   pagePath = mainContent.attributes['data-path'].value;
+  slackChannels = mainContent.getAttribute('data-slack-channels');
   const rawText = document.getElementById('raw-text-original');
   if (rawText) {
     pageContent = rawText.innerHTML;
@@ -181,10 +184,31 @@ if (writeCommentElem) {
     }
   };
   ReactDOM.render(
-    <CommentForm crowi={crowi} crowiOriginRenderer={crowiRenderer} pageId={pageId} revisionId={pageRevisionId} pagePath={pagePath} onPostComplete={postCompleteHandler} editorOptions={editorOptions}/>,
+    <CommentForm crowi={crowi}
+      crowiOriginRenderer={crowiRenderer}
+      pageId={pageId}
+      revisionId={pageRevisionId}
+      pagePath={pagePath}
+      onPostComplete={postCompleteHandler}
+      editorOptions={editorOptions}
+      slackChannels = {slackChannels}/>,
     writeCommentElem);
 }
 
+// render slack notification form
+const editorSlackElem = document.getElementById('editor-slack-notification');
+if (editorSlackElem) {
+  ReactDOM.render(
+    <SlackNotification
+      crowi={crowi}
+      pageId={pageId}
+      pagePath={pagePath}
+      isSlackEnabled={false}
+      slackChannels={slackChannels}
+      formName='pageForm' />,
+    editorSlackElem);
+}
+
 // render OptionsSelector
 const pageEditorOptionsSelectorElem = document.getElementById('page-editor-options-selector');
 if (pageEditorOptionsSelectorElem) {

+ 51 - 17
resource/js/components/PageComment/CommentForm.js

@@ -12,6 +12,7 @@ import GrowiRenderer from '../../util/GrowiRenderer';
 
 import Editor from '../PageEditor/Editor';
 import CommentPreview from '../PageComment/CommentPreview';
+import SlackNotification from '../SlackNotification';
 
 /**
  *
@@ -39,6 +40,9 @@ export default class CommentForm extends React.Component {
       isUploadable,
       isUploadableFile,
       errorMessage: undefined,
+      hasSlackConfig: config.hasSlackConfig,
+      isSlackEnabled: false,
+      slackChannels: this.props.slackChannels,
     };
 
     this.growiRenderer = new GrowiRenderer(this.props.crowi, this.props.crowiOriginRenderer, {mode: 'comment'});
@@ -50,6 +54,8 @@ export default class CommentForm extends React.Component {
     this.handleSelect = this.handleSelect.bind(this);
     this.apiErrorHandler = this.apiErrorHandler.bind(this);
     this.onUpload = this.onUpload.bind(this);
+    this.onChannelChange = this.onChannelChange.bind(this);
+    this.onSlackOnChange = this.onSlackOnChange.bind(this);
   }
 
   updateState(value) {
@@ -68,6 +74,14 @@ export default class CommentForm extends React.Component {
     this.renderHtml(this.state.comment);
   }
 
+  onSlackOnChange(value) {
+    this.setState({isSlackEnabled: value});
+  }
+
+  onChannelChange(value) {
+    this.setState({slackChannels: value});
+  }
+
   /**
    * Load data of comments and rerender <PageComments />
    */
@@ -83,26 +97,31 @@ export default class CommentForm extends React.Component {
         page_id: this.props.pageId,
         revision_id: this.props.revisionId,
         is_markdown: this.state.isMarkdown,
+      },
+      slackNotificationForm: {
+        isSlackEnabled: this.state.isSlackEnabled,
+        slackChannels: this.state.slackChannels,
       }
     })
-      .then((res) => {
-        if (this.props.onPostComplete != null) {
-          this.props.onPostComplete(res.comment);
-        }
-        this.setState({
-          comment: '',
-          isMarkdown: true,
-          html: '',
-          key: 1,
-          errorMessage: undefined,
-        });
-        // reset value
-        this.refs.editor.setValue('');
-      })
-      .catch(err => {
-        const errorMessage = err.message || 'An unknown error occured when posting comment';
-        this.setState({ errorMessage });
+    .then((res) => {
+      if (this.props.onPostComplete != null) {
+        this.props.onPostComplete(res.comment);
+      }
+      this.setState({
+        comment: '',
+        isMarkdown: true,
+        html: '',
+        key: 1,
+        errorMessage: undefined,
+        isSlackEnabled: false,
       });
+      // reset value
+      this.refs.editor.setValue('');
+    })
+    .catch(err => {
+      const errorMessage = err.message || 'An unknown error occured when posting comment';
+      this.setState({ errorMessage });
+    });
   }
 
   getCommentHtml() {
@@ -243,10 +262,24 @@ export default class CommentForm extends React.Component {
                       <input type="checkbox" id="comment-form-is-markdown" name="isMarkdown" checked={this.state.isMarkdown} value="1" onChange={this.updateStateCheckbox} /> Markdown
                     </label>
                   }
+
                   <div style={{flex: 1}}></div>{/* spacer */}
                   { this.state.errorMessage &&
                     <span className="text-danger text-right mr-2">{this.state.errorMessage}</span>
                   }
+                  { this.state.hasSlackConfig &&
+                    <div className="form-inline d-flex align-items-center">
+                      <SlackNotification
+                      crowi={this.props.crowi}
+                      pageId={this.props.pageId}
+                      pagePath={this.props.pagePath}
+                      onSlackOnChange={this.onSlackOnChange}
+                      onChannelChange={this.onChannelChange}
+                      isSlackEnabled={this.state.isSlackEnabled}
+                      slackChannels={this.state.slackChannels}
+                      />
+                    </div>
+                  }
                   <Button type="submit" value="Submit" bsStyle="primary" className="fcbtn btn btn-sm btn-primary btn-outline btn-rounded btn-1b">
                     Comment
                   </Button>
@@ -268,6 +301,7 @@ CommentForm.propTypes = {
   revisionId: PropTypes.string,
   pagePath: PropTypes.string,
   editorOptions: PropTypes.object,
+  slackChannels: PropTypes.string,
 };
 CommentForm.defaultProps = {
   editorOptions: {},

+ 78 - 0
resource/js/components/SlackNotification.js

@@ -0,0 +1,78 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+/**
+ *
+ * @author Yuki Takei <yuki@weseek.co.jp>
+ *
+ * @export
+ * @class SlackNotification
+ * @extends {React.Component}
+ */
+
+export default class SlackNotification extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      isSlackEnabled: this.props.isSlackEnabled,
+      slackChannels: this.props.slackChannels,
+    };
+
+    this.updateState = this.updateState.bind(this);
+    this.updateStateCheckbox = this.updateStateCheckbox.bind(this);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    this.setState({
+      isSlackEnabled: nextProps.isSlackEnabled,
+      slackChannels: nextProps.slackChannels
+    });
+  }
+
+  updateState(value) {
+    this.setState({slackChannels: value});
+    this.props.onChannelChange(value);
+  }
+
+  updateStateCheckbox(event) {
+    const value = event.target.checked;
+    this.setState({isSlackEnabled: value});
+    this.props.onSlackOnChange(value);
+  }
+
+  render() {
+    const formNameSlackOn = this.props.formName && this.props.formName + '[notify][slack][on]';
+    const formNameChannels = this.props.formName && this.props.formName + '[notify][slack][channel]';
+
+    return (
+      <div className="input-group input-group-sm input-group-slack extended-setting m-r-5">
+        <label className="input-group-addon">
+          <img id="slack-mark-white" src="/images/icons/slack/mark-monochrome_white.svg" width="18" height="18"/>
+          <img id="slack-mark-black" src="/images/icons/slack/mark-monochrome_black.svg" width="18" height="18"/>
+          <input type="checkbox" name={formNameSlackOn} value="1" checked={this.state.isSlackEnabled} onChange={this.updateStateCheckbox}/>
+        </label>
+        <input className="form-control" type="text" name={formNameChannels} value={this.state.slackChannels} placeholder="slack channel name"
+          data-toggle="popover"
+          title="Slack通知"
+          data-content="通知するにはチェックを入れてください。カンマ区切りで複数チャンネルに通知することができます。"
+          data-trigger="focus"
+          data-placement="top"
+          onChange={e => this.updateState(e.target.value)}
+          />
+      </div>
+    );
+  }
+}
+
+SlackNotification.propTypes = {
+  crowi: PropTypes.object.isRequired,
+  pageId: PropTypes.string,
+  pagePath: PropTypes.string,
+  onChannelChange: PropTypes.func,
+  onSlackOnChange: PropTypes.func,
+  isSlackEnabled: PropTypes.bool,
+  slackChannels: PropTypes.string,
+  formName: PropTypes.string,
+};

+ 0 - 26
resource/js/legacy/crowi-form.js

@@ -3,41 +3,15 @@ var pagePath= $('#content-main').data('path');
 
 require('bootstrap-select');
 
-// show/hide
-function FetchPagesUpdatePostAndInsert(path) {
-  $.get('/_api/pages.updatePost', {path: path}, function(res) {
-    if (res.ok) {
-      var $slackChannels = $('#page-form-slack-channel');
-      $slackChannels.val(res.updatePost.join(','));
-    }
-  });
-}
-
-var slackConfigured = $('#page-form-setting').data('slack-configured');
-
 // for new page
 if (!pageId) {
   if (!pageId && pagePath.match(/(20\d{4}|20\d{6}|20\d{2}_\d{1,2}|20\d{2}_\d{1,2}_\d{1,2})/)) {
     $('#page-warning-modal').modal('show');
   }
-
-  if (slackConfigured) {
-    FetchPagesUpdatePostAndInsert(pagePath);
-  }
 }
 
 $('a[data-toggle="tab"][href="#edit-form"]').on('show.bs.tab', function() {
   $('body').addClass('on-edit');
-
-  if (slackConfigured) {
-    var $slackChannels = $('#page-form-slack-channel');
-    var slackChannels = $slackChannels.val();
-    // if slackChannels is empty, then fetch default (admin setting)
-    // if not empty, it means someone specified this setting for the page.
-    if (slackChannels === '') {
-      FetchPagesUpdatePostAndInsert(pagePath);
-    }
-  }
 });
 
 $('a[data-toggle="tab"][href="#edit-form"]').on('hide.bs.tab', function() {

+ 10 - 1
resource/styles/agile-admin/inverse/colors/_apply-colors-dark.scss

@@ -154,7 +154,7 @@ legend {
 /*
  * GROWI admin page #themeOptions
  */
- .admin-page {
+.admin-page {
   #themeOptions {
     a.active {
       background-color: darken($themecolor,15%);
@@ -162,3 +162,12 @@ legend {
     }
   }
 }
+
+/*
+ * GROWI comment form
+ */
+.comment-form {
+  #slack-mark-black {
+    display: none;
+  }
+}

+ 10 - 1
resource/styles/agile-admin/inverse/colors/_apply-colors-light.scss

@@ -70,7 +70,7 @@
 /*
  * GROWI admin page #themeOptions
  */
- .admin-page {
+.admin-page {
   #themeOptions {
     a.active {
       background-color: lighten($themecolor,20%);
@@ -78,3 +78,12 @@
     }
   }
 }
+
+/*
+ * GROWI comment form
+ */
+.comment-form {
+  #slack-mark-white {
+    display: none;
+  }
+}

+ 10 - 0
resource/styles/scss/_notification.scss

@@ -0,0 +1,10 @@
+// Slack
+.input-group-slack {
+  .input-group-addon {
+    padding: 2px 8px;
+    line-height: 1em;
+    img, input {
+      vertical-align: middle;
+    }
+  }
+}

+ 0 - 12
resource/styles/scss/_on-edit.scss

@@ -113,18 +113,6 @@ body.on-edit {
           width: 100px;
         }
       }
-
-      // slack
-      .input-group-slack {
-        .input-group-addon {
-          padding: 2px 8px;
-          line-height: 1em;
-          img, input {
-            vertical-align: middle;
-          }
-        }
-      }
-
     }
   }
 

+ 1 - 0
resource/styles/scss/style.scss

@@ -27,6 +27,7 @@
 @import 'layout_crowi_sidebar';
 @import 'layout_growi';
 @import 'login';
+@import 'notification';
 @import 'on-edit';
 @import 'page_list';
 @import 'page';

+ 6 - 2
test/util/slack.test.js

@@ -10,7 +10,11 @@ describe('Slack Util', function () {
   var crowi = new (require(ROOT_DIR + '/lib/crowi'))(ROOT_DIR, process.env);
   var slack = require(crowi.libDir + '/util/slack')(crowi);
 
-  it('post method exists', function() {
-    expect(slack).to.respondTo('post');
+  it('post comment method exists', function() {
+    expect(slack).to.respondTo('postComment');
+  });
+
+  it('post page method exists', function() {
+    expect(slack).to.respondTo('postPage');
   });
 });