Bladeren bron

Merge pull request #456 from weseek/master

release v3.1.3
Yuki Takei 7 jaren geleden
bovenliggende
commit
44a37f8400
46 gewijzigde bestanden met toevoegingen van 680 en 214 verwijderingen
  1. 10 1
      CHANGES.md
  2. 3 0
      lib/crowi/express-init.js
  3. 0 1
      lib/crowi/index.js
  4. 1 1
      lib/form/admin/securityPassportLdap.js
  5. 2 1
      lib/form/comment.js
  6. 4 3
      lib/locales/en-US/translation.json
  7. 4 3
      lib/locales/ja/translation.json
  8. 4 2
      lib/models/comment.js
  9. 1 0
      lib/models/config.js
  10. 5 2
      lib/models/external-account.js
  11. 7 1
      lib/models/page.js
  12. 4 3
      lib/routes/comment.js
  13. 1 0
      lib/routes/index.js
  14. 3 1
      lib/routes/login-passport.js
  15. 44 26
      lib/routes/page.js
  16. 10 0
      lib/service/passport.js
  17. 12 0
      lib/util/getToday.js
  18. 13 0
      lib/util/templateChecker.js
  19. 18 0
      lib/views/admin/widget/passport/ldap.html
  20. 36 0
      lib/views/layout-growi/base/layout.html
  21. 1 26
      lib/views/layout-growi/widget/comments.html
  22. 1 0
      lib/views/layout/layout.html
  23. 33 20
      lib/views/modal/create_page.html
  24. 6 8
      lib/views/modal/create_template.html
  25. 1 1
      lib/views/widget/page_content.html
  26. 4 0
      lib/views/widget/page_list.html
  27. 1 1
      lib/views/widget/page_list_and_timeline.html
  28. 2 2
      package.json
  29. 15 11
      resource/js/app.js
  30. 2 1
      resource/js/components/Page/RevisionBody.js
  31. 69 1
      resource/js/components/PageComment/Comment.js
  32. 190 0
      resource/js/components/PageComment/CommentForm.js
  33. 35 0
      resource/js/components/PageComment/CommentPreview.js
  34. 0 65
      resource/js/components/PageCommentFormBehavior.js
  35. 23 12
      resource/js/components/PageComments.js
  36. 8 0
      resource/js/components/PageList/PageListMeta.js
  37. 1 1
      resource/js/legacy/crowi.js
  38. 9 2
      resource/js/util/GrowiRenderer.js
  39. 3 2
      resource/js/util/interceptor/detach-code-blocks.js
  40. 0 15
      resource/js/util/markdown-it/common-plugins.js
  41. 11 0
      resource/js/util/markdown-it/footernote.js
  42. 13 0
      resource/js/util/markdown-it/task-lists.js
  43. 7 0
      resource/styles/agile-admin/inverse/colors/_apply-colors.scss
  44. 3 0
      resource/styles/scss/_comment_growi.scss
  45. 55 0
      resource/styles/scss/_wiki.scss
  46. 5 1
      yarn.lock

+ 10 - 1
CHANGES.md

@@ -1,7 +1,16 @@
 CHANGES
 ========
 
-## 3.1.2-RC
+## 3.1.3-RC
+
+* Feature: Write comment with Markdown
+* Improvement: Support some placeholders for template page
+* Improvement: Omit unnecessary response header
+* Improvement: Support LDAP attribute mappings for user's full name
+* Fix: HTML escaped characters in markdown are unescaped unexpectedly after page is saved
+
+
+## 3.1.2
 
 * Feature: Template page
 * Improvement: Add 'future' theme

+ 3 - 0
lib/crowi/express-init.js

@@ -47,6 +47,9 @@ module.exports = function(crowi, app) {
       overloadTranslationOptionHandler: i18nSprintf.overloadTranslationOptionHandler
     });
 
