Răsfoiți Sursa

Merge pull request #428 from weseek/master

release v3.1.0
Yuki Takei 8 ani în urmă
părinte
comite
0671b6a36d
81 a modificat fișierele cu 2124 adăugiri și 2418 ștergeri
  1. 0 1
      .gitignore
  2. 4 0
      .vscode/settings.json
  3. 24 1
      CHANGES.md
  4. 10 0
      app.js
  5. 4 0
      app.json
  6. 18 4
      config/webpack.common.js
  7. 7 3
      lib/crowi/dev.js
  8. 37 44
      lib/crowi/index.js
  9. 1 1
      lib/form/admin/securityPassportLdap.js
  10. 1 0
      lib/form/revision.js
  11. 7 5
      lib/locales/en-US/translation.json
  12. 2 0
      lib/locales/index.js
  13. 7 5
      lib/locales/ja/translation.json
  14. 1 0
      lib/models/config.js
  15. 21 52
      lib/models/page-group-relation.js
  16. 30 29
      lib/models/page.js
  17. 0 12
      lib/models/user-group-relation.js
  18. 4 3
      lib/routes/index.js
  19. 12 6
      lib/routes/login-passport.js
  20. 14 2
      lib/routes/me.js
  21. 63 51
      lib/routes/page.js
  22. 8 0
      lib/service/passport.js
  23. 31 5
      lib/util/interceptor-manager.js
  24. 5 3
      lib/util/swigFunctions.js
  25. 5 16
      lib/views/_form.html
  26. 13 5
      lib/views/admin/customize.html
  27. 4 4
      lib/views/admin/markdown.html
  28. 9 5
      lib/views/admin/widget/passport/ldap.html
  29. 9 0
      lib/views/layout-growi/not_found.html
  30. 2 0
      lib/views/layout/layout.html
  31. 9 3
      lib/views/widget/page_alerts.html
  32. 1 1
      lib/views/widget/page_list.html
  33. 10 2
      lib/views/widget/page_tabs.html
  34. 34 3
      lib/views/widget/passport/ldap-association-tester.html
  35. 69 64
      package.json
  36. 40 2
      resource/js/app.js
  37. 5 3
      resource/js/components/Page.js
  38. 1 0
      resource/js/components/PageEditor.js
  39. 104 0
      resource/js/components/PageEditor/AbstractEditor.js
  40. 438 0
      resource/js/components/PageEditor/CodeMirrorEditor.js
  41. 76 349
      resource/js/components/PageEditor/Editor.js
  42. 270 0
      resource/js/components/PageEditor/GrantSelector.js
  43. 24 53
      resource/js/components/PageEditor/MarkdownListUtil.js
  44. 13 6
      resource/js/components/PageEditor/MarkdownTableInterceptor.js
  45. 5 13
      resource/js/components/PageEditor/MarkdownTableUtil.js
  46. 5 4
      resource/js/components/PageEditor/PreventMarkdownListInterceptor.js
  47. 224 0
      resource/js/components/PageEditor/TextAreaEditor.js
  48. 44 0
      resource/js/i18n.js
  49. 2 2
      resource/js/legacy/crowi.js
  50. 6 4
      resource/js/util/Crowi.js
  51. 7 4
      resource/js/util/GrowiRenderer.js
  52. 36 12
      resource/js/util/interceptor/detach-code-blocks.js
  53. 12 6
      resource/styles/agile-admin/inverse/colors/_apply-colors-dark.scss
  54. 12 0
      resource/styles/agile-admin/inverse/colors/_apply-colors-light.scss
  55. 10 1
      resource/styles/agile-admin/inverse/colors/_apply-colors.scss
  56. 0 152
      resource/styles/agile-admin/inverse/colors/blue-dark.scss
  57. 0 138
      resource/styles/agile-admin/inverse/colors/blue.scss
  58. 1 0
      resource/styles/agile-admin/inverse/colors/default-dark.scss
  59. 36 0
      resource/styles/agile-admin/inverse/colors/future.scss
  60. 0 148
      resource/styles/agile-admin/inverse/colors/gray-dark.scss
  61. 0 138
      resource/styles/agile-admin/inverse/colors/gray.scss
  62. 0 149
      resource/styles/agile-admin/inverse/colors/green-dark.scss
  63. 0 139
      resource/styles/agile-admin/inverse/colors/green.scss
  64. 0 150
      resource/styles/agile-admin/inverse/colors/megna-dark.scss
  65. 0 139
      resource/styles/agile-admin/inverse/colors/megna.scss
  66. 2 16
      resource/styles/agile-admin/inverse/colors/mono-blue.scss
  67. 0 153
      resource/styles/agile-admin/inverse/colors/purple-dark.scss
  68. 0 139
      resource/styles/agile-admin/inverse/colors/purple.scss
  69. 2 0
      resource/styles/agile-admin/inverse/variables.scss
  70. 0 5
      resource/styles/scss/_admin.scss
  71. 1 0
      resource/styles/scss/_comment_growi.scss
  72. 38 60
      resource/styles/scss/_on-edit.scss
  73. 9 0
      resource/styles/scss/_override-hljs.scss
  74. 1 1
      resource/styles/scss/_variables.scss
  75. 11 8
      resource/styles/scss/_wiki.scss
  76. 3 0
      resource/styles/scss/style.scss
  77. 8 0
      resource/styles/scss/theme/future.scss
  78. 78 0
      test/models/page.test.js
  79. 0 15
      webpack.config.js
  80. 11 6
      wercker.yml
  81. 113 73
      yarn.lock

+ 0 - 1
.gitignore

@@ -35,4 +35,3 @@ package-lock.json
 # IDE #
 .idea
 .vscode
-

+ 4 - 0
.vscode/settings.json

@@ -0,0 +1,4 @@
+{
+  // 既定の改行文字。LF の場合には \n を CRLF の場合には \r\n を使用してください。
+  "files.eol": "\n",
+}

+ 24 - 1
CHANGES.md

@@ -1,16 +1,39 @@
 CHANGES
 ========
 
-## 3.0.13-RC
+## 3.1.0-RC
+
+* Improvement: Group Access Control List - Select group modal
+* Improvement: Better input on mobile
+* Improvement: Add 'future' theme
+* Improvement: Detach code blocks correctly
+* Improvement: Auto-format markdown table which includes multibyte text
+* Improvement: Show icon when auto-format markdown table is activated
+* Improvement: Enable to switch show/hide border for highlight.js
+* Improvement: BindDN field allows also ActiveDirectory styles 
+* Improvement: Show LDAP logs when testing login
+* Fix: Comment body doesn't break long terms
+* Fix: lsx plugin lists up pages that hit by forward match wrongly
+    * Introduced by 3.0.4
+* Fix: Editor is broken on IE11
+* Support: Multilingualize React components with i18next
+* Support: Organize dependencies
+* Support: Upgrade libs
+    * elasticsearch
+    * googleapis
+
+## 3.0.13
 
 * Improvement: Add Vim/Emacs/Sublime-Text icons for keybindings menu
 * Improvement: Add 'mono-blue' theme
 * Fix: Unportalize process failed silently
+* Fix: Sidebar breaks editor layouts
 * Support: Switch the logger from 'pino' to 'bunyan'
 * Support: Set the alias for 'debug' to the debug function of 'bunyan'
 * Support: Translate /admin/security
 * Support: Optimize bundles
     * upgrade 'markdown-it-toc-and-anchor-with-slugid' and omit 'uslug'
+* Support: Optimize .eslintrc.js
 
 ## 3.0.12
 

+ 10 - 0
app.js

@@ -8,6 +8,7 @@
 require('module-alias/register');
 
 const logger = require('@alias/logger')('growi');
+const helpers = require('./config/helpers');
 const growi = new (require('./lib/crowi'))(__dirname, process.env);
 
 
@@ -23,7 +24,16 @@ process.on('unhandledRejection', (reason, p) => {
 });
 
 growi.start()
+  .then(express => {
+    if (helpers.hasProcessFlag('ci')) {
+      logger.info('"--ci" flag is detected. Exit process.');
+      express.close(() => {
+        process.exit();
+      });
+    }
+  })
   .catch(err => {
     logger.error('An error occurred, unable to start the server');
     logger.error(err);
+    process.exit(1);
   });

+ 4 - 0
app.json

