Sfoglia il codice sorgente

Merge branch 'master' into attach_an_icon_to_the_cursor

kawasaki hirotsugu 8 anni fa
parent
commit
288b612fa8

+ 5 - 1
CHANGES.md

@@ -1,9 +1,13 @@
 CHANGES
 ========
 
-## 3.0.14
+## 3.1.0-RC
 
+* Improvement: Group Access Control List - Select group modal
 * Improvement: Auto-format markdown tables which includes multibyte text
+* Improvement: Enable to switch show/hide border for highlight.js
+* Support: Upgrade libs
+    * elasticsearch
 
 ## 3.0.13
 

+ 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);
   });

+ 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 - 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,

+ 3 - 0
lib/models/page-group-relation.js

@@ -143,6 +143,9 @@ class PageGroupRelation {
    */
   static findByPage(page) {
 
+    if (page == null) {
+      return null;
+    }
     return this
       .find({ targetPage: page.id })
       .populate('relatedGroup')

+ 1 - 1
lib/models/page.js

@@ -344,7 +344,7 @@ module.exports = function(crowi) {
     grantLabels[GRANT_PUBLIC]     = 'Public'; // 公開
     grantLabels[GRANT_RESTRICTED] = 'Anyone with the link'; // リンクを知っている人のみ
     //grantLabels[GRANT_SPECIFIED]  = 'Specified users only'; // 特定ユーザーのみ
-    grantLabels[GRANT_USER_GROUP] = 'Only inside the group'; // 特定グループのみ
+    // grantLabels[GRANT_USER_GROUP] = 'Only inside the group'; // 特定グループのみ
     grantLabels[GRANT_OWNER]      = 'Just me'; // 自分のみ
 
     return grantLabels;

+ 24 - 7
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')
@@ -238,6 +239,7 @@ module.exports = function(crowi, app) {
       pages: [],
       tree: [],
       userRelatedGroups: [],
+      pageRelatedGroup: null,
     };
 
     var pageTeamplate = 'customlayout-selector/page';
@@ -343,9 +345,15 @@ module.exports = function(crowi, app) {
     .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);
+      if (groupRelations != null) {
+        renderVars.userRelatedGroups = groupRelations.map(relation => relation.relatedGroup);
+      }
+
+      return PageGroupRelation.findByPage(renderVars.page);
+    }).then((pageGroupRelation) => {
+      if (pageGroupRelation != null) {
+        renderVars.pageRelatedGroup = pageGroupRelation.relatedGroup;
+      }
 
       return Promise.resolve();
     });
@@ -423,10 +431,19 @@ module.exports = function(crowi, app) {
   function renderPage(pageData, req, res) {
     // create page
     if (!pageData) {
-      return res.render('customlayout-selector/not_found', {
-        author: {},
-        page: false,
-      });
+      var userRelatedGroups
+      UserGroupRelation.findAllRelationForUser(req.user)
+        .then((groupRelations) => {
+          userRelatedGroups = groupRelations.map(relation => relation.relatedGroup);
+          return Promise.resolve();
+        }).then(() => {
+          debug('not found page user group resolver : ', userRelatedGroups);
+          return res.render('customlayout-selector/not_found', {
+            author: {},
+            page: false,
+            userRelatedGroups: userRelatedGroups,
+          });
+        });
     }
 
     if (pageData.redirectTo) {

+ 35 - 7
lib/views/_form.html

@@ -49,19 +49,47 @@
       {% if forceGrant %}
       <input type="hidden" name="pageForm[grant]" value="{{ forceGrant }}">
       {% else %}
-      <select name="pageForm[grant]" class="m-r-5 selectpicker btn-group-sm">
+      <div>
+        <div id="page-grant-selector"></div>
+      </div>
+      <input type="hidden" id="page-grant" name="pageForm[grant]" value="{{ pageForm.grant|default(page.grant) }}">
+      <input id="grant-group" type="hidden" value="{% if pageForm.grant %}{{ pageForm.grant }}{% endif %}">
+<!--
+      <select id="select-grant" 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>
+        <option value="{{ grantId }}" {% if pageForm.grant|default(page.grant) == grantId %}selected{% endif %}>{{ t(grantLabel) }}</option>
         {% endfor %}
+        {% if user and user.admin && userRelatedGroups %}
+        <option id="no-group" value="/admin/user-groups">{{ t('Only inside the group') }} you have no groups.</option>
+        {% endif %}
+        <option id="group-grant" value="5">{{ t('Only inside the group') }}</option>
+        {% if pageForm.grant|default(page.grant) == "5" && pageRelatedGroup != null %}
+        <option id="group-grant" value="5" selected>{{pageRelatedGroup}}</option>
+        {% endif %}
       </select>
+      <input id="select-grant-pre" type="hidden" value="{{ page.grant }}">
       {% endif %}
+      <input id="grant-group" type="hidden" name="pageForm[grantUserGroupId]" value="">
       {% 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>
+      <div class="collapse width">
+        <select name="pageForm[grantUserGroupId]" class="selectpicker btn-group-sm">
+          {% for userGroup in userRelatedGroups %}
+          <option value="{{ userGroup.id }}">{{ userGroup.name }}</option>
+          {% endfor %}
+        </select>
+      </div>
+      {% endif %} -->
+      {% if userRelatedGroups.length != 0 %}
+      <div>
+        <select name="pageForm[grantUserGroupId]" class="selectpicker btn-group-sm">
+          {% for userGroup in userRelatedGroups %}
+          <option value="{{ userGroup.id }}">{{ userGroup.name }}</option>
+          {% endfor %}
+        </select>
+      </div>
       {% endif %}
+      <!-- <input type="hidden" id="page-grant" value="{{ page.grant }}"> -->
+      <input type="hidden" id="user-related-group-data" value="{{userRelatedGroups}}">
       <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>

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

@@ -281,7 +281,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 +299,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 +315,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 +507,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 +592,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>

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

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

+ 23 - 0
lib/views/modal/select_grant_group.html

@@ -0,0 +1,23 @@
+<div class="modal select-grant-group" id="select-grant-group">
+  <div class="modal-dialog">
+    <div class="modal-content">
+
+      <div class="modal-header bg-primary">
+        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+        <div class="modal-title">{{ t('SelectGrantGroup') }}</div>
+      </div>
+
+      <div class="modal-body">
+        <p>グループを下のリストから選択</p>
+
+        <ul class="list-inline">
+          {% for sGroup in userRelatedGroups %}
+          <li><button class="btn btn-xs btn-primary" onclick="$('#grant-group').val('{{sGroup.id}}')" data-dismiss="modal">{{sGroup.name}}</button></li>
+          {% endfor %}
+        </ul>
+
+      </div><!-- /.modal-body -->
+
+    </div><!-- /.modal-content -->
+  </div><!-- /.modal-dialog -->
+</div><!-- /.modal -->

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

@@ -3,3 +3,4 @@
 {% include '../modal/duplicate.html' %}
 {% include '../modal/put_back.html' %}
 {% include '../modal/page_name_warning.html' %}
+{% include '../modal/select_grant_group.html' %}

+ 63 - 62
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.0.14-RC",
+  "version": "3.1.0-RC",
   "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,8 +74,6 @@
     "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",
     "graceful-fs": "^4.1.11",
     "growi-pluginkit": "^1.1.0",
@@ -100,81 +81,101 @@
     "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",
     "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",
     "string-width": "^2.1.1",
-    "style-loader": "^0.21.0",
     "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"
   },
   "devDependencies": {
+    "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",
+    "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": ".",
@@ -182,9 +183,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": {

+ 30 - 0
resource/js/app.js

@@ -10,6 +10,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, { UserGroup, PageGrant } from './components/PageEditor/GrantSelector';
 import Page             from './components/Page';
 import PageListSearch   from './components/PageListSearch';
 import PageHistory      from './components/PageHistory';
@@ -39,6 +40,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');
@@ -178,6 +180,34 @@ if (pageEditorOptionsSelectorElem) {
     pageEditorOptionsSelectorElem
   );
 }
+// render GrantSelector
+const userRelatedGroups = JSON.parse(document.getElementById('user-related-group-data').textContent || '{}', (value) => {
+  return new UserGroup(value);
+});
+const pageEditorGrantSelectorElem = document.getElementById('page-grant-selector');
+const pageGrantElem = document.getElementById('page-grant');
+const pageGrantGroupElem = document.getElementById('grant-group');
+function updatePageGrantElems(newPageGrant) {
+  pageGrantElem.value = newPageGrant.grant;
+  pageGrantGroupElem.value = newPageGrant.grantGroup.userGroupId || '';
+}
+if (pageEditorGrantSelectorElem) {
+  pageGrant = new PageGrant();
+  pageGrant.grant = document.getElementById('page-grant').value;
+  const grantGroupData = JSON.parse(document.getElementById('grant-group').textContent || '{}');
+  if (grantGroupData != null) {
+    const grantGroup = new UserGroup();
+    grantGroup.userGroupId = grantGroupData.id;
+    grantGroup.userGroup = grantGroupData;
+    pageGrant.grantGroup = grantGroup;
+  }
+  ReactDOM.render(
+    <GrantSelector crowi={crowi}
+      userRelatedGroups={userRelatedGroups} pageGrant={pageGrant}
+      onChange={updatePageGrantElems} />,
+    pageEditorGrantSelectorElem
+  );
+}
 
 // render for admin
 const customCssEditorElem = document.getElementById('custom-css-editor');

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

@@ -0,0 +1,182 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import FormGroup from 'react-bootstrap/es/FormGroup';
+import FormControl from 'react-bootstrap/es/FormControl';
+import ControlLabel from 'react-bootstrap/es/ControlLabel';
+// import Button from 'react-bootstrap/es/Button';
+
+// import Modal from 'react-bootstrap/es/Modal';
+
+/**
+ * Page grant select component
+ *
+ * @export
+ * @class GrantSelector
+ * @extends {React.Component}
+ */
+export default class GrantSelector extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      pageGrant: this.props.pageGrant,
+      isGroupModalShown: false,
+    };
+
+    this.availableGrants = [1, 2, /*3, */4, 5];
+
+    this.availableGrantLabels = {
+      1: 'Public',
+      2: 'Anyone with the linc',
+      // 3:'Specified users only',
+      4: 'Just me',
+      5: 'Only inside the group',
+    };
+
+    this.onChangeGrant = this.onChangeGrant.bind(this);
+  }
+
+  // Init component when the component did mount.
+  componentDidMount() {
+    this.init();
+  }
+
+  // Initialize the component.
+  init() {
+    this.grantSelectorInputEl.value = this.state.pageGrant.grant;
+  }
+
+  /**
+   * On change event handler for pagegrant.
+   * @param {any} grant page grant
+   * @memberof GrantSelector
+   */
+  onChangeGrant(grant) {
+    const newValue = this.grantSelectorInputEl.value;
+    const newGrant = Object.assign(this.state.pageGrant, {grant: newValue});
+    this.setState({ pageGrant: newGrant });
+
+    // dispatch event
+    this.dispatchOnChange();
+  }
+
+  // (TBD)
+  // /**
+  //  * On click event handler for grant usergroup.
+  //  *
+  //  * @memberof GrantSelector
+  //  */
+  // onClickGrantGroup() {
+  //   const newValue = this.groupSelectorInputEl.value;
+  //   const newGrant = Object.assign(this.state.pageGrant, { grantGroup: newValue });
+  //   this.setState({ pageGrant: newGrant });
+
+  //   // dispatch event
+  //   this.dispatchOnChange();
+  //   // close group select modal
+  //   if (this.state.isModalShown) {
+  //     this.setState({ isGroupModalShown: false });
+  //   }
+  // }
+
+  /**
+   * dispatch onChange event
+   * @memberof GrantSelector
+   */
+  dispatchOnChange() {
+    if (this.props.onChange != null) {
+      this.props.onChange(this.state.pageGrant);
+    }
+  }
+
+  /**
+   * Render grant selector DOM.
+   * @returns
+   * @memberof GrantSelector
+   */
+  renderGrantSelector() {
+    const grantElems = this.availableGrants.map((grant) => {
+      return <option key={grant} value={grant}>{this.availableGrantLabels[grant]}</option>;
+    });
+
+    const bsClassName = 'form-control-dummy'; // set form-control* to shrink width
+
+    return (
+      <FormGroup controlId="formControlsSelect">
+        <ControlLabel>Grant:</ControlLabel>
+        <FormControl componentClass="select" placeholder="select" defaultValue={this.state.pageGrant.grant} bsClass={bsClassName} className="btn-group-sm selectpicker"
+          onChange={this.onChangeGrant}
+          inputRef={ el => this.grantSelectorInputEl=el }>
+
+          {grantElems}
+
+        </FormControl>
+      </FormGroup>
+    );
+  }
+
+  // (TBD)
+  // /**
+  //  * Render select grantgroup modal.
+  //  *
+  //  * @returns
+  //  * @memberof GrantSelector
+  //  */
+  // renderSelectGroupModal() {
+  //   // const userRelatedGroups = this.props.userRelatedGroups;
+  //   const groupList = this.userRelatedGroups.map((group) => {
+  //     return <li>
+  //         <Button onClick={this.onClickGrantGroup(group)} bsClass="btn btn-sm btn-primary">{group.name}</Button>
+  //       </li>;
+  //   });
+  //   return (
+  //     <Modal show={this.props.isGroupModalShown} className="select-grant-group">
+  //       <Modal.Header closeButton>
+  //         <Modal.Title>
+  //           Select a Group
+  //         </Modal.Title>
+  //       </Modal.Header>
+  //       <Modal.Body>
+
+  //         <ul className="list-inline">
+  //           {groupList}
+  //         </ul>
+  //       </Modal.Body>
+  //     </Modal>
+  //   );
+  // }
+
+  render() {
+    return <span>
+      <span className="m-l-5">{this.renderGrantSelector()}</span>
+    </span>;
+  }
+}
+
+export class PageGrant {
+  constructor(props) {
+    this.grant = '';
+    this.grantGroup = null;
+
+    Object.assign(this, props);
+  }
+}
+
+export class UserGroup {
+  constructor(props) {
+    this.userGroupId = '';
+    this.userGroup;
+
+    Object.assign(this, props);
+  }
+}
+
+GrantSelector.propTypes = {
+  crowi: PropTypes.object.isRequired,
+  isGroupModalShown: PropTypes.bool,
+  userRelatedGroups: PropTypes.object,
+  pageGrant: PropTypes.instanceOf(PageGrant),
+  onChange: PropTypes.func,
+};

+ 1 - 0
resource/js/util/Crowi.js

@@ -43,6 +43,7 @@ export default class Crowi {
     this.userById   = {};
     this.draft = {};
     this.editorOptions = {};
+    this.userRelatedGroups = {};
 
     this.recoverData();
   }

+ 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>`;
   }
 
 }

+ 11 - 45
resource/styles/scss/_on-edit.scss

@@ -83,6 +83,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)
@@ -97,6 +98,10 @@ body.on-edit {
           height: calc(100vh - #{$header-plus-footer});
         .react-codemirror2, .CodeMirror, .CodeMirror-scroll {
           height: calc(100vh - #{$editor-margin});
+          // less than smartphone
+          @media (max-width: $screen-xs) {
+            height: calc(100vh - #{$editor-margin-sm});
+          }
         }
       }
       }
@@ -160,7 +165,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;
     }
@@ -294,6 +299,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 {
@@ -345,50 +355,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;
+    }
+  }
+}

+ 0 - 5
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%;

+ 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';
 

+ 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

+ 5 - 1
yarn.lock

@@ -1688,7 +1688,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"