+  // omit unnecessary header
+  app.disable('x-powered-by');
+
   app.use(function(req, res, next) {
     var now = new Date()
       , baseUrl

+ 0 - 1
lib/crowi/index.js

@@ -389,7 +389,6 @@ Crowi.prototype.buildServer = function() {
   var express = require('express')()
     , env = this.node_env
     ;
-
   require('./express-init')(this, express);
 
   // import plugins

+ 1 - 1
lib/form/admin/securityPassportLdap.js

@@ -16,9 +16,9 @@ module.exports = form(
   field('settingForm[security:passport-ldap:bindDNPassword]'),
   field('settingForm[security:passport-ldap:searchFilter]'),
   field('settingForm[security:passport-ldap:attrMapUsername]'),
+  field('settingForm[security:passport-ldap:attrMapName]'),
   field('settingForm[security:passport-ldap:isSameUsernameTreatedAsIdenticalUser]').trim().toBooleanStrict(),
   field('settingForm[security:passport-ldap:groupSearchBase]'),
   field('settingForm[security:passport-ldap:groupSearchFilter]'),
   field('settingForm[security:passport-ldap:groupDnProperty]')
 );
-

+ 2 - 1
lib/form/comment.js

@@ -7,5 +7,6 @@ module.exports = form(
   field('commentForm.page_id').trim().required(),
   field('commentForm.revision_id').trim().required(),
   field('commentForm.comment').trim().required(),
-  field('commentForm.comment_position').trim().toInt()
+  field('commentForm.comment_position').trim().toInt(),
+  field('commentForm.is_markdown').trim().toBooleanStrict()
 );

+ 4 - 3
lib/locales/en-US/translation.json

@@ -231,11 +231,11 @@
       "create/edit": "Create/Edit Template page..",
       "select": "Select template page type"
     },
-    "local": {
+    "children": {
       "label": "Template for children",
       "desc": "Applies only to the same level pages which the template exists"
     },
-    "global": {
+    "decendants": {
       "label": "Template for descendants",
       "desc": "Applies to all decendant pages"
     }
@@ -351,7 +351,8 @@
       "search_filter_detail3": "If empty, the filter <code>(uid=&#123;&#123;username&#125;&#125;)</code> is used.",
       "search_filter_example1": "Match with 'uid' or 'mail'",
       "search_filter_example2": "Match with 'sAMAccountName' for Active Directory",
-      "username_detail": "Specification of mappings when creating new users",
+      "username_detail": "Specification of mappings for <code>username</code> when creating new users",
+      "name_detail": "Specification of mappings for <code>name</code> when creating new users",
       "Treat username matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>username</code> match",
   		"Treat username matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>username</code>.",
       "group_search_base_DN": "Group Search Base DN",

+ 4 - 3
lib/locales/ja/translation.json

@@ -246,11 +246,11 @@
       "select": "テンプレートタイプを選択してください",
       "create/edit": "テンプレートページの作成/編集.."
     },
-    "local": {
+    "children": {
       "label": "同一階層テンプレート",
       "desc": "テンプレートページが存在する階層にのみ適応されます"
     },
-    "global": {
+    "decendants": {
       "label": "下位層テンプレート",
       "desc": "テンプレートページが存在する下位層のすべてのページに適応されます"
     }
@@ -368,7 +368,8 @@
       "search_filter_detail3": "空欄の場合 <code>(uid=&#123;&#123;username&#125;&#125;)</code> が使用されます。",
       "search_filter_example1": "'uid' または 'mail' に一致",
       "search_filter_example2": "'sAMAccountName' に一致 (Active Directory)",
-      "username_detail": "新規ユーザーの関連付けを設定",
+      "username_detail": "新規ユーザーのアカウント名(<code>username</code>)に関連付ける属性",
+      "name_detail": "新規ユーザーの表示名(<code>name</code>)に関連付ける属性",
       "Treat username matching as identical": "新規ログイン時、<code>username</code> が一致したローカルアカウントが存在した場合は自動的に紐付ける",
       "Treat username matching as identical_warn": "WARNING: <code>username</code> の一致を以て同一ユーザーであるとみなすので、セキュリティに注意してください",
       "group_search_base_DN": "グループ検索ベース DN",

+ 4 - 2
lib/models/comment.js

@@ -12,10 +12,11 @@ module.exports = function(crowi) {
     revision: { type: ObjectId, ref: 'Revision', index: true },
     comment: { type: String, required: true },
     commentPosition: { type: Number, default: -1 },
-    createdAt: { type: Date, default: Date.now }
+    createdAt: { type: Date, default: Date.now },
+    isMarkdown: { type: Boolean, default: false}
   });
 
-  commentSchema.statics.create = function(pageId, creatorId, revisionId, comment, position) {
+  commentSchema.statics.create = function(pageId, creatorId, revisionId, comment, position, isMarkdown) {
     var Comment = this,
       commentPosition = position || -1;
 
@@ -28,6 +29,7 @@ module.exports = function(crowi) {
       newComment.revision = revisionId;
       newComment.comment = comment;
       newComment.commentPosition = position;
+      newComment.isMarkdown = isMarkdown || false;
 
       newComment.save(function(err, data) {
         if (err) {

+ 1 - 0
lib/models/config.js

@@ -59,6 +59,7 @@ module.exports = function(crowi) {
       'security:passport-ldap:bindDNPassword' : undefined,
       'security:passport-ldap:searchFilter' : undefined,
       'security:passport-ldap:attrMapUsername' : undefined,
+      'security:passport-ldap:attrMapName' : undefined,
       'security:passport-ldap:groupSearchBase' : undefined,
       'security:passport-ldap:groupSearchFilter' : undefined,
       'security:passport-ldap:groupDnProperty' : undefined,

+ 5 - 2
lib/models/external-account.js

@@ -68,7 +68,7 @@ class ExternalAccount {
    * @returns {Promise<ExternalAccount>}
    * @memberof ExternalAccount
    */
-  static findOrRegister(providerType, accountId, usernameToBeRegistered) {
+  static findOrRegister(providerType, accountId, usernameToBeRegistered, nameToBeRegistered) {
 
     return this.findOne({ providerType, accountId })
       .then(account => {
@@ -86,10 +86,13 @@ class ExternalAccount {
             if (user != null) {
               throw new DuplicatedUsernameException(`User '${usernameToBeRegistered}' already exists`, user);
             }
+            if (nameToBeRegistered == null) {
+              nameToBeRegistered = '';
+            }
 
             // create a new User with STATUS_ACTIVE
             debug(`ExternalAccount '${accountId}' is not found, it is going to be registered.`);
-            return User.createUser('', usernameToBeRegistered, undefined, undefined, undefined, User.STATUS_ACTIVE);
+            return User.createUser(nameToBeRegistered, usernameToBeRegistered, undefined, undefined, undefined, User.STATUS_ACTIVE);
           })
           .then(newUser => {
             return this.associate(providerType, accountId, newUser);

+ 7 - 1
lib/models/page.js

@@ -2,6 +2,7 @@ module.exports = function(crowi) {
   var debug = require('debug')('growi:models:page')
     , mongoose = require('mongoose')
     , escapeStringRegexp = require('escape-string-regexp')
+    , templateChecker = require('../util/templateChecker')
     , ObjectId = mongoose.Schema.Types.ObjectId
     , GRANT_PUBLIC = 1
     , GRANT_RESTRICTED = 2
@@ -96,6 +97,10 @@ module.exports = function(crowi) {
     return isPortalPath(this.path);
   };
 
+  pageSchema.methods.isTemplate = function() {
+    return templateChecker(this.path);
+  };
+
   pageSchema.methods.isCreator = function(userData) {
     // ゲスト閲覧の場合は userData に false が入る
     if (!userData) {
@@ -570,11 +575,12 @@ module.exports = function(crowi) {
   };
 
   const generatePathsOnTree = (path, pathList) => {
+    pathList.push(path);
+
     if (path === '') {
       return pathList;
     }
 
-    pathList.push(path);
     const newPath = cutOffLastSlash(path);
 
     return generatePathsOnTree(newPath, pathList);

+ 4 - 3
lib/routes/comment.js

@@ -62,8 +62,9 @@ module.exports = function(crowi, app) {
     var revisionId = form.revision_id;
     var comment = form.comment;
     var position = form.comment_position || -1;
+    var isMarkdown = form.is_markdown;
 
-    return Comment.create(pageId, req.user._id, revisionId, comment, position)
+    return Comment.create(pageId, req.user._id, revisionId, comment, position, isMarkdown)
       .then(function(createdComment) {
         createdComment.creator = req.user;
         return res.json(ApiResponse.success({comment: createdComment}));
@@ -92,11 +93,11 @@ module.exports = function(crowi, app) {
           return Page.updateCommentCount(comment.page);
         })
         .then(function() {
-          return res.json(ApiResponse.success({})); 
+          return res.json(ApiResponse.success({}));
         });
       })
       .catch(function(err) {
-        return res.json(ApiResponse.error(err)); 
+        return res.json(ApiResponse.error(err));
       });
 
   };

+ 1 - 0
lib/routes/index.js

@@ -167,6 +167,7 @@ module.exports = function(crowi, app) {
   app.post('/_api/pages.revertRemove' , loginRequired(crowi, app) , csrf, page.api.revertRemove); // (Avoid from API Token)
   app.post('/_api/pages.unlink'       , loginRequired(crowi, app) , csrf, page.api.unlink); // (Avoid from API Token)
   app.post('/_api/pages.duplicate'    , accessTokenParser, loginRequired(crowi, app), csrf, page.api.duplicate);
+  app.get('/_api/pages.templates'   , accessTokenParser , loginRequired(crowi, app, false) , page.api.templates);
   app.get('/_api/comments.get'        , accessTokenParser , loginRequired(crowi, app, false) , comment.api.get);
   app.post('/_api/comments.add'       , form.comment, accessTokenParser , loginRequired(crowi, app) , csrf, comment.api.add);
   app.post('/_api/comments.remove'    , accessTokenParser , loginRequired(crowi, app) , csrf, comment.api.remove);

+ 3 - 1
lib/routes/login-passport.js

@@ -108,10 +108,12 @@ module.exports = function(crowi, app) {
       const ldapAccountId = passportService.getLdapAccountIdFromReq(req);
 
       const attrMapUsername = passportService.getLdapAttrNameMappedToUsername();
+      const attrMapName = passportService.getLdapAttrNameMappedToName();
       const usernameToBeRegistered = ldapAccountInfo[attrMapUsername];
+      const nameToBeRegistered = ldapAccountInfo[attrMapName];
 
       // find or register(create) user
-      ExternalAccount.findOrRegister('ldap', ldapAccountId, usernameToBeRegistered)
+      ExternalAccount.findOrRegister('ldap', ldapAccountId, usernameToBeRegistered, nameToBeRegistered)
         .catch((err) => {
           if (err.name === 'DuplicatedUsernameException') {
             // get option

+ 44 - 26
lib/routes/page.js

@@ -13,6 +13,8 @@ module.exports = function(crowi, app) {
     , ApiResponse = require('../util/apiResponse')
     , interceptorManager = crowi.getInterceptorManager()
     , pagePathUtil = require('../util/pagePathUtil')
+    , swig = require('swig-templates')
+    , getToday = require('../util/getToday')
 
     , actions = {};
 
@@ -252,14 +254,11 @@ module.exports = function(crowi, app) {
       tree: [],
       pageRelatedGroup: null,
       template: null,
-      childrenTemplateExists: false,
-      decendantsTemplateExists: false,
     };
 
     var pageTeamplate = 'customlayout-selector/page';
 
     var isRedirect = false;
-    var originalPath = path;
     Page.findPage(path, req.user, req.query.revision)
     .then(function(page) {
       debug('Page found', page._id, page.path);
@@ -283,11 +282,7 @@ module.exports = function(crowi, app) {
           renderVars.tree = tree;
         })
         .then(function() {
-          return Page.checkIfTemplatesExist(originalPath);
-        })
-        .then(function(templateInfo) {
-          renderVars.childrenTemplateExists = templateInfo.childrenTemplateExists;
-          renderVars.decendantsTemplateExists = templateInfo.decendantsTemplateExists;
+          return Page.checkIfTemplatesExist(path);
         })
         .then(() => {
           return PageGroupRelation.findByPage(renderVars.page);
@@ -333,8 +328,12 @@ module.exports = function(crowi, app) {
     .catch(function(err) {
       pageTeamplate = 'customlayout-selector/not_found';
 
-      return Page.findTemplate(originalPath)
+      return Page.findTemplate(path)
         .then(template => {
+          if (template) {
+            template = replacePlaceholders(template, req);
+          }
+
           renderVars.template = template;
         });
     })
@@ -371,6 +370,17 @@ module.exports = function(crowi, app) {
     });
   };
 
+  const replacePlaceholders = (template, req) => {
+    const definitions = {
+      pagepath: getPathFromRequest(req),
+      username: req.user.name,
+      today: getToday(),
+    };
+    const compiledTemplate = swig.compile(template);
+
+    return compiledTemplate(definitions);
+  };
+
   actions.deletedPageListShow = function(req, res) {
     var path = '/trash' + getPathFromRequest(req);
     var limit = 50;
@@ -443,8 +453,13 @@ module.exports = function(crowi, app) {
   function renderPage(pageData, req, res) {
     // create page
     if (!pageData) {
-      return Page.findTemplate(getPathFromRequest(req))
-        .then((template) => {
+      const path = getPathFromRequest(req);
+      return Page.findTemplate(path)
+        .then(template => {
+          if (template) {
+            template = replacePlaceholders(template, req);
+          }
+
           return res.render('customlayout-selector/not_found', {
             author: {},
             page: false,
@@ -463,8 +478,6 @@ module.exports = function(crowi, app) {
       page: pageData,
       revision: pageData.revision || {},
       author: pageData.revision.author || false,
-      childrenTemplateExists: false,
-      decendantsTemplateExists: false,
     };
     var userPage = isUserPage(pageData.path);
     var userData = null;
@@ -509,12 +522,6 @@ module.exports = function(crowi, app) {
       }
     }).then(function() {
       return interceptorManager.process('beforeRenderPage', req, res, renderVars);
-    }).then(function() {
-      return Page.checkIfTemplatesExist(pageData.path)
-        .then(function(templateInfo) {
-          renderVars.childrenTemplateExists = templateInfo.childrenTemplateExists;
-          renderVars.decendantsTemplateExists = templateInfo.decendantsTemplateExists;
-        });
     }).then(function() {
       var defaultPageTeamplate = 'customlayout-selector/page';
       if (userData) {
@@ -670,13 +677,6 @@ module.exports = function(crowi, app) {
         }
       }
 
-      return res.redirect(redirectPath);
-    }).catch(function(err) {
-      debug('Page create or edit error.', err);
-      if (pageData && !req.form.isValid) {
-        return renderPage(pageData, req, res);
-      }
-
       return res.redirect(redirectPath);
     });
   };
@@ -1186,5 +1186,23 @@ module.exports = function(crowi, app) {
     });
   };
 
+  /**
+   * @api {get} /pages.templates Check if templates exist for page
+   * @apiName FindTemplates
+   * @apiGroup Page
+   *
+   * @apiParam {String} path
+   */
+  api.templates = function(req, res) {
+    const pagePath = req.query.path;
+    const templateFinder = Page.checkIfTemplatesExist(pagePath);
+
+    templateFinder.then(function(templateInfo) {
+      return res.json(ApiResponse.success(templateInfo));
+    }).catch(function(err) {
+      return res.json(ApiResponse.error(err));
+    });
+  };
+
   return actions;
 };

+ 10 - 0
lib/service/passport.js

@@ -137,6 +137,16 @@ class PassportService {
     const config = this.crowi.config;
     return config.crowi['security:passport-ldap:attrMapUsername'] || 'uid';
   }
+  /**
+   * return attribute name for mapping to name of Crowi DB
+   *
+   * @returns
+   * @memberof PassportService
+   */
+  getLdapAttrNameMappedToName() {
+    const config = this.crowi.config;
+    return config.crowi['security:passport-ldap:attrMapName'] || '';
+  }
 
   /**
    * CAUTION: this method is capable to use only when `req.body.loginForm` is not null

+ 12 - 0
lib/util/getToday.js

@@ -0,0 +1,12 @@
+/**
+ * getToday
+ */
+
+module.exports = function() {
+  const today = new Date();
+  const month = ('0' + (today.getMonth() + 1)).slice(-2);
+  const day = ('0' + today.getDate()).slice(-2);
+  const dateString = today.getFullYear() + '/' + month + '/' + day;
+
+  return dateString;
+};

+ 13 - 0
lib/util/templateChecker.js

@@ -0,0 +1,13 @@
+/**
+ * templateChecker
+ */
+
+module.exports = function(path) {
+  'use strict';
+
+  if (path.match(/.*\/_{1,2}template$/)) {
+    return true;
+  }
+
+  return false;
+};

+ 18 - 0
lib/views/admin/widget/passport/ldap.html

@@ -120,6 +120,7 @@
       <h4>Attribute Mapping ({{ t("security_setting.optional") }})</h4>
 
       <div class="form-group">
+        <div class="row">
         <label for="settingForm[security:passport-ldap:attrMapUsername]" class="col-xs-3 control-label">username</label>
         <div class="col-xs-6">
           <input class="form-control" type="text" placeholder="Default: uid"
@@ -129,7 +130,11 @@
               {{ t("security_setting.ldap.username_detail") }}
             </small>
           </p>
+        </div>
+        </div>
 
+        <div class="row">
+        <div class="col-xs-6 col-xs-offset-3">
           <div class="checkbox checkbox-info">
             <input type="checkbox" id="cbSameUsernameTreatedAsIdenticalUser" name="settingForm[security:passport-ldap:isSameUsernameTreatedAsIdenticalUser]" value="1"
                 {% if settingForm['security:passport-ldap:isSameUsernameTreatedAsIdenticalUser'] %}checked{% endif %} />
@@ -143,8 +148,21 @@
             </p>
           </div>
         </div>
+        </div>
       </div>
 
+      <div class="row">
+        <label for="settingForm[security:passport-ldap:attrMapName]" class="col-xs-3 control-label">name</label>
+        <div class="col-xs-6">
+          <input class="form-control" type="text"
+              name="settingForm[security:passport-ldap:attrMapName]" value="{{ settingForm['security:passport-ldap:attrMapName'] || '' }}">
+          <p class="help-block">
+            <small>
+              {{ t("security_setting.ldap.name_detail") }}
+            </small>
+          </p>
+        </div>
+        </div>
 
       <h4>{{ t("security_setting.ldap.group_search_filter") }} ({{ t("security_setting.optional") }})</h4>
 

+ 36 - 0
lib/views/layout-growi/base/layout.html

@@ -26,6 +26,42 @@
 
 </div><!-- /.container-fluid -->
 
+
+<!-- Side Scroll Bar-->
+<script>
+  /*
+   * Disabled temporally -- 2018.06.06 Yuki Takei
+   * see https://weseek.myjetbrains.com/youtrack/issue/GC-278
+   *
+  function DrawScrollbar() {
+    var h = window.innerHeight - document.getElementById('page-header').clientHeight ;
+    $('#revision-toc-content').slimScroll({
+      railVisible: true,
+      position: 'right',
+      height: h,
+    });
+  }
+
+  $(function(){
+    DrawScrollbar();
+  });
+
+  (function () {
+    var timer = 0;
+
+    window.onresize = function () {
+      if (timer > 0) {
+        clearTimeout(timer);
+      }
+
+      timer = setTimeout(function () {
+        DrawScrollbar();
+      }, 200);
+    };
+  }());
+  */
+  </script>
+
 <footer class="footer">
   {% include '../../widget/system-version.html' %}
 </footer>

+ 1 - 26
lib/views/layout-growi/widget/comments.html

@@ -19,32 +19,7 @@
     </div>
 
     {% if page and not page.isDeleted() %}
-    <form class="form page-comment-form" id="page-comment-form" onsubmit="return false;">
-      <div class="comment-form">
-        <div class="comment-form-user">
-            <img src="{{ user|picture }}" class="picture img-circle" width="25" alt="{{ user.name }}" title="{{ user.name }}" />
-        </div>
-        <div class="comment-form-main">
-          <div class="comment-write" id="comment-write">
-            <textarea class="comment-form-comment form-control" id="comment-form-comment" name="commentForm[comment]"
-                required placeholder="Write comments here..." {% if not user %}disabled{% endif %}></textarea>
-          </div>
-          <div class="comment-submit">
-            <input type="hidden" name="_csrf" value="{{ csrf() }}">
-            <input type="hidden" name="commentForm[page_id]" value="{{ page._id.toString() }}">
-            <input type="hidden" name="commentForm[revision_id]" value="{{ revision._id.toString() }}">
-            <div class="pull-right">
-              <span class="text-danger" id="comment-form-message"></span>
-              <button type="submit" id="comment-form-button" class="fcbtn btn btn-sm btn-outline btn-rounded btn-primary btn-1b" {% if not user %}disabled{% endif %}>
-                Comment
-              </button>
-            </div>
-            <div class="clearfix"></div>
-          </div>
-        </div>
-      </div>
-    </form>
-    <div id="page-comment-form-behavior"></div>
+    <div id="page-comment-write"></div>
     {% endif %}
 
   </div>

+ 1 - 0
lib/views/layout/layout.html

@@ -16,6 +16,7 @@
 
   {{ customHeader() }}
 
+
   <!-- polyfills for IE11 -->
   <script>
     var userAgent = window.navigator.userAgent.toLowerCase();

+ 33 - 20
lib/views/modal/create_page.html

@@ -44,19 +44,21 @@
           </fieldset>
         </form>
 
-        <div id = "template-form" class="row form-horizontal m-t-15">
+        <div id="template-form" class="row form-horizontal m-t-15">
           <fieldset class="col-xs-12">
             <legend>{{ t('template.modal_label.Create template under', parentPath(path)) }}</legend>
             <div class="d-flex create-page-input-container">
               <div class="create-page-input-row d-flex align-items-center">
-                <select id="template-type" class="page-name-input form-control">
-                  <option value="" disabled selected>{{ t('template.option_label.select') }}</option>
-                  <option value="children">{{ t('template.local.label') }}(_template) - {{ t('template.local.desc') }}</option>
-                  <option value="decentants">{{ t('template.global.label') }}(__template) - {{ t('template.global.desc') }}</option>
+                <select id="template-type" class="form-control selectpicker" title="{{ t('template.option_label.select') }}">
+                  <option value="children" data-subtext="- {{ t('template.children.desc') }}">{{ t('template.children.label') }}(_template)</option>
+                  <option value="decentants" data-subtext="- {{ t('template.decendants.desc') }}">{{ t('template.decendants.label') }}(__template)</option>
                 </select>
               </div>
               <div class="create-page-button-container">
-                  <a id="link-to-template" href="{{ page.path || path }}"><button class="fcbtn btn btn-outline btn-rounded btn-primary btn-1b"><i class="icon-fw icon-doc"></i>{{ t('Create') }}</button></a>
+                <a id="link-to-template" href="{{ page.path || path }}" class="fcbtn btn btn-outline btn-rounded btn-primary btn-1b disabled">
+                  <i class="icon-fw icon-doc"></i>
+                  <span id="create-template-button-link">{{ t('Create') }}/{{ t('Edit') }}</span>
+                </a>
               </div>
             </div>
           </fieldset>
@@ -68,22 +70,33 @@
   </div><!-- /.modal-dialog -->
 </div><!-- /.modal -->
 <script>
-  if($("#create-page")) {
-    let pagePath = $("#link-to-template").attr("href");
+  let buttonTextChildren;
+  let buttonTextDecendants;
+  let pagePath = $("#link-to-template").attr("href");
 
-    if (pagePath.endsWith("/")) {
+  if (pagePath.endsWith("/")) {
       pagePath = pagePath.slice(0, -1);
-    };
+  };
 
-    $("#template-type").on("change", () => {
-      if ($("#template-type").val() === "children") {
-        href = pagePath + "/_template#edit-form";
-        $("#link-to-template").attr("href", href);
-      }
-      else if ($("#template-type").val() === "decentants") {
-        href = pagePath + "/__template#edit-form";
-        $("#link-to-template").attr("href", href);
-      };
+  $.get(`/_api/pages.templates?path=${pagePath}`)
+    .then(templateInfo => {
+      buttonTextChildren = templateInfo.childrenTemplateExists ? `{{ t('Edit') }}` : `{{ t('Create') }}`;
+      buttonTextDecendants = templateInfo.decendantsTemplateExists ? `{{ t('Edit') }}` : `{{ t('Create') }}`;
     });
-  };
+
+  $("#template-type").on("change", () => {
+    // enable button
+    $('#link-to-template').removeClass("disabled");
+
+    if ($("#template-type").val() === "children") {
+      href = pagePath + "/_template#edit-form";
+      $("#link-to-template").attr("href", href);
+      $('#create-template-button-link').text(buttonTextChildren);
+    }
+    else if ($("#template-type").val() === "decentants") {
+      href = pagePath + "/__template#edit-form";
+      $("#link-to-template").attr("href", href);
+      $('#create-template-button-link').text(buttonTextDecendants);
+    };
+  });
 </script>

+ 6 - 8
lib/views/modal/create_template.html

@@ -12,30 +12,28 @@
           <div class="row">
             <div class="col-sm-6">
               <div class="panel panel-default panel-select-template">
-                <div class="panel-heading">{{ t('template.local.label') }}</div>
+                <div class="panel-heading">{{ t('template.children.label') }}</div>
                 <div class="panel-body">
                   <p class="text-center"><code>_template</code></p>
-                  <p class="help-block text-center"><small>{{ t('template.local.desc') }}</small></p>
+                  <p class="help-block text-center"><small>{{ t('template.children.desc') }}</small></p>
                 </div>
                 <div class="panel-footer text-center">
                   <a href="{% if page.path.endsWith('/') %}{{ page.path }}{% else %}{{ page.path}}/{% endif %}_template#edit-form"
-                      class="btn btn-sm btn-primary">
-                    {% if childrenTemplateExists %}{{ t('Edit') }}{% else %}{{ t('Create') }}{% endif %}
+                      class="btn btn-sm btn-primary" id="template-button-children">
                   </a>
                 </div>
               </div>
             </div>
             <div class="col-sm-6">
               <div class="panel panel-default panel-select-template">
-                <div class="panel-heading">{{ t('template.global.label') }}</div>
+                <div class="panel-heading">{{ t('template.decendants.label') }}</div>
                 <div class="panel-body">
                   <p class="text-center"><code>__template</code></p>
-                  <p class="help-block text-center"><small>{{ t('template.global.desc') }}</small></p>
+                  <p class="help-block text-center"><small>{{ t('template.decendants.desc') }}</small></p>
                 </div>
                 <div class="panel-footer text-center">
                   <a href="{% if page.path.endsWith('/') %}{{ page.path }}{% else %}{{ page.path}}/{% endif %}__template#edit-form"
-                      class="btn btn-sm btn-primary">
-                  {% if decendantsTemplateExists %}{{ t('Edit') }}{% else %}{{ t('Create') }}{% endif %}
+                      class="btn btn-sm btn-primary" id="template-button-decendants">
                   </a>
                 </div>
               </div>

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

@@ -15,7 +15,7 @@
   <div class="tab-content">
 
     {% if page %}
-      <script type="text/template" id="raw-text-original">{{ revision.body }}</script>
+      <script type="text/template" id="raw-text-original">{{ revision.body.toString() }}</script>
 
       {# formatted text #}
       <div class="tab-pane {% if not req.body.pageForm %}active{% endif %}" id="revision-body">

+ 4 - 0
lib/views/widget/page_list.html

@@ -19,6 +19,10 @@
       <span class="label label-info">PORTAL</span>
     {% endif  %}
 
+    {% if page.isTemplate() %}
+      <span class="label label-info">TMPL</span>
+    {% endif  %}
+
     {% if page.commentCount > 0 %}
     <span>
       <i class="icon-bubble"></i>{{ page.commentCount }}

+ 1 - 1
lib/views/widget/page_list_and_timeline.html

@@ -33,7 +33,7 @@
             <div class="revision-body wiki"></div>
           </div>
         </div>
-        <script type="text/template">{{ page.revision.body }}</script>
+        <script type="text/template">{{ page.revision.body.toString() }}</script>
       </div>
       <hr>
       {% endfor %}

+ 2 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.1.2-RC",
+  "version": "3.1.3-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -65,6 +65,7 @@
     "cookie-parser": "^1.4.3",
     "cross-env": "^5.0.5",
     "csrf": "~3.0.3",
+    "diff": "^3.5.0",
     "elasticsearch": "^15.0.0",
     "entities": "^1.1.1",
     "env-cmd": "^8.0.1",
@@ -130,7 +131,6 @@
     "css-loader": "^0.28.0",
     "csv-to-markdown-table": "^0.4.0",
     "date-fns": "^1.29.0",
-    "diff": "^3.3.0",
     "diff2html": "^2.3.3",
     "eazy-logger": "^3.0.2",
     "eslint": "^4.19.1",

+ 15 - 11
resource/js/app.js

@@ -18,7 +18,7 @@ import Page             from './components/Page';
 import PageListSearch   from './components/PageListSearch';
 import PageHistory      from './components/PageHistory';
 import PageComments     from './components/PageComments';
-import PageCommentFormBehavior from './components/PageCommentFormBehavior';
+import CommentForm from './components/PageComment/CommentForm';
 import PageAttachment   from './components/PageAttachment';
 import SeenUserList     from './components/SeenUserList';
 import RevisionPath     from './components/Page/RevisionPath';
@@ -30,8 +30,6 @@ import CustomCssEditor  from './components/Admin/CustomCssEditor';
 import CustomScriptEditor from './components/Admin/CustomScriptEditor';
 import CustomHeaderEditor from './components/Admin/CustomHeaderEditor';
 
-import * as entities from 'entities';
-
 if (!window) {
   window = {};
 }
@@ -46,7 +44,6 @@ let pageRevisionCreatedAt = null;
 let pagePath;
 let pageContent = '';
 let markdown = '';
-let pageGrant = null;
 if (mainContent !== null) {
   pageId = mainContent.getAttribute('data-page-id');
   pageRevisionId = mainContent.getAttribute('data-page-revision-id');
@@ -54,9 +51,8 @@ if (mainContent !== null) {
   pagePath = mainContent.attributes['data-path'].value;
   const rawText = document.getElementById('raw-text-original');
   if (rawText) {
-    pageContent = rawText.innerHTML;
+    markdown = rawText.innerHTML;
   }
-  markdown = entities.decodeHTML(pageContent);
 }
 const isLoggedin = document.querySelector('.main-container.nologin') == null;
 
@@ -116,7 +112,7 @@ const componentMappings = {
 };
 // additional definitions if data exists
 if (pageId) {
-  componentMappings['page-comments-list'] = <PageComments pageId={pageId} revisionId={pageRevisionId} revisionCreatedAt={pageRevisionCreatedAt} crowi={crowi} />;
+  componentMappings['page-comments-list'] = <PageComments pageId={pageId} revisionId={pageRevisionId} revisionCreatedAt={pageRevisionCreatedAt} crowi={crowi} crowiOriginRenderer={crowiRenderer} />;
   componentMappings['page-attachment'] = <PageAttachment pageId={pageId} pageContent={pageContent} crowi={crowi} />;
 }
 if (pagePath) {
@@ -134,10 +130,18 @@ Object.keys(componentMappings).forEach((key) => {
   }
 });
 
-// render components with refs to another component
-const elem = document.getElementById('page-comment-form-behavior');
-if (elem) {
-  ReactDOM.render(<PageCommentFormBehavior crowi={crowi} pageComments={componentInstances['page-comments-list']} />, elem);
+// render comment form
+const writeCommentElem = document.getElementById('page-comment-write');
+if (writeCommentElem) {
+  const pageCommentsElem = componentInstances['page-comments-list'];
+  const postCompleteHandler = (comment) => {
+    if (pageCommentsElem != null) {
+      pageCommentsElem.retrieveData();
+    }
+  };
+  ReactDOM.render(
+    <CommentForm crowi={crowi} pageId={pageId} revisionId={pageRevisionId} onPostComplete={postCompleteHandler} crowiRenderer={crowiRenderer}/>,
+    writeCommentElem);
 }
 
 /*

+ 2 - 1
resource/js/components/Page/RevisionBody.js

@@ -51,7 +51,7 @@ export default class RevisionBody extends React.Component {
             this.props.inputRef(elm);
           }
         }}
-        className="wiki" dangerouslySetInnerHTML={this.generateInnerHtml(this.props.html)}>
+        className={'wiki ' + this.props.additionalClassName} dangerouslySetInnerHTML={this.generateInnerHtml(this.props.html)}>
       </div>
     );
   }
@@ -63,4 +63,5 @@ RevisionBody.propTypes = {
   isMathJaxEnabled: PropTypes.bool,
   renderMathJaxOnInit: PropTypes.bool,
   renderMathJaxInRealtime: PropTypes.bool,
+  additionalClassName: PropTypes.string,
 };

+ 69 - 1
resource/js/components/PageComment/Comment.js

@@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
 
 import dateFnsFormat from 'date-fns/format';
 
+import RevisionBody from '../Page/RevisionBody';
+
 import ReactUtils from '../ReactUtils';
 import UserPicture from '../User/UserPicture';
 
@@ -19,11 +21,30 @@ export default class Comment extends React.Component {
   constructor(props) {
     super(props);
 
+    this.state = {
+      html: '',
+    };
+
     this.isCurrentUserIsAuthor = this.isCurrentUserEqualsToAuthor.bind(this);
     this.isCurrentRevision = this.isCurrentRevision.bind(this);
     this.getRootClassName = this.getRootClassName.bind(this);
     this.getRevisionLabelClassName = this.getRevisionLabelClassName.bind(this);
     this.deleteBtnClickedHandler = this.deleteBtnClickedHandler.bind(this);
+    this.renderHtml = this.renderHtml.bind(this);
+  }
+
+  componentWillMount() {
+    this.renderHtml(this.props.comment.comment);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    this.renderHtml(nextProps.comment.comment);
+  }
+
+  //not used
+  setMarkdown(markdown) {
+    this.setState({ markdown });
+    this.renderHtml(markdown);
   }
 
   isCurrentUserEqualsToAuthor() {
@@ -48,13 +69,58 @@ export default class Comment extends React.Component {
     this.props.deleteBtnClicked(this.props.comment);
   }
 
+  renderRevisionBody() {
+    const config = this.props.crowi.getConfig();
+    const isMathJaxEnabled = !!config.env.MATHJAX;
+    return (
+      <RevisionBody html={this.state.html}
+          inputRef={el => this.revisionBodyElement = el}
+          isMathJaxEnabled={isMathJaxEnabled}
+          renderMathJaxOnInit={true}
+          additionalClassName="comment" />
+    );
+  }
+
+  renderHtml(markdown) {
+    var context = {
+      markdown,
+      dom: this.revisionBodyElement,
+    };
+
+    const crowiRenderer = this.props.crowiRenderer;
+    const interceptorManager = this.props.crowi.interceptorManager;
+    interceptorManager.process('preRenderComment', context)
+      .then(() => interceptorManager.process('prePreProcess', context))
+      .then(() => {
+        context.markdown = crowiRenderer.preProcess(context.markdown);
+      })
+      .then(() => interceptorManager.process('postPreProcess', context))
+      .then(() => {
+        var parsedHTML = crowiRenderer.process(context.markdown);
+        context['parsedHTML'] = parsedHTML;
+      })
+      .then(() => interceptorManager.process('prePostProcess', context))
+      .then(() => {
+        context.parsedHTML = crowiRenderer.postProcess(context.parsedHTML, context.dom);
+      })
+      .then(() => interceptorManager.process('postPostProcess', context))
+      .then(() => interceptorManager.process('preRenderCommentHtml', context))
+      .then(() => {
+        this.setState({ html: context.parsedHTML });
+      })
+      // process interceptors for post rendering
+      .then(() => interceptorManager.process('postRenderCommentHtml', context));
+
+  }
+
   render() {
     const comment = this.props.comment;
     const creator = comment.creator;
+    const isMarkdown = comment.isMarkdown;
 
     const rootClassName = this.getRootClassName();
     const commentDate = dateFnsFormat(comment.createdAt, 'YYYY/MM/DD HH:mm');
-    const commentBody = ReactUtils.nl2br(comment.comment);
+    const commentBody = isMarkdown ? this.renderRevisionBody(): ReactUtils.nl2br(comment.comment);
     const creatorsPage = `/user/${creator.username}`;
     const revHref = `?revision=${comment.revision}`;
     const revFirst8Letters = comment.revision.substr(-8);
@@ -90,4 +156,6 @@ Comment.propTypes = {
   currentRevisionId: PropTypes.string.isRequired,
   currentUserId: PropTypes.string.isRequired,
   deleteBtnClicked: PropTypes.func.isRequired,
+  crowi: PropTypes.object.isRequired,
+  crowiRenderer: PropTypes.object.isRequired,
 };

+ 190 - 0
resource/js/components/PageComment/CommentForm.js

@@ -0,0 +1,190 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ReactUtils from '../ReactUtils';
+
+import CommentPreview from '../PageComment/CommentPreview';
+
+import Button from 'react-bootstrap/es/Button';
+import Tab from 'react-bootstrap/es/Tab';
+import Tabs from 'react-bootstrap/es/Tabs';
+import UserPicture from '../User/UserPicture';
+
+/**
+ *
+ * @author Yuki Takei <yuki@weseek.co.jp>
+ *
+ * @export
+ * @class Comment
+ * @extends {React.Component}
+ */
+
+export default class CommentForm extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      comment: '',
+      isMarkdown: true,
+      html: '',
+      key: 1,
+    };
+
+    this.updateState = this.updateState.bind(this);
+    this.postComment = this.postComment.bind(this);
+    this.renderHtml = this.renderHtml.bind(this);
+    this.handleSelect = this.handleSelect.bind(this);
+  }
+
+  updateState(event) {
+    const target = event.target;
+    const value = target.type === 'checkbox' ? target.checked : target.value;
+    const name = target.name;
+
+    this.setState({
+      [name]: value
+    });
+  }
+
+  handleSelect(key) {
+    this.setState({ key });
+    this.renderHtml(this.state.comment);
+  }
+
+  /**
+   * Load data of comments and rerender <PageComments />
+   */
+  postComment(event) {
+    event.preventDefault();
+    this.props.crowi.apiPost('/comments.add', {
+      commentForm: {
+        comment: this.state.comment,
+        _csrf: this.props.crowi.csrfToken,
+        page_id: this.props.pageId,
+        revision_id: this.props.revisionId,
+        is_markdown: this.state.isMarkdown,
+      }
+    })
+      .then((res) => {
+        if (this.props.onPostComplete != null) {
+          this.props.onPostComplete(res.comment);
+        }
+        this.setState({
+          comment: '',
+          isMarkdown: true,
+          html: '',
+          key: 1,
+        });
+      });
+  }
+
+  getCommentHtml() {
+    return (
+      <CommentPreview
+        html={this.state.html}
+        inputRef={el => this.previewElement = el}/>
+    );
+  }
+
+  renderHtml(markdown) {
+    var context = {
+      markdown,
+      dom: this.previewElement,
+    };
+
+    const crowiRenderer = this.props.crowiRenderer;
+    const interceptorManager = this.props.crowi.interceptorManager;
+    interceptorManager.process('preRenderCommnetPreview', context)
+      .then(() => interceptorManager.process('prePreProcess', context))
+      .then(() => {
+        context.markdown = crowiRenderer.preProcess(context.markdown);
+      })
+      .then(() => interceptorManager.process('postPreProcess', context))
+      .then(() => {
+        var parsedHTML = crowiRenderer.process(context.markdown);
+        context['parsedHTML'] = parsedHTML;
+      })
+      .then(() => interceptorManager.process('prePostProcess', context))
+      .then(() => {
+        context.parsedHTML = crowiRenderer.postProcess(context.parsedHTML, context.dom);
+      })
+      .then(() => interceptorManager.process('postPostProcess', context))
+      .then(() => interceptorManager.process('preRenderCommentPreviewHtml', context))
+      .then(() => {
+        this.setState({ html: context.parsedHTML });
+      })
+      // process interceptors for post rendering
+      .then(() => interceptorManager.process('postRenderCommentPreviewHtml', context));
+  }
+
+  generateInnerHtml(html) {
+    return {__html: html};
+  }
+
+
+  render() {
+    const crowi = this.props.crowi;
+    const username = crowi.me;
+    const user = crowi.findUser(username);
+    const creatorsPage = `/user/${username}`;
+    const comment = this.state.comment;
+    const commentPreview = this.state.isMarkdown ? this.getCommentHtml(): ReactUtils.nl2br(comment);
+
+    return (
+      <div>
+        <form className="form page-comment-form" id="page-comment-form" onSubmit={this.postComment}>
+          { username &&
+            <div className="comment-form">
+              <div className="comment-form-user">
+                <a href={creatorsPage}>
+                  <UserPicture user={user} />
+                </a>
+              </div>
+              <div className="comment-form-main">
+                <div className="comment-write">
+                  <Tabs activeKey={this.state.key} id="comment-form-tabs" onSelect={this.handleSelect} animation={false}>
+                    <Tab eventKey={1} title="Write">
+                      <textarea className="comment-form-comment form-control" id="comment-form-comment" name="comment" required placeholder="Write comments here..." value={this.state.comment} onChange={this.updateState} >
+                      </textarea>
+                    </Tab>
+                    { this.state.isMarkdown == true &&
+                    <Tab eventKey={2} title="Preview">
+                      <div className="comment-form-preview">
+                       {commentPreview}
+                      </div>
+                    </Tab>
+                    }
+                  </Tabs>
+                </div>
+                <div className="comment-submit">
+                  <div className="pull-left">
+                  { this.state.key == 1 &&
+                    <label>
+                      <input type="checkbox" id="comment-form-is-markdown" name="isMarkdown" checked={this.state.isMarkdown} value="1" onChange={this.updateState} /> Markdown
+                    </label>
+                  }
+                  </div>
+                  <div className="pull-right">
+                    <Button type="submit" value="Submit" bsStyle="primary" className="fcbtn btn btn-sm btn-primary btn-outline btn-rounded btn-1b">
+                        Comment
+                    </Button>
+                  </div>
+                  <div className="clearfix">
+                  </div>
+                </div>
+              </div>
+            </div>
+          }
+        </form>
+      </div>
+    );
+  }
+}
+
+CommentForm.propTypes = {
+  crowi: PropTypes.object.isRequired,
+  onPostComplete: PropTypes.func,
+  pageId: PropTypes.string,
+  revisionId: PropTypes.string,
+  crowiRenderer:  PropTypes.object.isRequired,
+};

+ 35 - 0
resource/js/components/PageComment/CommentPreview.js

@@ -0,0 +1,35 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import RevisionBody from '../Page/RevisionBody';
+
+/**
+ * Wrapper component for Page/RevisionBody
+ */
+export default class CommentPreview extends React.Component {
+
+  constructor(props) {
+    super(props);
+  }
+
+  render() {
+    return (
+      <div className="page-comment-preview-body"
+          ref={(elm) => {
+            this.previewElement = elm;
+            this.props.inputRef(elm);
+          }}>
+
+        <RevisionBody
+          {...this.props}
+          additionalClassName="comment"
+        />
+      </div>
+    );
+  }
+}
+
+CommentPreview.propTypes = {
+  html: PropTypes.string,
+  inputRef: PropTypes.func.isRequired,  // for getting div element
+};

+ 0 - 65
resource/js/components/PageCommentFormBehavior.js

@@ -1,65 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import PageComments from './PageComments';
-
-/**
- * Set the behavior that post comments to #page-comment-form
- *
- * This is transplanted from legacy/crowi.js -- 2017.06.03 Yuki Takei
- *
- * @author Yuki Takei <yuki@weseek.co.jp>
- *
- * @export
- * @class PageCommentFormBehavior
- * @extends {React.Component}
- */
-export default class PageCommentFormBehavior extends React.Component {
-
-  constructor(props) {
-    super(props);
-  }
-
-  componentWillMount() {
-    const pageComments = this.props.pageComments;
-
-    if (pageComments === undefined) {
-      return;
-    }
-
-    $('#page-comment-form').on('submit', function() {
-      var $button = $('#comment-form-button');
-      $button.attr('disabled', 'disabled');
-      $.post('/_api/comments.add', $(this).serialize(), function(data) {
-        $button.prop('disabled', false);
-        if (data.ok) {
-
-          // reload comments
-          pageComments.init();
-
-          $('#comment-form-comment').val('');
-          $('#comment-form-message').text('');
-        }
-        else {
-          $('#comment-form-message').text(data.error);
-        }
-      }).fail(function(data) {
-        if (data.status !== 200) {
-          $('#comment-form-message').text(data.statusText);
-        }
-      });
-
-      return false;
-    });
-  }
-
-  render() {
-    // render nothing
-    return <div></div>;
-  }
-}
-
-PageCommentFormBehavior.propTypes = {
-  pageComments: PropTypes.instanceOf(PageComments),
-  crowi: PropTypes.object.isRequired,
-};

+ 23 - 12
resource/js/components/PageComments.js

@@ -1,6 +1,8 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
+import GrowiRenderer from '../util/GrowiRenderer';
+
 import Comment from './PageComment/Comment';
 import DeleteCommentModal from './PageComment/DeleteCommentModal';
 
@@ -30,6 +32,8 @@ export default class PageComments extends React.Component {
       errorMessageForDeleting: undefined,
     };
 
+    this.growiRenderer = new GrowiRenderer(this.props.crowi, this.props.crowiOriginRenderer, {mode: 'comment'});
+
     this.init = this.init.bind(this);
     this.confirmToDeleteComment = this.confirmToDeleteComment.bind(this);
     this.deleteComment = this.deleteComment.bind(this);
@@ -43,6 +47,8 @@ export default class PageComments extends React.Component {
     if (pageId) {
       this.init();
     }
+
+    this.retrieveData = this.retrieveData.bind(this);
   }
 
   init() {
@@ -50,21 +56,23 @@ export default class PageComments extends React.Component {
       return ;
     }
 
-    const pageId = this.props.pageId;
-
     const layoutType = this.props.crowi.getConfig()['layoutType'];
     this.setState({isLayoutTypeGrowi: 'crowi-plus' === layoutType || 'growi' === layoutType});
 
-    // get data (desc order array)
-    this.props.crowi.apiGet('/comments.get', {page_id: pageId})
-    .then(res => {
-      if (res.ok) {
-        this.setState({comments: res.comments});
-      }
-    }).catch(err => {
-
-    });
+    this.retrieveData();
+  }
 
+  /**
+   * Load data of comments and store them in state
+   */
+  retrieveData() {
+    // get data (desc order array)
+    this.props.crowi.apiGet('/comments.get', {page_id: this.props.pageId})
+      .then(res => {
+        if (res.ok) {
+          this.setState({comments: res.comments});
+        }
+      });
   }
 
   confirmToDeleteComment(comment) {
@@ -123,7 +131,9 @@ export default class PageComments extends React.Component {
         <Comment key={comment._id} comment={comment}
           currentUserId={this.props.crowi.me}
           currentRevisionId={this.props.revisionId}
-          deleteBtnClicked={this.confirmToDeleteComment} />
+          deleteBtnClicked={this.confirmToDeleteComment}
+          crowi={this.props.crowi}
+          crowiRenderer={this.growiRenderer} />
       );
     });
   }
@@ -239,4 +249,5 @@ PageComments.propTypes = {
   revisionId: PropTypes.string,
   revisionCreatedAt: PropTypes.number,
   crowi: PropTypes.object.isRequired,
+  crowiOriginRenderer: PropTypes.object.isRequired,
 };

+ 8 - 0
resource/js/components/PageList/PageListMeta.js

@@ -14,6 +14,7 @@ export default class PageListMeta extends React.Component {
   render() {
     // TODO isPortal()
     const page = this.props.page;
+    const templateChecker = require('../../../../lib/util/templateChecker');
 
     // portal check
     let PortalLabel;
@@ -21,6 +22,12 @@ export default class PageListMeta extends React.Component {
       PortalLabel = <span className="label label-info">PORTAL</span>;
     }
 
+    // template check
+    let TemplateLabel;
+    if (templateChecker(page.path)) {
+      TemplateLabel = <span className="label label-info">TMPL</span>;
+    }
+
     let CommentCount;
     if (page.commentCount > 0) {
       CommentCount = <span><i className="icon-bubble" />{page.commentCount}</span>;
@@ -35,6 +42,7 @@ export default class PageListMeta extends React.Component {
     return (
       <span className="page-list-meta">
         {PortalLabel}
+        {TemplateLabel}
         {CommentCount}
         {LikerCount}
       </span>

+ 1 - 1
resource/js/legacy/crowi.js

@@ -430,7 +430,7 @@ $(function() {
         var revisionPath = '#' + id + ' .revision-path';
         /* eslint-enable */
         var pagePath = document.getElementById(id).getAttribute('data-page-path');
-        var markdown = entities.decodeHTML($(contentId).html());
+        var markdown = $(contentId).html();
 
         ReactDOM.render(<Page crowi={crowi} crowiRenderer={growiRendererForTimeline} markdown={markdown} pagePath={pagePath} />, revisionBodyElem);
       });

+ 9 - 2
resource/js/util/GrowiRenderer.js

@@ -6,13 +6,14 @@ import CsvToTable    from './PreProcessor/CsvToTable';
 import XssFilter     from './PreProcessor/XssFilter';
 import CrowiTemplate from './PostProcessor/CrowiTemplate';
 
-import CommonPluginsConfigurer from './markdown-it/common-plugins';
 import EmojiConfigurer from './markdown-it/emoji';
+import FooternoteConfigurer from './markdown-it/footernote';
 import HeaderLineNumberConfigurer from './markdown-it/header-line-number';
 import HeaderConfigurer from './markdown-it/header';
 import MathJaxConfigurer from './markdown-it/mathjax';
 import PlantUMLConfigurer from './markdown-it/plantuml';
 import TableConfigurer from './markdown-it/table';
+import TaskListsConfigurer from './markdown-it/task-lists';
 import TocAndAnchorConfigurer from './markdown-it/toc-and-anchor';
 
 export default class GrowiRenderer {
@@ -68,7 +69,7 @@ export default class GrowiRenderer {
     this.isMarkdownItConfigured = false;
 
     this.markdownItConfigurers = [
-      new CommonPluginsConfigurer(crowi),
+      new TaskListsConfigurer(crowi),
       new HeaderConfigurer(crowi),
       new TableConfigurer(crowi),
       new EmojiConfigurer(crowi),
@@ -81,15 +82,21 @@ export default class GrowiRenderer {
     switch (mode) {
       case 'page':
         this.markdownItConfigurers = this.markdownItConfigurers.concat([
+          new FooternoteConfigurer(crowi),
           new TocAndAnchorConfigurer(crowi, options.renderToc),
           new HeaderLineNumberConfigurer(crowi),
         ]);
         break;
       case 'editor':
         this.markdownItConfigurers = this.markdownItConfigurers.concat([
+          new FooternoteConfigurer(crowi),
           new HeaderLineNumberConfigurer(crowi)
         ]);
         break;
+      case 'comment':
+        this.markdownItConfigurers = this.markdownItConfigurers.concat([
+        ]);
+        break;
       default:
         break;
     }

+ 3 - 2
resource/js/util/interceptor/detach-code-blocks.js

@@ -104,14 +104,15 @@ export class RestoreCodeBlockInterceptor extends BasicInterceptor {
    * @inheritdoc
    */
   isInterceptWhen(contextName) {
-    return /^postPreProcess|preRenderHtml|preRenderPreviewHtml$/.test(contextName);
+    return /^postPreProcess|preRenderHtml|preRenderPreviewHtml|preRenderCommentHtml|preRenderCommentPreviewHtml$/.test(contextName);
   }
 
   getTargetKey(contextName) {
     if (contextName === 'postPreProcess') {
       return 'markdown';
     }
-    else if (contextName === 'preRenderHtml' || contextName === 'preRenderPreviewHtml') {
+    else if (contextName === 'preRenderHtml' || contextName === 'preRenderPreviewHtml'
+        || contextName === 'preRenderCommentHtml' || contextName === 'preRenderCommentPreviewHtml') {
       return 'parsedHTML';
     }
   }

+ 0 - 15
resource/js/util/markdown-it/common-plugins.js

@@ -1,15 +0,0 @@
-export default class CommonPluginsConfigurer {
-
-  constructor(crowi) {
-    this.crowi = crowi;
-  }
-
-  configure(md) {
-    md.use(require('markdown-it-footnote'))
-      .use(require('markdown-it-task-lists'), {
-        enabled: true,
-      })
-    ;
-  }
-
-}

+ 11 - 0
resource/js/util/markdown-it/footernote.js

@@ -0,0 +1,11 @@
+export default class FooternoteConfigurer {
+
+  constructor(crowi) {
+    this.crowi = crowi;
+  }
+
+  configure(md) {
+    md.use(require('markdown-it-footnote'));
+  }
+
+}

+ 13 - 0
resource/js/util/markdown-it/task-lists.js

@@ -0,0 +1,13 @@
+export default class TaskListsConfigurer {
+
+  constructor(crowi) {
+    this.crowi = crowi;
+  }
+
+  configure(md) {
+    md.use(require('markdown-it-task-lists'), {
+      enabled: true,
+    });
+  }
+
+}

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

@@ -317,6 +317,13 @@ body.on-edit {
     &:before {
       border-right-color: darken($bodycolor, 4%);
     }
+    .nav.nav-tabs {
+      > li.active > a {
+        background: transparent;
+        border-bottom: solid 1px darken($bodycolor, 4%);
+        border-bottom-color: darken($bodycolor, 4%);
+      }
+    }
   }
 }
 

+ 3 - 0
resource/styles/scss/_comment_growi.scss

@@ -94,6 +94,9 @@
     .comment-write {
       margin-bottom: 0.5em;
     }
+    .tab-content{
+      padding-top: 10px;
+    }
     .comment-form-comment {
       height: 80px;
       &:focus, &:not(:invalid) {

+ 55 - 0
resource/styles/scss/_wiki.scss

@@ -143,6 +143,61 @@ div.body {
       opacity: 1 !important;
     }
   }
+
+  $ratio: 0.95;
+  &.comment {
+    line-height: 1.5em;
+    font-size: 14px;
+
+    h1, h2, h3, h4, h5, h6 {
+      margin-top: 1.6em * $ratio;
+      margin-bottom: .8em * $ratio;
+    }
+
+    h1 {
+      margin-top: 2em * $ratio;
+      padding-bottom: 0.3em * $ratio;
+      font-size: 1.8em * $ratio;
+      line-height: 1.1em * $ratio;
+    }
+    h2 {
+      padding-bottom: 0.5em * $ratio;
+      font-size: 1.4em * $ratio;
+      line-height: 1.225 * $ratio;
+    }
+    h3 {
+      font-size: 1.2em * $ratio;
+    }
+
+    blockquote {
+      font-size: .9em * $ratio;
+    }
+
+    img.emojione {
+      margin-top: -0.3em * $ratio !important;
+    }
+
+    ul, ol {
+      li {
+        line-height: 1.8em * $ratio;
+      }
+    }
+
+    // borrowed from https://www.npmjs.com/package/github-markdown-css
+    .contains-task-list {
+      .task-list-item input {
+        margin: 0 0.2em * $ratio 0.25em * $ratio -1.6em * $ratio;
+      }
+    }
+
+    .revision-head {
+      .revision-head-link,
+      .revision-head-edit-button {
+        margin-left: 0.5em * $ratio;
+        font-size: 0.6em * $ratio;
+      }
+    }
+  }
 }
 
 // mobile

+ 5 - 1
yarn.lock

@@ -2238,10 +2238,14 @@ diff@3.3.1:
   version "3.3.1"
   resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75"
 
-diff@^3.1.0, diff@^3.3.0, diff@^3.3.1:
+diff@^3.1.0, diff@^3.3.1:
   version "3.4.0"
   resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c"
 
+diff@^3.5.0:
+  version "3.5.0"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
+
 diffie-hellman@^5.0.0:
   version "5.0.2"
   resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e"