@@ -8,6 +8,10 @@
   "repository": "https://github.com/weseek/growi",
   "success_url": "/",
   "env": {
+    "NODE_ENV": {
+      "description": "DO NOT CHANGE - 'yarn' needs this to install devDependencies",
+      "value": "development"
+    },
     "SECRET_TOKEN": {
       "description": "A secret key for verifying the integrity of signed cookies.",
       "generator": "secret"

+ 18 - 4
config/webpack.common.js

@@ -3,7 +3,6 @@
  */
 
 const webpack = require('webpack');
-const path = require('path');
 const helpers = require('./helpers');
 
 /*
@@ -31,6 +30,7 @@ module.exports = function(options) {
       'style-theme-default-dark':  './resource/styles/scss/theme/default-dark.scss',
       'style-theme-nature':   './resource/styles/scss/theme/nature.scss',
       'style-theme-mono-blue':   './resource/styles/scss/theme/mono-blue.scss',
+      'style-theme-future': './resource/styles/scss/theme/future.scss',
       'style-presentation':   './resource/styles/scss/style-presentation.scss',
     },
     externals: {
@@ -44,8 +44,9 @@ module.exports = function(options) {
       extensions: ['.js', '.json'],
       modules: [helpers.root('src'), helpers.root('node_modules')],
       alias: {
-        '@root': path.resolve(__dirname, '../'),
-        '@alias/logger': path.resolve(__dirname, '../lib/service/logger'),
+        '@root': helpers.root('/'),
+        '@alias/logger': helpers.root('lib/service/logger'),
+        '@alias/locales': helpers.root('lib/locales'),
         // replace bunyan
         'bunyan': 'browser-bunyan',
       }
@@ -54,7 +55,13 @@ module.exports = function(options) {
       rules: [
         {
           test: /.jsx?$/,
-          exclude: /node_modules/,
+          exclude: {
+            test:    helpers.root('node_modules'),
+            exclude: [  // include as a result
+              helpers.root('node_modules/string-width'),
+              helpers.root('node_modules/is-fullwidth-code-point'), // depends from string-width
+            ]
+          },
           use: [{
             loader: 'babel-loader?cacheDirectory',
             options: {
@@ -62,6 +69,13 @@ module.exports = function(options) {
             }
           }]
         },
+        {
+          test: /locales/,
+          loader: '@alienfast/i18next-loader',
+          options: {
+            basenameAsNamespace: true,
+          }
+        },
         {
           test: /\.css$/,
           use: ['style-loader', 'css-loader'],

+ 7 - 3
lib/crowi/dev.js

@@ -41,9 +41,13 @@ class CrowiDev {
    */
   requireForAutoReloadServer() {
     // load all json files for live reloading
-    fs.readdirSync(this.crowi.localeDir).map((dirname) => {
-      require(path.join(this.crowi.localeDir, dirname, 'translation.json'));
-    });
+    fs.readdirSync(this.crowi.localeDir)
+      .filter(filename => {
+        return fs.statSync(path.join(this.crowi.localeDir, filename)).isDirectory();
+      })
+      .map((dirname) => {
+        require(path.join(this.crowi.localeDir, dirname, 'translation.json'));
+      });
   }
 
   /**

+ 37 - 44
lib/crowi/index.js

@@ -9,16 +9,11 @@ var debug = require('debug')('growi:crowi')
 
   , mongoose    = require('mongoose')
 
-  , eazyLogger = require('eazy-logger')
   , models = require('../models')
   ;
 
 function Crowi(rootdir, env) {
   var self = this;
-  this.logger = eazyLogger.Logger({
-    prefix: '[{green:GROWI}] ',
-    useLevelPrefixes: false,
-  });
 
   this.version = pkg.version;
   this.runtimeVersions = undefined;   // initialized by scanRuntimeVersions()
@@ -144,13 +139,7 @@ Crowi.prototype.setupDatabase = function() {
 
   const mongoUri = getMongoUrl(this.env);
 
-  return mongoose.connect(mongoUri).then(
-    () => {},
-    err => {
-      logger.error('DB Connect Error: ', err);
-      logger.error('DB Connect Error: ', mongoUri);
-    }
-  );
+  return mongoose.connect(mongoUri);
 };
 
 Crowi.prototype.setupSessionConfig = function() {
@@ -346,7 +335,6 @@ Crowi.prototype.getTokens = function() {
 
 Crowi.prototype.start = function() {
   var self = this
-    , http = require('http')
     , server
     , io;
 
@@ -364,40 +352,45 @@ Crowi.prototype.start = function() {
     .then(function() {
       return self.buildServer();
     })
-    .then(function(app) {
-      server = http.createServer(app).listen(self.port, function() {
-        logger.info(`[${self.node_env}] Express server listening on port ${self.port}`);
-
-        self.logger.info('{bold:Server URLs:}');
-        self.logger.unprefixed('info', '{grey:=======================================}');
-        self.logger.unprefixed('info', `         APP: {magenta:http://localhost:${self.port}}`);
-        self.logger.unprefixed('info', '{grey:=======================================}');
-
-        // setup for dev
-        if (self.node_env === 'development') {
-          self.crowiDev.setup(server, app);
-        }
-      });
-
-      io = require('socket.io')(server);
-      io.sockets.on('connection', function(socket) {
+    .then(function(express) {
+      return new Promise((resolve) => {
+        server = express.listen(self.port, function() {
+          logger.info(`[${self.node_env}] Express server listening on port ${self.port}`);
+
+          // setup for dev
+          if (self.node_env === 'development') {
+            const eazyLogger = require('eazy-logger').Logger({
+              prefix: '[{green:GROWI}] ',
+              useLevelPrefixes: false,
+            });
+
+            eazyLogger.info('{bold:Server URLs:}');
+            eazyLogger.unprefixed('info', '{grey:=======================================}');
+            eazyLogger.unprefixed('info', `         APP: {magenta:http://localhost:${self.port}}`);
+            eazyLogger.unprefixed('info', '{grey:=======================================}');
+
+            self.crowiDev.setup(server, express);
+          }
+          resolve(server);
+        });
+
+        io = require('socket.io')(server);
+        io.sockets.on('connection', function(socket) {
+        });
+        self.io = io;
+
+        // setup Express Routes
+        self.setupRoutesAtLast(express);
       });
-      self.io = io;
-
-      // setup Express Routes
-      self.setupRoutesAtLast(app);
-
-      return app;
     });
 };
 
 Crowi.prototype.buildServer = function() {
-  var express  = require('express')
-    , app = express()
+  var express = require('express')()
     , env = this.node_env
     ;
 
-  require('./express-init')(this, app);
+  require('./express-init')(this, express);
 
   // import plugins
   var Config = this.model('Config');
@@ -405,11 +398,11 @@ Crowi.prototype.buildServer = function() {
   if (isEnabledPlugins) {
     debug('Plugins are enabled');
     var PluginService = require('../plugins/plugin.service');
-    var pluginService = new PluginService(this, app);
+    var pluginService = new PluginService(this, express);
     pluginService.autoDetectAndLoadPlugins();
 
     if (env == 'development') {
-      this.crowiDev.loadPlugins(app);
+      this.crowiDev.loadPlugins(express);
     }
   }
 
@@ -417,7 +410,7 @@ Crowi.prototype.buildServer = function() {
   if (env == 'production') {
     const expressBunyanLogger = require('express-bunyan-logger');
     const logger = require('@alias/logger')('express');
-    app.use(expressBunyanLogger({
+    express.use(expressBunyanLogger({
       logger,
       excludes: ['*'],
     }));
@@ -425,10 +418,10 @@ Crowi.prototype.buildServer = function() {
   // use morgan
   else {
     const morgan = require('morgan');
-    app.use(morgan('dev'));
+    express.use(morgan('dev'));
   }
 
-  return Promise.resolve(app);
+  return Promise.resolve(express);
 };
 
 /**

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

@@ -12,7 +12,7 @@ module.exports = form(
   field('settingForm[security:passport-ldap:isUserBind]').trim().toBooleanStrict(),
   field('settingForm[security:passport-ldap:bindDN]').trim()
       // https://regex101.com/r/jK8lpO/1
-      .is(/^(,?[^,=\s]+=[^,=\s]+){1,}$/, 'Bind DN is invalid. <small><a href="https://regex101.com/r/jK8lpO/1">&gt;&gt; Regex</a></small>'),
+      .is(/^(,?[^,=\s]+=[^,=\s]+){1,}$|^[^@\s]+@[^@\s]+$/, 'Bind DN is invalid. <small><a href="https://regex101.com/r/jK8lpO/3">&gt;&gt; Regex</a></small>'),
   field('settingForm[security:passport-ldap:bindDNPassword]'),
   field('settingForm[security:passport-ldap:searchFilter]'),
   field('settingForm[security:passport-ldap:attrMapUsername]'),

+ 1 - 0
lib/form/revision.js

@@ -8,5 +8,6 @@ module.exports = form(
   field('pageForm.body').required().custom(function(value) { return value.replace(/\r/g, '\n') }),
   field('pageForm.currentRevision'),
   field('pageForm.grant').toInt().required(),
+  field('pageForm.grantUserGroupId'),
   field('pageForm.notify')
 );

+ 7 - 5
lib/locales/en-US/translation.json

@@ -86,6 +86,7 @@
   "Specified users only": "Specified users only",
   "Just me": "Just me",
   "Only inside the group": "Only inside the group",
+  "Reselect the group": "Reselect the group",
   "Shareable link": "Shareable link",
 
   "Show latest": "Show latest",
@@ -326,7 +327,8 @@
       "search_filter_detail1": "The query used to locate the authenticated user.",
       "search_filter_detail2": "Use <code>&#123;&#123;username&#125;&#125;</code> to reference the username entered in the login page.",
       "search_filter_detail3": "If empty, the filter <code>(uid=&#123;&#123;username&#125;&#125;)</code> is used.",
-      "search_filter_example": "Example to match with 'uid' or 'mail'",
+      "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",
       "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>.",
@@ -352,10 +354,10 @@
 
   "markdown_setting": {
     "markdown_rendering": "You can change Markdown rendering settings.",
-    "validate Line Break": "Validate Line Break",
-    "treat_text": "Treat line breaking in the text page as <code>&lt;br&gt;</code> in HTML",
-    "validate_comment": "Validate Line Break in the comment section",
-    "treat_comment": "Treat line breaking in the comment section as <code>&lt;br&gt;</code> in HTML",
+    "Enable Line Break": "Enable Line Break",
+    "Enable Line Break desc": "Treat line break in the text page as <code>&lt;br&gt;</code> in HTML",
+    "Enable Line Break for comment": "Enable Line Break in comment",
+    "Enable Line Break for comment desc": "Treat line break in comment as <code>&lt;br&gt;</code> in HTML",
     "TBD": "(TBD: Markdown function in the comment section has not been implemented yet)"
   },
 

+ 2 - 0
lib/locales/index.js

@@ -0,0 +1,2 @@
+// !!DO NOT REMOVE THIS FILE!!
+// entry point for @alienfast/i18next-loader

+ 7 - 5
lib/locales/ja/translation.json

@@ -100,6 +100,7 @@
   "Specified users": "特定ユーザーのみ",
   "Just me": "自分のみ",
   "Only inside the group": "特定グループのみ",
+  "Reselect the group": "グループの再選択",
   "Shareable link": "このページの共有用URL",
   "The whitelist of registration permission E-mail address": "登録許可メールアドレスの<br>ホワイトリスト",
   "Selecting authentication mechanism": "認証機構選択",
@@ -345,7 +346,8 @@
       "search_filter_detail1": "認証されるユーザーを一意に決定するための LDAP フィルタ",
       "search_filter_detail2": "ログイン時のユーザー名を使用するには <code>&#123;&#123;username&#125;&#125;</code> の形式を使用してください。",
       "search_filter_detail3": "空欄の場合 <code>(uid=&#123;&#123;username&#125;&#125;)</code> が使用されます。",
-      "search_filter_example": "'uid' または 'mail' に一致させる場合の例",
+      "search_filter_example1": "'uid' または 'mail' に一致",
+      "search_filter_example2": "'sAMAccountName' に一致 (Active Directory)",
       "username_detail": "新規ユーザーの関連付けを設定",
       "Treat username matching as identical": "新規ログイン時、<code>username</code> が一致したローカルアカウントが存在した場合は自動的に紐付ける",
       "Treat username matching as identical_warn": "WARNING: <code>username</code> の一致を以て同一ユーザーであるとみなすので、セキュリティに注意してください",
@@ -369,10 +371,10 @@
   },
   "markdown_setting": {
     "markdown_rendering": "Markdownレンダリングの設定を変更できます。",
-    "validate Line Break": "Line Break を有効にする",
-    "treat_text": "ページテキスト中の改行を、HTML内で<code>&lt;br&gt;</code>として扱います",
-    "validate_comment": "コメント欄で Line Break を有効にする",
-    "treat_comment": "コメント中の改行を、HTML内で<code>&lt;br&gt;</code>として扱います",
+    "Enable Line Break": "Line Break を有効にする",
+    "Enable Line Break desc": "ページテキスト中の改行を、HTML内で<code>&lt;br&gt;</code>として扱います",
+    "Enable Line Break for comment": "コメント欄で Line Break を有効にする",
+    "Enable Line Break for comment desc": "コメント中の改行を、HTML内で<code>&lt;br&gt;</code>として扱います",
     "TBD": "(TBD: コメント欄の Markdown 化は未だ実装されていません)"
 
   },

+ 1 - 0
lib/models/config.js

@@ -460,6 +460,7 @@ module.exports = function(crowi) {
       behaviorType: Config.behaviorType(config),
       layoutType: Config.layoutType(config),
       isEnabledLineBreaks: Config.isEnabledLinebreaks(config),
+      highlightJsStyleBorder: Config.highlightJsStyleBorder(config),
       isSavedStatesOfTabChanges: Config.isSavedStatesOfTabChanges(config),
       env: {
         PLANTUML_URI: env.PLANTUML_URI || null,

+ 21 - 52
lib/models/page-group-relation.js

@@ -88,21 +88,18 @@ class PageGroupRelation {
    * @returns {Promise<any>} mongoose-paginate result object
    * @memberof UserGroupRelation
    */
-  static findPageGroupRelationsWithPagination(userGroup, opts) {
-    const query = { relatedGroup: userGroup };
-    const options = Object.assign({}, opts);
-    if (options.page == null) {
-      options.page = 1;
-    }
-    if (options.limit == null) {
-      options.limit = UserGroupRelation.PAGE_ITEMS;
-    }
-
-    return this.paginate(query, options)
-      .catch((err) => {
-        debug('Error on pagination:', err);
-      });
-  }
+  // static findPageGroupRelationsWithPagination(userGroup, opts) {
+  //   const query = { relatedGroup: userGroup };
+  //   const options = Object.assign({}, opts);
+  //   if (options.page == null) {
+  //     options.page = 1;
+  //   }
+  //   if (options.limit == null) {
+  //     options.limit = UserGroupRelation.PAGE_ITEMS;
+  //   }
+
+  //   return this.paginate(query, options);
+  // }
 
   /**
    * find the relation or create(if not exists) for page and group
@@ -126,10 +123,6 @@ class PageGroupRelation {
         else {
           return this.createRelation(userGroup, page);
         }
-      })
-      .catch((err) => {
-        debug('An Error occured.', err);
-        return reject(err);
       });
   }
 
@@ -143,8 +136,11 @@ class PageGroupRelation {
    */
   static findByPage(page) {
 
+    if (page == null) {
+      return null;
+    }
     return this
-      .find({ targetPage: page.id })
+      .findOne({ targetPage: page.id })
       .populate('relatedGroup')
       .exec();
   }
@@ -162,25 +158,8 @@ class PageGroupRelation {
     var UserGroupRelation = PageGroupRelation.crowi.model('UserGroupRelation');
 
     return this.findByPage(pageData)
-      .then((pageRelations) => {
-        return pageRelations.map((pageRelation) => {
-          return UserGroupRelation.isRelatedUserForGroup(userData, pageRelation.relatedGroup);
-        });
-      })
-      .then((checkPromises) => {
-        return Promise.all(checkPromises);
-      })
-      .then((checkResults) => {
-        var checkResult = false;
-        checkResults.map((result) => {
-          if (result) {
-            checkResult = true;
-          }
-        });
-        return checkResult;
-      })
-      .catch((err) => {
-        return reject(err);
+      .then(pageRelation => {
+        return UserGroupRelation.isRelatedUserForGroup(userData, pageRelation.relatedGroup);
       });
   }
 
@@ -234,15 +213,9 @@ class PageGroupRelation {
   static removeAllByPage(page) {
 
     return this.findByPage(page)
-      .then((relations) => {
-        debug('remove relations are ', relations);
-        if (relations == null) {
-          return;
-        }
-        else {
-          relations.map((relation) => {
-            relation.remove();
-          });
+      .then(relation => {
+        if (relation != null) {
+          relation.remove();
         }
       });
   }
@@ -265,10 +238,6 @@ class PageGroupRelation {
         else {
           relationData.remove();
         }
-      })
-      .catch((err) => {
-        debug('Error on find a removing page-group-relation', err);
-        return reject(err);
       });
   }
 }

+ 30 - 29
lib/models/page.js

@@ -507,8 +507,8 @@ module.exports = function(crowi) {
 
         if (!pageData.isGrantedFor(userData)) {
           PageGroupRelation.isExistsGrantedGroupForPageAndUser(pageData, userData)
-            .then(function(checkResult) {
-              if (!checkResult) {
+            .then(isExists => {
+              if (!isExists) {
                 return reject(new Error('Page is not granted for the user')); //PAGE_GRANT_ERROR, null);
               }
               else {
@@ -713,6 +713,7 @@ module.exports = function(crowi) {
 
     // ignore other pages than descendants
     path = Page.addSlashOfEnd(path);
+
     // add option to escape the regex strings
     const combinedOption = Object.assign({isRegExpEscapedFromPath: true}, option);
 
@@ -722,8 +723,12 @@ module.exports = function(crowi) {
   /**
    * generate the query to find pages that start with `path`
    *
-   * If `path` has `/` at the end, returns '{path}/*' and '{path}' self.
-   * If `path` doesn't have `/` at the end, returns '{path}*'
+   * (GROWI) If 'isRegExpEscapedFromPath' is true, `path` should have `/` at the end
+   *   -> returns '{path}/*' and '{path}' self.
+   * (Crowi) If 'isRegExpEscapedFromPath' is false and `path` has `/` at the end
+   *   -> returns '{path}*'
+   * (Crowi) If 'isRegExpEscapedFromPath' is false and `path` doesn't have `/` at the end
+   *   -> returns '{path}*'
    *
    * *option*
    *   - includeDeletedPage -- if true, search deleted pages (default: false)
@@ -735,17 +740,22 @@ module.exports = function(crowi) {
     var includeDeletedPage = option.includeDeletedPage || false;
     var isRegExpEscapedFromPath = option.isRegExpEscapedFromPath || false;
 
+    /*
+     * 1. add condition for finding the page completely match with `path` w/o last slash
+     */
     let pathSlashOmitted = path;
     if (path.match(/\/$/)) {
-      // add condition for finding the page completely match with `path`
       pathSlashOmitted = path.substr(0, path.length -1);
       pathCondition.push({path: pathSlashOmitted});
     }
 
-    // create forward match pattern
+    /*
+     * 2. add decendants
+     */
     var pattern = (isRegExpEscapedFromPath)
-      ? escapeStringRegexp(pathSlashOmitted)  // escape
+      ? escapeStringRegexp(path)  // escape
       : pathSlashOmitted;
+
     var queryReg = new RegExp('^' + pattern);
     pathCondition.push({path: queryReg});
 
@@ -791,10 +801,11 @@ module.exports = function(crowi) {
   pageSchema.statics.updateGrant = function(page, grant, userData, grantUserGroupId) {
     var Page = this;
 
-    if (grant == GRANT_USER_GROUP && grantUserGroupId == null) {
-      throw new Error('grant userGroupId is not specified');
-    }
     return new Promise(function(resolve, reject) {
+      if (grant == GRANT_USER_GROUP && grantUserGroupId == null) {
+        reject('grant userGroupId is not specified');
+      }
+
       page.grant = grant;
       if (grant == GRANT_PUBLIC || grant == GRANT_USER_GROUP) {
         page.grantedUsers = [];
@@ -828,12 +839,12 @@ module.exports = function(crowi) {
       return UserGroupRelation.findByGroupIdAndUser(grantUserGroupId, userData)
       .then((relation) => {
         if (relation == null) {
-          return reject(new Error('no relations were exist for group and user.'));
+          return new Error('no relations were exist for group and user.');
         }
         return PageGroupRelation.findOrCreateRelationForPageAndGroup(page, relation.relatedGroup);
       })
       .catch((err) => {
-        return reject(new Error('No UserGroup is exists. userGroupId : ', grantUserGroupId));
+        return new Error('No UserGroup is exists. userGroupId : ', grantUserGroupId);
       });
     }
     else {
@@ -957,25 +968,15 @@ module.exports = function(crowi) {
     // update existing page
     var newRevision = Revision.prepareRevision(pageData, body, user);
 
-    return new Promise(function(resolve, reject) {
-      Page.pushRevision(pageData, newRevision, user)
+    return Page.pushRevision(pageData, newRevision, user)
       .then(function(revision) {
-        if (grant != pageData.grant) {
-          return Page.updateGrant(pageData, grant, user, grantUserGroupId).then(function(data) {
-            debug('Page grant update:', data);
-            resolve(data);
-            pageEvent.emit('update', data, user);
-          });
-        }
-        else {
-          resolve(pageData);
-          pageEvent.emit('update', pageData, user);
-        }
-      }).catch(function(err) {
-        debug('Error on update', err);
-        debug('Error on update', err.stack);
+        return Page.updateGrant(pageData, grant, user, grantUserGroupId);
+      })
+      .then(function(data) {
+        debug('Page grant update:', data);
+        pageEvent.emit('update', data, user);
+        return data;
       });
-    });
   };
 
   pageSchema.statics.deletePage = function(pageData, user, options) {

+ 0 - 12
lib/models/user-group-relation.js

@@ -66,8 +66,6 @@ class UserGroupRelation {
    */
   static findAllRelationForUserGroup(userGroup) {
     debug('findAllRelationForUserGroup is called', userGroup);
-    var UserGroupRelation = this;
-
     return this
       .find({ relatedGroup: userGroup })
       .populate('relatedUser')
@@ -83,7 +81,6 @@ class UserGroupRelation {
    * @memberof UserGroupRelation
    */
   static findAllRelationForUserGroups(userGroups) {
-
     return this
       .find({ relatedGroup: { $in: userGroups } })
       .populate('relatedUser')
@@ -99,7 +96,6 @@ class UserGroupRelation {
    * @memberof UserGroupRelation
    */
   static findAllRelationForUser(user) {
-
     return this
       .find({ relatedUser: user.id })
       .populate('relatedGroup')
@@ -199,10 +195,6 @@ class UserGroupRelation {
       .then((count) => {
         // return true or false of the relation is exists(not count)
         return (0 < count);
-      })
-      .catch((err) => {
-        debug('An Error occured.', err);
-        reject(err);
       });
   }
 
@@ -263,10 +255,6 @@ class UserGroupRelation {
         else {
           relationData.remove();
         }
-      })
-      .catch((err) => {
-        debug('Error on find a removing user-group-relation', err);
-        reject(err);
       });
   }
 

+ 4 - 3
lib/routes/index.js

@@ -148,9 +148,10 @@ module.exports = function(crowi, app) {
   app.get( '/_search'                 , loginRequired(crowi, app, false) , search.searchPage);
   app.get( '/_api/search'             , accessTokenParser , loginRequired(crowi, app, false) , search.api.search);
 
-  app.get( '/_api/check_username'     , user.api.checkUsername);
-  app.post('/_api/me/picture/upload'  , loginRequired(crowi, app) , uploads.single('userPicture'), me.api.uploadPicture);
-  app.get( '/_api/user/bookmarks'     , loginRequired(crowi, app, false) , user.api.bookmarks);
+  app.get( '/_api/check_username'           , user.api.checkUsername);
+  app.post('/_api/me/picture/upload'        , loginRequired(crowi, app) , uploads.single('userPicture'), me.api.uploadPicture);
+  app.get( '/_api/me/user-group-relations'  , accessTokenParser , loginRequired(crowi, app) , me.api.userGroupRelations);
+  app.get( '/_api/user/bookmarks'           , loginRequired(crowi, app, false) , user.api.bookmarks);
 
   // HTTP RPC Styled API (に徐々に移行していいこうと思う)
   app.get('/_api/users.list'          , accessTokenParser , loginRequired(crowi, app, false) , user.api.list);

+ 12 - 6
lib/routes/login-passport.js

@@ -2,6 +2,7 @@ module.exports = function(crowi, app) {
   'use strict';
 
   var debug = require('debug')('growi:routes:login-passport')
+    , logger = require('@alias/logger')('growi:routes:login-passport')
     , passport = require('passport')
     , config = crowi.getConfig()
     , Config = crowi.model('Config')
@@ -18,7 +19,7 @@ module.exports = function(crowi, app) {
     // update lastLoginAt
     user.updateLastLoginAt(new Date(), (err, userData) => {
       if (err) {
-        console.log(`updateLastLoginAt dumps error: ${err}`);
+        logger.error(`updateLastLoginAt dumps error: ${err}`);
         debug(`updateLastLoginAt dumps error: ${err}`);
       }
     });
@@ -71,8 +72,6 @@ module.exports = function(crowi, app) {
       return next();
     }
 
-    const loginForm = req.body.loginForm;
-
     if (!req.form.isValid) {
       debug('invalid form');
       return res.render('login', {
@@ -89,7 +88,7 @@ module.exports = function(crowi, app) {
       debug('info', info);
 
       if (err) {  // DB Error
-        console.log('LDAP Server Error: ', err);
+        logger.error('LDAP Server Error: ', err);
         req.flash('warningMessage', 'LDAP Server Error occured.');
         return next(); // pass and the flash message is displayed when all of authentications are failed.
       }
@@ -171,16 +170,19 @@ module.exports = function(crowi, app) {
       }
 
       if (err) {  // DB Error
-        console.log('LDAP Server Error: ', err);
+        logger.error('LDAP Server Error: ', err);
         return res.json({
           status: 'warning',
           message: 'LDAP Server Error occured.',
+          err
         });
       }
       if (info && info.message) {
         return res.json({
           status: 'warning',
           message: info.message,
+          ldapConfiguration: req.ldapConfiguration,
+          ldapAccountInfo: req.ldapAccountInfo,
         });
       }
       if (user) {
@@ -189,11 +191,15 @@ module.exports = function(crowi, app) {
           return res.json({
             status: 'warning',
             message: 'The user is found, but that has no groups.',
+            ldapConfiguration: req.ldapConfiguration,
+            ldapAccountInfo: req.ldapAccountInfo,
           });
         }
         return res.json({
           status: 'success',
           message: 'Successfully authenticated.',
+          ldapConfiguration: req.ldapConfiguration,
+          ldapAccountInfo: req.ldapAccountInfo,
         });
       }
     })(req, res, () => {});
@@ -217,7 +223,7 @@ module.exports = function(crowi, app) {
       debug('info', info);
 
       if (err) {  // DB Error
-        console.log('Database Server Error: ', err);
+        logger.error('Database Server Error: ', err);
         req.flash('warningMessage', 'Database Server Error occured.');
         return next(); // pass and the flash message is displayed when all of authentications are failed.
       }

+ 14 - 2
lib/routes/me.js

@@ -5,10 +5,10 @@ module.exports = function(crowi, app) {
     , fs = require('fs')
     , models = crowi.models
     , config = crowi.getConfig()
-    , Page = models.Page
     , User = models.User
+    , UserGroupRelation = models.UserGroupRelation
     , ExternalAccount = models.ExternalAccount
-    , Revision = models.Revision
+    , ApiResponse = require('../util/apiResponse')
     //, pluginService = require('../service/plugin')
     , actions = {}
     , api = {}
@@ -76,6 +76,18 @@ module.exports = function(crowi, app) {
     });
   };
 
+  /**
+   * retrieve user-group-relation documents
+   * @param {object} req
+   * @param {object} res
+   */
+  api.userGroupRelations = function(req, res) {
+    UserGroupRelation.findAllRelationForUser(req.user)
+      .then(userGroupRelations => {
+        return res.json(ApiResponse.success({userGroupRelations}));
+      });
+  };
+
   actions.index = function(req, res) {
     var userForm = req.body.userForm;
     var userData = req.user;

+ 63 - 51
lib/routes/page.js

@@ -9,6 +9,7 @@ module.exports = function(crowi, app) {
     , Revision = crowi.model('Revision')
     , Bookmark = crowi.model('Bookmark')
     , UserGroupRelation = crowi.model('UserGroupRelation')
+    , PageGroupRelation = crowi.model('PageGroupRelation')
     , ApiResponse = require('../util/apiResponse')
     , interceptorManager = crowi.getInterceptorManager()
     , pagePathUtil = require('../util/pagePathUtil')
@@ -180,11 +181,13 @@ module.exports = function(crowi, app) {
       else {
         return Promise.resolve([]);
       }
-    }).then(function(tree) {
+    })
+    .then(function(tree) {
       renderVars.tree = tree;
 
       return Page.findListByStartWith(path, req.user, queryOptions);
-    }).then(function(pageList) {
+    })
+    .then(function(pageList) {
 
       if (pageList.length > limit) {
         pageList.pop();
@@ -197,6 +200,16 @@ module.exports = function(crowi, app) {
       };
       renderVars.pager = generatePager(pagerOptions);
       renderVars.pages = pagePathUtil.encodePagesPath(pageList);
+    })
+    .then(() => {
+      return PageGroupRelation.findByPage(renderVars.page);
+    })
+    .then((pageGroupRelation) => {
+      if (pageGroupRelation != null) {
+        renderVars.pageRelatedGroup = pageGroupRelation.relatedGroup;
+      }
+    })
+    .then(() => {
       res.render('customlayout-selector/page_list', renderVars);
     }).catch(function(err) {
       debug('Error on rendering pageListShow', err);
@@ -237,7 +250,7 @@ module.exports = function(crowi, app) {
       author: false,
       pages: [],
       tree: [],
-      userRelatedGroups: [],
+      pageRelatedGroup: null,
     };
 
     var pageTeamplate = 'customlayout-selector/page';
@@ -264,8 +277,16 @@ module.exports = function(crowi, app) {
         return Revision.findRevisionList(page.path, {})
         .then(function(tree) {
           renderVars.tree = tree;
-          return Promise.resolve();
-        }).then(function() {
+        })
+        .then(() => {
+          return PageGroupRelation.findByPage(renderVars.page);
+        })
+        .then((pageGroupRelation) => {
+          if (pageGroupRelation != null) {
+            renderVars.pageRelatedGroup = pageGroupRelation.relatedGroup;
+          }
+        })
+        .then(function() {
           var userPage = isUserPage(page.path);
           var userData = null;
 
@@ -294,14 +315,8 @@ module.exports = function(crowi, app) {
               // pass
             });
           }
-          else {
-            return Promise.resolve();
-          }
         });
       }
-      else {
-        return Promise.resolve();
-      }
     })
     // page not exists
     .catch(function(err) {
@@ -313,41 +328,32 @@ module.exports = function(crowi, app) {
     .then(function() {
       if (!isRedirect) {
         Page.findListWithDescendants(path, req.user, queryOptions)
-        .then(function(pageList) {
-          if (pageList.length > limit) {
-            pageList.pop();
-          }
-
-          pagerOptions.length = pageList.length;
-
-          renderVars.viewConfig = {
-            seener_threshold: SEENER_THRESHOLD,
-          };
-          renderVars.pager = generatePager(pagerOptions);
-          renderVars.pages = pagePathUtil.encodePagesPath(pageList);
-
-          return Promise.resolve();
-        })
-        .then(function() {
-          return interceptorManager.process('beforeRenderPage', req, res, renderVars);
-        })
-        .then(function() {
-          res.render(req.query.presentation ? 'page_presentation' : pageTeamplate, renderVars);
-        })
-        .catch(function(err) {
-          console.log(err);
-          debug('Error on rendering pageListShowForCrowiPlus', err);
-        });
+          .then(function(pageList) {
+            if (pageList.length > limit) {
+              pageList.pop();
+            }
+
+            pagerOptions.length = pageList.length;
+
+            renderVars.viewConfig = {
+              seener_threshold: SEENER_THRESHOLD,
+            };
+            renderVars.pager = generatePager(pagerOptions);
+            renderVars.pages = pagePathUtil.encodePagesPath(pageList);
+
+            return;
+          })
+          .then(function() {
+            return interceptorManager.process('beforeRenderPage', req, res, renderVars);
+          })
+          .then(function() {
+            res.render(req.query.presentation ? 'page_presentation' : pageTeamplate, renderVars);
+          })
+          .catch(function(err) {
+            console.log(err);
+            debug('Error on rendering pageListShowForCrowiPlus', err);
+          });
       }
-    })
-    .then(function() {
-      return UserGroupRelation.findAllRelationForUser(req.user);
-    }).then(function(groupRelations) {
-      debug('findPage : relatedGroups ', groupRelations);
-      renderVars.userRelatedGroups = groupRelations.map(relation => relation.relatedGroup);
-      debug('findPage : groups ', renderVars.userRelatedGroups);
-
-      return Promise.resolve();
     });
   };
 
@@ -445,9 +451,16 @@ module.exports = function(crowi, app) {
     Revision.findRevisionList(pageData.path, {})
     .then(function(tree) {
       renderVars.tree = tree;
-
-      return Promise.resolve();
-    }).then(function() {
+    })
+    .then(() => {
+      return PageGroupRelation.findByPage(renderVars.page);
+    })
+    .then((pageGroupRelation) => {
+      if (pageGroupRelation != null) {
+        renderVars.pageRelatedGroup = pageGroupRelation.relatedGroup;
+      }
+    })
+    .then(function() {
       if (userPage) {
         return User.findUserByUsername(User.getUsernameByPath(pageData.path))
         .then(function(data) {
@@ -491,7 +504,6 @@ module.exports = function(crowi, app) {
 
   actions.pageShow = function(req, res) {
     var path = path || getPathFromRequest(req);
-    var options = {};
 
     // FIXME: せっかく getPathFromRequest になってるのにここが生 params[0] だとダサイ
     var isMarkdown = req.params[0].match(/.+\.md$/) || false;
@@ -606,12 +618,12 @@ module.exports = function(crowi, app) {
 
       if (data) {
         previousRevision = data.revision;
-        return Page.updatePage(data, body, req.user, { grant: grant, grantUserGroupId: grantUserGroupId});
+        return Page.updatePage(data, body, req.user, { grant, grantUserGroupId });
       }
       else {
         // new page
         updateOrCreate = 'create';
-        return Page.create(path, body, req.user, { grant: grant, grantUserGroupId: grantUserGroupId});
+        return Page.create(path, body, req.user, { grant, grantUserGroupId });
       }
     }).then(function(data) {
       // data is a saved page data.

+ 8 - 0
lib/service/passport.js

@@ -115,6 +115,10 @@ class PassportService {
     passport.use(new LdapStrategy(this.getLdapConfigurationFunc(config, {passReqToCallback: true}),
       (req, ldapAccountInfo, done) => {
         debug('LDAP authentication has succeeded', ldapAccountInfo);
+
+        // store ldapAccountInfo to req
+        req.ldapAccountInfo = ldapAccountInfo;
+
         done(null, ldapAccountInfo);
       }
     ));
@@ -212,6 +216,10 @@ class PassportService {
           server: serverOpt,
         }, opts);
         debug('ldap configuration: ', mergedOpts);
+
+        // store configuration to req
+        req.ldapConfiguration = mergedOpts;
+
         callback(null, mergedOpts);
       });
     };

+ 31 - 5
lib/util/interceptor-manager.js

@@ -6,25 +6,51 @@ const logger = require('@alias/logger')('growi:InterceptorManager');
 class InterceptorManager {
 
   constructor() {
+    this.interceptorAndOrders = []; /* [
+                                          {interceptor: instanceA, order: 200 },
+                                          {interceptor: instanceB, order: 100 },
+                                          ...
+                                       ] */
     this.interceptors = [];
   }
 
   /**
    * add an Interceptor
    * @param {BasicInterceptor} interceptor
+   * @param {number} order
    */
-  addInterceptor(interceptor) {
-    this.addInterceptors([interceptor]);
+  addInterceptor(interceptor, order) {
+    this.addInterceptors([interceptor], order);
   }
 
   /**
    * add Interceptors
    * @param {BasicInterceptor[]} interceptors
+   * @param {number} order
    */
-  addInterceptors(interceptors) {
+  addInterceptors(interceptors, order) {
+    let isDefaultOrder = false;
+    if (order == null) {
+      order = 100;
+      isDefaultOrder = true;
+    }
+
     const interceptorIds = interceptors.map((i) => i.getId());
-    logger.debug(`adding interceptors '${interceptorIds}'`);
-    this.interceptors = this.interceptors.concat(interceptors);
+    logger.info(`'addInterceptors' invoked. adding interceptors '${interceptorIds}' at order=${order}${isDefaultOrder ? '(default)' : ''}`);
+
+    this.interceptorAndOrders = this.interceptorAndOrders.concat(
+      interceptors.map(interceptor => {
+        return { interceptor, order };
+      })
+    );
+
+    // sort asc
+    this.interceptorAndOrders.sort((a, b) => a.order - b.order);
+    // store sorted list
+    this.interceptors = this.interceptorAndOrders.map(obj => obj.interceptor);
+
+    const thisInterceptorIds = this.interceptors.map((i) => i.getId());
+    logger.info(`interceptors list has initialized: ${thisInterceptorIds}`);
   }
 
   /**

+ 5 - 3
lib/util/swigFunctions.js

@@ -1,5 +1,6 @@
 module.exports = function(crowi, app, req, locals) {
   var debug = require('debug')('growi:lib:swigFunctions')
+    , stringWidth = require('string-width')
     , Page = crowi.model('Page')
     , Config = crowi.model('Config')
     , User = crowi.model('User')
@@ -28,10 +29,11 @@ module.exports = function(crowi, app, req, locals) {
   };
 
   locals.getAppTitleFontSize = function(appTitle) {
+    let appTitleWidth = stringWidth(appTitle);
     let fontSize = 22;
-    if (appTitle.length < 13) { /* do nothing */ }
-    else if (appTitle.length < 21) {
-      fontSize -= 3 * (Math.floor((appTitle.length - 13) / 3) + 1);
+    if (appTitleWidth < 13) { /* do nothing */ }
+    else if (appTitleWidth < 21) {
+      fontSize -= 3 * (Math.floor((appTitleWidth - 13) / 3) + 1);
     }
     else  {
       fontSize = 11;

+ 5 - 16
lib/views/_form.html

@@ -46,26 +46,15 @@
       </span>
       {% endif %}
 
-      {% if forceGrant %}
-      <input type="hidden" name="pageForm[grant]" value="{{ forceGrant }}">
-      {% else %}
-      <select name="pageForm[grant]" class="m-r-5 selectpicker btn-group-sm">
-        {% for grantId, grantLabel in consts.pageGrants %}
-        <option value="{{ grantId }}" {% if pageForm.grant|default(page.grant) == grantId %}selected{% endif %} {% if grantId == 5 && userRelatedGroups.length == 0 %}disabled{% endif %}>{{ t(grantLabel) }}</option>
-        {% endfor %}
-      </select>
-      {% endif %}
-      {% if userRelatedGroups.length != 0 %}
-      <select name="pageForm[grantUserGroupId]" class="selectpicker btn-group-sm">
-        {% for userGroup in userRelatedGroups %}
-        <option value="{{ userGroup.id }}">{{ userGroup.name }}</option>
-        {% endfor %}
-      </select>
-      {% endif %}
+      <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() }}">
       <button type="submit" class="btn btn-primary btn-submit" id="edit-form-submit">{{ t('Update') }}</button>
     </div>
   </div>
 </form>
+<input type="hidden" id="grant-group-name" value="{{ pageRelatedGroup.name }}">{# for storing group name #}
 <div class="file-module hidden">
 </div>

+ 13 - 5
lib/views/admin/customize.html

@@ -73,6 +73,7 @@
             {# Dark Themes #}
             <div class="d-flex">
               {% include 'widget/theme-colorbox.html' with { name: 'default-dark', bg: '#212731', topbar: '#151515', theme: '#f75b36' } %}
+              {% include 'widget/theme-colorbox.html' with { name: 'future', bg: '#16282D', topbar: '#011414', theme: '#04B4AE' } %}
             </div>
           </div>
 
@@ -281,7 +282,7 @@
       </fieldset>
       </form>
 
-      <form action="/_api/admin/customize/highlightJsStyle" method="post" class="form-horizontal" id="cutomhighlightJsStyleSettingForm" role="form">
+      <form action="/_api/admin/customize/highlightJsStyle" method="post" class="form-horizontal" id="customhighlightJsStyleSettingForm" role="form">
         <fieldset>
           <legend>{{ t('customize_page.Code Highlight') }}</legend>
           <div class="form-group">
@@ -299,11 +300,11 @@
             <label for="settingForm[customize:highlightJsStyleBorder]" class="col-xs-3 control-label">(TBD) Border</label>
             <div class="col-xs-9">
               <div class="btn-group btn-toggle" data-toggle="buttons">
-                <label class="btn btn-default btn-rounded btn-outline {% if settingForm['customize:highlightJsStyleBorder'] %}active{% endif %}" data-active-class="primary">
+                <label class="btn btn-default btn-rounded btn-outline {% if settingForm['customize:highlightJsStyleBorder'] %}active{% endif %}" data-active-class="primary" onclick="selectBorderOn()">
                   <input name="settingForm[customize:highlightJsStyleBorder]" value="true" type="radio"
                       {% if true === settingForm['customize:highlightJsStyleBorder'] %}checked{% endif %}> ON
                 </label>
-                <label class="btn btn-default btn-rounded btn-outline {% if !settingForm['customize:highlightJsStyleBorder'] %}active{% endif %}" data-active-class="default">
+                <label class="btn btn-default btn-rounded btn-outline {% if !settingForm['customize:highlightJsStyleBorder'] %}active{% endif %}" data-active-class="default" onclick="selectBorderOff()">
                   <input name="settingForm[customize:highlightJsStyleBorder]" value="false" type="radio"
                       {% if !settingForm['customize:highlightJsStyleBorder'] %}checked{% endif %}> OFF
                 </label>
@@ -315,7 +316,7 @@
 
           <p class="help-block">
             Examples:
-            <pre class="hljs"><code class="highlightjs-demo">function $initHighlight(block, cls) {
+            <pre class="hljs {% if !settingForm['customize:highlightJsStyleBorder'] %}hljs-no-border{% endif %}"><code class="highlightjs-demo">function $initHighlight(block, cls) {
   try {
     if (cls.search(/\bno\-highlight\b/) != -1)
       return process(block, true, 0x0F) +
@@ -507,7 +508,7 @@ window.addEventListener('load', (event) => {
 {% block body_end %}
   {% parent %}
   <script>
-    $(`#customthemeSettingForm, #cutomlayoutSettingForm, #cutombehaviorSettingForm, #cutomhighlightJsStyleSettingForm,
+    $(`#customthemeSettingForm, #cutomlayoutSettingForm, #cutombehaviorSettingForm, #customhighlightJsStyleSettingForm,
        #customfeaturesSettingForm, #cutomheaderSettingForm, #cutomcssSettingForm, #cutomscriptSettingForm, #customtitleSettingForm`
     ).each(function() {
       $(this).submit(function()
@@ -592,6 +593,13 @@ window.addEventListener('load', (event) => {
       $('#themeOptions .active').removeClass('active');
       $(`#themeOptions #theme-option-${theme}`).addClass('active');
     }
+
+    function selectBorderOn(){
+      $('.hljs-no-border').removeClass('hljs-no-border');
+    }
+    function selectBorderOff(){
+      $('#customhighlightJsStyleSettingForm .hljs').addClass('hljs-no-border')
+    }
   </script>
 
 </div>

+ 4 - 4
lib/views/admin/markdown.html

@@ -44,7 +44,7 @@
 
         <div class="form-group">
           <label for="markdownSetting[markdown:isEnabledLinebreaks]" class="col-xs-4 control-label">
-            {{ t('markdown_setting.validate Line Break') }}
+            {{ t('markdown_setting.Enable Line Break') }}
           </label>
           <div class="col-xs-5">
             <div class="btn-group btn-toggle" data-toggle="buttons">
@@ -57,14 +57,14 @@
                     {% if !markdownSetting['markdown:isEnabledLinebreaks'] %}checked{% endif %}> OFF
               </label>
             </div>
-            <p class="help-block">{{ t("markdown_setting.treat_text") }}
+            <p class="help-block">{{ t("markdown_setting.Enable Line Break desc") }}
 </p>
           </div>
         </div>
 
         <div class="form-group">
           <label for="markdownSetting[markdown:isEnabledLinebreaksInComments]" class="col-xs-4 control-label">
-            (TBD)<br>{{ t("markdown_setting.validate_comment") }}
+            (TBD)<br>{{ t("markdown_setting.Enable Line Break for comment") }}
           </label>
           <div class="col-xs-5">
             <div class="btn-group btn-toggle" data-toggle="buttons">
@@ -77,7 +77,7 @@
                     {% if !markdownSetting['markdown:isEnabledLinebreaksInComments'] %}checked{% endif %}> OFF
               </label>
             </div>
-            <p class="help-block">{{ t("markdown_setting.treat_comment") }}<br>{{ t("markdown_setting.TBD") }}</p>
+            <p class="help-block">{{ t("markdown_setting.Enable Line Break for comment desc") }}<br>{{ t("markdown_setting.TBD") }}</p>
           </div>
         </div>
 

+ 9 - 5
lib/views/admin/widget/passport/ldap.html

@@ -62,14 +62,17 @@
               name="settingForm[security:passport-ldap:bindDN]" value="{{ settingForm['security:passport-ldap:bindDN'] || '' }}">
           <p class="help-block passport-ldap-managerbind" {% if isUserBind %}style="display: none;"{% endif %}>
             <small>
-              {{ t("security_setting.ldap.bind_DN_manager_detail") }}
+              {{ t("security_setting.ldap.bind_DN_manager_detail") }}<br>
+              {{ t("security_setting.example") }}1: <code>uid=admin,dc=domain,dc=com</code><br>
+              {{ t("security_setting.example") }}2: <code>admin@domain.com</code>
             </small>
           </p>
           <p class="help-block passport-ldap-userbind" {% if !isUserBind %}style="display: none;"{% endif %}>
             <small>
               {{ t("security_setting.ldap.bind_DN_user_detail1") }}<br>
               {{ t("security_setting.ldap.bind_DN_user_detail2") }}<br>
-              {{ t("security_setting.example") }}: <code>uid={% raw %}{{username}}{% endraw %},dc=domain,dc=com</code><br>
+              {{ t("security_setting.example") }}1: <code>uid={% raw %}{{username}}{% endraw %},dc=domain,dc=com</code><br>
+              {{ t("security_setting.example") }}2: <code>{% raw %}{{username}}{% endraw %}@domain.com</code>
             </small>
           </p>
           </div>
@@ -105,9 +108,10 @@
               {{ t("security_setting.ldap.search_filter_detail3") }}
             </small>
           </p>
-          <p>
+          <p class="help-block">
             <small>
-              {{ t("security_setting.ldap.search_filter_example") }}: <code>(|(uid={% raw %}{{username}}{% endraw %})(mail={% raw %}{{username}}{% endraw %}))</code>
+              {{ t("security_setting.example") }}1 - {{ t("security_setting.ldap.search_filter_example1") }}: <code>(|(uid={% raw %}{{username}}{% endraw %})(mail={% raw %}{{username}}{% endraw %}))</code><br>
+              {{ t("security_setting.example") }}2 - {{ t("security_setting.ldap.search_filter_example2") }}: <code>(sAMAccountName={% raw %}{{username}}{% endraw %})</code>
             </small>
           </p>
         </div>
@@ -307,7 +311,7 @@
 
       <div class="modal-body">
 
-        {% include '../../../widget/passport/ldap-association-tester.html' %}
+        {% include '../../../widget/passport/ldap-association-tester.html' with { showLog: true } %}
 
       </div><!-- /.modal-body -->
 

+ 9 - 0
lib/views/layout-growi/not_found.html

@@ -18,3 +18,12 @@
     </div> {# /.col- #}
   </div>
 {% endblock %}
+
+{% block body_end %}
+  <div id="presentation-layer" class="fullscreen-layer">
+    <div id="presentation-container"></div>
+  </div>
+
+  <div id="crowi-modals">
+  </div>
+{% endblock %}

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

@@ -118,10 +118,12 @@ gh/highlightjs/cdn-release@9.12.0/build/languages/yaml.min.js
   class="main-container content-wrapper {% block html_base_css %}{% endblock %}
       {% if !layoutType() || 'crowi' === layoutType() %}crowi{% else %}growi{% endif %}"
   data-me="{{ user._id.toString() }}"
+  data-is-admin="{{ user.admin }}"
   data-plugin-enabled="{{ isEnabledPlugins() }}"
   {% block html_base_attr %}{% endblock %}
   data-csrftoken="{{ csrf() }}"
   data-current-username="{% if user %}{{ user.username }}{% endif %}"
+  data-userlang="{% if user %}{{ user.lang }}{% endif %}"
  >
 
 <div id="wrapper">

+ 9 - 3
lib/views/widget/page_alerts.html

@@ -1,9 +1,15 @@
 <div class="row row-alerts">
   <div class="col-xs-12">
     {% if page && page.grant != 1 %}
-    <p class="alert alert-inverse alert-grant">
-      <i class="icon-fw icon-lock"></i><strong>{{ consts.pageGrants[page.grant] }}</strong> ({{ t('Browsing of this page is restricted') }})
-    </p>
+      <p class="alert alert-inverse alert-grant">
+      {% if page.grant == 2 %}
+        <i class="icon-fw icon-link"></i><strong>{{ consts.pageGrants[page.grant] }}</strong> ({{ t('Browsing of this page is restricted') }})
+      {% elseif page.grant == 4 %}
+        <i class="icon-fw icon-lock"></i><strong>{{ consts.pageGrants[page.grant] }}</strong> ({{ t('Browsing of this page is restricted') }})
+      {% elseif page.grant == 5 %}
+        <i class="icon-fw icon-organization"></i><strong>'{{ pageRelatedGroup.name }}' only</strong> ({{ t('Browsing of this page is restricted') }})
+      {% endif %}
+      </p>
     {% endif %}
 
     {% if page.isDeleted() %}

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

@@ -39,7 +39,7 @@
 
     {% if !page.isPublic() %}
     <span>
-      <i class="fa fa-lock"></i>
+      <i class="icon icon-lock"></i>
     </span>
     {% endif %}
   </span>

+ 10 - 2
lib/views/widget/page_tabs.html

@@ -49,9 +49,17 @@
     {% endif %}
   {% endif %}
 
-  <li class="nav-main-right-tab pull-right"><a href="#revision-history" data-toggle="tab"><i class="icon-layers"></i> History</a></li>
+  <li class="nav-main-right-tab pull-right">
+    <a href="#revision-history" data-toggle="tab">
+      <i class="icon-layers"></i><span class="hidden-xs">  History</span>
+    </a>
+  </li>
   {% if not isPortal %}
-    <li class="nav-main-right-tab pull-right"><a href="?presentation=1" class="toggle-presentation"><i class="icon-film"></i> {{ t('Presentation Mode') }}</a></li>
+    <li class="nav-main-right-tab pull-right">
+      <a href="?presentation=1" class="toggle-presentation">
+        <i class="icon-film"></i><span class="hidden-xs"> {{ t('Presentation Mode') }}</span>
+      </a>
+    </li>
   {% endif %}
 
 </ul>

+ 34 - 3
lib/views/widget/passport/ldap-association-tester.html

@@ -14,12 +14,20 @@
       </div>
     </div>
 
-    <div class="form-group">
-      <button type="button" class="btn btn-default col-xs-offset-5 col-xs-2" onclick="testLdapCredentials()">{{ t('Test') }}</button>
-    </div>
 
   </fieldset>
 
+  {% if showLog %}
+  <fieldset>
+    <h5>Logs</h5>
+    <textarea id="taLogs" class="col-xs-12" rows="4" readonly></textarea>
+  </fieldset>
+  {% endif %}
+
+  <fieldset class="mt-4">
+    <button type="button" class="btn btn-default col-xs-offset-5 col-xs-2" onclick="testLdapCredentials()">{{ t('Test') }}</button>
+  </fieldset>
+
   <script>
     /**
      * test association (ajax)
@@ -44,6 +52,14 @@
           }, 5000);
         }
       }
+      /**
+       * add logs
+       */
+      function addLogs(formId, log) {
+        const textarea = $(`#${formId} #taLogs`);
+        const newLog = `${new Date()} - ${log}\n\n`;
+        textarea.val(`${newLog}${textarea.val()}`);
+      }
 
       var $form = $('#formTestLdapCredentials');
       var $action = '/_api/login/testLdap';
@@ -59,6 +75,21 @@
           else {
             showMessage($id, data.message, data.status);
           }
+
+          // add logs
+          if ('true' === '{{showLog}}') {
+            if (data.err) {
+              addLogs($id, data.err);
+            }
+            if (data.ldapConfiguration) {
+              const prettified = JSON.stringify(data.ldapConfiguration.server, undefined, 4);
+              addLogs($id, `LDAP Configuration : ${prettified}`);
+            }
+            if (data.ldapAccountInfo) {
+              const prettified = JSON.stringify(data.ldapAccountInfo, undefined, 4);
+              addLogs($id, `Retrieved LDAP Account : ${prettified}`);
+            }
+          }
         })
         .fail(function() {
           showMessage($id, 'エラーが発生しました', 'danger');

+ 69 - 64
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.0.13-RC",
+  "version": "3.1.0-RC2",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -41,6 +41,7 @@
     "postserver:prod:container": "echo ---------------------------------------- && echo [WARNING] && echo   'server:prod:container' is deprecated. && echo   Please use 'sever:prod' && echo ----------------------------------------",
     "server:dev": "env-cmd config/env.dev.js node-dev --respawn app.js",
     "server:prod:container": "npm run server:prod",
+    "server:prod:ci": "npm run server:prod -- --ci",
     "server:prod": "env-cmd config/env.prod.js node app.js",
     "server": "npm run server:dev",
     "start": "npm run server:prod",
@@ -49,39 +50,21 @@
     "webpack": "webpack"
   },
   "dependencies": {
-    "assets-webpack-plugin": "~3.5.1",
     "async": "^2.3.0",
-    "autoprefixer": "^8.2.0",
     "aws-sdk": "^2.88.0",
     "axios": "^0.18.0",
-    "babel-core": "^6.25.0",
-    "babel-loader": "^7.1.1",
-    "babel-plugin-lodash": "^3.3.2",
-    "babel-preset-env": "^1.6.0",
-    "babel-preset-react": "^6.24.1",
     "basic-auth-connect": "~1.0.0",
     "body-parser": "^1.18.2",
-    "bootstrap-sass": "~3.3.6",
-    "bootstrap-select": "^1.12.4",
-    "browser-bunyan": "^1.3.0",
     "bunyan": "^1.8.12",
-    "bunyan-debug": "^2.0.0",
     "bunyan-format": "^0.2.1",
     "check-node-version": "^3.1.1",
-    "codemirror": "^5.37.0",
     "connect-flash": "~0.1.1",
     "connect-mongo": "^2.0.1",
     "connect-redis": "^3.3.0",
     "cookie-parser": "^1.4.3",
     "cross-env": "^5.0.5",
     "csrf": "~3.0.3",
-    "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",
-    "elasticsearch": "^14.0.0",
+    "elasticsearch": "^15.0.0",
     "entities": "^1.1.1",
     "env-cmd": "^8.0.1",
     "escape-string-regexp": "^1.0.5",
@@ -91,89 +74,111 @@
     "express-sanitizer": "^1.0.4",
     "express-session": "~1.15.0",
     "express-webpack-assets": "^0.1.0",
-    "extract-text-webpack-plugin": "^3.0.2",
-    "file-loader": "^1.1.0",
-    "googleapis": "^29.0.0",
+    "googleapis": "^30.0.0",
     "graceful-fs": "^4.1.11",
     "growi-pluginkit": "^1.1.0",
     "i18next": "^11.1.1",
     "i18next-express-middleware": "^1.1.1",
     "i18next-node-fs-backend": "^1.0.0",
     "i18next-sprintf-postprocessor": "^0.2.2",
-    "jquery-slimscroll": "^1.3.8",
-    "jquery-ui": "^1.12.1",
-    "jquery.cookie": "~1.4.1",
-    "load-css-file": "^1.0.0",
-    "markdown-it": "^8.4.0",
-    "markdown-it-emoji": "^1.4.0",
-    "markdown-it-footnote": "^3.0.1",
-    "markdown-it-mathjax": "^2.0.0",
-    "markdown-it-named-headers": "^0.0.4",
-    "markdown-it-plantuml": "^1.0.0",
-    "markdown-it-task-lists": "^2.1.0",
-    "markdown-it-toc-and-anchor-with-slugid": "^1.1.3",
-    "markdown-table": "^1.1.1",
     "md5": "^2.2.1",
     "method-override": "^2.3.10",
-    "metismenu": "^2.7.4",
     "mkdirp": "~0.5.1",
     "module-alias": "^2.0.6",
     "mongoose": "^5.0.0",
     "mongoose-paginate": "^5.0.0",
     "mongoose-unique-validator": "^2.0.0",
     "multer": "~1.3.0",
-    "node-sass": "^4.5.0",
     "nodemailer": "^4.0.1",
     "nodemailer-ses-transport": "~1.5.0",
-    "normalize-path": "^3.0.0",
     "npm-run-all": "^4.1.2",
-    "optimize-js-plugin": "0.0.4",
     "passport": "^0.4.0",
     "passport-ldapauth": "^2.0.0",
     "passport-local": "^1.0.0",
-    "plantuml-encoder": "^1.2.5",
-    "postcss-loader": "^2.1.3",
     "react": "^16.2.0",
-    "react-bootstrap": "^0.32.1",
-    "react-bootstrap-typeahead": "^3.0.3",
-    "react-clipboard.js": "^1.1.3",
-    "react-codemirror2": "^5.0.0",
     "react-dom": "^16.2.0",
-    "react-dropzone": "^4.2.7",
-    "reveal.js": "^3.5.0",
+    "react-i18next": "^7.6.1",
     "rimraf": "^2.6.1",
-    "sass-loader": "^7.0.1",
-    "simple-load-script": "^1.0.2",
     "slack-node": "^0.1.8",
     "socket.io": "^2.0.3",
-    "socket.io-client": "^2.0.3",
-    "style-loader": "^0.21.0",
+    "string-width": "^2.1.1",
     "swig-templates": "^2.0.2",
-    "throttle-debounce": "^1.0.1",
-    "toastr": "^2.1.2",
     "uglifycss": "^0.0.29",
-    "url-join": "^4.0.0",
-    "webpack": "3.11.0",
-    "webpack-bundle-analyzer": "^2.9.0",
-    "webpack-merge": "~4.1.0",
-    "xss": "^0.3.5"
+    "xss": "^0.3.7"
   },
   "devDependencies": {
+    "@alienfast/i18next-loader": "^1.0.16",
+    "assets-webpack-plugin": "~3.5.1",
+    "autoprefixer": "^8.2.0",
+    "babel-core": "^6.25.0",
+    "babel-loader": "^7.1.1",
+    "babel-plugin-lodash": "^3.3.2",
+    "babel-preset-env": "^1.6.0",
+    "babel-preset-react": "^6.24.1",
+    "bootstrap-sass": "~3.3.6",
+    "bootstrap-select": "^1.12.4",
+    "browser-bunyan": "^1.3.0",
     "browser-sync": "^2.23.6",
+    "bunyan-debug": "^2.0.0",
     "chai": "^4.1.0",
     "cli": "~1.0.1",
-    "colors": "^1.1.2",
+    "codemirror": "^5.37.0",
+    "colors": "^1.2.5",
     "commander": "^2.11.0",
     "connect-browser-sync": "^2.1.0",
+    "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",
     "eslint-plugin-react": "^7.7.0",
+    "extract-text-webpack-plugin": "^3.0.2",
+    "file-loader": "^1.1.0",
+    "i18next-browser-languagedetector": "^2.2.0",
+    "jquery-slimscroll": "^1.3.8",
+    "jquery-ui": "^1.12.1",
+    "jquery.cookie": "~1.4.1",
+    "load-css-file": "^1.0.0",
+    "markdown-it": "^8.4.0",
+    "markdown-it-emoji": "^1.4.0",
+    "markdown-it-footnote": "^3.0.1",
+    "markdown-it-mathjax": "^2.0.0",
+    "markdown-it-named-headers": "^0.0.4",
+    "markdown-it-plantuml": "^1.0.0",
+    "markdown-it-task-lists": "^2.1.0",
+    "markdown-it-toc-and-anchor-with-slugid": "^1.1.3",
+    "markdown-table": "^1.1.1",
+    "metismenu": "^2.7.4",
     "mocha": "^5.0.0",
     "morgan": "^1.9.0",
     "node-dev": "^3.1.3",
+    "node-sass": "^4.5.0",
+    "normalize-path": "^3.0.0",
     "on-headers": "^1.0.1",
+    "optimize-js-plugin": "0.0.4",
+    "plantuml-encoder": "^1.2.5",
+    "postcss-loader": "^2.1.3",
+    "react-bootstrap": "^0.32.1",
+    "react-bootstrap-typeahead": "^3.0.3",
+    "react-clipboard.js": "^1.1.3",
+    "react-codemirror2": "^5.0.0",
+    "react-dropzone": "^4.2.7",
+    "reveal.js": "^3.5.0",
+    "sass-loader": "^7.0.1",
+    "simple-load-script": "^1.0.2",
     "sinon": "^5.0.2",
     "sinon-chai": "^3.0.0",
-    "webpack-dll-bundles-plugin": "^1.0.0-beta.5"
+    "socket.io-client": "^2.0.3",
+    "style-loader": "^0.21.0",
+    "throttle-debounce": "^1.0.1",
+    "toastr": "^2.1.2",
+    "url-join": "^4.0.0",
+    "webpack": "3.11.0",
+    "webpack-bundle-analyzer": "^2.9.0",
+    "webpack-dll-bundles-plugin": "^1.0.0-beta.5",
+    "webpack-merge": "~4.1.0"
   },
   "_moduleAliases": {
     "@root": ".",
@@ -181,9 +186,9 @@
     "debug": "lib/service/logger/alias-for-debug"
   },
   "engines": {
-    "node": ">=6.11 <9",
-    "npm": ">=4",
-    "yarn": "^1.3.1"
+    "node": ">=8.11.1 <9",
+    "npm": ">=5.6.0 <6",
+    "yarn": "^1.5.1"
   },
   "config": {
     "blanket": {

+ 40 - 2
resource/js/app.js

@@ -1,5 +1,8 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
+import { I18nextProvider } from 'react-i18next';
+
+import i18nFactory from './i18n';
 
 import Crowi from './util/Crowi';
 // import CrowiRenderer from './util/CrowiRenderer';
@@ -10,6 +13,7 @@ import SearchPage       from './components/SearchPage';
 import PageEditor       from './components/PageEditor';
 import OptionsSelector  from './components/PageEditor/OptionsSelector';
 import { EditorOptions, PreviewOptions } from './components/PageEditor/OptionsSelector';
+import GrantSelector    from './components/PageEditor/GrantSelector';
 import Page             from './components/Page';
 import PageListSearch   from './components/PageListSearch';
 import PageHistory      from './components/PageHistory';
@@ -21,7 +25,6 @@ import RevisionPath     from './components/Page/RevisionPath';
 import RevisionUrl      from './components/Page/RevisionUrl';
 import BookmarkButton   from './components/BookmarkButton';
 import NewPageNameInputter from './components/NewPageNameInputter';
-import SearchTypeahead  from './components/SearchTypeahead';
 
 import CustomCssEditor  from './components/Admin/CustomCssEditor';
 import CustomScriptEditor from './components/Admin/CustomScriptEditor';
@@ -29,11 +32,13 @@ import CustomHeaderEditor from './components/Admin/CustomHeaderEditor';
 
 import * as entities from 'entities';
 
-
 if (!window) {
   window = {};
 }
 
+const userlang = $('body').data('userlang');
+const i18n = i18nFactory(userlang);
+
 const mainContent = document.querySelector('#content-main');
 let pageId = null;
 let pageRevisionId = null;
@@ -41,6 +46,7 @@ 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');
@@ -57,6 +63,7 @@ const isLoggedin = document.querySelector('.main-container.nologin') == null;
 // FIXME
 const crowi = new Crowi({
   me: $('body').data('current-username'),
+  isAdmin: $('body').data('is-admin'),
   csrfToken: $('body').data('csrftoken'),
 }, window);
 window.crowi = crowi;
@@ -180,6 +187,37 @@ if (pageEditorOptionsSelectorElem) {
     pageEditorOptionsSelectorElem
   );
 }
+// render GrantSelector
+const pageEditorGrantSelectorElem = document.getElementById('page-grant-selector');
+if (pageEditorGrantSelectorElem) {
+  const grantElem = document.getElementById('page-grant');
+  const grantGroupElem = document.getElementById('grant-group');
+  const grantGroupNameElem = document.getElementById('grant-group-name');
+  /* eslint-disable no-inner-declarations */
+  function updateGrantElem(pageGrant) {
+    grantElem.value = pageGrant;
+  }
+  function updateGrantGroupElem(grantGroupId) {
+    grantGroupElem.value = grantGroupId;
+  }
+  function updateGrantGroupNameElem(grantGroupName) {
+    grantGroupNameElem.value = grantGroupName;
+  }
+  /* eslint-enable */
+  const pageGrant = +grantElem.value;
+  const pageGrantGroupId = grantGroupElem.value;
+  const pageGrantGroupName = grantGroupNameElem.value;
+  ReactDOM.render(
+    <I18nextProvider i18n={i18n}>
+      <GrantSelector crowi={crowi}
+        pageGrant={pageGrant} pageGrantGroupId={pageGrantGroupId} pageGrantGroupName={pageGrantGroupName}
+        onChangePageGrant={updateGrantElem}
+        onDeterminePageGrantGroupId={updateGrantGroupElem}
+        onDeterminePageGrantGroupName={updateGrantGroupNameElem} />
+    </I18nextProvider>,
+    pageEditorGrantSelectorElem
+  );
+}
 
 // render for admin
 const customCssEditorElem = document.getElementById('custom-css-editor');

+ 5 - 3
resource/js/components/Page.js

@@ -109,15 +109,17 @@ export default class Page extends React.Component {
 
   render() {
     const config = this.props.crowi.getConfig();
+    const isMobile = this.props.crowi.isMobile;
     const isMathJaxEnabled = !!config.env.MATHJAX;
 
-    return (
-      <RevisionBody html={this.state.html}
+    return <div className={isMobile ? 'page-mobile' : ''}>
+      <RevisionBody
+          html={this.state.html}
           inputRef={el => this.revisionBodyElement = el}
           isMathJaxEnabled={isMathJaxEnabled}
           renderMathJaxOnInit={true}
       />
-    );
+    </div>;
   }
 }
 

+ 1 - 0
resource/js/components/PageEditor.js

@@ -368,6 +368,7 @@ export default class PageEditor extends React.Component {
         <div className="col-md-6 col-sm-12 page-editor-editor-container">
           <Editor ref="editor" value={this.state.markdown}
             editorOptions={this.state.editorOptions}
+            isMobile={this.props.crowi.isMobile}
             isUploadable={this.state.isUploadable}
             isUploadableFile={this.state.isUploadableFile}
             emojiStrategy={emojiStrategy}

+ 104 - 0
resource/js/components/PageEditor/AbstractEditor.js

@@ -0,0 +1,104 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+export default class AbstractEditor extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.forceToFocus = this.forceToFocus.bind(this);
+    this.setCaretLine = this.setCaretLine.bind(this);
+    this.setScrollTopByLine = this.setScrollTopByLine.bind(this);
+
+    this.getStrFromBol = this.getStrFromBol.bind(this);
+    this.getStrToEol = this.getStrToEol.bind(this);
+    this.insertText = this.insertText.bind(this);
+    this.insertLinebreak = this.insertLinebreak.bind(this);
+
+    this.dispatchSave = this.dispatchSave.bind(this);
+  }
+
+  forceToFocus() {
+  }
+
+  /**
+   * set caret position of codemirror
+   * @param {string} number
+   */
+  setCaretLine(line) {
+  }
+
+  /**
+   * scroll
+   * @param {number} line
+   */
+  setScrollTopByLine(line) {
+  }
+
+  /**
+   * return strings from BOL(beginning of line) to current position
+   */
+  getStrFromBol() {
+    throw new Error('this method should be impelemented in subclass');
+  }
+
+  /**
+   * return strings from current position to EOL(end of line)
+   */
+  getStrToEol() {
+    throw new Error('this method should be impelemented in subclass');
+  }
+
+  /**
+   * replace Beggining Of Line to current position with param 'text'
+   * @param {string} text
+   */
+  replaceBolToCurrentPos(text) {
+    throw new Error('this method should be impelemented in subclass');
+  }
+
+  /**
+   * insert text
+   * @param {string} text
+   */
+  insertText(text) {
+  }
+
+  /**
+   * insert line break to the current position
+   */
+  insertLinebreak() {
+    this.insertText('\n');
+  }
+
+  /**
+   * dispatch onSave event
+   */
+  dispatchSave() {
+    if (this.props.onSave != null) {
+      this.props.onSave();
+    }
+  }
+
+  /**
+   * dispatch onPasteFiles event
+   * @param {object} event
+   */
+  dispatchPasteFiles(event) {
+    if (this.props.onPasteFiles != null) {
+      this.props.onPasteFiles(event);
+    }
+  }
+}
+
+AbstractEditor.propTypes = {
+  value: PropTypes.string,
+  editorOptions: PropTypes.object,
+  onChange: PropTypes.func,
+  onScroll: PropTypes.func,
+  onScrollCursorIntoView: PropTypes.func,
+  onSave: PropTypes.func,
+  onPasteFiles: PropTypes.func,
+  onDragEnter: PropTypes.func,
+};
+

+ 438 - 0
resource/js/components/PageEditor/CodeMirrorEditor.js

@@ -0,0 +1,438 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import AbstractEditor from './AbstractEditor';
+
+import urljoin from 'url-join';
+const loadScript = require('simple-load-script');
+const loadCssSync = require('load-css-file');
+
+import * as codemirror from 'codemirror';
+
+import { UnControlled as ReactCodeMirror } from 'react-codemirror2';
+require('codemirror/addon/display/autorefresh');
+require('codemirror/addon/edit/matchbrackets');
+require('codemirror/addon/edit/matchtags');
+require('codemirror/addon/edit/closetag');
+require('codemirror/addon/edit/continuelist');
+require('codemirror/addon/hint/show-hint');
+require('codemirror/addon/hint/show-hint.css');
+require('codemirror/addon/search/searchcursor');
+require('codemirror/addon/search/match-highlighter');
+require('codemirror/addon/selection/active-line');
+require('codemirror/addon/scroll/annotatescrollbar');
+require('codemirror/addon/fold/foldcode');
+require('codemirror/addon/fold/foldgutter');
+require('codemirror/addon/fold/foldgutter.css');
+require('codemirror/addon/fold/markdown-fold');
+require('codemirror/addon/fold/brace-fold');
+require('codemirror/mode/gfm/gfm');
+
+import pasteHelper from './PasteHelper';
+import EmojiAutoCompleteHelper from './EmojiAutoCompleteHelper';
+
+import InterceptorManager from '../../../../lib/util/interceptor-manager';
+
+import PreventMarkdownListInterceptor from './PreventMarkdownListInterceptor';
+import MarkdownTableInterceptor from './MarkdownTableInterceptor';
+import mtu from './MarkdownTableUtil';
+
+export default class CodeMirrorEditor extends AbstractEditor {
+
+  constructor(props) {
+    super(props);
+    this.logger = require('@alias/logger')('growi:PageEditor:CodeMirrorEditor');
+
+    this.state = {
+      value: this.props.value,
+      isEnabledEmojiAutoComplete: false,
+      isLoadingKeymap: false,
+      additionalClass: '',
+    };
+
+    this.init();
+
+    this.getCodeMirror = this.getCodeMirror.bind(this);
+
+    this.getBol = this.getBol.bind(this);
+    this.getEol = this.getEol.bind(this);
+
+    this.loadTheme = this.loadTheme.bind(this);
+    this.loadKeymapMode = this.loadKeymapMode.bind(this);
+    this.setKeymapMode = this.setKeymapMode.bind(this);
+    this.handleEnterKey = this.handleEnterKey.bind(this);
+
+    this.scrollCursorIntoViewHandler = this.scrollCursorIntoViewHandler.bind(this);
+    this.pasteHandler = this.pasteHandler.bind(this);
+    this.cursorHandler = this.cursorHandler.bind(this);
+
+    this.renderLoadingKeymapOverlay = this.renderLoadingKeymapOverlay.bind(this);
+  }
+
+  init() {
+    this.cmCdnRoot = 'https://cdn.jsdelivr.net/npm/codemirror@5.37.0';
+
+    this.interceptorManager = new InterceptorManager();
+    this.interceptorManager.addInterceptors([
+      new PreventMarkdownListInterceptor(),
+      new MarkdownTableInterceptor(),
+    ]);
+
+    this.loadedThemeSet = new Set(['eclipse', 'elegant']);   // themes imported in _vendor.scss
+    this.loadedKeymapSet = new Set();
+  }
+
+  componentWillMount() {
+    if (this.props.emojiStrategy != null) {
+      this.emojiAutoCompleteHelper = new EmojiAutoCompleteHelper(this.props.emojiStrategy);
+      this.setState({isEnabledEmojiAutoComplete: true});
+    }
+  }
+
+  componentDidMount() {
+    // initialize caret line
+    this.setCaretLine(0);
+    // set save handler
+    codemirror.commands.save = this.dispatchSave;
+
+    // set CodeMirror instance as 'CodeMirror' so that CDN addons can reference
+    window.CodeMirror = require('codemirror');
+  }
+
+  componentWillReceiveProps(nextProps) {
+    // load theme
+    const theme = nextProps.editorOptions.theme;
+    this.loadTheme(theme);
+
+    // set keymap
+    const keymapMode = nextProps.editorOptions.keymapMode;
+    this.setKeymapMode(keymapMode);
+  }
+
+  getCodeMirror() {
+    return this.refs.cm.editor;
+  }
+
+  /**
+   * @inheritDoc
+   */
+  forceToFocus() {
+    const editor = this.getCodeMirror();
+    // use setInterval with reluctance -- 2018.01.11 Yuki Takei
+    const intervalId = setInterval(() => {
+      this.getCodeMirror().focus();
+      if (editor.hasFocus()) {
+        clearInterval(intervalId);
+        // refresh
+        editor.refresh();
+      }
+    }, 100);
+  }
+
+  /**
+   * @inheritDoc
+   */
+  setCaretLine(line) {
+    if (isNaN(line)) {
+      return;
+    }
+
+    const editor = this.getCodeMirror();
+    const linePosition = Math.max(0, line);
+
+    editor.setCursor({line: linePosition});   // leave 'ch' field as null/undefined to indicate the end of line
+    this.setScrollTopByLine(linePosition);
+  }
+
+  /**
+   * @inheritDoc
+   */
+  setScrollTopByLine(line) {
+    if (isNaN(line)) {
+      return;
+    }
+
+    const editor = this.getCodeMirror();
+    // get top position of the line
+    var top = editor.charCoords({line, ch: 0}, 'local').top;
+    editor.scrollTo(null, top);
+  }
+
+  /**
+   * @inheritDoc
+   */
+  getStrFromBol() {
+    const editor = this.getCodeMirror();
+    const curPos = editor.getCursor();
+    return editor.getDoc().getRange(this.getBol(), curPos);
+  }
+
+  /**
+   * @inheritDoc
+   */
+  getStrToEol() {
+    const editor = this.getCodeMirror();
+    const curPos = editor.getCursor();
+    return editor.getDoc().getRange(curPos, this.getEol());
+  }
+
+  /**
+   * @inheritDoc
+   */
+  replaceBolToCurrentPos(text) {
+    const editor = this.getCodeMirror();
+    editor.getDoc().replaceRange(text, this.getBol(), editor.getCursor());
+  }
+
+  /**
+   * @inheritDoc
+   */
+  insertText(text) {
+    const editor = this.getCodeMirror();
+    editor.getDoc().replaceSelection(text);
+  }
+
+  /**
+   * return the postion of the BOL(beginning of line)
+   */
+  getBol() {
+    const editor = this.getCodeMirror();
+    const curPos = editor.getCursor();
+    return { line: curPos.line, ch: 0 };
+  }
+
+  /**
+   * return the postion of the EOL(end of line)
+   */
+  getEol() {
+    const editor = this.getCodeMirror();
+    const curPos = editor.getCursor();
+    const lineLength = editor.getDoc().getLine(curPos.line).length;
+    return { line: curPos.line, ch: lineLength };
+  }
+
+  loadCss(source) {
+    return new Promise((resolve) => {
+      loadCssSync(source);
+      resolve();
+    });
+  }
+
+  /**
+   * load Theme
+   * @see https://codemirror.net/doc/manual.html#config
+   *
+   * @param {string} theme
+   */
+  loadTheme(theme) {
+    if (!this.loadedThemeSet.has(theme)) {
+      this.loadCss(urljoin(this.cmCdnRoot, `theme/${theme}.min.css`));
+
+      // update Set
+      this.loadedThemeSet.add(theme);
+    }
+  }
+
+  /**
+   * load assets for Key Maps
+   * @param {*} keymapMode 'default' or 'vim' or 'emacs' or 'sublime'
+   */
+  loadKeymapMode(keymapMode) {
+    const loadCss = this.loadCss;
+    let scriptList = [];
+    let cssList = [];
+
+    // add dependencies
+    if (this.loadedKeymapSet.size == 0) {
+      scriptList.push(loadScript(urljoin(this.cmCdnRoot, 'addon/dialog/dialog.min.js')));
+      cssList.push(loadCss(urljoin(this.cmCdnRoot, 'addon/dialog/dialog.min.css')));
+    }
+    // load keymap
+    if (!this.loadedKeymapSet.has(keymapMode)) {
+      scriptList.push(loadScript(urljoin(this.cmCdnRoot, `keymap/${keymapMode}.min.js`)));
+      // update Set
+      this.loadedKeymapSet.add(keymapMode);
+    }
+
+    // set loading state
+    this.setState({ isLoadingKeymap: true });
+
+    return Promise.all(scriptList.concat(cssList))
+      .then(() => {
+        this.setState({ isLoadingKeymap: false });
+      });
+  }
+
+  /**
+   * set Key Maps
+   * @see https://codemirror.net/doc/manual.html#keymaps
+   *
+   * @param {string} keymapMode 'default' or 'vim' or 'emacs' or 'sublime'
+   */
+  setKeymapMode(keymapMode) {
+    if (!keymapMode.match(/^(vim|emacs|sublime)$/)) {
+      // reset
+      this.getCodeMirror().setOption('keyMap', 'default');
+      return;
+    }
+
+    this.loadKeymapMode(keymapMode)
+      .then(() => {
+        this.getCodeMirror().setOption('keyMap', keymapMode);
+      });
+  }
+
+  /**
+   * handle ENTER key
+   */
+  handleEnterKey() {
+    var context = {
+      handlers: [],  // list of handlers which process enter key
+      editor: this,
+    };
+
+    const interceptorManager = this.interceptorManager;
+    interceptorManager.process('preHandleEnter', context)
+      .then(() => {
+        if (context.handlers.length == 0) {
+          codemirror.commands.newlineAndIndentContinueMarkdownList(this.getCodeMirror());
+        }
+      });
+  }
+
+  scrollCursorIntoViewHandler(editor, event) {
+    if (this.props.onScrollCursorIntoView != null) {
+      const line = editor.getCursor().line;
+      this.props.onScrollCursorIntoView(line);
+    }
+  }
+
+  cursorHandler(editor, event) {
+    const strFromBol = this.getStrFromBol();
+    if (mtu.isEndOfLine(editor) && mtu.linePartOfTableRE.test(strFromBol)) {
+      this.setState({additionalClass: 'autoformat-markdown-table-activated'});
+    }
+    else {
+      this.setState({additionalClass: ''});
+    }
+  }
+
+  /**
+   * CodeMirror paste event handler
+   * see: https://codemirror.net/doc/manual.html#events
+   * @param {any} editor An editor instance of CodeMirror
+   * @param {any} event
+   */
+  pasteHandler(editor, event) {
+    const types = event.clipboardData.types;
+
+    // text
+    if (types.includes('text/plain')) {
+      pasteHelper.pasteText(this, event);
+    }
+    // files
+    else if (types.includes('Files')) {
+      this.dispatchPasteFiles(event);
+    }
+  }
+
+  getOverlayStyle() {
+    return {
+      position: 'absolute',
+      zIndex: 4,  // forward than .CodeMirror-gutters
+      top: 0,
+      right: 0,
+      bottom: 0,
+      left: 0,
+    };
+  }
+
+  renderLoadingKeymapOverlay() {
+    const overlayStyle = this.getOverlayStyle();
+
+    return this.state.isLoadingKeymap
+      ? <div style={overlayStyle} className="loading-keymap overlay">
+          <span className="overlay-content">
+            <div className="speeding-wheel d-inline-block"></div> Loading Keymap ...
+          </span>
+        </div>
+      : '';
+  }
+
+  render() {
+    const theme = this.props.editorOptions.theme || 'elegant';
+    const styleActiveLine = this.props.editorOptions.styleActiveLine || undefined;
+    return <React.Fragment>
+      <ReactCodeMirror
+        ref="cm"
+        className={this.state.additionalClass}
+        editorDidMount={(editor) => {
+          // add event handlers
+          editor.on('paste', this.pasteHandler);
+          editor.on('scrollCursorIntoView', this.scrollCursorIntoViewHandler);
+        }}
+        value={this.state.value}
+        options={{
+          mode: 'gfm',
+          theme: theme,
+          styleActiveLine: styleActiveLine,
+          lineNumbers: true,
+          tabSize: 4,
+          indentUnit: 4,
+          lineWrapping: true,
+          autoRefresh: true,
+          autoCloseTags: true,
+          matchBrackets: true,
+          matchTags: {bothTags: true},
+          // folding
+          foldGutter: true,
+          gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
+          // match-highlighter, matchesonscrollbar, annotatescrollbar options
+          highlightSelectionMatches: {annotateScrollbar: true},
+          // markdown mode options
+          highlightFormatting: true,
+          // continuelist, indentlist
+          extraKeys: {
+            'Enter': this.handleEnterKey,
+            'Tab': 'indentMore',
+            'Shift-Tab': 'indentLess',
+            'Ctrl-Q': (cm) => { cm.foldCode(cm.getCursor()) },
+          }
+        }}
+        onCursor={this.cursorHandler}
+        onScroll={(editor, data) => {
+          if (this.props.onScroll != null) {
+            // add line data
+            const line = editor.lineAtHeight(data.top, 'local');
+            data.line = line;
+            this.props.onScroll(data);
+          }
+        }}
+        onChange={(editor, data, value) => {
+          if (this.props.onChange != null) {
+            this.props.onChange(value);
+          }
+
+          // Emoji AutoComplete
+          if (this.state.isEnabledEmojiAutoComplete) {
+            this.emojiAutoCompleteHelper.showHint(editor);
+          }
+        }}
+        onDragEnter={(editor, event) => {
+          if (this.props.onDragEnter != null) {
+            this.props.onDragEnter(event);
+          }
+        }}
+      />
+
+      { this.renderLoadingKeymapOverlay() }
+
+    </React.Fragment>;
+  }
+
+}
+
+CodeMirrorEditor.propTypes = Object.assign({
+  emojiStrategy: PropTypes.object,
+}, AbstractEditor.propTypes);
+

+ 76 - 349
resource/js/components/PageEditor/Editor.js

@@ -1,234 +1,74 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
-import urljoin from 'url-join';
-const loadScript = require('simple-load-script');
-const loadCssSync = require('load-css-file');
-
-import * as codemirror from 'codemirror';
-
-import { UnControlled as ReactCodeMirror } from 'react-codemirror2';
-require('codemirror/addon/display/autorefresh');
-require('codemirror/addon/edit/matchbrackets');
-require('codemirror/addon/edit/matchtags');
-require('codemirror/addon/edit/closetag');
-require('codemirror/addon/edit/continuelist');
-require('codemirror/addon/hint/show-hint');
-require('codemirror/addon/hint/show-hint.css');
-require('codemirror/addon/search/searchcursor');
-require('codemirror/addon/search/match-highlighter');
-require('codemirror/addon/selection/active-line');
-require('codemirror/addon/scroll/annotatescrollbar');
-require('codemirror/addon/fold/foldcode');
-require('codemirror/addon/fold/foldgutter');
-require('codemirror/addon/fold/foldgutter.css');
-require('codemirror/addon/fold/markdown-fold');
-require('codemirror/addon/fold/brace-fold');
-require('codemirror/mode/gfm/gfm');
-
+import AbstractEditor from './AbstractEditor';
+import CodeMirrorEditor from './CodeMirrorEditor';
+import TextAreaEditor from './TextAreaEditor';
 
 import Dropzone from 'react-dropzone';
 
 import pasteHelper from './PasteHelper';
-import EmojiAutoCompleteHelper from './EmojiAutoCompleteHelper';
-
-import InterceptorManager from '../../../../lib/util/interceptor-manager';
-
-import MarkdownListInterceptor from './MarkdownListInterceptor';
-import MarkdownTableInterceptor from './MarkdownTableInterceptor';
 
-export default class Editor extends React.Component {
+export default class Editor extends AbstractEditor {
 
   constructor(props) {
     super(props);
 
-    this.cmCdnRoot = 'https://cdn.jsdelivr.net/npm/codemirror@5.37.0';
-
-    this.interceptorManager = new InterceptorManager();
-    this.interceptorManager.addInterceptors([
-      new MarkdownListInterceptor(),
-      new MarkdownTableInterceptor(),
-    ]);
-
     this.state = {
-      value: this.props.value,
       dropzoneActive: false,
-      isEnabledEmojiAutoComplete: false,
       isUploading: false,
-      isLoadingKeymap: false,
     };
 
-    this.loadedThemeSet = new Set(['eclipse', 'elegant']);   // themes imported in _vendor.scss
-    this.loadedKeymapSet = new Set();
-
-    this.getCodeMirror = this.getCodeMirror.bind(this);
-    this.setCaretLine = this.setCaretLine.bind(this);
-    this.setScrollTopByLine = this.setScrollTopByLine.bind(this);
-    this.loadTheme = this.loadTheme.bind(this);
-    this.loadKeymapMode = this.loadKeymapMode.bind(this);
-    this.setKeymapMode = this.setKeymapMode.bind(this);
-    this.forceToFocus = this.forceToFocus.bind(this);
-    this.dispatchSave = this.dispatchSave.bind(this);
-    this.handleEnterKey = this.handleEnterKey.bind(this);
+    this.getEditorSubstance = this.getEditorSubstance.bind(this);
 
-    this.onScrollCursorIntoView = this.onScrollCursorIntoView.bind(this);
-    this.onPaste = this.onPaste.bind(this);
+    this.pasteFilesHandler = this.pasteFilesHandler.bind(this);
 
-    this.onDragEnterForCM = this.onDragEnterForCM.bind(this);
-    this.onDragLeave = this.onDragLeave.bind(this);
-    this.onDrop = this.onDrop.bind(this);
+    this.dragEnterHandler = this.dragEnterHandler.bind(this);
+    this.dragLeaveHandler = this.dragLeaveHandler.bind(this);
+    this.dropHandler = this.dropHandler.bind(this);
 
     this.getDropzoneAccept = this.getDropzoneAccept.bind(this);
     this.getDropzoneClassName = this.getDropzoneClassName.bind(this);
     this.renderDropzoneOverlay = this.renderDropzoneOverlay.bind(this);
-
-    this.renderLoadingKeymapOverlay = this.renderLoadingKeymapOverlay.bind(this);
-  }
-
-  componentWillMount() {
-    if (this.props.emojiStrategy != null) {
-      this.emojiAutoCompleteHelper = new EmojiAutoCompleteHelper(this.props.emojiStrategy);
-      this.setState({isEnabledEmojiAutoComplete: true});
-    }
   }
 
   componentDidMount() {
     // initialize caret line
     this.setCaretLine(0);
-    // set save handler
-    codemirror.commands.save = this.dispatchSave;
-
-    // set CodeMirror instance as 'CodeMirror' so that CDN addons can reference
-    window.CodeMirror = require('codemirror');
   }
 
-  componentWillReceiveProps(nextProps) {
-    // load theme
-    const theme = nextProps.editorOptions.theme;
-    this.loadTheme(theme);
-
-    // set keymap
-    const keymapMode = nextProps.editorOptions.keymapMode;
-    this.setKeymapMode(keymapMode);
-  }
-
-  getCodeMirror() {
-    return this.refs.cm.editor;
-  }
-
-  loadCss(source) {
-    return new Promise((resolve) => {
-      loadCssSync(source);
-      resolve();
-    });
+  getEditorSubstance() {
+    return this.props.isMobile
+      ? this.refs.taEditor
+      : this.refs.cmEditor;
   }
 
+  /**
+   * @inheritDoc
+   */
   forceToFocus() {
-    const editor = this.getCodeMirror();
-    // use setInterval with reluctance -- 2018.01.11 Yuki Takei
-    const intervalId = setInterval(() => {
-      this.getCodeMirror().focus();
-      if (editor.hasFocus()) {
-        clearInterval(intervalId);
-        // refresh
-        editor.refresh();
-      }
-    }, 100);
+    this.getEditorSubstance().forceToFocus();
   }
 
   /**
-   * set caret position of codemirror
-   * @param {string} number
+   * @inheritDoc
    */
   setCaretLine(line) {
-    if (isNaN(line)) {
-      return;
-    }
-
-    const editor = this.getCodeMirror();
-    const linePosition = Math.max(0, line);
-
-    editor.setCursor({line: linePosition});   // leave 'ch' field as null/undefined to indicate the end of line
-    this.setScrollTopByLine(linePosition);
+    this.getEditorSubstance().setCaretLine(line);
   }
 
   /**
-   * scroll
-   * @param {number} line
+   * @inheritDoc
    */
   setScrollTopByLine(line) {
-    if (isNaN(line)) {
-      return;
-    }
-
-    const editor = this.getCodeMirror();
-    // get top position of the line
-    var top = editor.charCoords({line, ch: 0}, 'local').top;
-    editor.scrollTo(null, top);
-  }
-
-  /**
-   * load Theme
-   * @see https://codemirror.net/doc/manual.html#config
-   *
-   * @param {string} theme
-   */
-  loadTheme(theme) {
-    if (!this.loadedThemeSet.has(theme)) {
-      this.loadCss(urljoin(this.cmCdnRoot, `theme/${theme}.min.css`));
-
-      // update Set
-      this.loadedThemeSet.add(theme);
-    }
-  }
-
-  /**
-   * load assets for Key Maps
-   * @param {*} keymapMode 'default' or 'vim' or 'emacs' or 'sublime'
-   */
-  loadKeymapMode(keymapMode) {
-    const loadCss = this.loadCss;
-    let scriptList = [];
-    let cssList = [];
-
-    // add dependencies
-    if (this.loadedKeymapSet.size == 0) {
-      scriptList.push(loadScript(urljoin(this.cmCdnRoot, 'addon/dialog/dialog.min.js')));
-      cssList.push(loadCss(urljoin(this.cmCdnRoot, 'addon/dialog/dialog.min.css')));
-    }
-    // load keymap
-    if (!this.loadedKeymapSet.has(keymapMode)) {
-      scriptList.push(loadScript(urljoin(this.cmCdnRoot, `keymap/${keymapMode}.min.js`)));
-      // update Set
-      this.loadedKeymapSet.add(keymapMode);
-    }
-
-    // set loading state
-    this.setState({ isLoadingKeymap: true });
-
-    return Promise.all(scriptList.concat(cssList))
-      .then(() => {
-        this.setState({ isLoadingKeymap: false });
-      });
+    this.getEditorSubstance().setScrollTopByLine(line);
   }
 
   /**
-   * set Key Maps
-   * @see https://codemirror.net/doc/manual.html#keymaps
-   *
-   * @param {string} keymapMode 'default' or 'vim' or 'emacs' or 'sublime'
+   * @inheritDoc
    */
-  setKeymapMode(keymapMode) {
-    if (!keymapMode.match(/^(vim|emacs|sublime)$/)) {
-      // reset
-      this.getCodeMirror().setOption('keyMap', 'default');
-      return;
-    }
-
-    this.loadKeymapMode(keymapMode)
-      .then(() => {
-        this.getCodeMirror().setOption('keyMap', keymapMode);
-      });
+  insertText(text) {
+    this.getEditorSubstance().insertText(text);
   }
 
   /**
@@ -241,24 +81,6 @@ export default class Editor extends React.Component {
     });
   }
 
-  /**
-   * insert text
-   * @param {string} text
-   */
-  insertText(text) {
-    const editor = this.getCodeMirror();
-    editor.getDoc().replaceSelection(text);
-  }
-
-  /**
-   * dispatch onSave event
-   */
-  dispatchSave() {
-    if (this.props.onSave != null) {
-      this.props.onSave();
-    }
-  }
-
   /**
    * dispatch onUpload event
    */
@@ -268,68 +90,26 @@ export default class Editor extends React.Component {
     }
   }
 
-  /**
-   * handle ENTER key
-   */
-  handleEnterKey() {
-
-    const editor = this.getCodeMirror();
-    var context = {
-      handlers: [],  // list of handlers which process enter key
-      editor: editor,
-    };
-
-    const interceptorManager = this.interceptorManager;
-    interceptorManager.process('preHandleEnter', context)
-      .then(() => {
-        if (context.handlers.length == 0) {
-          codemirror.commands.newlineAndIndentContinueMarkdownList(editor);
-        }
-      });
-  }
-
-  onScrollCursorIntoView(editor, event) {
-    if (this.props.onScrollCursorIntoView != null) {
-      const line = editor.getCursor().line;
-      this.props.onScrollCursorIntoView(line);
-    }
-  }
-
-  /**
-   * CodeMirror paste event handler
-   * see: https://codemirror.net/doc/manual.html#events
-   * @param {any} editor An editor instance of CodeMirror
-   * @param {any} event
-   */
-  onPaste(editor, event) {
-    const types = event.clipboardData.types;
+  pasteFilesHandler(event) {
+    const dropzone = this.refs.dropzone;
+    const items = event.clipboardData.items || event.clipboardData.files || [];
 
-    // text
-    if (types.includes('text/plain')) {
-      pasteHelper.pasteText(editor, event);
+    // abort if length is not 1
+    if (items.length != 1) {
+      return;
     }
-    // files
-    else if (types.includes('Files')) {
-      const dropzone = this.refs.dropzone;
-      const items = event.clipboardData.items || event.clipboardData.files || [];
-
-      // abort if length is not 1
-      if (items.length != 1) {
-        return;
-      }
 
-      const file = items[0].getAsFile();
-      // check type and size
-      if (pasteHelper.fileAccepted(file, dropzone.props.accept) &&
-          pasteHelper.fileMatchSize(file, dropzone.props.maxSize, dropzone.props.minSize)) {
+    const file = items[0].getAsFile();
+    // check type and size
+    if (pasteHelper.fileAccepted(file, dropzone.props.accept) &&
+        pasteHelper.fileMatchSize(file, dropzone.props.maxSize, dropzone.props.minSize)) {
 
-        this.dispatchUpload(file);
-        this.setState({ isUploading: true });
-      }
+      this.dispatchUpload(file);
+      this.setState({ isUploading: true });
     }
   }
 
-  onDragEnterForCM(editor, event) {
+  dragEnterHandler(event) {
     const dataTransfer = event.dataTransfer;
 
     // do nothing if contents is not files
@@ -340,11 +120,11 @@ export default class Editor extends React.Component {
     this.setState({ dropzoneActive: true });
   }
 
-  onDragLeave() {
+  dragLeaveHandler() {
     this.setState({ dropzoneActive: false });
   }
 
-  onDrop(accepted, rejected) {
+  dropHandler(accepted, rejected) {
     // rejected
     if (accepted.length != 1) { // length should be 0 or 1 because `multiple={false}` is set
       this.setState({ dropzoneActive: false });
@@ -418,18 +198,6 @@ export default class Editor extends React.Component {
     );
   }
 
-  renderLoadingKeymapOverlay() {
-    const overlayStyle = this.getOverlayStyle();
-
-    return this.state.isLoadingKeymap
-      ? <div style={overlayStyle} className="loading-keymap overlay">
-          <span className="overlay-content">
-            <div className="speeding-wheel d-inline-block"></div> Loading Keymap ...
-          </span>
-        </div>
-      : '';
-  }
-
   render() {
     const flexContainer = {
       height: '100%',
@@ -437,79 +205,45 @@ export default class Editor extends React.Component {
       flexDirection: 'column',
     };
 
-    const theme = this.props.editorOptions.theme || 'elegant';
-    const styleActiveLine = this.props.editorOptions.styleActiveLine || undefined;
+    const isMobile = this.props.isMobile;
+
     return <React.Fragment>
       <div style={flexContainer}>
         <Dropzone
-          ref="dropzone"
-          disableClick
-          disablePreview={true}
-          accept={this.getDropzoneAccept()}
-          className={this.getDropzoneClassName()}
-          acceptClassName="dropzone-accepted"
-          rejectClassName="dropzone-rejected"
-          multiple={false}
-          onDragLeave={this.onDragLeave}
-          onDrop={this.onDrop}
-        >
+            ref="dropzone"
+            disableClick
+            disablePreview={true}
+            accept={this.getDropzoneAccept()}
+            className={this.getDropzoneClassName()}
+            acceptClassName="dropzone-accepted"
+            rejectClassName="dropzone-rejected"
+            multiple={false}
+            onDragLeave={this.dragLeaveHandler}
+            onDrop={this.dropHandler}
+          >
+
           { this.state.dropzoneActive && this.renderDropzoneOverlay() }
 
-          <ReactCodeMirror
-            ref="cm"
-            editorDidMount={(editor) => {
-              // add event handlers
-              editor.on('paste', this.onPaste);
-              editor.on('scrollCursorIntoView', this.onScrollCursorIntoView);
-            }}
-            value={this.state.value}
-            options={{
-              mode: 'gfm',
-              theme: theme,
-              styleActiveLine: styleActiveLine,
-              lineNumbers: true,
-              tabSize: 4,
-              indentUnit: 4,
-              lineWrapping: true,
-              autoRefresh: true,
-              autoCloseTags: true,
-              matchBrackets: true,
-              matchTags: {bothTags: true},
-              // folding
-              foldGutter: true,
-              gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
-              // match-highlighter, matchesonscrollbar, annotatescrollbar options
-              highlightSelectionMatches: {annotateScrollbar: true},
-              // markdown mode options
-              highlightFormatting: true,
-              // continuelist, indentlist
-              extraKeys: {
-                'Enter': this.handleEnterKey,
-                'Tab': 'indentMore',
-                'Shift-Tab': 'indentLess',
-                'Ctrl-Q': (cm) => { cm.foldCode(cm.getCursor()) },
-              }
-            }}
-            onScroll={(editor, data) => {
-              if (this.props.onScroll != null) {
-                // add line data
-                const line = editor.lineAtHeight(data.top, 'local');
-                data.line = line;
-                this.props.onScroll(data);
-              }
-            }}
-            onChange={(editor, data, value) => {
-              if (this.props.onChange != null) {
-                this.props.onChange(value);
-              }
-
-              // Emoji AutoComplete
-              if (this.state.isEnabledEmojiAutoComplete) {
-                this.emojiAutoCompleteHelper.showHint(editor);
-              }
-            }}
-            onDragEnter={this.onDragEnterForCM}
-          />
+          {/* for PC */}
+          { !isMobile &&
+            <CodeMirrorEditor
+              ref="cmEditor"
+              onPasteFiles={this.pasteFilesHandler}
+              onDragEnter={this.dragEnterHandler}
+              {...this.props}
+            />
+          }
+
+          {/* for mobile */}
+          { isMobile &&
+            <TextAreaEditor
+              ref="taEditor"
+              onPasteFiles={this.pasteFilesHandler}
+              onDragEnter={this.dragEnterHandler}
+              {...this.props}
+            />
+          }
+
         </Dropzone>
 
         <button type="button" className="btn btn-default btn-block btn-open-dropzone"
@@ -521,8 +255,6 @@ export default class Editor extends React.Component {
           or pasting from the clipboard.
         </button>
 
-        { this.renderLoadingKeymapOverlay() }
-
       </div>
 
     </React.Fragment>;
@@ -530,17 +262,12 @@ export default class Editor extends React.Component {
 
 }
 
-Editor.propTypes = {
-  value: PropTypes.string,
-  options: PropTypes.object,
-  editorOptions: PropTypes.object,
+Editor.propTypes = Object.assign({
+  isMobile: PropTypes.bool,
   isUploadable: PropTypes.bool,
   isUploadableFile: PropTypes.bool,
   emojiStrategy: PropTypes.object,
   onChange: PropTypes.func,
-  onScroll: PropTypes.func,
-  onScrollCursorIntoView: PropTypes.func,
-  onSave: PropTypes.func,
   onUpload: PropTypes.func,
-};
+}, AbstractEditor.propTypes);
 

+ 270 - 0
resource/js/components/PageEditor/GrantSelector.js

@@ -0,0 +1,270 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { translate } from 'react-i18next';
+
+import FormGroup from 'react-bootstrap/es/FormGroup';
+import FormControl from 'react-bootstrap/es/FormControl';
+import ListGroup from 'react-bootstrap/es/ListGroup';
+import ListGroupItem from 'react-bootstrap/es/ListGroupItem';
+import Modal from 'react-bootstrap/es/Modal';
+
+const SPECIFIED_GROUP_VALUE = 'specifiedGroup';
+
+/**
+ * Page grant select component
+ *
+ * @export
+ * @class GrantSelector
+ * @extends {React.Component}
+ */
+class GrantSelector extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.availableGrants = [
+      { pageGrant: 1, iconClass: 'icon-people', styleClass: '', label: 'Public' },
+      { pageGrant: 2, iconClass: 'icon-link', styleClass: 'text-info', label: 'Anyone with the link' },
+      // { pageGrant: 3, iconClass: '', label: 'Specified users only' },
+      { pageGrant: 4, iconClass: 'icon-lock', styleClass: 'text-danger', label: 'Just me' },
+      { pageGrant: 5, iconClass: 'icon-options', styleClass: '', label: 'Only inside the group' },  // appeared only one of these 'pageGrant: 5'
+      { pageGrant: 5, iconClass: 'icon-options', styleClass: '', label: 'Reselect the group' },     // appeared only one of these 'pageGrant: 5'
+    ];
+
+    this.state = {
+      pageGrant: this.props.pageGrant || 1,  // default: 1
+      userRelatedGroups: [],
+      isSelectGroupModalShown: false,
+    };
+    if (this.props.pageGrantGroupId !== '') {
+      this.state.pageGrantGroup = {
+        _id: this.props.pageGrantGroupId,
+        name: this.props.pageGrantGroupName
+      };
+    }
+
+    this.showSelectGroupModal = this.showSelectGroupModal.bind(this);
+    this.hideSelectGroupModal = this.hideSelectGroupModal.bind(this);
+
+    this.getGroupName = this.getGroupName.bind(this);
+
+    this.changeGrantHandler = this.changeGrantHandler.bind(this);
+    this.groupListItemClickHandler = this.groupListItemClickHandler.bind(this);
+  }
+
+  componentDidUpdate(prevProps, prevState) {
+    /*
+     * set SPECIFIED_GROUP_VALUE to grant selector
+     *  cz: bootstrap-select input element has the defferent state to React component
+     */
+    if (this.state.pageGrantGroup != null) {
+      this.grantSelectorInputEl.value = SPECIFIED_GROUP_VALUE;
+    }
+
+    // refresh bootstrap-select
+    // see https://silviomoreto.github.io/bootstrap-select/methods/#selectpickerrefresh
+    $('.page-grant-selector.selectpicker').selectpicker('refresh');
+    //// DIRTY HACK -- 2018.05.25 Yuki Takei
+    // set group name to the bootstrap-select options
+    //  cz: .selectpicker('refresh') doesn't replace data-content
+    $('.page-grant-selector .group-name').text(this.getGroupName());
+
+  }
+
+  showSelectGroupModal() {
+    this.retrieveUserGroupRelations();
+    this.setState({ isSelectGroupModalShown: true });
+  }
+  hideSelectGroupModal() {
+    this.setState({ isSelectGroupModalShown: false });
+  }
+
+  getGroupName() {
+    const pageGrantGroup = this.state.pageGrantGroup;
+    return pageGrantGroup ? pageGrantGroup.name : '';
+  }
+
+  /**
+   * Retrieve user-group-relations data from backend
+   */
+  retrieveUserGroupRelations() {
+    this.props.crowi.apiGet('/me/user-group-relations')
+      .then(res => {
+        return res.userGroupRelations;
+      })
+      .then(userGroupRelations => {
+        const userRelatedGroups = userGroupRelations.map(relation => {
+          return relation.relatedGroup;
+        });
+        this.setState({userRelatedGroups});
+      });
+  }
+
+  /**
+   * change event handler for pageGrant selector
+   */
+  changeGrantHandler() {
+    const pageGrant = +this.grantSelectorInputEl.value;
+
+    // select group
+    if (pageGrant === 5) {
+      this.showSelectGroupModal();
+      /*
+       * reset grant selector to state
+       */
+      this.grantSelectorInputEl.value = this.state.pageGrant;
+      return;
+    }
+
+    this.setState({ pageGrant, pageGrantGroup: null });
+    // dispatch event
+    this.dispatchOnChangePageGrant(pageGrant);
+    this.dispatchOnDeterminePageGrantGroup(null);
+  }
+
+  groupListItemClickHandler(pageGrantGroup) {
+    this.setState({ pageGrant: 5, pageGrantGroup });
+
+    // dispatch event
+    this.dispatchOnChangePageGrant(5);
+    this.dispatchOnDeterminePageGrantGroup(pageGrantGroup);
+
+    // hide modal
+    this.hideSelectGroupModal();
+  }
+
+  dispatchOnChangePageGrant(pageGrant) {
+    if (this.props.onChangePageGrant != null) {
+      this.props.onChangePageGrant(pageGrant);
+    }
+  }
+
+  dispatchOnDeterminePageGrantGroup(pageGrantGroup) {
+    if (this.props.onDeterminePageGrantGroupId != null) {
+      this.props.onDeterminePageGrantGroupId(pageGrantGroup ? pageGrantGroup._id : '');
+    }
+    if (this.props.onDeterminePageGrantGroupName != null) {
+      this.props.onDeterminePageGrantGroupName(pageGrantGroup ? pageGrantGroup.name : '');
+    }
+  }
+
+  /**
+   * Render grant selector DOM.
+   * @returns
+   * @memberof GrantSelector
+   */
+  renderGrantSelector() {
+    const { t } = this.props;
+
+    let index = 0;
+    let selectedValue = this.state.pageGrant;
+    const grantElems = this.availableGrants.map((grant) => {
+      const dataContent = `<i class="icon icon-fw ${grant.iconClass} ${grant.styleClass}"></i> <span class="${grant.styleClass}">${t(grant.label)}</span>`;
+      return <option key={index++} value={grant.pageGrant} data-content={dataContent}>{t(grant.label)}</option>;
+    });
+
+    const pageGrantGroup = this.state.pageGrantGroup;
+    if (pageGrantGroup != null) {
+      selectedValue = SPECIFIED_GROUP_VALUE;
+      // DIRTY HACK -- 2018.05.25 Yuki Takei
+      // remove 'Only inside the group' item
+      //  cz: .selectpicker('refresh') doesn't replace data-content
+      grantElems.splice(3, 1);
+    }
+    else {
+      // DIRTY HACK -- 2018.05.25 Yuki Takei
+      // remove 'Reselect the group' item
+      //  cz: .selectpicker('refresh') doesn't replace data-content
+      grantElems.splice(4, 1);
+    }
+
+    /*
+     * react-bootstrap couldn't be rendered only with React feature.
+     * see also 'componentDidUpdate'
+     */
+
+    // add specified group option
+    grantElems.push(
+      <option ref="specifiedGroupOption" key="specifiedGroupKey" value={SPECIFIED_GROUP_VALUE} style={{ display: pageGrantGroup ? 'inherit' : 'none' }}
+          data-content={`<i class="icon icon-fw icon-organization text-success"></i> <span class="group-name text-success">${this.getGroupName()}</span>`}>
+        {this.getGroupName()}
+      </option>
+    );
+
+    const bsClassName = 'form-control-dummy'; // set form-control* to shrink width
+    return (
+      <FormGroup className="m-b-0">
+        <FormControl componentClass="select" placeholder="select" defaultValue={selectedValue} bsClass={bsClassName} className="btn-group-sm page-grant-selector selectpicker"
+          onChange={this.changeGrantHandler}
+          inputRef={ el => this.grantSelectorInputEl=el }>
+
+          {grantElems}
+
+        </FormControl>
+      </FormGroup>
+    );
+  }
+
+  /**
+   * Render select grantgroup modal.
+   *
+   * @returns
+   * @memberof GrantSelector
+   */
+  renderSelectGroupModal() {
+    const generateGroupListItems = () => {
+      return this.state.userRelatedGroups.map((group) => {
+        return <ListGroupItem key={group._id} header={group.name} onClick={() => { this.groupListItemClickHandler(group) }}>
+            (TBD) List group members
+          </ListGroupItem>;
+      });
+    };
+
+    let content = this.state.userRelatedGroups.length === 0
+      ? <div>
+          <h4>There is no group to which you belong.</h4>
+          { this.props.crowi.isAdmin &&
+            <p><a href="/admin/user-groups"><i className="icon icon-fw icon-login"></i> Manage Groups</a></p>
+          }
+        </div>
+      : <ListGroup>
+        {generateGroupListItems()}
+      </ListGroup>;
+
+    return (
+        <Modal className="select-grant-group"
+          container={this} show={this.state.isSelectGroupModalShown} onHide={this.hideSelectGroupModal}
+        >
+          <Modal.Header closeButton>
+            <Modal.Title>
+              Select a Group
+            </Modal.Title>
+          </Modal.Header>
+          <Modal.Body>
+            {content}
+          </Modal.Body>
+        </Modal>
+    );
+  }
+
+  render() {
+    return <React.Fragment>
+      <div className="m-r-5">{this.renderGrantSelector()}</div>
+      {this.renderSelectGroupModal()}
+    </React.Fragment>;
+  }
+}
+
+GrantSelector.propTypes = {
+  t: PropTypes.func.isRequired,               // i18next
+  crowi: PropTypes.object.isRequired,
+  isGroupModalShown: PropTypes.bool,
+  pageGrant: PropTypes.number,
+  pageGrantGroupId: PropTypes.string,
+  pageGrantGroupName: PropTypes.string,
+  onChangePageGrant: PropTypes.func,
+  onDeterminePageGrantGroupId: PropTypes.func,
+  onDeterminePageGrantGroupName: PropTypes.func,
+};
+
+export default translate()(GrantSelector);

+ 24 - 53
resource/js/components/PageEditor/MarkdownListUtil.js

@@ -1,5 +1,3 @@
-import * as codemirror from 'codemirror';
-
 /**
  * Utility for markdown list
  */
@@ -11,26 +9,41 @@ class MarkdownListUtil {
     this.indentAndMarkRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/;
     this.indentAndMarkOnlyRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/;
 
+    this.newlineAndIndentContinueMarkdownList = this.newlineAndIndentContinueMarkdownList.bind(this);
     this.pasteText = this.pasteText.bind(this);
+  }
+
+  /**
+   * Self Implementation with AbstractEditor interface
+   * @param {AbstractEditor} editor An instance of AbstractEditor
+   */
+  newlineAndIndentContinueMarkdownList(editor) {
+    const strFromBol = editor.getStrFromBol();
 
-    this.getBol = this.getBol.bind(this);
-    this.getEol = this.getEol.bind(this);
-    this.getStrFromBol = this.getStrFromBol.bind(this);
-    this.getStrToEol = this.getStrToEol.bind(this);
-    this.newlineWithoutIndent = this.newlineWithoutIndent.bind(this);
+    if (this.indentAndMarkOnlyRE.test(strFromBol)) {
+      // clear current line and end list
+      editor.replaceBolToCurrentPos('\n');
+    }
+    else if (this.indentAndMarkRE.test(strFromBol)) {
+      // continue list
+      const indentAndMark = strFromBol.match(this.indentAndMarkRE)[0];
+      editor.insertText(`\n${indentAndMark}`);
+    }
+    else {
+      editor.insertLinebreak();
+    }
   }
 
   /**
    * paste text
-   * @param {any} editor An editor instance of CodeMirror
+   * @param {AbstractEditor} editor An instance of AbstractEditor
    * @param {any} event
    * @param {string} text
    */
   pasteText(editor, event, text) {
     // get strings from BOL(beginning of line) to current position
-    const strFromBol = this.getStrFromBol(editor);
+    const strFromBol = editor.getStrFromBol();
 
-    const matched = strFromBol.match(this.indentAndMarkRE);
     // when match indentAndMarkOnlyRE
     // (this means the current position is the beginning of the list item)
     if (this.indentAndMarkOnlyRE.test(strFromBol)) {
@@ -39,7 +52,7 @@ class MarkdownListUtil {
       // replace
       if (adjusted != null) {
         event.preventDefault();
-        editor.getDoc().replaceRange(adjusted, this.getBol(editor), editor.getCursor());
+        editor.replaceBolToCurrentPos(adjusted);
       }
     }
   }
@@ -112,48 +125,6 @@ class MarkdownListUtil {
     return isListful;
   }
 
-  /**
-   * return the postion of the BOL(beginning of line)
-   */
-  getBol(editor) {
-    const curPos = editor.getCursor();
-    return { line: curPos.line, ch: 0 };
-  }
-
-  /**
-   * return the postion of the EOL(end of line)
-   */
-  getEol(editor) {
-    const curPos = editor.getCursor();
-    const lineLength = editor.getDoc().getLine(curPos.line).length;
-    return { line: curPos.line, ch: lineLength };
-  }
-
-  /**
-   * return strings from BOL(beginning of line) to current position
-   */
-  getStrFromBol(editor) {
-    const curPos = editor.getCursor();
-    return editor.getDoc().getRange(this.getBol(editor), curPos);
-  }
-
-  /**
-   * return strings from current position to EOL(end of line)
-   */
-  getStrToEol(editor) {
-    const curPos = editor.getCursor();
-    return editor.getDoc().getRange(curPos, this.getEol(editor));
-  }
-
-  /**
-   * insert newline without indent
-   */
-  newlineWithoutIndent(editor, strToEol) {
-    codemirror.commands.newlineAndIndent(editor);
-
-    // replace the line with strToEol (abort auto indent)
-    editor.getDoc().replaceRange(strToEol, this.getBol(editor), this.getEol(editor));
-  }
 }
 
 // singleton pattern

+ 13 - 6
resource/js/components/PageEditor/MarkdownTableInterceptor.js

@@ -32,25 +32,32 @@ export default class MarkdownTableInterceptor extends BasicInterceptor {
    */
   process(contextName, ...args) {
     const context = Object.assign(args[0]);   // clone
-    const editor = context.editor;
+    const editor = context.editor;            // AbstractEditor instance
+
+    // do nothing if editor is not a CodeMirrorEditor
+    if (editor == null || editor.getCodeMirror() == null) {
+      return Promise.resolve(context);
+    }
+
+    const cm = editor.getCodeMirror();
 
     // get strings from BOL(beginning of line) to current position
-    const strFromBol = mtu.getStrFromBol(editor);
+    const strFromBol = editor.getStrFromBol();
 
-    if (mtu.isEndOfLine(editor) && mtu.linePartOfTableRE.test(strFromBol)) {
+    if (mtu.isEndOfLine(cm) && mtu.linePartOfTableRE.test(strFromBol)) {
       // get lines all of table from current position to beginning of table
-      const strFromBot = mtu.getStrFromBot(editor);
+      const strFromBot = mtu.getStrFromBot(cm);
       let table = mtu.parseFromTableStringToMarkdownTable(strFromBot);
 
       mtu.addRowToMarkdownTable(table);
 
-      const strToEot = mtu.getStrToEot(editor);
+      const strToEot = mtu.getStrToEot(cm);
       const tableBottom = mtu.parseFromTableStringToMarkdownTable(strToEot);
       if (tableBottom.table.length > 0) {
         table = mtu.mergeMarkdownTable([table, tableBottom]);
       }
 
-      mtu.replaceMarkdownTableWithReformed(editor, table);
+      mtu.replaceMarkdownTableWithReformed(cm, table);
 
       // report to manager that handling was done
       context.handlers.push(this.className);

+ 5 - 13
resource/js/components/PageEditor/MarkdownTableUtil.js

@@ -1,4 +1,5 @@
-import markdown_table from 'markdown-table';
+import markdownTable from 'markdown-table';
+import stringWidth from 'string-width';
 
 /**
  * Utility for markdown table
@@ -10,14 +11,13 @@ class MarkdownTableUtil {
     // https://regex101.com/r/7BN2fR/7
     this.tableAlignmentLineRE = /^[-:|][-:|\s]*$/;
     this.tableAlignmentLineNegRE = /^[^-:]*$/;  // it is need to check to ignore empty row which is matched above RE
-    this.linePartOfTableRE = /^\|[^\r\n]*|[^\r\n]*\|$|([^\|\r\n]+\|[^\|\r\n]*)+/; // own idea
+    this.linePartOfTableRE = /^\|[^\r\n]*|[^\r\n]*\|$|([^|\r\n]+\|[^|\r\n]*)+/; // own idea
 
     this.getBot = this.getBot.bind(this);
     this.getEot = this.getEot.bind(this);
     this.getBol = this.getBol.bind(this);
     this.getStrFromBot = this.getStrFromBot.bind(this);
     this.getStrToEot = this.getStrToEot.bind(this);
-    this.getStrFromBol = this.getStrFromBol.bind(this);
 
     this.parseFromTableStringToMarkdownTable = this.parseFromTableStringToMarkdownTable.bind(this);
     this.replaceMarkdownTableWithReformed = this.replaceMarkdownTableWithReformed.bind(this);
@@ -84,14 +84,6 @@ class MarkdownTableUtil {
     return editor.getDoc().getRange(curPos, this.getEot(editor));
   }
 
-  /**
-   * return strings from BOL(beginning of line) to current position
-   */
-  getStrFromBol(editor) {
-    const curPos = editor.getCursor();
-    return editor.getDoc().getRange(this.getBol(editor), curPos);
-  }
-
   /**
    * returns markdown table whose described by 'markdown-table' format
    *   ref. https://github.com/wooorm/markdown-table
@@ -128,7 +120,7 @@ class MarkdownTableUtil {
         contents.push(row);
       }
     }
-    return (new MarkdownTable(contents, { align: aligns }));
+    return (new MarkdownTable(contents, { align: aligns, stringLength: stringWidth }));
   }
 
   /**
@@ -200,7 +192,7 @@ class MarkdownTable {
   }
 
   toString() {
-    return markdown_table(this.table, this.options);
+    return markdownTable(this.table, this.options);
   }
 }
 

+ 5 - 4
resource/js/components/PageEditor/MarkdownListInterceptor.js → resource/js/components/PageEditor/PreventMarkdownListInterceptor.js

@@ -1,7 +1,7 @@
 import { BasicInterceptor } from 'growi-pluginkit';
 import mlu from './MarkdownListUtil';
 
-export default class MarkdownListInterceptor extends BasicInterceptor {
+export default class PreventMarkdownListInterceptor extends BasicInterceptor {
 
   constructor() {
     super();
@@ -28,12 +28,13 @@ export default class MarkdownListInterceptor extends BasicInterceptor {
    */
   process(contextName, ...args) {
     const context = Object.assign(args[0]);   // clone
-    const editor = context.editor;
+    const editor = context.editor;            // AbstractEditor instance
 
     // get strings from current position to EOL(end of line) before break the line
-    const strToEol = mlu.getStrToEol(editor);
+    const strToEol = editor.getStrToEol();
     if (mlu.indentAndMarkRE.test(strToEol)) {
-      mlu.newlineWithoutIndent(editor, strToEol);
+      // newline simply
+      editor.insertLinebreak();
 
       // report to manager that handling was done
       context.handlers.push(this.className);

+ 224 - 0
resource/js/components/PageEditor/TextAreaEditor.js

@@ -0,0 +1,224 @@
+import React from 'react';
+// import PropTypes from 'prop-types';
+
+import FormControl from 'react-bootstrap/es/FormControl';
+
+import AbstractEditor from './AbstractEditor';
+
+import pasteHelper from './PasteHelper';
+import mlu from './MarkdownListUtil';
+
+import InterceptorManager from '../../../../lib/util/interceptor-manager';
+
+import PreventMarkdownListInterceptor from './PreventMarkdownListInterceptor';
+
+export default class TextAreaEditor extends AbstractEditor {
+
+  constructor(props) {
+    super(props);
+    this.logger = require('@alias/logger')('growi:PageEditor:TextAreaEditor');
+
+    this.state = {
+      value: this.props.value,
+    };
+
+    this.init();
+
+    this.handleEnterKey = this.handleEnterKey.bind(this);
+
+    this.keyPressHandler = this.keyPressHandler.bind(this);
+    this.pasteHandler = this.pasteHandler.bind(this);
+    this.dragEnterHandler = this.dragEnterHandler.bind(this);
+  }
+
+  init() {
+    this.interceptorManager = new InterceptorManager();
+    this.interceptorManager.addInterceptors([
+      new PreventMarkdownListInterceptor(),
+    ]);
+  }
+
+  componentDidMount() {
+    // initialize caret line
+    this.setCaretLine(0);
+
+    // set event handlers
+    this.textarea.addEventListener('keypress', this.keyPressHandler);
+    this.textarea.addEventListener('paste', this.pasteHandler);
+    this.textarea.addEventListener('dragenter', this.dragEnterHandler);
+  }
+
+  /**
+   * @inheritDoc
+   */
+  forceToFocus() {
+    setTimeout(() => {
+      this.textarea.focus();
+    }, 150);
+  }
+
+  /**
+   * @inheritDoc
+   */
+  setCaretLine(line) {
+    if (isNaN(line)) {
+      return;
+    }
+
+    // scroll to bottom
+    this.textarea.scrollTop = this.textarea.scrollHeight;
+
+    const lines = this.textarea.value.split('\n').slice(0, line+1);
+    const pos = lines
+        .map(lineStr => lineStr.length + 1) // correct length+1 of each lines
+        .reduce((a, x) => a += x, 0)        // sum
+        - 1;                                // -1
+
+    this.textarea.setSelectionRange(pos, pos);
+  }
+
+  /**
+   * @inheritDoc
+   */
+  setScrollTopByLine(line) {
+    // do nothing
+  }
+
+  /**
+   * @inheritDoc
+   */
+  insertText(text) {
+    const startPos = this.textarea.selectionStart;
+    const endPos = this.textarea.selectionEnd;
+    this.replaceValue(text, startPos, endPos);
+  }
+
+  /**
+   * @inheritDoc
+   */
+  getStrFromBol() {
+    const currentPos = this.textarea.selectionStart;
+    return this.textarea.value.substring(this.getBolPos(), currentPos);
+  }
+
+  /**
+   * @inheritDoc
+   */
+  getStrToEol() {
+    const currentPos = this.textarea.selectionStart;
+    return this.textarea.value.substring(currentPos, this.getEolPos());
+  }
+
+  /**
+   * @inheritDoc
+   */
+  replaceBolToCurrentPos(text) {
+    const currentPos = this.textarea.selectionStart;
+    this.replaceValue(text, this.getBolPos(), currentPos);
+  }
+
+  getBolPos() {
+    const currentPos = this.textarea.selectionStart;
+    return this.textarea.value.lastIndexOf('\n', currentPos-1) + 1;
+  }
+
+  getEolPos() {
+    const currentPos = this.textarea.selectionStart;
+    const pos = this.textarea.value.indexOf('\n', currentPos);
+    if (pos < 0) {  // not found but EOF
+      return this.textarea.value.length;
+    }
+    return pos;
+  }
+
+  replaceValue(text, startPos, endPos) {
+    // create new value
+    const value = this.textarea.value;
+    const newValue = value.substring(0, startPos) + text + value.substring(endPos, value.length);
+    // calculate new position
+    const newPos = startPos + text.length;
+
+    this.textarea.value = newValue;
+    this.textarea.setSelectionRange(newPos, newPos);
+  }
+
+  /**
+   * keypress event handler
+   * @param {string} event
+   */
+  keyPressHandler(event) {
+    const key = event.key.toLowerCase();
+    if (key === 'enter') {
+      if (event.ctrlKey || event.altKey || event.metaKey) {
+        return;
+      }
+
+      event.preventDefault();
+      this.handleEnterKey();
+    }
+  }
+
+  /**
+   * handle ENTER key
+   */
+  handleEnterKey() {
+    var context = {
+      handlers: [],  // list of handlers which process enter key
+      editor: this,
+    };
+
+    const interceptorManager = this.interceptorManager;
+    interceptorManager.process('preHandleEnter', context)
+      .then(() => {
+        if (context.handlers.length == 0) {
+          mlu.newlineAndIndentContinueMarkdownList(this);
+        }
+      });
+  }
+
+  /**
+   * paste event handler
+   * @param {any} event
+   */
+  pasteHandler(event) {
+    const types = event.clipboardData.types;
+
+    // text
+    if (types.includes('text/plain')) {
+      pasteHelper.pasteText(this, event);
+    }
+    // files
+    else if (types.includes('Files')) {
+      this.dispatchPasteFiles(event);
+    }
+  }
+
+  dragEnterHandler(event) {
+    this.dispatchDragEnter(event);
+  }
+
+  dispatchDragEnter(event) {
+    if (this.props.onDragEnter != null) {
+      this.props.onDragEnter(event);
+    }
+  }
+
+  render() {
+    return <React.Fragment>
+      <FormControl
+        componentClass="textarea" className="textarea-editor"
+        inputRef={ref => { this.textarea = ref }}
+        defaultValue={this.state.value}
+        onChange={(e) => {
+          if (this.props.onChange != null) {
+            this.props.onChange(e.target.value);
+          }
+        }} />
+    </React.Fragment>;
+  }
+
+}
+
+TextAreaEditor.propTypes = Object.assign({
+}, AbstractEditor.propTypes);
+

+ 44 - 0
resource/js/i18n.js

@@ -0,0 +1,44 @@
+import i18n from 'i18next';
+import LanguageDetector from 'i18next-browser-languagedetector';
+import { reactI18nextModule } from 'react-i18next';
+
+import resources from '@alias/locales';
+
+export default (userlang) => {
+  // setup LanguageDetector
+  const langDetector = new LanguageDetector();
+  langDetector.addDetector({
+    name: 'userSettingDetector',
+    lookup(options) {
+      return userlang;
+    },
+    cacheUserlanguage(lng, options) {
+    },
+  });
+
+  return i18n
+    .use(langDetector)
+    .use(reactI18nextModule) // if not using I18nextProvider
+    .init({
+      debug: (process.env.NODE_ENV !== 'production'),
+      resources,
+      load: 'currentOnly',
+
+      fallbackLng: 'en-US',
+      detection: {
+        order: ['userSettingDetector', 'querystring', 'localStorage'],
+      },
+
+      interpolation: {
+        escapeValue: false, // not needed for react!!
+      },
+
+      // react i18next special options (optional)
+      react: {
+        wait: false,
+        bindI18n: 'languageChanged loaded',
+        bindStore: 'added removed',
+        nsMode: 'default'
+      }
+    });
+};

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

@@ -83,8 +83,8 @@ Crowi.setCaretLineAndFocusToEditor = function() {
 
   const line = pageEditorDom.getAttribute('data-caret-line');
 
-  if (line != null) {
-    crowi.setCaretLine(line);
+  if (line != null && !isNaN(line)) {
+    crowi.setCaretLine(+line);
     // reset data-caret-line attribute
     pageEditorDom.removeAttribute('data-caret-line');
   }

+ 6 - 4
resource/js/util/Crowi.js

@@ -18,6 +18,9 @@ export default class Crowi {
     this.config = {};
     this.csrfToken = context.csrfToken;
 
+    const userAgent = window.navigator.userAgent.toLowerCase();
+    this.isMobile = /iphone|ipad|android/.test(userAgent);
+
     this.window = window;
     this.location = window.location || {};
     this.document = window.document || {};
@@ -30,13 +33,12 @@ export default class Crowi {
     this.apiRequest = this.apiRequest.bind(this);
 
     this.interceptorManager = new InterceptorManager();
-    this.interceptorManager.addInterceptors([
-      new DetachCodeBlockInterceptor(this),
-      new RestoreCodeBlockInterceptor(this),
-    ]);
+    this.interceptorManager.addInterceptor(new DetachCodeBlockInterceptor(this), 10);       // process as soon as possible
+    this.interceptorManager.addInterceptor(new RestoreCodeBlockInterceptor(this), 900);     // process as late as possible
 
     // FIXME
     this.me = context.me;
+    this.isAdmin = context.isAdmin;
 
     this.users = [];
     this.userByName = {};

+ 7 - 4
resource/js/util/GrowiRenderer.js

@@ -139,6 +139,9 @@ export default class GrowiRenderer {
   }
 
   codeRenderer(code, langExt) {
+    const config = this.crowi.getConfig();
+    const noborder = (!config.highlightJsStyleBorder) ? 'hljs-no-border' : '';
+
     if (langExt) {
       const langAndFn = langExt.split(':');
       const lang = langAndFn[0];
@@ -152,18 +155,18 @@ export default class GrowiRenderer {
       const citeTag = (langFn) ? `<cite>${langFn}</cite>` : '';
       if (hljs.getLanguage(lang)) {
         try {
-          return `<pre class="hljs">${citeTag}<code class="language-${lang}">${hljs.highlight(lang, code, true).value}</code></pre>`;
+          return `<pre class="hljs ${noborder}">${citeTag}<code class="language-${lang}">${hljs.highlight(lang, code, true).value}</code></pre>`;
         }
         catch (__) {
-          return `<pre class="hljs">${citeTag}<code class="language-${lang}">${code}}</code></pre>`;
+          return `<pre class="hljs ${noborder}">${citeTag}<code class="language-${lang}">${code}}</code></pre>`;
         }
       }
       else {
-        return `<pre class="hljs">${citeTag}<code>${code}</code></pre>`;
+        return `<pre class="hljs ${noborder}">${citeTag}<code>${code}</code></pre>`;
       }
     }
 
-    return `<pre class="hljs"><code>${code}</code></pre>`;
+    return `<pre class="hljs ${noborder}"><code>${code}</code></pre>`;
   }
 
 }

+ 36 - 12
resource/js/util/interceptor/detach-code-blocks.js

@@ -3,7 +3,7 @@ import { BasicInterceptor } from 'growi-pluginkit';
 
 class DetachCodeBlockUtil {
   static createReplaceStr(replaceId) {
-    return `<pre>${replaceId}</pre>`;
+    return `<pre class="detached-code-block">${replaceId}</pre>`;
   }
 }
 
@@ -14,6 +14,8 @@ export class DetachCodeBlockInterceptor extends BasicInterceptor {
 
   constructor(crowi) {
     super();
+    this.logger = require('@alias/logger')('growi:DetachCodeBlockInterceptor');
+
     this.crowi = crowi;
     this.crowiForJquery = crowi.getCrowiForJquery();
   }
@@ -22,27 +24,37 @@ export class DetachCodeBlockInterceptor extends BasicInterceptor {
    * @inheritdoc
    */
   isInterceptWhen(contextName) {
-    return (
-      contextName === 'prePreProcess'
-    );
+    return /^prePreProcess|prePostProcess$/.test(contextName);
+  }
+
+  getTargetKey(contextName) {
+    if (contextName === 'prePreProcess') {
+      return 'markdown';
+    }
+    else if (contextName === 'prePostProcess') {
+      return 'parsedHTML';
+    }
   }
 
   /**
    * @inheritdoc
    */
   process(contextName, ...args) {
+    this.logger.debug(`processing: 'contextName'=${contextName}`);
+
     const context = Object.assign(args[0]);   // clone
-    const markdown = context.markdown;
+    const targetKey = this.getTargetKey(contextName);
     /* eslint-disable no-unused-vars */
     const currentPagePath = context.currentPagePath;
     /* eslint-enable */
 
     context.dcbContextMap = {};
 
-    // see: https://regex101.com/r/8PAEcC/3
-    context.markdown = markdown.replace(/((```|~~~)(.|[\r\n])*?(```|~~~))|(`[^\r\n]*?`)/gm, (all) => {
+    // see: https://regex101.com/r/8PAEcC/4
+    context[targetKey] = context[targetKey].replace(/((```|~~~)(.|[\r\n])*?(```|~~~))|(`[^\r\n]*?`)|(<pre>(.|[\r\n])*?<\/pre>)|(<pre\s[^>]*>(.|[\r\n])*?<\/pre>)/gm, (all) => {
       // create ID
       const replaceId = 'dcb-' + this.createRandomStr(8);
+      this.logger.debug(`'replaceId'=${replaceId} : `, all);
 
       // register to context
       let dcbContext = {};
@@ -82,6 +94,8 @@ export class RestoreCodeBlockInterceptor extends BasicInterceptor {
 
   constructor(crowi) {
     super();
+    this.logger = require('@alias/logger')('growi:DetachCodeBlockInterceptor');
+
     this.crowi = crowi;
     this.crowiForJquery = crowi.getCrowiForJquery();
   }
@@ -90,16 +104,26 @@ export class RestoreCodeBlockInterceptor extends BasicInterceptor {
    * @inheritdoc
    */
   isInterceptWhen(contextName) {
-    return (
-      contextName === 'postPreProcess'
-    );
+    return /^postPreProcess|preRenderHtml|preRenderPreviewHtml$/.test(contextName);
+  }
+
+  getTargetKey(contextName) {
+    if (contextName === 'postPreProcess') {
+      return 'markdown';
+    }
+    else if (contextName === 'preRenderHtml' || contextName === 'preRenderPreviewHtml') {
+      return 'parsedHTML';
+    }
   }
 
   /**
    * @inheritdoc
    */
   process(contextName, ...args) {
+    this.logger.debug(`processing: 'contextName'=${contextName}`);
+
     const context = Object.assign(args[0]);   // clone
+    const targetKey = this.getTargetKey(contextName);
 
     // forEach keys of dcbContextMap
     Object.keys(context.dcbContextMap).forEach((replaceId) => {
@@ -107,8 +131,8 @@ export class RestoreCodeBlockInterceptor extends BasicInterceptor {
       let dcbContext = context.dcbContextMap[replaceId];
 
       // replace it with content by using getter function so that the doller sign does not work
-      // see: https://github.com/weseek/crowi-plus/issues/285
-      context.markdown = context.markdown.replace(dcbContext.substituteContent, () => { return dcbContext.content });
+      // see: https://github.com/weseek/growi/issues/285
+      context[targetKey] = context[targetKey].replace(dcbContext.substituteContent, () => { return dcbContext.content });
     });
 
     // resolve

+ 12 - 6
resource/styles/agile-admin/inverse/colors/_apply-colors-dark.scss

@@ -4,12 +4,6 @@
   }
 }
 
-/*
- * Code
- */
-code {
-  background-color: darken($bodycolor, 5%);
-}
 
 /*
  * Button
@@ -156,3 +150,15 @@ legend {
     border-color: $border;
   }
 }
+
+/*
+ * GROWI admin page #themeOptions
+ */
+ .admin-page {
+  #themeOptions {
+    a.active {
+      background-color: darken($themecolor,15%);
+      border-color: darken($themecolor,15%);
+    }
+  }
+}

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

@@ -57,3 +57,15 @@
     border-color: $border;
   }
 }
+
+/*
+ * GROWI admin page #themeOptions
+ */
+ .admin-page {
+  #themeOptions {
+    a.active {
+      background-color: lighten($themecolor,20%);
+      border-color: lighten($themecolor,20%);
+    }
+  }
+}

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

@@ -148,6 +148,15 @@ body{
   color:$white;
 }
 
+/*
+ * code color of inline-code
+ */
+:not(.hljs) > code:not(.hljs) {
+  color: $inline-code-color;
+  background-color: $inline-code-bg;
+}
+
+
 /*
  * Legend
  */
@@ -303,7 +312,7 @@ body.on-edit {
     border-top-color: $border;
   }
   .page-comment .page-comment-main,
-  .comment-form-main {
+  .page-comment-form .comment-form-main {
     background-color: darken($bodycolor, 4%);
     &:before {
       border-right-color: darken($bodycolor, 4%);

+ 0 - 152
resource/styles/agile-admin/inverse/colors/blue-dark.scss

@@ -1,152 +0,0 @@
-@import "../variables.scss";
-
-/*Just change your choise color here its theme Colors*/
-
-$topbar:#008efa;
-$sidebar:#4F5467;
-$bodycolor:#f1f2f7;
-$headingtext: #2b2b2b;
-$bodytext: #686868;
-$sidebar-text:#a6acbc;
-$themecolor:#008efa;
-$dark-themecolor:#4F5467;
-
-
-body{
-  background:$sidebar;
-}
-
-/*Top Header Part*/
-.top-left-part{
-  background:$sidebar;
-}
-.logo b{
-   
-}
-.logo i{ color:$white;}
-
-.navbar-header{ 
-     background:$topbar;
-}
-.navbar-top-links > li > a{
-    color:$white;
-}
-
-
-
-/*Right panel*/
-.right-sidebar .rpanel-title{
-  background:$themecolor;
-}
-
-/*Bread Crumb*/
-.bg-title .breadcrumb .active{
-    color:$themecolor;
-}
-.bg-title{
-  
-}
-
-/*Sidebar*/
-
-.sidebar {
-    background:$sidebar;
-    box-shadow:1px 0px 20px rgba(0, 0, 0, 0.08);
-  .label-custom{
-      background:$megna;
-  }  
-}
-#side-menu li a{
-    color:$sidebar-text;
-}
-#side-menu li a{
-    color:$sidebar-text;
-  border-left:0px solid $sidebar;
-}
-
-#side-menu > li > a {
-    &:hover, &:focus{
-        background:rgba(0, 0, 0, 0.07);
-    }
-   &.active {
-            border-left:3px solid $themecolor;
-            color:$white;
-            
-            font-weight:500;
-        i{
-          color:$themecolor;
-        }
-        }
-
-}
-#side-menu ul > li > a {
-    &:hover{
-        color:$themecolor;
-        
-    }
-    &.active{
-     color:$white;
-     font-weight:500;
-    }
-}
-.sidebar #side-menu .user-pro{
-  .nav-second-level a:hover{
-          color:$themecolor;
-        }
-  }
- .content-wrapper .sidebar .nav-second-level{
-  li{
-    background:#444859;
-  }
-}  
-@media(min-width:768px) {
-  .content-wrapper { 
-        #side-menu ul, .sidebar #side-menu > li:hover, .sidebar .nav-second-level > li > a{
-              background:#444859;
-         }
-
-   }     
-}
-
-.user-profile .user-pro-body .u-dropdown{
-  color:$sidebar-text;
-}
-/*themecolor*/
-
-.bg-theme {
-  background-color: #707cd2 !important;
-}
-.bg-theme-dark {
-  background-color: $themecolor !important;
-}
-
-/*Chat widget*/
-.chat-list .odd .chat-text{
-    background:$themecolor;
-}
-/*Button*/
-.btn-custom{
-  background:$themecolor;
-  border:1px solid $themecolor;
-  color:$white;
-  &:hover{
-    background:$themecolor;
-    opacity:0.8;
-    color:$white;
-    border:1px solid $themecolor;
-  }
-}
-/*Custom tab*/
-.customtab li.active a, .customtab li.active a:hover,  .customtab li.active a:focus{
- border-bottom:2px solid $themecolor;
- color:$themecolor;
-}
-.tabs-vertical li.active a, .tabs-vertical li.active a:hover,  .tabs-vertical li.active a:focus{
-  background:$themecolor;
-  border-right:2px solid $themecolor;
-}
-/*Nav-pills*/
-.nav-pills>li.active>a, .nav-pills>li.active>a:focus, .nav-pills>li.active>a:hover{
-  background:$themecolor;
-  color:$white;
-}

+ 0 - 138
resource/styles/agile-admin/inverse/colors/blue.scss

@@ -1,138 +0,0 @@
-@import "../variables.scss";
-
-/*Just change your choise color here its theme Colors*/
-
-$topbar:#008efa;
-$sidebar:#fff;
-$bodycolor:#f1f2f7;
-$headingtext: #2b2b2b;
-$bodytext: #686868;
-$sidebar-text:#54667a;
-$themecolor:#008efa;
-$dark-themecolor:#4F5467;
-
-
-body{
-  background:$sidebar;
-}
-
-/*Top Header Part*/
-
-.logo i{ color:$white;}
-.top-left-part{ 
-  .light-logo{
-    display:inline-block;
-  }
-  .dark-logo{
-    display:none;
-  }
-}
-.navbar-header{ 
-     background:$topbar;
-}
-.navbar-top-links > li > a{
-    color:$white;
-}
-
-
-
-/*Right panel*/
-.right-sidebar .rpanel-title{
-  background:$themecolor;
-}
-
-/*Bread Crumb*/
-.bg-title .breadcrumb .active{
-    color:$themecolor;
-}
-.bg-title{
-  
-}
-
-/*Sidebar*/
-
-.sidebar {
-    background:$sidebar;
-    box-shadow:1px 0px 20px rgba(0, 0, 0, 0.08);
-  .label-custom{
-      background:$megna;
-  }  
-}
-#side-menu li a{
-    color:$sidebar-text;
-}
-#side-menu li a{
-    color:$sidebar-text;
-  border-left:0px solid $sidebar;
-}
-
-#side-menu > li > a {
-    &:hover, &:focus{
-        background:rgba(0, 0, 0, 0.03);
-    }
-   &.active {
-            border-left:3px solid $themecolor;
-            color:$dark;
-            
-            font-weight:500;
-        i{
-          color:$themecolor;
-        }
-        }
-
-}
-#side-menu ul > li > a {
-    &:hover{
-        color:$themecolor;
-        
-    }
-    &.active{
-     color:$dark;
-     font-weight:500;
-    }
-}
-.sidebar #side-menu .user-pro{
-  .nav-second-level a:hover{
-          color:$themecolor;
-        }
-  }
-
-/*themecolor*/
-
-.bg-theme {
-  background-color: #707cd2 !important;
-}
-.bg-theme-dark {
-  background-color: $themecolor !important;
-}
-
-/*Chat widget*/
-.chat-list .odd .chat-text{
-    background:$themecolor;
-}
-/*Button*/
-.btn-custom{
-  background:$themecolor;
-  border:1px solid $themecolor;
-  color:$white;
-  &:hover{
-    background:$themecolor;
-    opacity:0.8;
-    color:$white;
-    border:1px solid $themecolor;
-  }
-}
-/*Custom tab*/
-.customtab li.active a, .customtab li.active a:hover,  .customtab li.active a:focus{
- border-bottom:2px solid $themecolor;
- color:$themecolor;
-}
-.tabs-vertical li.active a, .tabs-vertical li.active a:hover,  .tabs-vertical li.active a:focus{
-  background:$themecolor;
-  border-right:2px solid $themecolor;
-}
-/*Nav-pills*/
-.nav-pills>li.active>a, .nav-pills>li.active>a:focus, .nav-pills>li.active>a:hover{
-  background:$themecolor;
-  color:$white;
-}

+ 1 - 0
resource/styles/agile-admin/inverse/colors/default-dark.scss

@@ -23,6 +23,7 @@ $border: lighten($basecolor, 15%);
 $navbar-border: lighten($border, 10%);
 $active-navbar-border: darken($border, 3%);
 $btn-default-bgcolor: darken($basecolor, 10%);
+$inline-code-bg: darken($bodycolor, 5%);
 
 @import 'apply-colors';
 @import 'apply-colors-dark';

+ 36 - 0
resource/styles/agile-admin/inverse/colors/future.scss

@@ -0,0 +1,36 @@
+@import '../variables';
+
+$basecolor: #16282D;
+$themecolor:rgba(11, 79, 104, 0.616);
+
+$topbar:#011414;
+$sidebar:#fff;
+$bodycolor:$basecolor;
+$headingtext: #D9A364;
+$bodytext: #97D7CF;
+$linktext: darken($themecolor, 15%);
+$linktext-hover: lighten($linktext, 80%);
+$sidebar-text:rgb(65, 133, 124);
+$dark-themecolor:#4F5467;
+
+
+$primary: $themecolor;
+$info: lighten($themecolor,20%);
+
+$logo-mark-fill: rgb(170, 245, 237);
+$wikilinktext: saturate($bodytext, 20%);
+$wikilinktext-hover: darken($wikilinktext, 5%);
+
+$dark: darken($bodytext, 5%);
+$border: lighten($basecolor, 15%);
+$navbar-border: lighten($border, 10%);
+$active-navbar-border: darken($border, 3%);
+$btn-default-bgcolor: darken($basecolor, 10%);
+$inline-code-bg: darken($bodycolor, 5%);
+
+@import 'apply-colors';
+@import 'apply-colors-dark';
+
+.bg-title{
+  border-bottom: 1px solid rgb(131, 228, 215);
+}

+ 0 - 148
resource/styles/agile-admin/inverse/colors/gray-dark.scss

@@ -1,148 +0,0 @@
-@import "../variables.scss";
-
-/*Just change your choise color here its theme Colors*/
-
-$topbar:#a0aec4;
-$sidebar:#4F5467;
-$bodycolor:#f1f2f7;
-$headingtext: #2b2b2b;
-$bodytext: #686868;
-$sidebar-text:#a6acbc;
-$themecolor:#a0aec4;
-$dark-themecolor:#4F5467;
-
-
-body{
-  background:$sidebar;
-}
-
-/*Top Header Part*/
-.top-left-part{
-  background:$sidebar;
-}
-
-.logo i{ color:$white;}
-
-.navbar-header{ 
-     background:$topbar;
-}
-.navbar-top-links > li > a{
-    color:$white;
-}
-
-
-
-/*Right panel*/
-.right-sidebar .rpanel-title{
-  background:$themecolor;
-}
-
-/*Bread Crumb*/
-.bg-title .breadcrumb .active{
-    color:$themecolor;
-}
-.bg-title{
-  
-}
-
-/*Sidebar*/
-
-.sidebar {
-    background:$sidebar;
-    box-shadow:1px 0px 20px rgba(0, 0, 0, 0.08);
-  .label-custom{
-      background:$megna;
-  }  
-}
-#side-menu li a{
-    color:$sidebar-text;
-}
-#side-menu li a{
-    color:$sidebar-text;
-  border-left:0px solid $sidebar;
-}
-
-#side-menu > li > a {
-    &:hover, &:focus{
-        background:rgba(0, 0, 0, 0.07);
-    }
-   &.active {
-            border-left:3px solid $themecolor;
-            color:$white;
-            
-            font-weight:500;
-        i{
-          color:$themecolor;
-        }
-        }
-
-}
-#side-menu ul > li > a {
-    &:hover{
-        color:$themecolor;
-        
-    }
-    &.active{
-     color:$white;
-     font-weight:500;
-    }
-}
-.sidebar #side-menu .user-pro{
-  .nav-second-level a:hover{
-          color:$themecolor;
-        }
-  }
-.content-wrapper .sidebar .nav-second-level{
-  li{
-    background:#444859;
-  }
-}   
-@media(min-width:768px) {
-  .content-wrapper { 
-        #side-menu ul, .sidebar #side-menu > li:hover, .sidebar .nav-second-level > li > a{
-              background:#444859;
-         }
-   }     
-}
-.user-profile .user-pro-body .u-dropdown{
-  color:$sidebar-text;
-}
-/*themecolor*/
-
-.bg-theme {
-  background-color: $danger !important;
-}
-.bg-theme-dark {
-  background-color: $megna !important;
-}
-
-/*Chat widget*/
-.chat-list .odd .chat-text{
-    background:$themecolor;
-}
-/*Button*/
-.btn-custom{
-  background:$themecolor;
-  border:1px solid $themecolor;
-  color:$white;
-  &:hover{
-    background:$themecolor;
-    opacity:0.8;
-    color:$white;
-    border:1px solid $themecolor;
-  }
-}
-/*Custom tab*/
-.customtab li.active a, .customtab li.active a:hover,  .customtab li.active a:focus{
- border-bottom:2px solid $themecolor;
- color:$themecolor;
-}
-.tabs-vertical li.active a, .tabs-vertical li.active a:hover,  .tabs-vertical li.active a:focus{
-  background:$themecolor;
-  border-right:2px solid $themecolor;
-}
-/*Nav-pills*/
-.nav-pills>li.active>a, .nav-pills>li.active>a:focus, .nav-pills>li.active>a:hover{
-  background:$themecolor;
-  color:$white;
-}

+ 0 - 138
resource/styles/agile-admin/inverse/colors/gray.scss

@@ -1,138 +0,0 @@
-@import "../variables.scss";
-
-/*Just change your choise color here its theme Colors*/
-
-$topbar:#a0aec4;
-$sidebar:#fff;
-$bodycolor:#f1f2f7;
-$headingtext: #2b2b2b;
-$bodytext: #686868;
-$sidebar-text:#54667a;
-$themecolor:#a0aec4;
-$dark-themecolor:#4F5467;
-
-
-body{
-  background:$sidebar;
-}
-
-/*Top Header Part*/
-
-.top-left-part{ 
-  .light-logo{
-    display:inline-block;
-  }
-  .dark-logo{
-    display:none;
-  }
-}
-.logo i{ color:$white;}
-
-.navbar-header{ 
-     background:$topbar;
-}
-.navbar-top-links > li > a{
-    color:$white;
-}
-
-
-
-/*Right panel*/
-.right-sidebar .rpanel-title{
-  background:$themecolor;
-}
-
-/*Bread Crumb*/
-.bg-title .breadcrumb .active{
-    color:$themecolor;
-}
-.bg-title{
-  
-}
-
-/*Sidebar*/
-
-.sidebar {
-    background:$sidebar;
-    box-shadow:1px 0px 20px rgba(0, 0, 0, 0.08);
-  .label-custom{
-      background:$megna;
-  }  
-}
-#side-menu li a{
-    color:$sidebar-text;
-}
-#side-menu li a{
-    color:$sidebar-text;
-  border-left:0px solid $sidebar;
-}
-
-#side-menu > li > a {
-    &:hover, &:focus{
-        background:rgba(0, 0, 0, 0.03);
-    }
-   &.active {
-            border-left:3px solid $themecolor;
-            color:$dark;
-            
-            font-weight:500;
-        i{
-          color:$themecolor;
-        }
-        }
-
-}
-#side-menu ul > li > a {
-    &:hover{
-        color:$themecolor;
-        
-    }
-    &.active{
-     color:$dark;
-     font-weight:500;
-    }
-}
-.sidebar #side-menu .user-pro{
-  .nav-second-level a:hover{
-          color:$themecolor;
-        }
-  }
-/*themecolor*/
-
-.bg-theme {
-  background-color: $danger !important;
-}
-.bg-theme-dark {
-  background-color: $megna !important;
-}
-
-/*Chat widget*/
-.chat-list .odd .chat-text{
-    background:$themecolor;
-}
-/*Button*/
-.btn-custom{
-  background:$themecolor;
-  border:1px solid $themecolor;
-  color:$white;
-  &:hover{
-    background:$themecolor;
-    opacity:0.8;
-    color:$white;
-    border:1px solid $themecolor;
-  }
-}
-/*Custom tab*/
-.customtab li.active a, .customtab li.active a:hover,  .customtab li.active a:focus{
- border-bottom:2px solid $themecolor;
- color:$themecolor;
-}
-.tabs-vertical li.active a, .tabs-vertical li.active a:hover,  .tabs-vertical li.active a:focus{
-  background:$themecolor;
-  border-right:2px solid $themecolor;
-}
-/*Nav-pills*/
-.nav-pills>li.active>a, .nav-pills>li.active>a:focus, .nav-pills>li.active>a:hover{
-  background:$themecolor;
-  color:$white;
-}

+ 0 - 149
resource/styles/agile-admin/inverse/colors/green-dark.scss

@@ -1,149 +0,0 @@
-@import "../variables.scss";
-
-/*Just change your choise color here its theme Colors*/
-
-$topbar:#00c292;
-$sidebar:#4F5467;
-$bodycolor:#f1f2f7;
-$headingtext: #2b2b2b;
-$bodytext: #686868;
-$sidebar-text:#a6acbc;
-$themecolor:#00c292;
-$dark-themecolor:#4F5467;
-
-
-body{
-  background:$sidebar;
-}
-
-/*Top Header Part*/
-.top-left-part{
-  background:$sidebar;
-}
-.logo b{
-   
-}
-.logo i{ color:$white;}
-
-.navbar-header{ 
-     background:$topbar;
-}
-.navbar-top-links > li > a{
-    color:$white;
-}
-
-
-/*Right panel*/
-.right-sidebar .rpanel-title{
-  background:$themecolor;
-}
-
-/*Bread Crumb*/
-.bg-title .breadcrumb .active{
-    color:$themecolor;
-}
-.bg-title{
-  
-}
-
-/*Sidebar*/
-
-.sidebar {
-    background:$sidebar;
-    box-shadow:1px 0px 20px rgba(0, 0, 0, 0.08);
-  .label-custom{
-      background:$megna;
-  }  
-}
-#side-menu li a{
-    color:$sidebar-text;
-}
-#side-menu li a{
-    color:$sidebar-text;
-  border-left:0px solid $sidebar;
-}
-
-#side-menu > li > a {
-    &:hover, &:focus{
-        background:rgba(0, 0, 0, 0.07);
-    }
-   &.active {
-            border-left:3px solid $themecolor;
-            color:$white;
-            
-            font-weight:500;
-        i{
-          color:$themecolor;
-        }
-        }
-
-}
-#side-menu ul > li > a {
-    &:hover{
-        color:$themecolor;
-        
-    }
-    &.active{
-     color:$white;
-     font-weight:500;
-    }
-}
-.sidebar #side-menu .user-pro{
-  .nav-second-level a:hover{
-          color:$themecolor;
-        }
-  }
-.content-wrapper .sidebar .nav-second-level{
-  li{
-    background:#444859;
-  }
-}   
-@media(min-width:768px) {
-  .content-wrapper { 
-        #side-menu ul, .sidebar #side-menu > li:hover, .sidebar .nav-second-level > li > a{
-              background:#444859;
-         }
-   }     
-}
-.user-profile .user-pro-body .u-dropdown{
-  color:$sidebar-text;
-}
-/*themecolor*/
-
-.bg-theme {
-  background-color: #6e8680 !important;
-}
-.bg-theme-dark {
-  background-color: $themecolor !important;
-}
-
-/*Chat widget*/
-.chat-list .odd .chat-text{
-    background:$themecolor;
-}
-/*Button*/
-.btn-custom{
-  background:$themecolor;
-  border:1px solid $themecolor;
-  color:$white;
-  &:hover{
-    background:$themecolor;
-    opacity:0.8;
-    color:$white;
-    border:1px solid $themecolor;
-  }
-}
-/*Custom tab*/
-.customtab li.active a, .customtab li.active a:hover,  .customtab li.active a:focus{
- border-bottom:2px solid $themecolor;
- color:$themecolor;
-}
-.tabs-vertical li.active a, .tabs-vertical li.active a:hover,  .tabs-vertical li.active a:focus{
-  background:$themecolor;
-  border-right:2px solid $themecolor;
-}
-/*Nav-pills*/
-.nav-pills>li.active>a, .nav-pills>li.active>a:focus, .nav-pills>li.active>a:hover{
-  background:$themecolor;
-  color:$white;
-}

+ 0 - 139
resource/styles/agile-admin/inverse/colors/green.scss

@@ -1,139 +0,0 @@
-@import "../variables.scss";
-
-/*Just change your choise color here its theme Colors*/
-
-$topbar:#00c292;
-$sidebar:#fff;
-$bodycolor:#f1f2f7;
-$headingtext: #2b2b2b;
-$bodytext: #686868;
-$sidebar-text:#54667a;
-$themecolor:#00c292;
-$dark-themecolor:#4F5467;
-
-
-
-body{
-  background:$sidebar;
-}
-
-/*Top Header Part*/
-
-.logo i{ color:$white;}
-.top-left-part{ 
-  .light-logo{
-    display:inline-block;
-  }
-  .dark-logo{
-    display:none;
-  }
-}
-.navbar-header{ 
-     background:$topbar;
-}
-.navbar-top-links > li > a{
-    color:$white;
-}
-
-
-
-/*Right panel*/
-.right-sidebar .rpanel-title{
-  background:$themecolor;
-}
-
-/*Bread Crumb*/
-.bg-title .breadcrumb .active{
-    color:$themecolor;
-}
-.bg-title{
-  
-}
-
-/*Sidebar*/
-
-.sidebar {
-    background:$sidebar;
-    box-shadow:1px 0px 20px rgba(0, 0, 0, 0.08);
-  .label-custom{
-      background:$megna;
-  }  
-}
-#side-menu li a{
-    color:$sidebar-text;
-}
-#side-menu li a{
-    color:$sidebar-text;
-  border-left:0px solid $sidebar;
-}
-
-#side-menu > li > a {
-    &:hover, &:focus{
-        background:rgba(0, 0, 0, 0.03);
-    }
-   &.active {
-            border-left:3px solid $themecolor;
-            color:$dark;
-            
-            font-weight:500;
-        i{
-          color:$themecolor;
-        }
-        }
-
-}
-#side-menu ul > li > a {
-    &:hover{
-        color:$themecolor;
-        
-    }
-    &.active{
-     color:$dark;
-     font-weight:500;
-    }
-}
-.sidebar #side-menu .user-pro{
-  .nav-second-level a:hover{
-          color:$themecolor;
-        }
-  }
-
-/*themecolor*/
-
-.bg-theme {
-  background-color: #6e8680 !important;
-}
-.bg-theme-dark {
-  background-color: $themecolor !important;
-}
-
-/*Chat widget*/
-.chat-list .odd .chat-text{
-    background:$themecolor;
-}
-/*Button*/
-.btn-custom{
-  background:$themecolor;
-  border:1px solid $themecolor;
-  color:$white;
-  &:hover{
-    background:$themecolor;
-    opacity:0.8;
-    color:$white;
-    border:1px solid $themecolor;
-  }
-}
-/*Custom tab*/
-.customtab li.active a, .customtab li.active a:hover,  .customtab li.active a:focus{
- border-bottom:2px solid $themecolor;
- color:$themecolor;
-}
-.tabs-vertical li.active a, .tabs-vertical li.active a:hover,  .tabs-vertical li.active a:focus{
-  background:$themecolor;
-  border-right:2px solid $themecolor;
-}
-/*Nav-pills*/
-.nav-pills>li.active>a, .nav-pills>li.active>a:focus, .nav-pills>li.active>a:hover{
-  background:$themecolor;
-  color:$white;
-}

+ 0 - 150
resource/styles/agile-admin/inverse/colors/megna-dark.scss

@@ -1,150 +0,0 @@
-@import "../variables.scss";
-
-/*Just change your choise color here its theme Colors*/
-
-$topbar:#00b5c2;
-$sidebar:#4F5467;
-$bodycolor:#f1f2f7;
-$headingtext: #2b2b2b;
-$bodytext: #686868;
-$sidebar-text:#a6acbc;
-$themecolor:#00b5c2;
-$dark-themecolor:#4F5467;
-
-
-body{
-  background:$sidebar;
-}
-
-/*Top Header Part*/
-.top-left-part{
-  background:$sidebar;
-}
-.logo b{
-   
-}
-.logo i{ color:$white;}
-
-.navbar-header{ 
-     background:$topbar;
-}
-.navbar-top-links > li > a{
-    color:$white;
-}
-
-
-/*Right panel*/
-.right-sidebar .rpanel-title{
-  background:$themecolor;
-}
-
-/*Bread Crumb*/
-.bg-title .breadcrumb .active{
-    color:$themecolor;
-}
-.bg-title{
-  
-}
-
-/*Sidebar*/
-
-.sidebar {
-    background:$sidebar;
-    box-shadow:1px 0px 20px rgba(0, 0, 0, 0.08);
-  .label-custom{
-      background:$megna;
-  }  
-}
-#side-menu li a{
-    color:$sidebar-text;
-}
-#side-menu li a{
-    color:$sidebar-text;
-  border-left:0px solid $sidebar;
-}
-
-#side-menu > li > a {
-    &:hover, &:focus{
-        background:rgba(0, 0, 0, 0.07);
-    }
-   &.active {
-            border-left:3px solid $themecolor;
-            color:$white;
-            
-            font-weight:500;
-        i{
-          color:$themecolor;
-        }
-        }
-
-}
-#side-menu ul > li > a {
-    &:hover{
-        color:$themecolor;
-        
-    }
-    &.active{
-     color:$white;
-     font-weight:500;
-    }
-}
-.sidebar #side-menu .user-pro{
-  .nav-second-level a:hover{
-          color:$themecolor;
-        }
-  }
-.content-wrapper .sidebar .nav-second-level{
-  li{
-    background:#444859;
-  }
-}   
-@media(min-width:768px) {
-  .content-wrapper { 
-        #side-menu ul, .sidebar #side-menu > li:hover, .sidebar .nav-second-level > li > a{
-              background:#444859;
-         }
-   }     
-}
-
-.user-profile .user-pro-body .u-dropdown{
-  color:$sidebar-text;
-}
-/*themecolor*/
-
-.bg-theme {
-  background-color: $danger !important;
-}
-.bg-theme-dark {
-  background-color: $themecolor !important;
-}
-
-/*Chat widget*/
-.chat-list .odd .chat-text{
-    background:$themecolor;
-}
-/*Button*/
-.btn-custom{
-  background:$themecolor;
-  border:1px solid $themecolor;
-  color:$white;
-  &:hover{
-    background:$themecolor;
-    opacity:0.8;
-    color:$white;
-    border:1px solid $themecolor;
-  }
-}
-/*Custom tab*/
-.customtab li.active a, .customtab li.active a:hover,  .customtab li.active a:focus{
- border-bottom:2px solid $themecolor;
- color:$themecolor;
-}
-.tabs-vertical li.active a, .tabs-vertical li.active a:hover,  .tabs-vertical li.active a:focus{
-  background:$themecolor;
-  border-right:2px solid $themecolor;
-}
-/*Nav-pills*/
-.nav-pills>li.active>a, .nav-pills>li.active>a:focus, .nav-pills>li.active>a:hover{
-  background:$themecolor;
-  color:$white;
-}

+ 0 - 139
resource/styles/agile-admin/inverse/colors/megna.scss

@@ -1,139 +0,0 @@
-@import "../variables.scss";
-
-/*Just change your choise color here its theme Colors*/
-
-$topbar:#00b5c2;
-$sidebar:#fff;
-$bodycolor:#f1f2f7;
-$headingtext: #2b2b2b;
-$bodytext: #686868;
-$sidebar-text:#54667a;
-$themecolor:#00b5c2;
-$dark-themecolor:#4F5467;
-
-
-
-body{
-  background:$sidebar;
-}
-
-/*Top Header Part*/
-
-.logo i{ color:$white;}
-.top-left-part{ 
-  .light-logo{
-    display:inline-block;
-  }
-  .dark-logo{
-    display:none;
-  }
-}
-.navbar-header{ 
-     background:$topbar;
-}
-.navbar-top-links > li > a{
-    color:$white;
-}
-
-
-
-/*Right panel*/
-.right-sidebar .rpanel-title{
-  background:$themecolor;
-}
-
-/*Bread Crumb*/
-.bg-title .breadcrumb .active{
-    color:$themecolor;
-}
-.bg-title{
-  
-}
-
-/*Sidebar*/
-
-.sidebar {
-    background:$sidebar;
-    box-shadow:1px 0px 20px rgba(0, 0, 0, 0.08);
-  .label-custom{
-      background:$megna;
-  }  
-}
-#side-menu li a{
-    color:$sidebar-text;
-}
-#side-menu li a{
-    color:$sidebar-text;
-  border-left:0px solid $sidebar;
-}
-
-#side-menu > li > a {
-    &:hover, &:focus{
-        background:rgba(0, 0, 0, 0.03);
-    }
-   &.active {
-            border-left:3px solid $themecolor;
-            color:$dark;
-            
-            font-weight:500;
-        i{
-          color:$themecolor;
-        }
-        }
-
-}
-#side-menu ul > li > a {
-    &:hover{
-        color:$themecolor;
-        
-    }
-    &.active{
-     color:$dark;
-     font-weight:500;
-    }
-}
-.sidebar #side-menu .user-pro{
-  .nav-second-level a:hover{
-          color:$themecolor;
-        }
-  }
-
-/*themecolor*/
-
-.bg-theme {
-  background-color: $danger !important;
-}
-.bg-theme-dark {
-  background-color: $themecolor !important;
-}
-
-/*Chat widget*/
-.chat-list .odd .chat-text{
-    background:$themecolor;
-}
-/*Button*/
-.btn-custom{
-  background:$themecolor;
-  border:1px solid $themecolor;
-  color:$white;
-  &:hover{
-    background:$themecolor;
-    opacity:0.8;
-    color:$white;
-    border:1px solid $themecolor;
-  }
-}
-/*Custom tab*/
-.customtab li.active a, .customtab li.active a:hover,  .customtab li.active a:focus{
- border-bottom:2px solid $themecolor;
- color:$themecolor;
-}
-.tabs-vertical li.active a, .tabs-vertical li.active a:hover,  .tabs-vertical li.active a:focus{
-  background:$themecolor;
-  border-right:2px solid $themecolor;
-}
-/*Nav-pills*/
-.nav-pills>li.active>a, .nav-pills>li.active>a:focus, .nav-pills>li.active>a:hover{
-  background:$themecolor;
-  color:$white;
-}

+ 2 - 16
resource/styles/agile-admin/inverse/colors/mono-blue.scss

@@ -21,17 +21,13 @@ $info: lighten($themecolor,20%);
 $logo-mark-fill: lighten(desaturate($topbar, 30%), 20%);
 $wikilinktext: lighten($themecolor, 20%);
 $wikilinktext-hover: lighten($wikilinktext, 20%);
+$inline-code-color: $subthemecolor;
+$inline-code-bg: lighten($subthemecolor,70%);
 
 @import 'apply-colors';
 @import 'apply-colors-light';
 
 
-// change color of inline-code (default: red)
-:not(.hljs) > code:not(.hljs) {
-  background-color: lighten($subthemecolor,70%);
-  color: $subthemecolor;
-}
-
 // change color of highlighted header in wiki (default: orange)
 .wiki {
   .code-line.revision-head.highlighted {
@@ -39,13 +35,3 @@ $wikilinktext-hover: lighten($wikilinktext, 20%);
     color: $themelight;
   }
 }
-
-// change color of surrounding border of theme option icon at admin/customize (default: green)
-.admin-page {
-  #themeOptions {
-    a.active {
-      background-color: lighten($themecolor,20%);
-      border-color: lighten($themecolor,20%);
-    }
-  }
-}

+ 0 - 153
resource/styles/agile-admin/inverse/colors/purple-dark.scss

@@ -1,153 +0,0 @@
-@import "../variables.scss";
-
-/*Just change your choise color here its theme Colors*/
-
-$topbar:#2c5ca9;
-$sidebar:#4F5467;
-$bodycolor:#f1f2f7;
-$headingtext: #2b2b2b;
-$bodytext: #686868;
-$sidebar-text:#a6acbc;
-$themecolor:#2c5ca9;
-$dark-themecolor:#4F5467;
-
-
-
-
-body{
-  background:$sidebar;
-}
-
-/*Top Header Part*/
-.top-left-part{
-  background:$sidebar;
-}
-.logo b{
-   
-}
-.logo i{ color:$white;}
-
-.navbar-header{ 
-     background:$topbar;
-}
-.navbar-top-links > li > a{
-    color:$white;
-}
-
-
-
-/*Right panel*/
-.right-sidebar .rpanel-title{
-  background:$themecolor;
-}
-
-/*Bread Crumb*/
-.bg-title .breadcrumb .active{
-    color:$themecolor;
-}
-.bg-title{
-  
-}
-
-/*Sidebar*/
-
-.sidebar {
-    background:$sidebar;
-    box-shadow:1px 0px 20px rgba(0, 0, 0, 0.08);
-  .label-custom{
-      background:$megna;
-  }  
-}
-#side-menu li a{
-    color:$sidebar-text;
-}
-#side-menu li a{
-    color:$sidebar-text;
-  border-left:0px solid $sidebar;
-}
-
-#side-menu > li > a {
-    &:hover, &:focus{
-        background:rgba(0, 0, 0, 0.07);
-    }
-   &.active {
-            border-left:3px solid $themecolor;
-            color:$white;
-            
-            font-weight:500;
-        i{
-          color:$themecolor;
-        }
-        }
-
-}
-#side-menu ul > li > a {
-    &:hover{
-        color:$themecolor;
-        
-    }
-    &.active{
-     color:$white;
-     font-weight:500;
-    }
-}
-.sidebar #side-menu .user-pro{
-  .nav-second-level a:hover{
-          color:$themecolor;
-        }
-  }
-.content-wrapper .sidebar .nav-second-level{
-  li{
-    background:#444859;
-  }
-}   
-@media(min-width:768px) {
-  .content-wrapper { 
-        #side-menu ul, .sidebar #side-menu > li:hover, .sidebar .nav-second-level > li > a{
-              background:#444859;
-         }
-
-   }     
-}
-
-.user-profile .user-pro-body .u-dropdown{
-  color:$sidebar-text;
-}
-/*themecolor*/
-
-.bg-theme {
-  background-color: $info !important;
-}
-.bg-theme-dark {
-  background-color: $themecolor !important;
-}
-/*Chat widget*/
-.chat-list .odd .chat-text{
-    background:$themecolor;
-}
-/*Button*/
-.btn-custom{
-  background:$themecolor;
-  border:1px solid $themecolor;
-  color:$white;
-  &:hover{
-    background:$themecolor;
-    opacity:0.8;
-    color:$white;
-    border:1px solid $themecolor;
-  }
-}
-/*Custom tab*/
-.customtab li.active a, .customtab li.active a:hover,  .customtab li.active a:focus{
- border-bottom:2px solid $themecolor;
- color:$themecolor;
-}
-.tabs-vertical li.active a, .tabs-vertical li.active a:hover,  .tabs-vertical li.active a:focus{
-  background:$themecolor;
-  border-right:2px solid $themecolor;
-}
-/*Nav-pills*/
-.nav-pills>li.active>a, .nav-pills>li.active>a:focus, .nav-pills>li.active>a:hover{
-  background:$themecolor;
-  color:$white;
-}

+ 0 - 139
resource/styles/agile-admin/inverse/colors/purple.scss

@@ -1,139 +0,0 @@
-@import "../variables.scss";
-
-/*Just change your choise color here its theme Colors*/
-
-$topbar:#2c5ca9;
-$sidebar:#fff;
-$bodycolor:#f1f2f7;
-$headingtext: #2b2b2b;
-$bodytext: #686868;
-$sidebar-text:#54667a;
-$themecolor:#2c5ca9;
-$dark-themecolor:#4F5467;
-
-
-
-body{
-  background:$sidebar;
-}
-
-/*Top Header Part*/
-
-.logo i{ color:$white;}
-.top-left-part{ 
-  .light-logo{
-    display:inline-block;
-  }
-  .dark-logo{
-    display:none;
-  }
-}
-.navbar-header{ 
-     background:$topbar;
-}
-.navbar-top-links > li > a{
-    color:$white;
-}
-
-
-
-/*Right panel*/
-.right-sidebar .rpanel-title{
-  background:$themecolor;
-}
-
-/*Bread Crumb*/
-.bg-title .breadcrumb .active{
-    color:$themecolor;
-}
-.bg-title{
-  
-}
-
-/*Sidebar*/
-
-.sidebar {
-    background:$sidebar;
-    box-shadow:1px 0px 20px rgba(0, 0, 0, 0.08);
-  .label-custom{
-      background:$megna;
-  }  
-}
-#side-menu li a{
-    color:$sidebar-text;
-}
-#side-menu li a{
-    color:$sidebar-text;
-  border-left:0px solid $sidebar;
-}
-
-#side-menu > li > a {
-    &:hover, &:focus{
-        background:rgba(0, 0, 0, 0.03);
-    }
-   &.active {
-            border-left:3px solid $themecolor;
-            color:$dark;
-            
-            font-weight:500;
-        i{
-          color:$themecolor;
-        }
-        }
-
-}
-#side-menu ul > li > a {
-    &:hover{
-        color:$themecolor;
-        
-    }
-    &.active{
-     color:$dark;
-     font-weight:500;
-    }
-}
-.sidebar #side-menu .user-pro{
-  .nav-second-level a:hover{
-          color:$themecolor;
-        }
-  }
-
-/*themecolor*/
-
-.bg-theme {
-  background-color: $info !important;
-}
-.bg-theme-dark {
-  background-color: $themecolor !important;
-}
-
-/*Chat widget*/
-.chat-list .odd .chat-text{
-    background:$themecolor;
-}
-/*Button*/
-.btn-custom{
-  background:$themecolor;
-  border:1px solid $themecolor;
-  color:$white;
-  &:hover{
-    background:$themecolor;
-    opacity:0.8;
-    color:$white;
-    border:1px solid $themecolor;
-  }
-}
-/*Custom tab*/
-.customtab li.active a, .customtab li.active a:hover,  .customtab li.active a:focus{
- border-bottom:2px solid $themecolor;
- color:$themecolor;
-}
-.tabs-vertical li.active a, .tabs-vertical li.active a:hover,  .tabs-vertical li.active a:focus{
-  background:$themecolor;
-  border-right:2px solid $themecolor;
-}
-/*Nav-pills*/
-.nav-pills>li.active>a, .nav-pills>li.active>a:focus, .nav-pills>li.active>a:hover{
-  background:$themecolor;
-  color:$white;
-}

+ 2 - 0
resource/styles/agile-admin/inverse/variables.scss

@@ -54,6 +54,8 @@ $dark-text:#848a96 !default;
 $navbar-border: #ccc !default;
 $active-navbar-border: lighten($navbar-border, 10%) !default;
 $btn-default-bgcolor: darken(#fff, 10%) !default;
+$inline-code-color: #c7254e !default;
+$inline-code-bg: #f9f2f4 !default;
 
 /*Border radius*/
 $radius: 0 !default;

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

@@ -26,11 +26,6 @@
         margin-left: 1em;
       }
     }
-
-    // override CodeMirror styles
-    .CodeMirror pre {
-      font-family: $font-family-monospace;
-    }
   }
 
   .admin-security {

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

@@ -58,6 +58,7 @@
     // コメント本文
     .page-comment-body {
       margin-bottom: 0.5em;
+      word-wrap: break-word;
     }
 
     .page-comment-meta {

+ 38 - 60
resource/styles/scss/_on-edit.scss

@@ -29,15 +29,6 @@ body.on-edit {
     display: none;
   }
 
-  /*
-   * right tabs
-   */
-  .nav-main-right-tab:not(.dropdown) {
-    // hide if screen size is less than smartphone
-    @media (max-width: $screen-xs) {
-      display: none;
-    }
-  }
 
   /*****************
    * Expand Editor
@@ -83,6 +74,7 @@ body.on-edit {
                          + 1px                      // .page-editor-footer border-top
                          + 40px;                    // .page-editor-footer min-height
       $editor-margin: $header-plus-footer + 22px;   // .btn-open-dropzone height
+      $editor-margin-sm: $header-plus-footer;
 
       #page-editor {
         // right(preview)
@@ -90,16 +82,25 @@ body.on-edit {
         .row,
         .page-editor-preview-container,
         .page-editor-preview-body {
+          min-height: calc(100vh - #{$header-plus-footer});   // for IE11
           height: calc(100vh - #{$header-plus-footer});
         }
         // left(editor)
         .page-editor-editor-container {
+          min-height: calc(100vh - #{$header-plus-footer});   // for IE11
           height: calc(100vh - #{$header-plus-footer});
-        .react-codemirror2, .CodeMirror, .CodeMirror-scroll {
-          height: calc(100vh - #{$editor-margin});
+
+          .react-codemirror2, .CodeMirror, .CodeMirror-scroll,
+          .textarea-editor {
+            height: calc(100vh - #{$editor-margin});
+            // less than smartphone
+            @media (max-width: $screen-xs) {
+              height: calc(100vh - #{$editor-margin-sm});
+            }
+          }
         }
       }
-      }
+
 
       .page-editor-footer {
         width: 100%;
@@ -160,7 +161,7 @@ body.on-edit {
       }
     }
 
-    // hide if screen size is less than smartphone
+    // hide if screen size is less than tablet
     @media (max-width: $screen-sm) {
       display: none;
     }
@@ -174,9 +175,6 @@ body.on-edit {
     padding-right: 0;
     // override CodeMirror styles
     .CodeMirror {
-      pre {
-        font-family: $font-family-monospace-not-strictly;
-      }
       .cm-matchhighlight {
         background-color: cyan;
       }
@@ -208,6 +206,13 @@ body.on-edit {
         color: #444;
       }
     }
+    // add icon on cursor
+    .autoformat-markdown-table-activated .CodeMirror-cursor {
+      &:after {
+        font-family: 'FontAwesome';
+        content: '\f0ce';
+      }
+    }
 
     // for Dropzone
     .dropzone {
@@ -280,6 +285,11 @@ body.on-edit {
       }
     } // end of.dropzone
 
+    .textarea-editor {
+      border: none;
+      font-family: monospace;
+    }
+
     .loading-keymap {
       @include overlay-processing-style();
     }
@@ -297,6 +307,11 @@ body.on-edit {
       &:active {
         box-shadow: none;
       }
+
+      // hide if screen size is less than smartphone
+      @media (max-width: $screen-xs) {
+        display: none;
+      }
     }
   }
   .page-editor-preview-container {
@@ -336,6 +351,13 @@ body.on-edit {
     }
   }
 
+  #page-grant-selector {
+    .btn-group {
+      min-width: 150px;
+    }
+  }
+
+
 } // }}}
 
 /*
@@ -348,50 +370,6 @@ body.on-edit {
   display: block;
 }
 
-
-/*
-.crowi.main-container .main .page-list.content-main { // {{{ Edit Form of Page List
-
-  .close-button {
-    display: none;
-  }
-}
-.crowi.main-container .main .page-list.content-main.on-edit { // {{{ Edit Form of Page List
-  .close-button {
-    display: block;
-  }
-
-  .page-list-container {
-    display: none;
-  }
-
-  .portal-form-header {
-    height: 16px;
-    padding: 8px;
-    border-bottom: solid 1px #ccc;
-  }
-} // }}}
-*/
-/*
-@media (max-width: $screen-sm-max) { // {{{ less than tablet size
-
-  .content-main.on-edit {
-    .form-group.form-submit-group {
-      select.form-control {
-        display: inline-block;
-        max-width: 50%;
-      }
-    }
-  }
-
-} // }}}
-
-@media (max-width: $screen-xs-max) { // {{{ less than smartphone size
-  #edit-form-submit {
-    float: right;
-  }
-} // }}}
-*/
 // overwrite .CodeMirror-hints
 .CodeMirror-hints {
   // FIXME: required because .content-main.on-edit has 'z-index:1050'

+ 9 - 0
resource/styles/scss/_override-hljs.scss

@@ -0,0 +1,9 @@
+// override Highlight Js Style Border
+.wiki, .admin-customize {
+  pre.hljs {
+    border-radius: 3px;
+    &.hljs-no-border {
+      border: none;
+    }
+  }
+}

+ 1 - 1
resource/styles/scss/_variables.scss

@@ -1,2 +1,2 @@
 // override bootstrap
-$font-family-monospace-not-strictly:   Monaco, Menlo, Consolas, "Courier New", MeiryoKe_Console, "M+ 1m", monospace;
+$font-family-monospace-not-strictly: Monaco, Menlo, Consolas, "Courier New", MeiryoKe_Gothic, monospace;

+ 11 - 8
resource/styles/scss/_wiki.scss

@@ -53,11 +53,6 @@ div.body {
     color: lighten($gray-dark, 35%);
   }
 
-  pre {
-    border: none;
-    border-radius: 3px;
-  }
-
   img {
     margin: 5px 0;
     max-width: 100%;
@@ -139,17 +134,25 @@ div.body {
 
     .revision-head-link,
     .revision-head-edit-button {
-      visibility: hidden;
+      margin-left: 0.5em;
       font-size: 0.6em;
+      opacity: 0;
     }
     &:hover .revision-head-link,
     &:hover .revision-head-edit-button {
-      visibility: unset;
-      margin-left: 0.5em;
+      opacity: 1 !important;
     }
   }
 }
 
+// mobile
+.page-mobile .wiki .revision-head {
+  .revision-head-link,
+  .revision-head-edit-button {
+    opacity: 0.3;
+  }
+}
+
 @media (max-width: $screen-sm-max) { // {{{ tablet and iphone size
   .main-container .main .wiki {
     img {

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

@@ -8,6 +8,9 @@
 // vendor
 @import 'vendor';
 
+// override highlightJsStyle
+@import 'override-hljs';
+
 // override react-bootstrap-typeahead styles
 @import 'override-rbt';
 

+ 8 - 0
resource/styles/scss/theme/future.scss

@@ -0,0 +1,8 @@
+// import colors
+@import '../../agile-admin/inverse/colors/future';
+
+// apply agile-admin theme
+@import '../../agile-admin/inverse/style';
+
+// override
+@import 'override-agileadmin';

+ 78 - 0
test/models/page.test.js

@@ -74,6 +74,21 @@ describe('Page', () => {
           grantedUsers: [],
           creator: testUser1,
         },
+        {
+          path: '/page1',
+          grant: Page.GRANT_PUBLIC,
+          creator: testUser0,
+        },
+        {
+          path: '/page1/child1',
+          grant: Page.GRANT_PUBLIC,
+          creator: testUser0,
+        },
+        {
+          path: '/page2',
+          grant: Page.GRANT_PUBLIC,
+          creator: testUser0,
+        },
       ];
 
       return testDBUtil.generateFixture(conn, 'Page', fixture);
@@ -394,4 +409,67 @@ describe('Page', () => {
     });
   });
 
+  context('generateQueryToListByStartWith', () => {
+    it('should return only /page/', done => {
+      const user = createdUsers[0];
+      Page.generateQueryToListByStartWith('/page/', user, { isRegExpEscapedFromPath: true })
+      .then(pages => {
+        // assert length
+        expect(pages.length).to.equal(1);
+        // assert paths
+        const pagePaths = pages.map(page => page.path);
+        expect(pagePaths).to.include.members(['/page/for/extended'])
+        done();
+      })
+      .catch((err) => {
+        done(err);
+      });
+    });
+    it('should return only /page1/', done => {
+      const user = createdUsers[0];
+      Page.generateQueryToListByStartWith('/page1/', user, { isRegExpEscapedFromPath: true })
+      .then(pages => {
+        // assert length
+        expect(pages.length).to.equal(2);
+        // assert paths
+        const pagePaths = pages.map(page => page.path);
+        expect(pagePaths).to.include.members(['/page1', '/page1/child1'])
+        done();
+      })
+      .catch((err) => {
+        done(err);
+      });
+    });
+    it('should return pages which starts with /page', done => {
+      const user = createdUsers[0];
+      Page.generateQueryToListByStartWith('/page', user, {})
+      .then(pages => {
+        // assert length
+        expect(pages.length).to.equal(4);
+        // assert paths
+        const pagePaths = pages.map(page => page.path);
+        expect(pagePaths).to.include.members(['/page/for/extended', '/page1', '/page1/child1', '/page2'])
+        done();
+      })
+      .catch((err) => {
+        done(err);
+      });
+    });
+    it('should process with regexp', done => {
+      const user = createdUsers[0];
+      Page.generateQueryToListByStartWith('/page\\d{1}/', user, {})
+      .then(pages => {
+        // assert length
+        expect(pages.length).to.equal(3);
+        // assert paths
+        const pagePaths = pages.map(page => page.path);
+        expect(pagePaths).to.include.members(['/page1', '/page1/child1', '/page2'])
+        done();
+      })
+      .catch((err) => {
+        done(err);
+      });
+    });
+  });
+
 });

+ 0 - 15
webpack.config.js

@@ -1,15 +0,0 @@
-// Look in ./config folder for webpack.dev.js
-switch (process.env.NODE_ENV) {
-  case 'prod':
-  case 'production':
-    module.exports = require('./config/webpack.prod')({env: 'production'});
-    break;
-  case 'test':
-  case 'testing':
-    module.exports = require('./config/webpack.test')({env: 'test'});
-    break;
-  case 'dev':
-  case 'development':
-  default:
-    module.exports = require('./config/webpack.dev')({env: 'development'});
-}

+ 11 - 6
wercker.yml

@@ -43,13 +43,12 @@ build-prod:
     - script:
       name: install dependencies
       code: |
-        yarn install --production
+        yarn
 
     - script:
       name: install plugins
       code: |
-        yarn add growi-plugin-lsx
-        yarn add growi-plugin-pukiwiki-like-linker
+        yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker
 
     - script:
       name: print dependencies
@@ -60,6 +59,13 @@ build-prod:
       code: |
         npm run build:prod:analyze
 
+    - script:
+      name: npm run server:prod:ci
+      code: |
+        export MONGO_URI=mongodb://$MONGO_PORT_27017_TCP_ADDR/growi
+        echo "export MONGO_URI=$MONGO_URI"
+        npm run server:prod:ci
+
   after-steps:
     - script:
       name: copy report to artifacts
@@ -82,13 +88,12 @@ build-dev:
     - script:
       name: install dependencies
       code: |
-        yarn install
+        yarn
 
     - script:
       name: install plugins
       code: |
-        yarn add growi-plugin-lsx
-        yarn add growi-plugin-pukiwiki-like-linker
+        yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker
 
     - script:
       name: print dependencies

+ 113 - 73
yarn.lock

@@ -2,6 +2,15 @@
 # yarn lockfile v1
 
 
+"@alienfast/i18next-loader@^1.0.16":
+  version "1.0.16"
+  resolved "https://registry.yarnpkg.com/@alienfast/i18next-loader/-/i18next-loader-1.0.16.tgz#218e9736c94457d36bdae68a5085ed68434c9bbf"
+  dependencies:
+    glob-all "^3.1.0"
+    js-yaml "^3.11.0"
+    loader-utils "^1.1.0"
+    lodash "^4.17.10"
+
 "@browser-bunyan/console-formatted-stream@^1.3.0":
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/@browser-bunyan/console-formatted-stream/-/console-formatted-stream-1.3.0.tgz#3dc059aa5c1b2a7a1f26e2706e2bdeb9a09bbe57"
@@ -133,9 +142,11 @@ after@0.8.2:
   version "0.8.2"
   resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
 
-agentkeepalive@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-2.2.0.tgz#c5d1bd4b129008f1163f236f86e5faea2026e2ef"
+agentkeepalive@^3.4.1:
+  version "3.4.1"
+  resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.4.1.tgz#aa95aebc3a749bca5ed53e3880a09f5235b48f0c"
+  dependencies:
+    humanize-ms "^1.2.1"
 
 ajv-keywords@^2.1.0:
   version "2.1.1"
@@ -454,13 +465,6 @@ aws4@^1.2.1, aws4@^1.6.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
 
-axios@^0.17.0, axios@^0.17.1:
-  version "0.17.1"
-  resolved "https://registry.yarnpkg.com/axios/-/axios-0.17.1.tgz#2d8e3e5d0bdbd7327f91bc814f5c57660f81824d"
-  dependencies:
-    follow-redirects "^1.2.5"
-    is-buffer "^1.1.5"
-
 axios@^0.18.0:
   version "0.18.0"
   resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102"
@@ -1688,7 +1692,11 @@ colors@1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
 
-colors@^1.1.2, colors@~1.1.2:
+colors@^1.2.5:
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/colors/-/colors-1.2.5.tgz#89c7ad9a374bc030df8013241f68136ed8835afc"
+
+colors@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
 
@@ -2311,16 +2319,13 @@ ejs@^2.5.6:
   version "2.5.7"
   resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.7.tgz#cc872c168880ae3c7189762fd5ffc00896c9518a"
 
-elasticsearch@^14.0.0:
-  version "14.0.0"
-  resolved "https://registry.yarnpkg.com/elasticsearch/-/elasticsearch-14.0.0.tgz#560317ca87121a73c0895818ae291f5553857bda"
+elasticsearch@^15.0.0:
+  version "15.0.0"
+  resolved "https://registry.yarnpkg.com/elasticsearch/-/elasticsearch-15.0.0.tgz#d888ceb858bba30221b68698d72c54bdcfdf2fba"
   dependencies:
-    agentkeepalive "^2.2.0"
+    agentkeepalive "^3.4.1"
     chalk "^1.0.0"
-    lodash "2.4.2"
-    lodash.get "^4.4.2"
-    lodash.isempty "^4.4.0"
-    lodash.trimend "^4.5.1"
+    lodash "^4.17.10"
 
 electron-releases@^2.1.0:
   version "2.1.0"
@@ -2959,12 +2964,6 @@ flatten@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
 
-follow-redirects@^1.2.5:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.3.0.tgz#f684871fc116d2e329fda55ef67687f4fabc905c"
-  dependencies:
-    debug "^3.1.0"
-
 follow-redirects@^1.3.0:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.4.1.tgz#d8120f4518190f55aac65bb6fc7b85fcd666d6aa"
@@ -3100,13 +3099,13 @@ gaze@^1.0.0:
   dependencies:
     globule "^1.0.0"
 
-gcp-metadata@^0.6.0:
-  version "0.6.1"
-  resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-0.6.1.tgz#62d54871fc6aeeac6a688e094abc886cb7aaacd0"
+gcp-metadata@^0.6.3:
+  version "0.6.3"
+  resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-0.6.3.tgz#4550c08859c528b370459bd77a7187ea0bdbc4ab"
   dependencies:
-    axios "^0.17.1"
+    axios "^0.18.0"
     extend "^3.0.1"
-    retry-axios "0.3.0"
+    retry-axios "0.3.2"
 
 generate-function@^2.0.0:
   version "2.0.0"
@@ -3140,6 +3139,13 @@ getpass@^0.1.1:
   dependencies:
     assert-plus "^1.0.0"
 
+glob-all@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/glob-all/-/glob-all-3.1.0.tgz#8913ddfb5ee1ac7812656241b03d5217c64b02ab"
+  dependencies:
+    glob "^7.0.5"
+    yargs "~1.2.6"
+
 glob-base@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
@@ -3207,16 +3213,16 @@ good-listener@^1.2.2:
   dependencies:
     delegate "^3.1.2"
 
-google-auth-library@^1.3.1:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-1.3.1.tgz#a0456166c9538ee062c51b8e70765a5a0e13e757"
+google-auth-library@^1.4.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-1.5.0.tgz#d9068f8bad9017224a4c41abcdcb6cf6a704e83b"
   dependencies:
     axios "^0.18.0"
-    gcp-metadata "^0.6.0"
-    gtoken "^2.1.0"
+    gcp-metadata "^0.6.3"
+    gtoken "^2.3.0"
     jws "^3.1.4"
     lodash.isstring "^4.0.1"
-    lru-cache "^4.1.1"
+    lru-cache "^4.1.2"
     retry-axios "^0.3.2"
 
 google-p12-pem@^1.0.0:
@@ -3226,11 +3232,11 @@ google-p12-pem@^1.0.0:
     node-forge "^0.7.1"
     pify "^3.0.0"
 
-googleapis@^29.0.0:
-  version "29.0.0"
-  resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-29.0.0.tgz#b1b2c080b7c5722621671f732a278a879758f4dd"
+googleapis@^30.0.0:
+  version "30.0.0"
+  resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-30.0.0.tgz#4673ba34878217539ca5aa4216fef4db6c247649"
   dependencies:
-    google-auth-library "^1.3.1"
+    google-auth-library "^1.4.0"
     pify "^3.0.0"
     qs "^6.5.1"
     url-template "^2.0.8"
@@ -3252,14 +3258,14 @@ growly@^1.2.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
 
-gtoken@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-2.1.0.tgz#e65028d32d1d52eeb17b00f85ef0f7484f0fd36f"
+gtoken@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-2.3.0.tgz#4e0ffc16432d7041a1b3dbc1d97aac17a5dc964a"
   dependencies:
-    axios "^0.17.0"
+    axios "^0.18.0"
     google-p12-pem "^1.0.0"
-    jws "^3.0.0"
-    mime "^2.0.3"
+    jws "^3.1.4"
+    mime "^2.2.0"
     pify "^3.0.0"
 
 gzip-size@^3.0.0:
@@ -3406,6 +3412,10 @@ hogan.js@^3.0.2:
     mkdirp "0.3.0"
     nopt "1.0.10"
 
+hoist-non-react-statics@^2.3.1:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz#d2ca2dfc19c5a91c5a6615ce8e564ef0347e2a40"
+
 home-or-tmp@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
@@ -3421,6 +3431,12 @@ html-comment-regex@^1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e"
 
+html-parse-stringify2@2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz#dc5670b7292ca158b7bc916c9a6735ac8872834a"
+  dependencies:
+    void-elements "^2.0.1"
+
 http-errors@1.6.2, http-errors@~1.6.1, http-errors@~1.6.2:
   version "1.6.2"
   resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736"
@@ -3465,6 +3481,16 @@ https-browserify@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
 
+humanize-ms@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
+  dependencies:
+    ms "^2.0.0"
+
+i18next-browser-languagedetector@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-2.2.0.tgz#5f41abe61964a56dce70102ab31c3ed5d5866edc"
+
 i18next-express-middleware@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/i18next-express-middleware/-/i18next-express-middleware-1.1.1.tgz#9204f28c8800ac3bff87fbee01945367956f349c"
@@ -3857,7 +3883,7 @@ js-yaml@3.5.4:
     argparse "^1.0.2"
     esprima "^2.6.0"
 
-js-yaml@^3.4.3, js-yaml@^3.9.1:
+js-yaml@^3.11.0, js-yaml@^3.4.3, js-yaml@^3.9.1:
   version "3.11.0"
   resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef"
   dependencies:
@@ -3969,7 +3995,7 @@ jwa@^1.1.4:
     ecdsa-sig-formatter "1.0.9"
     safe-buffer "^5.0.1"
 
-jws@^3.0.0, jws@^3.1.4:
+jws@^3.1.4:
   version "3.1.4"
   resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2"
   dependencies:
@@ -4229,10 +4255,6 @@ lodash.isarray@^3.0.0:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
 
-lodash.isempty@^4.4.0:
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e"
-
 lodash.isfinite@^3.3.2:
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz#fb89b65a9a80281833f0b7478b3a5104f898ebb3"
@@ -4315,18 +4337,10 @@ lodash.toplainobject@^3.0.0:
     lodash._basecopy "^3.0.0"
     lodash.keysin "^3.0.0"
 
-lodash.trimend@^4.5.1:
-  version "4.5.1"
-  resolved "https://registry.yarnpkg.com/lodash.trimend/-/lodash.trimend-4.5.1.tgz#12804437286b98cad8996b79414e11300114082f"
-
 lodash.uniq@^4.5.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
 
-lodash@2.4.2:
-  version "2.4.2"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-2.4.2.tgz#fadd834b9683073da179b3eae6d9c0d15053f73e"
-
 lodash@^3.10.1:
   version "3.10.1"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
@@ -4339,6 +4353,10 @@ lodash@^4.15.0, lodash@^4.2.0, lodash@^4.3.0:
   version "4.17.5"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
 
+lodash@^4.17.10:
+  version "4.17.10"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
+
 lolex@^1.6.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6"
@@ -4364,14 +4382,14 @@ loud-rejection@^1.0.0:
     currently-unhandled "^0.4.1"
     signal-exit "^3.0.0"
 
-lru-cache@4.1.x:
+lru-cache@4.1.x, lru-cache@^4.1.2:
   version "4.1.3"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c"
   dependencies:
     pseudomap "^1.0.2"
     yallist "^2.1.2"
 
-lru-cache@^4.0.1, lru-cache@^4.0.2, lru-cache@^4.1.1:
+lru-cache@^4.0.1, lru-cache@^4.0.2:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55"
   dependencies:
@@ -4600,9 +4618,9 @@ mime@1.4.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
 
-mime@^2.0.3:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/mime/-/mime-2.2.0.tgz#161e541965551d3b549fa1114391e3a3d55b923b"
+mime@^2.2.0:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369"
 
 mimic-fn@^1.0.0:
   version "1.1.0"
@@ -4626,6 +4644,10 @@ minimist@0.0.8:
   version "0.0.8"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
 
+minimist@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.1.0.tgz#99df657a52574c21c9057497df742790b2b4c0de"
+
 minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
@@ -4774,6 +4796,10 @@ ms@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
 
+ms@^2.0.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
+
 multer@~1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/multer/-/multer-1.3.0.tgz#092b2670f6846fa4914965efc8cf94c20fec6cd2"
@@ -5987,6 +6013,14 @@ react-dropzone@^4.2.7:
     attr-accept "^1.0.3"
     prop-types "^15.5.7"
 
+react-i18next@^7.6.1:
+  version "7.6.1"
+  resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-7.6.1.tgz#c61d8284f3c695893d51033f67c39e65f01212b6"
+  dependencies:
+    hoist-non-react-statics "^2.3.1"
+    html-parse-stringify2 "2.0.1"
+    prop-types "^15.6.0"
+
 react-onclickoutside@^6.1.1:
   version "6.7.0"
   resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.7.0.tgz#997a4d533114c9a0a104913638aa26afc084f75c"
@@ -6409,11 +6443,7 @@ restore-cursor@^2.0.0:
     onetime "^2.0.0"
     signal-exit "^3.0.2"
 
-retry-axios@0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/retry-axios/-/retry-axios-0.3.0.tgz#7858ad369872d6acaf05fd97b0490969c9c35ee2"
-
-retry-axios@^0.3.2:
+retry-axios@0.3.2, retry-axios@^0.3.2:
   version "0.3.2"
   resolved "https://registry.yarnpkg.com/retry-axios/-/retry-axios-0.3.2.tgz#5757c80f585b4cc4c4986aa2ffd47a60c6d35e13"
 
@@ -7435,6 +7465,10 @@ vm-browserify@0.0.4:
   dependencies:
     indexof "0.0.1"
 
+void-elements@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
+
 warning@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c"
@@ -7625,9 +7659,9 @@ xmlhttprequest-ssl@~1.5.4:
   version "1.5.4"
   resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.4.tgz#04f560915724b389088715cc0ed7813e9677bf57"
 
-xss@^0.3.5:
-  version "0.3.7"
-  resolved "https://registry.yarnpkg.com/xss/-/xss-0.3.7.tgz#1df6dc85c0240b455b5e5f0428bdeccd739ab4ee"
+xss@^0.3.7:
+  version "0.3.8"
+  resolved "https://registry.yarnpkg.com/xss/-/xss-0.3.8.tgz#d0cbe23bde490bc98c139f08de3899165a68af0e"
   dependencies:
     commander "^2.9.0"
     cssfilter "0.0.10"
@@ -7767,6 +7801,12 @@ yargs@^8.0.2:
     y18n "^3.2.1"
     yargs-parser "^7.0.0"
 
+yargs@~1.2.6:
+  version "1.2.6"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-1.2.6.tgz#9c7b4a82fd5d595b2bf17ab6dcc43135432fe34b"
+  dependencies:
+    minimist "^0.1.0"
+
 yargs@~3.10.0:
   version "3.10.0"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"