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

Merge branch 'master' into react-bookmark-button

Sotaro KARASAWA 9 лет назад
Родитель
Сommit
1092bb0369
68 измененных файлов с 1094 добавлено и 642 удалено
  1. 1 0
      .babelrc
  2. 9 0
      CHANGES.md
  3. 2 2
      README.md
  4. 5 2
      gulpfile.js
  5. 1 1
      lib/crowi/express-init.js
  6. 0 2
      lib/crowi/index.js
  7. 27 18
      lib/models/attachment.js
  8. 1 0
      lib/models/page.js
  9. 67 10
      lib/routes/attachment.js
  10. 1 1
      lib/routes/index.js
  11. 0 1
      lib/routes/page.js
  12. 11 0
      lib/views/layout/layout.html
  13. 3 5
      lib/views/page.html
  14. 110 12
      local_modules/crowi-fileupload-aws/index.js
  15. 5 1
      local_modules/crowi-fileupload-local/index.js
  16. 271 467
      npm-shrinkwrap.json
  17. 14 10
      package.json
  18. BIN
      public/android-chrome-192x192.png
  19. BIN
      public/apple-touch-icon-114x114-precomposed.png
  20. BIN
      public/apple-touch-icon-120x120.png
  21. BIN
      public/apple-touch-icon-180x180.png
  22. BIN
      public/apple-touch-icon-57x57-precomposed.png
  23. BIN
      public/apple-touch-icon-72x72-precomposed.png
  24. BIN
      public/apple-touch-icon-72x72.png
  25. BIN
      public/apple-touch-icon-precomposed.png
  26. BIN
      public/apple-touch-icon.png
  27. BIN
      public/favicon-16x16.png
  28. BIN
      public/favicon-32x32.png
  29. BIN
      public/favicon-96x96.png
  30. BIN
      public/favicon.ico
  31. 23 0
      public/nginx-mime.types
  32. 9 0
      resource/css/_user.scss
  33. 6 2
      resource/css/crowi.scss
  34. 15 4
      resource/js/app.js
  35. 10 3
      resource/js/components/Common/Icon.js
  36. 69 0
      resource/js/components/Common/Modal.js
  37. 4 3
      resource/js/components/Common/UserDate.js
  38. 3 5
      resource/js/components/HeaderSearchBox.js
  39. 5 4
      resource/js/components/HeaderSearchBox/SearchForm.js
  40. 4 3
      resource/js/components/HeaderSearchBox/SearchSuggest.js
  41. 3 2
      resource/js/components/Page/PageBody.js
  42. 2 1
      resource/js/components/Page/PagePath.js
  43. 114 0
      resource/js/components/PageAttachment.js
  44. 58 0
      resource/js/components/PageAttachment/Attachment.js
  45. 81 0
      resource/js/components/PageAttachment/DeleteAttachmentModal.js
  46. 30 0
      resource/js/components/PageAttachment/PageAttachmentList.js
  47. 3 2
      resource/js/components/PageHistory.js
  48. 4 3
      resource/js/components/PageHistory/PageRevisionList.js
  49. 3 2
      resource/js/components/PageHistory/Revision.js
  50. 4 3
      resource/js/components/PageHistory/RevisionDiff.js
  51. 3 2
      resource/js/components/PageList/ListView.js
  52. 3 2
      resource/js/components/PageList/Page.js
  53. 2 1
      resource/js/components/PageList/PageListMeta.js
  54. 2 1
      resource/js/components/PageList/PagePath.js
  55. 12 22
      resource/js/components/PageListSearch.js
  56. 4 5
      resource/js/components/SearchPage.js
  57. 2 1
      resource/js/components/SearchPage/SearchForm.js
  58. 5 4
      resource/js/components/SearchPage/SearchResult.js
  59. 3 2
      resource/js/components/SearchPage/SearchResultList.js
  60. 3 1
      resource/js/components/SeenUserList/UserList.js
  61. 41 0
      resource/js/components/User/User.js
  62. 3 2
      resource/js/components/User/UserPicture.js
  63. 2 1
      resource/js/crowi-form.js
  64. 0 19
      resource/js/crowi.js
  65. 8 2
      resource/js/util/Crowi.js
  66. 16 1
      resource/js/util/CrowiRenderer.js
  67. 0 0
      resource/js/util/PostProcessor/Emoji.js
  68. 7 7
      webpack.config.js

+ 1 - 0
.babelrc

@@ -1,5 +1,6 @@
 {
 {
   "presets": [
   "presets": [
+    "env",
     "es2015",
     "es2015",
     "react"
     "react"
   ]
   ]

+ 9 - 0
CHANGES.md

@@ -1,6 +1,15 @@
 CHANGES
 CHANGES
 ========
 ========
 
 
+## 1.6.0
+
+- I18N
+- Improved diff view
+- - Minus search
+- - Supports Elasticsearch 5.x
+- Special thank you for the great pull requests: @b4b4r07 @kaz @hasete2 @okonomi
+- And also special thanks for the translation: @Hidsm
+
 ## 1.5.3
 ## 1.5.3
 
 
 * Added node-shrinkwrap.json
 * Added node-shrinkwrap.json

+ 2 - 2
README.md

@@ -4,7 +4,7 @@ Crowi - The Simple & Powerful Communication Tool Based on Wiki
 ================================================================
 ================================================================
 
 
 
 
-[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/crowi/crowi/tree/v1.5.3)
+[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/crowi/crowi/tree/v1.6.0)
 
 
 [![Circle CI](https://circleci.com/gh/crowi/crowi.svg?style=svg)](https://circleci.com/gh/crowi/crowi)
 [![Circle CI](https://circleci.com/gh/crowi/crowi.svg?style=svg)](https://circleci.com/gh/crowi/crowi)
 [![Join the chat at https://gitter.im/crowi/general](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/crowi/general)
 [![Join the chat at https://gitter.im/crowi/general](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/crowi/general)
@@ -38,7 +38,7 @@ Dependencies
 
 
 * Node.js (6.x)
 * Node.js (6.x)
 * MongoDB
 * MongoDB
-* Elasticsearch (optional)
+* Elasticsearch (optional) ([Doc is here](https://github.com/crowi/crowi/wiki/Configure-Search-Functions))
 * Redis (optional)
 * Redis (optional)
 * Amazon S3 (optional)
 * Amazon S3 (optional)
 * Google Project (optional)
 * Google Project (optional)

+ 5 - 2
gulpfile.js

@@ -10,7 +10,8 @@ var concat = require('gulp-concat');
 var rename = require('gulp-rename');
 var rename = require('gulp-rename');
 var jshint = require('gulp-jshint');
 var jshint = require('gulp-jshint');
 var source = require('vinyl-source-stream');
 var source = require('vinyl-source-stream');
-var webpack = require('webpack-stream');
+var webpack = require('webpack');
+var webpackStream = require('webpack-stream');
 
 
 var del     = require('del');
 var del     = require('del');
 var stylish = require('jshint-stylish');
 var stylish = require('jshint-stylish');
@@ -90,7 +91,9 @@ gulp.task('js:concat', ['js:del'], function() {
 // move task for css and js to webpack over time.
 // move task for css and js to webpack over time.
 gulp.task('webpack', ['js:concat'], function() {
 gulp.task('webpack', ['js:concat'], function() {
   return gulp.src(js.src)
   return gulp.src(js.src)
-    .pipe(webpack(require('./webpack.config.js')))
+    .pipe(webpackStream(
+      require('./webpack.config.js'),
+      webpack))   // pass webpack2 to webpack-stream
     .pipe(gulp.dest(dirs.jsDist));
     .pipe(gulp.dest(dirs.jsDist));
 });
 });
 
 

+ 1 - 1
lib/crowi/express-init.js

@@ -34,7 +34,7 @@ module.exports = function(crowi, app) {
       fallbackLng: [User.LANG_EN_US],
       fallbackLng: [User.LANG_EN_US],
       whitelist: Object.keys(User.getLanguageLabels()).map((k) => User[k]),
       whitelist: Object.keys(User.getLanguageLabels()).map((k) => User[k]),
       backend: {
       backend: {
-        loadPath: 'locales/{{lng}}/translation.json'
+        loadPath: crowi.localeDir + '{{lng}}/translation.json'
       },
       },
       detection: {
       detection: {
         order: ['userSettingDetector', 'header', 'navigator'],
         order: ['userSettingDetector', 'header', 'navigator'],

+ 0 - 2
lib/crowi/index.js

@@ -91,8 +91,6 @@ Crowi.prototype.init = function() {
     return self.setupSlack();
     return self.setupSlack();
   }).then(function() {
   }).then(function() {
     return self.setupCsrf();
     return self.setupCsrf();
-  }).then(function() {
-    return self.buildServer();
   });
   });
 }
 }
 
 

+ 27 - 18
lib/models/attachment.js

@@ -24,11 +24,11 @@ module.exports = function(crowi) {
   }, {
   }, {
     toJSON: {
     toJSON: {
       virtuals: true
       virtuals: true
-    }
+    },
   });
   });
 
 
   attachmentSchema.virtual('fileUrl').get(function() {
   attachmentSchema.virtual('fileUrl').get(function() {
-    return fileUploader.generateUrl(this.filePath);
+    return `/files/${this._id}`;
   });
   });
 
 
   attachmentSchema.statics.findById = function(id) {
   attachmentSchema.statics.findById = function(id) {
@@ -121,24 +121,33 @@ module.exports = function(crowi) {
 
 
   };
   };
 
 
-  attachmentSchema.statics.createCacheFileName = function(attachment) {
-    return crowi.cacheDir + 'attachment-' + attachment._id;
+  attachmentSchema.statics.findDeliveryFile = function(attachment, forceUpdate) {
+    var Attachment = this;
+
+    // TODO
+    var forceUpdate = forceUpdate || false;
+
+    return fileUploader.findDeliveryFile(attachment._id, attachment.filePath);
   };
   };
 
 
-  attachmentSchema.statics.findDeliveryFile = function(attachment) {
-    // find local
-    var fs = require('fs');
-    var deliveryFile = {
-      filename: '',
-      options: {
-        headers: {
-          'Content-Type': attachment.fileFormat,
-        },
-      },
-    };
-    var cacheFileName = this.createCacheFileName(attachment);
-    // とちゅう
-    return deliveryFile;
+  attachmentSchema.statics.removeAttachment = function(attachment) {
+    const Attachment = this;
+    const filePath = attachment.filePath;
+
+    return new Promise((resolve, reject) => {
+      Attachment.remove({_id: attachment._id}, (err, data) => {
+        if (err) {
+          return reject(err);
+        }
+
+        fileUploader.deleteFile(attachment._id, filePath)
+        .then(data => {
+          resolve(data); // this may null
+        }).catch(err => {
+          reject(err);
+        });
+      });
+    });
   };
   };
 
 
   return mongoose.model('Attachment', attachmentSchema);
   return mongoose.model('Attachment', attachmentSchema);

+ 1 - 0
lib/models/page.js

@@ -383,6 +383,7 @@ module.exports = function(crowi) {
       /^\/_r\/.*/,
       /^\/_r\/.*/,
       /^\/user\/[^\/]+\/(bookmarks|comments|activities|pages|recent-create|recent-edit)/, // reserved
       /^\/user\/[^\/]+\/(bookmarks|comments|activities|pages|recent-create|recent-edit)/, // reserved
       /^\/?https?:\/\/.+$/, // avoid miss in renaming
       /^\/?https?:\/\/.+$/, // avoid miss in renaming
+      /\/{2,}/,             // avoid miss in renaming
       /.+\/edit$/,
       /.+\/edit$/,
       /.+\.md$/,
       /.+\.md$/,
       /^\/(installer|register|login|logout|admin|me|files|trash|paste|comments)(\/.*|$)/,
       /^\/(installer|register|login|logout|admin|me|files|trash|paste|comments)(\/.*|$)/,

+ 67 - 10
lib/routes/attachment.js

@@ -14,19 +14,38 @@ module.exports = function(crowi, app) {
 
 
   actions.api = api;
   actions.api = api;
 
 
-  api.redirector = function(req, res){
+  api.redirector = function(req, res, next){
     var id = req.params.id;
     var id = req.params.id;
 
 
     Attachment.findById(id)
     Attachment.findById(id)
     .then(function(data) {
     .then(function(data) {
 
 
       // TODO: file delivery plugin for cdn
       // TODO: file delivery plugin for cdn
-      var deliveryFile = Attachment.findDeliveryFile(data);
-      return res.sendFile(deliveryFile.filename, deliveryFile.options);
-    }).catch(function(err) {
-
+      Attachment.findDeliveryFile(data)
+      .then(fileName => {
+
+        var deliveryFile = {
+          fileName: fileName,
+          options: {
+            headers: {
+              'Content-Type': data.fileFormat,
+            },
+          },
+        };
+
+        if (deliveryFile.fileName.match(/^\/uploads/)) {
+          debug('Using loacal file module, just redirecting.')
+          return res.redirect(deliveryFile.fileName);
+        } else {
+          return res.sendFile(deliveryFile.fileName, deliveryFile.options);
+        }
+      }).catch(err => {
+        //debug('error', err);
+      });
+    }).catch((err) => {
+      //debug('err', err);
       // not found
       // not found
-      return res.sendFile(crowi.publicDir + '/images/file-not-found.png');
+      return res.status(404).sendFile(crowi.publicDir + '/images/file-not-found.png');
     });
     });
   };
   };
 
 
@@ -45,8 +64,15 @@ module.exports = function(crowi, app) {
 
 
     Attachment.getListByPageId(id)
     Attachment.getListByPageId(id)
     .then(function(attachments) {
     .then(function(attachments) {
+      var config = crowi.getConfig();
+      var baseUrl = (config.crowi['app:url'] || '');
       return res.json(ApiResponse.success({
       return res.json(ApiResponse.success({
-        attachments: attachments
+        attachments: attachments.map(at => {
+          var fileUrl = at.fileUrl;
+          at = at.toObject();
+          at.url = baseUrl + fileUrl;
+          return at;
+        })
       }));
       }));
     });
     });
   };
   };
@@ -107,11 +133,19 @@ module.exports = function(crowi, app) {
           // TODO size
           // TODO size
           return Attachment.create(id, req.user, filePath, originalName, fileName, fileType, fileSize);
           return Attachment.create(id, req.user, filePath, originalName, fileName, fileType, fileSize);
         }).then(function(data) {
         }).then(function(data) {
-          var imageUrl = fileUploader.generateUrl(data.filePath);
+          var fileUrl = data.fileUrl;
+          var config = crowi.getConfig();
+
+          // isLocalUrl??
+          if (!fileUrl.match(/^https?/)) {
+            fileUrl = (config.crowi['app:url'] || '') + fileUrl;
+          }
+
           var result = {
           var result = {
             page: page.toObject(),
             page: page.toObject(),
             attachment: data.toObject(),
             attachment: data.toObject(),
-            filename: imageUrl,
+            url: fileUrl,
+            filename: fileUrl, // this is for inline-attachemnets plugin http://inlineattachment.readthedocs.io/en/latest/pages/configuration.html
             pageCreated: pageCreated,
             pageCreated: pageCreated,
           };
           };
 
 
@@ -139,8 +173,31 @@ module.exports = function(crowi, app) {
     });
     });
   };
   };
 
 
+  /**
+   * @api {post} /attachments.remove Remove attachments
+   * @apiName RemoveAttachments
+   * @apiGroup Attachment
+   *
+   * @apiParam {String} attachment_id
+   */
   api.remove = function(req, res){
   api.remove = function(req, res){
-    var id = req.params.id;
+    const id = req.body.attachment_id;
+
+    Attachment.findById(id)
+    .then(function(data) {
+      const attachment = data;
+
+      Attachment.removeAttachment(attachment)
+      .then(data => {
+        debug('removeAttachment', data);
+        return res.json(ApiResponse.success({}));
+      }).catch(err => {
+        return res.status(500).json(ApiResponse.error('Error while deleting file'));
+      });
+    }).catch(err => {
+      debug('Error', err);
+      return res.status(404);
+    });
   };
   };
 
 
   return actions;
   return actions;

+ 1 - 1
lib/routes/index.js

@@ -111,7 +111,7 @@ module.exports = function(crowi, app) {
   app.post('/_api/likes.add'          , accessTokenParser , loginRequired(crowi, app) , csrf, page.api.like);
   app.post('/_api/likes.add'          , accessTokenParser , loginRequired(crowi, app) , csrf, page.api.like);
   app.post('/_api/likes.remove'       , accessTokenParser , loginRequired(crowi, app) , csrf, page.api.unlike);
   app.post('/_api/likes.remove'       , accessTokenParser , loginRequired(crowi, app) , csrf, page.api.unlike);
   app.get( '/_api/attachments.list'   , accessTokenParser , loginRequired(crowi, app) , attachment.api.list);
   app.get( '/_api/attachments.list'   , accessTokenParser , loginRequired(crowi, app) , attachment.api.list);
-  app.post('/_api/attachments.add'    , accessTokenParser , loginRequired(crowi, app) , uploads.single('file'), csrf, attachment.api.add);
+  app.post('/_api/attachments.add'    , uploads.single('file'), accessTokenParser, loginRequired(crowi, app) ,csrf, attachment.api.add);
   app.post('/_api/attachments.remove' , accessTokenParser , loginRequired(crowi, app) , csrf, attachment.api.remove);
   app.post('/_api/attachments.remove' , accessTokenParser , loginRequired(crowi, app) , csrf, attachment.api.remove);
 
 
   app.get( '/_api/revisions.get'      , accessTokenParser , loginRequired(crowi, app) , revision.api.get);
   app.get( '/_api/revisions.get'      , accessTokenParser , loginRequired(crowi, app) , revision.api.get);

+ 0 - 1
lib/routes/page.js

@@ -612,7 +612,6 @@ module.exports = function(crowi, app) {
       debug('error on _api/pages.update', err);
       debug('error on _api/pages.update', err);
       return res.json(ApiResponse.error(err));
       return res.json(ApiResponse.error(err));
     });
     });
-
   };
   };
 
 
   /**
   /**

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

@@ -11,6 +11,17 @@
 
 
   <meta name="viewport" content="width=device-width,initial-scale=1">
   <meta name="viewport" content="width=device-width,initial-scale=1">
 
 
+  <meta name="apple-mobile-web-app-title" content="{{ config.crowi['app:title']|default('Crowi') }}">
+
+  <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
+  <link rel="apple-touch-icon"                 href="/apple-touch-icon.png">
+  <link rel="apple-touch-icon" sizes="72x72"   href="/apple-touch-icon-72x72.png">
+  <link rel="apple-touch-icon" sizes="120x120" href="/apple-touch-icon-120x120.png">
+  <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-180x180.png">
+  <link rel="icon" type="image/png" href="/favicon-32x32.png" sizes="32x32">
+  <link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96">
+  <link rel="icon" type="image/png" href="/android-chrome-192x192.png" sizes="192x192">
+
   <link rel="stylesheet" href="/css/crowi{% if env  == 'production' %}.min{% endif %}.css">
   <link rel="stylesheet" href="/css/crowi{% if env  == 'production' %}.min{% endif %}.css">
 
 
   <script src="{{ assets('/js/bundled.js') }}"></script>
   <script src="{{ assets('/js/bundled.js') }}"></script>

+ 3 - 5
lib/views/page.html

@@ -61,6 +61,7 @@
   data-page-revision-id="{% if revision %}{{ revision._id.toString() }}{% endif %}"
   data-page-revision-id="{% if revision %}{{ revision._id.toString() }}{% endif %}"
   data-page-revision-created="{% if revision %}{{ revision.createdAt|datetz('U') }}{% endif %}"
   data-page-revision-created="{% if revision %}{{ revision.createdAt|datetz('U') }}{% endif %}"
   data-page-is-seen="{% if page and page.isSeenUser(user) %}1{% else %}0{% endif %}"
   data-page-is-seen="{% if page and page.isSeenUser(user) %}1{% else %}0{% endif %}"
+  data-csrftoken="{{ csrf() }}"
   >
   >
 
 
   {% if not page %}
   {% if not page %}
@@ -189,13 +190,10 @@
 
 
 
 
 {% if page %}
 {% if page %}
-<div class="page-attachments meta">
-  <p>Attachments</p>
-  <ul>
-  </ul>
+<div class="page-attachments meta" id="page-attachment">
 </div>
 </div>
 
 
-<p class="meta">
+<p class="page-meta meta">
   Path: <span id="pagePath">{{ page.path }}</span><br>
   Path: <span id="pagePath">{{ page.path }}</span><br>
   {# for BC #}
   {# for BC #}
   {% if page.lastUpdateUser %}
   {% if page.lastUpdateUser %}

+ 110 - 12
local_modules/crowi-fileupload-aws/index.js

@@ -4,9 +4,9 @@ module.exports = function(crowi) {
   'use strict';
   'use strict';
 
 
   var aws = require('aws-sdk')
   var aws = require('aws-sdk')
+    , fs = require('fs')
+    , path = require('path')
     , debug = require('debug')('crowi:lib:fileUploaderAws')
     , debug = require('debug')('crowi:lib:fileUploaderAws')
-    , Config = crowi.model('Config')
-    , config = crowi.getConfig()
     , lib = {}
     , lib = {}
     , getAwsConfig = function() {
     , getAwsConfig = function() {
         var config = crowi.getConfig();
         var config = crowi.getConfig();
@@ -18,17 +18,13 @@ module.exports = function(crowi) {
         };
         };
       };
       };
 
 
-  lib.deleteFile = function(filePath) {
-    return new Promise(function(resolve, reject) {
-      debug('Unsupported file deletion.');
-      resolve('TODO: ...');
-    });
-  };
+  function S3Factory() {
+    const awsConfig = getAwsConfig();
+    const Config = crowi.model('Config');
+    const config = crowi.getConfig();
 
 
-  lib.uploadFile = function(filePath, contentType, fileStream, options) {
-    var awsConfig = getAwsConfig();
     if (!Config.isUploadable(config)) {
     if (!Config.isUploadable(config)) {
-      return Promise.reject(new Error('AWS is not configured.'));
+      throw new Error('AWS is not configured.');
     }
     }
 
 
     aws.config.update({
     aws.config.update({
@@ -36,7 +32,37 @@ module.exports = function(crowi) {
       secretAccessKey: awsConfig.secretAccessKey,
       secretAccessKey: awsConfig.secretAccessKey,
       region: awsConfig.region
       region: awsConfig.region
     });
     });
-    var s3 = new aws.S3();
+
+    return new aws.S3();
+  }
+
+  lib.deleteFile = function(fileId, filePath) {
+    const s3 = S3Factory();
+    const awsConfig = getAwsConfig();
+
+    const params = {
+      Bucket: awsConfig.bucket,
+      Key: filePath,
+    };
+
+    return new Promise((resolve, reject) => {
+      s3.deleteObject(params, (err, data) => {
+        if (err) {
+          debug('Failed to delete object from s3', err);
+          return reject(err);
+        }
+
+        // asynclonousely delete cache
+        lib.clearCache(fileId);
+
+        resolve(data);
+      });
+    });
+  };
+
+  lib.uploadFile = function(filePath, contentType, fileStream, options) {
+    const s3 = S3Factory();
+    const awsConfig = getAwsConfig();
 
 
     var params = {Bucket: awsConfig.bucket};
     var params = {Bucket: awsConfig.bucket};
     params.ContentType = contentType;
     params.ContentType = contentType;
@@ -62,6 +88,78 @@ module.exports = function(crowi) {
     return url;
     return url;
   };
   };
 
 
+  lib.findDeliveryFile = function (fileId, filePath) {
+    var cacheFile = lib.createCacheFileName(fileId);
+
+    return new Promise((resolve, reject) => {
+      debug('find delivery file', cacheFile);
+      if (!lib.shouldUpdateCacheFile(cacheFile)) {
+        return resolve(cacheFile);
+      }
+
+      var loader = require('https');
+
+      var fileStream = fs.createWriteStream(cacheFile);
+      var fileUrl = lib.generateUrl(filePath);
+      debug('Load attachement file into local cache file', fileUrl, cacheFile);
+      var request = loader.get(fileUrl, function(response) {
+        response.pipe(fileStream, { end: false });
+        response.on('end', () => {
+          fileStream.end();
+          resolve(cacheFile);
+        });
+      });
+    });
+  };
+
+  lib.clearCache = function(fileId) {
+    const cacheFile = lib.createCacheFileName(fileId);
+
+    (new Promise((resolve, reject) => {
+      fs.unlink(cacheFile, (err) => {
+        if (err) {
+          debug('Failed to delete cache file', err);
+          // through
+        }
+
+        resolve();
+      });
+    })).then(data => {
+      // success
+    }).catch(err => {
+      debug('Failed to delete cache file (file may not exists).', err);
+      // through
+    });
+  }
+
+  // private
+  lib.createCacheFileName = function(fileId) {
+    return path.join(crowi.cacheDir, `attachment-${fileId}`);
+  };
+
+  // private
+  lib.shouldUpdateCacheFile = function(filePath) {
+    try {
+      var stats = fs.statSync(filePath);
+
+      if (!stats.isFile()) {
+        debug('Cache file not found or the file is not a regular fil.');
+        return true;
+      }
+
+      if (stats.size <= 0) {
+        debug('Cache file found but the size is 0');
+        return true;
+      }
+    } catch (e) {
+      // no such file or directory
+      debug('Stats error', e);
+      return true;
+    }
+
+    return false;
+  };
+
   return lib;
   return lib;
 };
 };
 
 

+ 5 - 1
local_modules/crowi-fileupload-local/index.js

@@ -12,7 +12,7 @@ module.exports = function(crowi) {
     , lib = {}
     , lib = {}
     , basePath = path.join(crowi.publicDir, 'uploads'); // TODO: to configurable
     , basePath = path.join(crowi.publicDir, 'uploads'); // TODO: to configurable
 
 
-  lib.deleteFile = function(filePath) {
+  lib.deleteFile = function(fileId, filePath) {
     debug('File deletion: ' + filePath);
     debug('File deletion: ' + filePath);
     return new Promise(function(resolve, reject) {
     return new Promise(function(resolve, reject) {
       fs.unlink(path.join(basePath, filePath), function(err) {
       fs.unlink(path.join(basePath, filePath), function(err) {
@@ -54,6 +54,10 @@ module.exports = function(crowi) {
     return path.join('/uploads', filePath);
     return path.join('/uploads', filePath);
   };
   };
 
 
+  lib.findDeliveryFile = function (fileId, filePath) {
+    return Promise.resolve(lib.generateUrl(filePath));
+  };
+
   return lib;
   return lib;
 };
 };
 
 

Разница между файлами не показана из-за своего большого размера
+ 271 - 467
npm-shrinkwrap.json


+ 14 - 10
package.json

@@ -31,10 +31,13 @@
     "async": "~1.5.0",
     "async": "~1.5.0",
     "aws-sdk": "~2.2.26",
     "aws-sdk": "~2.2.26",
     "axios": "0.15.x",
     "axios": "0.15.x",
-    "babel-core": "~6.7.6",
-    "babel-loader": "~6.2.4",
-    "babel-preset-es2015": "~6.6.0",
-    "babel-preset-react": "~6.5.0",
+    "babel-cli": "~6.24.1",
+    "babel-core": "~6.24.1",
+    "babel-loader": "~7.0.0",
+    "babel-polyfill": "~6.23.0",
+    "babel-preset-env": "~1.4.0",
+    "babel-preset-es2015": "~6.24.1",
+    "babel-preset-react": "~6.24.1",
     "basic-auth-connect": "~1.0.0",
     "basic-auth-connect": "~1.0.0",
     "body-parser": "~1.14.1",
     "body-parser": "~1.14.1",
     "bootstrap-sass": "~3.3.6",
     "bootstrap-sass": "~3.3.6",
@@ -54,7 +57,7 @@
     "elasticsearch": "^12.1.3",
     "elasticsearch": "^12.1.3",
     "emojify.js": "^1.1.0",
     "emojify.js": "^1.1.0",
     "errorhandler": "~1.3.4",
     "errorhandler": "~1.3.4",
-    "express": "~4.14.0",
+    "express": "~4.15.2",
     "express-form": "~0.12.0",
     "express-form": "~0.12.0",
     "express-session": "~1.14.0",
     "express-session": "~1.14.0",
     "font-awesome": "~4.7.0",
     "font-awesome": "~4.7.0",
@@ -88,8 +91,9 @@
     "multer": "~1.2.1",
     "multer": "~1.2.1",
     "nodemailer": "~2.7.0",
     "nodemailer": "~2.7.0",
     "nodemailer-ses-transport": "~1.5.0",
     "nodemailer-ses-transport": "~1.5.0",
-    "react": "~15.0.1",
-    "react-dom": "~15.0.1",
+    "react": "~15.5.0",
+    "react-bootstrap": "~0.30.10",
+    "react-dom": "~15.5.0",
     "redis": "~2.6.5",
     "redis": "~2.6.5",
     "reveal.js": "~3.2.0",
     "reveal.js": "~3.2.0",
     "socket.io": "~1.3.0",
     "socket.io": "~1.3.0",
@@ -97,9 +101,9 @@
     "sprintf": "~0.1.5",
     "sprintf": "~0.1.5",
     "swig": "~1.4.0",
     "swig": "~1.4.0",
     "vinyl-source-stream": "~1.1.0",
     "vinyl-source-stream": "~1.1.0",
-    "webpack": "~1.13.0",
-    "webpack-manifest-plugin": "~1.0.1",
-    "webpack-stream": "~3.1.0"
+    "webpack": "~2.2.0",
+    "webpack-manifest-plugin": "~1.1.0",
+    "webpack-stream": "~3.2.0"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "chai": "~1.10.0",
     "chai": "~1.10.0",

BIN
public/android-chrome-192x192.png


BIN
public/apple-touch-icon-114x114-precomposed.png


BIN
public/apple-touch-icon-120x120.png


BIN
public/apple-touch-icon-180x180.png


BIN
public/apple-touch-icon-57x57-precomposed.png


BIN
public/apple-touch-icon-72x72-precomposed.png


BIN
public/apple-touch-icon-72x72.png


BIN
public/apple-touch-icon-precomposed.png


BIN
public/apple-touch-icon.png


BIN
public/favicon-16x16.png


BIN
public/favicon-32x32.png


BIN
public/favicon-96x96.png


BIN
public/favicon.ico


+ 23 - 0
public/nginx-mime.types

@@ -0,0 +1,23 @@
+
+.page-attachments {
+  .attachment-in-use {
+    margin: 0 0 0 4px;
+  }
+
+  .attachment-delete {
+    cursor: pointer;
+    margin: 0 0 0 4px;
+  }
+
+}
+
+.attachment-delete-modal {
+
+  .attachment-delete-image {
+    text-align: center;
+
+    img {
+      max-width: 100%;
+    }
+  }
+}

+ 9 - 0
resource/css/_user.scss

@@ -58,3 +58,12 @@
     }
     }
   } // }}}
   } // }}}
 }
 }
+
+.user-component {
+  img.picture {
+    margin-right: 4px;
+  }
+  span {
+    margin-right: 4px;
+  }
+}

+ 6 - 2
resource/css/crowi.scss

@@ -17,6 +17,7 @@
 @import 'user';
 @import 'user';
 @import 'portal';
 @import 'portal';
 @import 'search';
 @import 'search';
+@import 'attachments';
 
 
 
 
 ul {
 ul {
@@ -25,8 +26,7 @@ ul {
 
 
 
 
 .meta {
 .meta {
-
-  margin-top: 32px;
+  margin-top: 0;
   padding: 16px;
   padding: 16px;
   color: #666;
   color: #666;
   border-top: solid 1px #ccc;
   border-top: solid 1px #ccc;
@@ -40,6 +40,10 @@ ul {
   }
   }
 }
 }
 
 
+.page-meta {
+  margin-bottom: 0;
+}
+
 .help-block {
 .help-block {
   font-size: .9em;
   font-size: .9em;
 }
 }

+ 15 - 4
resource/js/app.js

@@ -8,6 +8,7 @@ import HeaderSearchBox  from './components/HeaderSearchBox';
 import SearchPage       from './components/SearchPage';
 import SearchPage       from './components/SearchPage';
 import PageListSearch   from './components/PageListSearch';
 import PageListSearch   from './components/PageListSearch';
 import PageHistory      from './components/PageHistory';
 import PageHistory      from './components/PageHistory';
+import PageAttachment   from './components/PageAttachment';
 import SeenUserList     from './components/SeenUserList';
 import SeenUserList     from './components/SeenUserList';
 import BookmarkButton   from './components/BookmarkButton';
 import BookmarkButton   from './components/BookmarkButton';
 //import PageComment  from './components/PageComment';
 //import PageComment  from './components/PageComment';
@@ -18,12 +19,20 @@ if (!window) {
 
 
 const mainContent = document.querySelector('#content-main');
 const mainContent = document.querySelector('#content-main');
 let pageId = null;
 let pageId = null;
+let pageContent = null;
 if (mainContent !== null) {
 if (mainContent !== null) {
   pageId = mainContent.attributes['data-page-id'].value;
   pageId = mainContent.attributes['data-page-id'].value;
+  const rawText = document.getElementById('raw-text-original');
+  if (rawText) {
+    pageContent = rawText.innerHTML;
+  }
 }
 }
 
 
 // FIXME
 // FIXME
-const crowi = new Crowi({me: $('#content-main').data('current-username')}, window);
+const crowi = new Crowi({
+  me: $('#content-main').data('current-username'),
+  csrfToken: $('#content-main').data('csrftoken'),
+}, window);
 window.crowi = crowi;
 window.crowi = crowi;
 crowi.fetchUsers();
 crowi.fetchUsers();
 
 
@@ -31,9 +40,11 @@ const crowiRenderer = new CrowiRenderer();
 window.crowiRenderer = crowiRenderer;
 window.crowiRenderer = crowiRenderer;
 
 
 const componentMappings = {
 const componentMappings = {
-  'search-top': <HeaderSearchBox />,
-  'search-page': <SearchPage />,
-  'page-list-search': <PageListSearch />,
+  'search-top': <HeaderSearchBox crowi={crowi} />,
+  'search-page': <SearchPage crowi={crowi} />,
+  'page-list-search': <PageListSearch crowi={crowi} />,
+  'page-attachment': <PageAttachment pageId={pageId} pageContent={pageContent} crowi={crowi} />,
+
   //'revision-history': <PageHistory pageId={pageId} />,
   //'revision-history': <PageHistory pageId={pageId} />,
   //'page-comment': <PageComment />,
   //'page-comment': <PageComment />,
   'seen-user-list': <SeenUserList />,
   'seen-user-list': <SeenUserList />,

+ 10 - 3
resource/js/components/Common/Icon.js

@@ -1,22 +1,29 @@
 import React from 'react';
 import React from 'react';
+import PropTypes from 'prop-types';
 
 
 export default class Icon extends React.Component {
 export default class Icon extends React.Component {
 
 
   render() {
   render() {
     const name = this.props.name || null;
     const name = this.props.name || null;
+    const isSpin = this.props.spin ? ' fa-spinner' : '';
 
 
     if (!name) {
     if (!name) {
       return '';
       return '';
     }
     }
 
 
     return (
     return (
-      <i className={"fa fa-" + name} />
+      <i className={`fa fa-${name} ${isSpin}`} />
     );
     );
   }
   }
 }
 }
 
 
-// TODO: support spin, size and so far
+// TODO: support size and so far
 Icon.propTypes = {
 Icon.propTypes = {
-  name: React.PropTypes.string.isRequired,
+  name: PropTypes.string.isRequired,
+  spin: PropTypes.bool,
+};
+
+Icon.defaltProps = {
+  spin: false,
 };
 };
 
 

+ 69 - 0
resource/js/components/Common/Modal.js

@@ -0,0 +1,69 @@
+import React from 'react';
+
+export default class Modal extends React.Component {
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      modalShown: false,
+    };
+  }
+
+  render() {
+    if (!this.state.modalShown) {
+      return '';
+    }
+
+    return (
+      <div class="modal in" id="renamePage" style="display: block;">
+        <div class="modal-dialog">
+          <div class="modal-content">
+
+          <form role="form" id="renamePageForm" onsubmit="return false;">
+
+            <div class="modal-header">
+              <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
+              <h4 class="modal-title">Rename page</h4>
+            </div>
+            <div class="modal-body">
+                <div class="form-group">
+                  <label for="">Current page name</label><br>
+                  <code>/user/sotarok/memo/2017/04/24</code>
+                </div>
+                <div class="form-group">
+                  <label for="newPageName">New page name</label><br>
+                  <div class="input-group">
+                    <span class="input-group-addon">http://localhost:3000</span>
+                    <input type="text" class="form-control" name="new_path" id="newPageName" value="/user/sotarok/memo/2017/04/24">
+                  </div>
+                </div>
+                <div class="checkbox">
+                   <label>
+                     <input name="create_redirect" value="1" type="checkbox"> Redirect
+                   </label>
+                   <p class="help-block"> Redirect to new page if someone accesses <code>/user/sotarok/memo/2017/04/24</code>
+                   </p>
+                </div>
+
+
+
+
+
+
+
+            </div>
+            <div class="modal-footer">
+              <p><small class="pull-left" id="newPageNameCheck"></small></p>
+              <input type="hidden" name="_csrf" value="RCs7uFdR-4nacCnqKfREe8VIlcYLP2J8xzpU">
+              <input type="hidden" name="path" value="/user/sotarok/memo/2017/04/24">
+              <input type="hidden" name="page_id" value="58fd0bd74c844b8f94c2e5b3">
+              <input type="hidden" name="revision_id" value="58fd126385edfb9d8a0c073a">
+              <input type="submit" class="btn btn-primary" value="Rename!">
+            </div>
+
+          </form>
+          </div><!-- /.modal-content -->
+        </div><!-- /.modal-dialog -->
+      </div>
+  );
+}

+ 4 - 3
resource/js/components/Common/UserDate.js

@@ -1,4 +1,5 @@
 import React from 'react';
 import React from 'react';
+import PropTypes from 'prop-types';
 
 
 import moment from 'moment';
 import moment from 'moment';
 
 
@@ -21,9 +22,9 @@ export default class UserDate extends React.Component {
 }
 }
 
 
 UserDate.propTypes = {
 UserDate.propTypes = {
-  dateTime: React.PropTypes.string.isRequired,
-  format: React.PropTypes.string,
-  className: React.PropTypes.string,
+  dateTime: PropTypes.string.isRequired,
+  format: PropTypes.string,
+  className: PropTypes.string,
 };
 };
 
 
 UserDate.defaultProps = {
 UserDate.defaultProps = {

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

@@ -1,18 +1,16 @@
 // This is the root component for #search-top
 // This is the root component for #search-top
 
 
 import React from 'react';
 import React from 'react';
+import PropTypes from 'prop-types';
 
 
 import SearchForm from './HeaderSearchBox/SearchForm';
 import SearchForm from './HeaderSearchBox/SearchForm';
 import SearchSuggest from './HeaderSearchBox/SearchSuggest';
 import SearchSuggest from './HeaderSearchBox/SearchSuggest';
-import axios from 'axios'
 
 
 export default class SearchBox extends React.Component {
 export default class SearchBox extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
-    this.crowi = window.crowi; // FIXME
-
     this.state = {
     this.state = {
       searchingKeyword: '',
       searchingKeyword: '',
       searchedPages: [],
       searchedPages: [],
@@ -45,7 +43,7 @@ export default class SearchBox extends React.Component {
       searching: true,
       searching: true,
     });
     });
 
 
-    this.crowi.apiGet('/search', {q: keyword})
+    this.props.crowi.apiGet('/search', {q: keyword})
     .then(res => {
     .then(res => {
       this.setState({
       this.setState({
         searchingKeyword: keyword,
         searchingKeyword: keyword,
@@ -81,7 +79,7 @@ export default class SearchBox extends React.Component {
 }
 }
 
 
 SearchBox.propTypes = {
 SearchBox.propTypes = {
-  //pollInterval: React.PropTypes.number,
+  //pollInterval: PropTypes.number,
 };
 };
 SearchBox.defaultProps = {
 SearchBox.defaultProps = {
   //pollInterval: 1000,
   //pollInterval: 1000,

+ 5 - 4
resource/js/components/HeaderSearchBox/SearchForm.js

@@ -1,4 +1,5 @@
 import React from 'react';
 import React from 'react';
+import PropTypes from 'prop-types';
 
 
 // Header.SearchForm
 // Header.SearchForm
 export default class SearchForm extends React.Component {
 export default class SearchForm extends React.Component {
@@ -73,7 +74,7 @@ export default class SearchForm extends React.Component {
         className="search-form form-group input-group search-top-input-group"
         className="search-form form-group input-group search-top-input-group"
       >
       >
         <input
         <input
-          autocomplete="off"
+          autoComplete="off"
           type="text"
           type="text"
           className="search-top-input form-control"
           className="search-top-input form-control"
           placeholder="Search ... Page Title (Path) and Content"
           placeholder="Search ... Page Title (Path) and Content"
@@ -95,9 +96,9 @@ export default class SearchForm extends React.Component {
 }
 }
 
 
 SearchForm.propTypes = {
 SearchForm.propTypes = {
-  onSearchFormChanged: React.PropTypes.func.isRequired,
-  isShown: React.PropTypes.func.isRequired,
-  pollInterval: React.PropTypes.number,
+  onSearchFormChanged: PropTypes.func.isRequired,
+  isShown: PropTypes.func.isRequired,
+  pollInterval: PropTypes.number,
 };
 };
 SearchForm.defaultProps = {
 SearchForm.defaultProps = {
   pollInterval: 1000,
   pollInterval: 1000,

+ 4 - 3
resource/js/components/HeaderSearchBox/SearchSuggest.js

@@ -1,4 +1,5 @@
 import React from 'react';
 import React from 'react';
+import PropTypes from 'prop-types';
 
 
 import ListView from '../PageList/ListView';
 import ListView from '../PageList/ListView';
 
 
@@ -46,9 +47,9 @@ export default class SearchSuggest extends React.Component {
 }
 }
 
 
 SearchSuggest.propTypes = {
 SearchSuggest.propTypes = {
-  searchedPages: React.PropTypes.array.isRequired,
-  searchingKeyword: React.PropTypes.string.isRequired,
-  searching: React.PropTypes.bool.isRequired,
+  searchedPages: PropTypes.array.isRequired,
+  searchingKeyword: PropTypes.string.isRequired,
+  searching: PropTypes.bool.isRequired,
 };
 };
 
 
 SearchSuggest.defaultProps = {
 SearchSuggest.defaultProps = {

+ 3 - 2
resource/js/components/Page/PageBody.js

@@ -1,4 +1,5 @@
 import React from 'react';
 import React from 'react';
+import PropTypes from 'prop-types';
 
 
 export default class PageBody extends React.Component {
 export default class PageBody extends React.Component {
 
 
@@ -31,8 +32,8 @@ export default class PageBody extends React.Component {
 }
 }
 
 
 PageBody.propTypes = {
 PageBody.propTypes = {
-  page: React.PropTypes.object.isRequired,
-  pageBody: React.PropTypes.string,
+  page: PropTypes.object.isRequired,
+  pageBody: PropTypes.string,
 };
 };
 
 
 PageBody.defaultProps = {
 PageBody.defaultProps = {

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

@@ -1,4 +1,5 @@
 import React from 'react';
 import React from 'react';
+import PropTypes from 'prop-types';
 
 
 export default class PagePath extends React.Component {
 export default class PagePath extends React.Component {
 
 
@@ -56,7 +57,7 @@ export default class PagePath extends React.Component {
 }
 }
 
 
 PagePath.propTypes = {
 PagePath.propTypes = {
-  page: React.PropTypes.object.isRequired,
+  page: PropTypes.object.isRequired,
 };
 };
 
 
 PagePath.defaultProps = {
 PagePath.defaultProps = {

+ 114 - 0
resource/js/components/PageAttachment.js

@@ -0,0 +1,114 @@
+import React from 'react';
+
+import Icon from './Common/Icon';
+import PageAttachmentList from './PageAttachment/PageAttachmentList';
+import DeleteAttachmentModal from './PageAttachment/DeleteAttachmentModal';
+
+export default class PageAttachment extends React.Component {
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      attachments: [],
+      inUse: {},
+      attachmentToDelete: null,
+      deleting: false,
+      deleteError: '',
+    };
+
+    this.onAttachmentDeleteClicked = this.onAttachmentDeleteClicked.bind(this);
+    this.onAttachmentDeleteClickedConfirm = this.onAttachmentDeleteClickedConfirm.bind(this);
+  }
+
+  componentDidMount() {
+    const pageId = this.props.pageId;
+
+    if (!pageId) {
+      return ;
+    }
+
+    this.props.crowi.apiGet('/attachments.list', {page_id: pageId })
+    .then(res => {
+      const attachments = res.attachments;
+      let inUse = {};
+
+      for (const attachment of attachments) {
+        inUse[attachment._id] = this.checkIfFileInUse(attachment);
+      }
+
+      this.setState({
+        attachments: attachments,
+        inUse: inUse,
+      });
+    });
+  }
+
+  checkIfFileInUse(attachment) {
+    if (this.props.pageContent.match(attachment.url)) {
+      return true;
+    }
+    return false;
+  }
+
+  onAttachmentDeleteClicked(attachment) {
+    this.setState({
+      attachmentToDelete: attachment,
+    });
+  }
+
+  onAttachmentDeleteClickedConfirm(attachment) {
+    const attachmentId = attachment._id;
+    this.setState({
+      deleting: true,
+    });
+
+    this.props.crowi.apiPost('/attachments.remove', {attachment_id: attachmentId})
+    .then(res => {
+      this.setState({
+        attachments: this.state.attachments.filter((at) => {
+          return at._id != attachmentId;
+        }),
+        attachmentToDelete: null,
+        deleting: false,
+      });
+    }).catch(err => {
+      this.setState({
+        deleteError: 'Something went wrong.',
+        deleting: false,
+      });
+    });
+  }
+
+  render() {
+    const attachmentToDelete = this.state.attachmentToDelete;
+    let deleteModalClose = () => this.setState({ attachmentToDelete: null });
+    let showModal = attachmentToDelete !== null;
+
+    let deleteInUse = null;
+    if (attachmentToDelete !== null) {
+      deleteInUse = this.state.inUse[attachmentToDelete._id] || false;
+    }
+
+    return (
+      <div>
+        <p>Attachments</p>
+        <PageAttachmentList
+          attachments={this.state.attachments}
+          inUse={this.state.inUse}
+          onAttachmentDeleteClicked={this.onAttachmentDeleteClicked}
+        />
+        <DeleteAttachmentModal
+          show={showModal}
+          animation={false}
+          onHide={deleteModalClose}
+
+          attachmentToDelete={attachmentToDelete}
+          inUse={deleteInUse}
+          deleting={this.state.deleting}
+          deleteError={this.state.deleteError}
+          onAttachmentDeleteClickedConfirm={this.onAttachmentDeleteClickedConfirm}
+        />
+      </div>
+    );
+  }
+}

+ 58 - 0
resource/js/components/PageAttachment/Attachment.js

@@ -0,0 +1,58 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import Icon from '../Common/Icon';
+import User from '../User/User';
+
+export default class Attachment extends React.Component {
+  constructor(props) {
+    super(props);
+
+    this._onAttachmentDeleteClicked = this._onAttachmentDeleteClicked.bind(this);
+  }
+
+  iconNameByFormat(format) {
+    if (format.match(/image\/.+/i)) {
+      return 'file-image-o';
+    }
+
+    return 'file-o';
+  }
+
+  _onAttachmentDeleteClicked(event) {
+    this.props.onAttachmentDeleteClicked(this.props.attachment);
+  }
+
+  render() {
+    const attachment = this.props.attachment;
+    const attachmentId = attachment._id
+    const formatIcon = this.iconNameByFormat(attachment.fileFormat);
+
+    let fileInUse = '';
+    if (this.props.inUse) {
+      fileInUse = <span className="attachment-in-use label label-info">In Use</span>;
+    }
+
+    return (
+      <li>
+          <User user={attachment.creator} />
+          <Icon name={formatIcon} />
+
+          <a href={attachment.url}> {attachment.originalName}</a>
+
+          {fileInUse}
+
+          <a className="text-danger attachment-delete" onClick={this._onAttachmentDeleteClicked}><Icon name="trash-o" /></a>
+      </li>
+    );
+  }
+}
+
+Attachment.propTypes = {
+  attachment: PropTypes.object.isRequired,
+  inUse: PropTypes.bool.isRequired,
+  onAttachmentDeleteClicked: PropTypes.func.isRequired,
+};
+
+Attachment.defaultProps = {
+};

+ 81 - 0
resource/js/components/PageAttachment/DeleteAttachmentModal.js

@@ -0,0 +1,81 @@
+import React from 'react';
+import { Button, Modal } from 'react-bootstrap';
+
+import Icon from '../Common/Icon';
+import User from '../User/User';
+
+export default class DeleteAttachmentModal extends React.Component {
+  constructor(props) {
+    super(props);
+
+    this._onDeleteConfirm = this._onDeleteConfirm.bind(this);
+  }
+
+  _onDeleteConfirm() {
+    this.props.onAttachmentDeleteClickedConfirm(this.props.attachmentToDelete);
+  }
+
+  renderByFileFormat(attachment) {
+    if (attachment.fileFormat.match(/image\/.+/i)) {
+      return (
+        <p className="attachment-delete-image">
+          <span>
+            {attachment.originalName} uploaded by <User user={attachment.creator} username />
+          </span>
+          <img src={attachment.url} />
+        </p>
+      );
+    }
+
+    return (
+        <p className="attachment-delete-file">
+          <Icon name="file-o" />
+        </p>
+    );
+  }
+
+  render() {
+    const attachment = this.props.attachmentToDelete;
+    if (attachment === null) {
+      return null;
+    }
+
+
+    const inUse = this.props.inUse;
+
+    const props = Object.assign({}, this.props);
+    delete props.onAttachmentDeleteClickedConfirm;
+    delete props.attachmentToDelete;
+    delete props.inUse;
+    delete props.deleting;
+    delete props.deleteError;
+
+    let deletingIndicator = '';
+    if (this.props.deleting) {
+      deletingIndicator = <Icon name="spinner" spin />;
+    }
+    if (this.props.deleteError) {
+      deletingIndicator = <p>{this.props.deleteError}</p>;
+    }
+
+    let renderAttachment = this.renderByFileFormat(attachment);
+
+    return (
+      <Modal {...props} className="attachment-delete-modal" bsSize="large" aria-labelledby="contained-modal-title-lg">
+        <Modal.Header closeButton>
+          <Modal.Title id="contained-modal-title-lg">Delete attachment?</Modal.Title>
+        </Modal.Header>
+        <Modal.Body>
+          {renderAttachment}
+        </Modal.Body>
+        <Modal.Footer>
+          {deletingIndicator}
+          <Button onClick={this._onDeleteConfirm} bsStyle="danger"
+            disabled={this.props.deleting}
+            >Delete!</Button>
+        </Modal.Footer>
+      </Modal>
+    );
+  }
+}
+

+ 30 - 0
resource/js/components/PageAttachment/PageAttachmentList.js

@@ -0,0 +1,30 @@
+import React from 'react';
+
+import Attachment from './Attachment';
+
+export default class PageAttachmentList extends React.Component {
+
+  render() {
+    if (this.props.attachments <= 0) {
+      return null;
+    }
+
+    const attachmentList = this.props.attachments.map((attachment, idx) => {
+      return (
+        <Attachment
+          key={"page:attachment:" + attachment._id}
+          attachment={attachment}
+          inUse={this.props.inUse[attachment._id] || false}
+          onAttachmentDeleteClicked={this.props.onAttachmentDeleteClicked}
+         />
+      );
+    });
+
+    return (
+      <ul>
+        {attachmentList}
+      </ul>
+    );
+  }
+}
+

+ 3 - 2
resource/js/components/PageHistory.js

@@ -1,4 +1,5 @@
 import React from 'react';
 import React from 'react';
+import PropTypes from 'prop-types';
 
 
 import Icon from './Common/Icon';
 import Icon from './Common/Icon';
 import PageRevisionList from './PageHistory/PageRevisionList';
 import PageRevisionList from './PageHistory/PageRevisionList';
@@ -134,6 +135,6 @@ export default class PageHistory extends React.Component {
 }
 }
 
 
 PageHistory.propTypes = {
 PageHistory.propTypes = {
-  pageId: React.PropTypes.string,
-  crowi: React.PropTypes.object.isRequired,
+  pageId: PropTypes.string,
+  crowi: PropTypes.object.isRequired,
 };
 };

+ 4 - 3
resource/js/components/PageHistory/PageRevisionList.js

@@ -1,4 +1,5 @@
 import React from 'react';
 import React from 'react';
+import PropTypes from 'prop-types';
 
 
 import Revision     from './Revision';
 import Revision     from './Revision';
 import RevisionDiff from './RevisionDiff';
 import RevisionDiff from './RevisionDiff';
@@ -47,8 +48,8 @@ export default class PageRevisionList extends React.Component {
 }
 }
 
 
 PageRevisionList.propTypes = {
 PageRevisionList.propTypes = {
-  revisions: React.PropTypes.array,
-  diffOpened: React.PropTypes.object,
-  onDiffOpenClicked: React.PropTypes.func.isRequired,
+  revisions: PropTypes.array,
+  diffOpened: PropTypes.object,
+  onDiffOpenClicked: PropTypes.func.isRequired,
 }
 }
 
 

+ 3 - 2
resource/js/components/PageHistory/Revision.js

@@ -1,4 +1,5 @@
 import React from 'react';
 import React from 'react';
+import PropTypes from 'prop-types';
 
 
 import UserDate     from '../Common/UserDate';
 import UserDate     from '../Common/UserDate';
 import Icon         from '../Common/Icon';
 import Icon         from '../Common/Icon';
@@ -53,7 +54,7 @@ export default class Revision extends React.Component {
 }
 }
 
 
 Revision.propTypes = {
 Revision.propTypes = {
-  revision: React.PropTypes.object,
-  onDiffOpenClicked: React.PropTypes.func.isRequired,
+  revision: PropTypes.object,
+  onDiffOpenClicked: PropTypes.func.isRequired,
 }
 }
 
 

+ 4 - 3
resource/js/components/PageHistory/RevisionDiff.js

@@ -1,4 +1,5 @@
 import React from 'react';
 import React from 'react';
+import PropTypes from 'prop-types';
 
 
 import { createPatch } from 'diff';
 import { createPatch } from 'diff';
 import { Diff2Html } from 'diff2html';
 import { Diff2Html } from 'diff2html';
@@ -36,7 +37,7 @@ export default class RevisionDiff extends React.Component {
 }
 }
 
 
 RevisionDiff.propTypes = {
 RevisionDiff.propTypes = {
-  currentRevision: React.PropTypes.object.isRequired,
-  previousRevision: React.PropTypes.object.isRequired,
-  revisionDiffOpened: React.PropTypes.bool.isRequired,
+  currentRevision: PropTypes.object.isRequired,
+  previousRevision: PropTypes.object.isRequired,
+  revisionDiffOpened: PropTypes.bool.isRequired,
 }
 }

+ 3 - 2
resource/js/components/PageList/ListView.js

@@ -1,4 +1,5 @@
 import React from 'react';
 import React from 'react';
+import PropTypes from 'prop-types';
 
 
 import Page from './Page';
 import Page from './Page';
 
 
@@ -6,7 +7,7 @@ export default class ListView extends React.Component {
 
 
   render() {
   render() {
     const listView = this.props.pages.map((page) => {
     const listView = this.props.pages.map((page) => {
-      return <Page page={page} />;
+      return <Page page={page} key={"page-list:list-view:" + page._id} />;
     });
     });
 
 
     return (
     return (
@@ -20,7 +21,7 @@ export default class ListView extends React.Component {
 }
 }
 
 
 ListView.propTypes = {
 ListView.propTypes = {
-  pages: React.PropTypes.array.isRequired,
+  pages: PropTypes.array.isRequired,
 };
 };
 
 
 ListView.defaultProps = {
 ListView.defaultProps = {

+ 3 - 2
resource/js/components/PageList/Page.js

@@ -1,4 +1,5 @@
 import React from 'react';
 import React from 'react';
+import PropTypes from 'prop-types';
 
 
 import UserPicture from '../User/UserPicture';
 import UserPicture from '../User/UserPicture';
 import PageListMeta from './PageListMeta';
 import PageListMeta from './PageListMeta';
@@ -27,8 +28,8 @@ export default class Page extends React.Component {
 }
 }
 
 
 Page.propTypes = {
 Page.propTypes = {
-  page: React.PropTypes.object.isRequired,
-  linkTo: React.PropTypes.string,
+  page: PropTypes.object.isRequired,
+  linkTo: PropTypes.string,
 };
 };
 
 
 Page.defaultProps = {
 Page.defaultProps = {

+ 2 - 1
resource/js/components/PageList/PageListMeta.js

@@ -1,4 +1,5 @@
 import React from 'react';
 import React from 'react';
+import PropTypes from 'prop-types';
 
 
 export default class PageListMeta extends React.Component {
 export default class PageListMeta extends React.Component {
 
 
@@ -42,7 +43,7 @@ export default class PageListMeta extends React.Component {
 }
 }
 
 
 PageListMeta.propTypes = {
 PageListMeta.propTypes = {
-  page: React.PropTypes.object.isRequired,
+  page: PropTypes.object.isRequired,
 };
 };
 
 
 PageListMeta.defaultProps = {
 PageListMeta.defaultProps = {

+ 2 - 1
resource/js/components/PageList/PagePath.js

@@ -1,4 +1,5 @@
 import React from 'react';
 import React from 'react';
+import PropTypes from 'prop-types';
 
 
 export default class PagePath extends React.Component {
 export default class PagePath extends React.Component {
 
 
@@ -40,7 +41,7 @@ export default class PagePath extends React.Component {
 }
 }
 
 
 PagePath.propTypes = {
 PagePath.propTypes = {
-  page: React.PropTypes.object.isRequired,
+  page: PropTypes.object.isRequired,
 };
 };
 
 
 PagePath.defaultProps = {
 PagePath.defaultProps = {

+ 12 - 22
resource/js/components/PageListSearch.js

@@ -1,7 +1,8 @@
 // This is the root component for #page-list-search
 // This is the root component for #page-list-search
 
 
 import React from 'react';
 import React from 'react';
-import axios from 'axios'
+import PropTypes from 'prop-types';
+
 import SearchResult from './SearchPage/SearchResult';
 import SearchResult from './SearchPage/SearchResult';
 
 
 export default class PageListSearch extends React.Component {
 export default class PageListSearch extends React.Component {
@@ -119,29 +120,18 @@ export default class PageListSearch extends React.Component {
     this.setState({
     this.setState({
       searchingKeyword: keyword,
       searchingKeyword: keyword,
     });
     });
-    axios.get('/_api/search', {params: {q: keyword, tree: tree}})
-    .then((res) => {
-      if (res.data.ok) {
-
-        this.setState({
-          searchedKeyword: keyword,
-          searchedPages: res.data.data,
-          searchResultMeta: res.data.meta,
-        });
-      } else {
-        this.setState({
-          searchError: res.data,
-        });
-      }
-
 
 
-      // TODO error
-    })
-    .catch((res) => {
+    this.props.crowi.apiGet('/search', {q: keyword, tree: tree})
+    .then((res) => {
+      this.setState({
+        searchedKeyword: keyword,
+        searchedPages: res.data,
+        searchResultMeta: res.meta,
+      });
+    }).catch(err => {
       this.setState({
       this.setState({
-        searchError: res.data,
+        searchError: err,
       });
       });
-      // TODO error
     });
     });
   };
   };
 
 
@@ -168,7 +158,7 @@ export default class PageListSearch extends React.Component {
 }
 }
 
 
 PageListSearch.propTypes = {
 PageListSearch.propTypes = {
-  query: React.PropTypes.object,
+  query: PropTypes.object,
 };
 };
 PageListSearch.defaultProps = {
 PageListSearch.defaultProps = {
   //pollInterval: 1000,
   //pollInterval: 1000,

+ 4 - 5
resource/js/components/SearchPage.js

@@ -1,7 +1,8 @@
 // This is the root component for #search-page
 // This is the root component for #search-page
 
 
 import React from 'react';
 import React from 'react';
-import Crowi from '../util/Crowi';
+import PropTypes from 'prop-types';
+
 import SearchForm from './SearchPage/SearchForm';
 import SearchForm from './SearchPage/SearchForm';
 import SearchResult from './SearchPage/SearchResult';
 import SearchResult from './SearchPage/SearchResult';
 
 
@@ -10,8 +11,6 @@ export default class SearchPage extends React.Component {
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
-    this.crowi = window.crowi; // FIXME
-
     this.state = {
     this.state = {
       location: location,
       location: location,
       searchingKeyword: this.props.query.q || '',
       searchingKeyword: this.props.query.q || '',
@@ -72,7 +71,7 @@ export default class SearchPage extends React.Component {
       searchingKeyword: keyword,
       searchingKeyword: keyword,
     });
     });
 
 
-    this.crowi.apiGet('/search', {q: keyword})
+    this.props.crowi.apiGet('/search', {q: keyword})
     .then(res => {
     .then(res => {
       this.changeURL(keyword);
       this.changeURL(keyword);
 
 
@@ -112,7 +111,7 @@ export default class SearchPage extends React.Component {
 }
 }
 
 
 SearchPage.propTypes = {
 SearchPage.propTypes = {
-  query: React.PropTypes.object,
+  query: PropTypes.object,
 };
 };
 SearchPage.defaultProps = {
 SearchPage.defaultProps = {
   //pollInterval: 1000,
   //pollInterval: 1000,

+ 2 - 1
resource/js/components/SearchPage/SearchForm.js

@@ -1,4 +1,5 @@
 import React from 'react';
 import React from 'react';
+import PropTypes from 'prop-types';
 
 
 // Search.SearchForm
 // Search.SearchForm
 export default class SearchForm extends React.Component {
 export default class SearchForm extends React.Component {
@@ -53,7 +54,7 @@ export default class SearchForm extends React.Component {
 }
 }
 
 
 SearchForm.propTypes = {
 SearchForm.propTypes = {
-  onSearchFormChanged: React.PropTypes.func.isRequired,
+  onSearchFormChanged: PropTypes.func.isRequired,
 };
 };
 SearchForm.defaultProps = {
 SearchForm.defaultProps = {
 };
 };

+ 5 - 4
resource/js/components/SearchPage/SearchResult.js

@@ -1,4 +1,5 @@
 import React from 'react';
 import React from 'react';
+import PropTypes from 'prop-types';
 
 
 import Page from '../PageList/Page';
 import Page from '../PageList/Page';
 import SearchResultList from './SearchResultList';
 import SearchResultList from './SearchResultList';
@@ -99,10 +100,10 @@ export default class SearchResult extends React.Component {
 }
 }
 
 
 SearchResult.propTypes = {
 SearchResult.propTypes = {
-  tree: React.PropTypes.string.isRequired,
-  pages: React.PropTypes.array.isRequired,
-  searchingKeyword: React.PropTypes.string.isRequired,
-  searchResultMeta: React.PropTypes.object.isRequired,
+  tree: PropTypes.string.isRequired,
+  pages: PropTypes.array.isRequired,
+  searchingKeyword: PropTypes.string.isRequired,
+  searchResultMeta: PropTypes.object.isRequired,
 };
 };
 SearchResult.defaultProps = {
 SearchResult.defaultProps = {
   tree: '',
   tree: '',

+ 3 - 2
resource/js/components/SearchPage/SearchResultList.js

@@ -1,4 +1,5 @@
 import React from 'react';
 import React from 'react';
+import PropTypes from 'prop-types';
 
 
 import PageBody from '../Page/PageBody.js';
 import PageBody from '../Page/PageBody.js';
 
 
@@ -50,8 +51,8 @@ export default class SearchResultList extends React.Component {
 }
 }
 
 
 SearchResultList.propTypes = {
 SearchResultList.propTypes = {
-  pages: React.PropTypes.array.isRequired,
-  searchingKeyword: React.PropTypes.string.isRequired,
+  pages: PropTypes.array.isRequired,
+  searchingKeyword: PropTypes.string.isRequired,
 };
 };
 
 
 SearchResultList.defaultProps = {
 SearchResultList.defaultProps = {

+ 3 - 1
resource/js/components/SeenUserList/UserList.js

@@ -1,4 +1,6 @@
 import React from 'react';
 import React from 'react';
+import PropTypes from 'prop-types';
+
 import UserPicture from '../User/UserPicture';
 import UserPicture from '../User/UserPicture';
 
 
 export default class UserList extends React.Component {
 export default class UserList extends React.Component {
@@ -34,7 +36,7 @@ export default class UserList extends React.Component {
 }
 }
 
 
 UserList.propTypes = {
 UserList.propTypes = {
-  users: React.PropTypes.array,
+  users: PropTypes.array,
 };
 };
 
 
 UserList.defaultProps = {
 UserList.defaultProps = {

+ 41 - 0
resource/js/components/User/User.js

@@ -0,0 +1,41 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import UserPicture from './UserPicture';
+
+export default class User extends React.Component {
+
+  render() {
+    const user = this.props.user;
+    const userLink = '/user/' + user.username;
+
+    const username = this.props.username;
+    const name = this.props.name;
+
+    return (
+      <span className="user-component">
+        <a href={userLink}>
+          <UserPicture user={user} />
+
+          {username &&
+              <span className="user-component-username">@{user.username}</span>
+          }
+          {name &&
+              <span className="user-component-name">({user.name})</span>
+          }
+        </a>
+      </span>
+    );
+  }
+}
+
+User.propTypes = {
+  user: PropTypes.object.isRequired,
+  name: PropTypes.bool.isRequired,
+  username: PropTypes.bool.isRequired,
+};
+
+User.defaultProps = {
+  name: false,
+  username: false,
+};

+ 3 - 2
resource/js/components/User/UserPicture.js

@@ -1,4 +1,5 @@
 import React from 'react';
 import React from 'react';
+import PropTypes from 'prop-types';
 
 
 // TODO UserComponent?
 // TODO UserComponent?
 export default class UserPicture extends React.Component {
 export default class UserPicture extends React.Component {
@@ -36,8 +37,8 @@ export default class UserPicture extends React.Component {
 }
 }
 
 
 UserPicture.propTypes = {
 UserPicture.propTypes = {
-  user: React.PropTypes.object.isRequired,
-  size: React.PropTypes.string,
+  user: PropTypes.object.isRequired,
+  size: PropTypes.string,
 };
 };
 
 
 UserPicture.defaultProps = {
 UserPicture.defaultProps = {

+ 2 - 1
resource/js/crowi-form.js

@@ -82,7 +82,8 @@ $(function() {
   var isFormChanged = false;
   var isFormChanged = false;
   $(window).on('beforeunload', function(e) {
   $(window).on('beforeunload', function(e) {
     if (isFormChanged) {
     if (isFormChanged) {
-      return '編集中の内容があります。内容を破棄してページを移動しますか?';
+      // TODO i18n
+      return 'You haven\'t finished your comment yet. Do you want to leave without finishing?';
     }
     }
   });
   });
   $('#form-body').on('keyup change', function(e) {
   $('#form-body').on('keyup change', function(e) {

+ 0 - 19
resource/js/crowi.js

@@ -558,25 +558,6 @@ $(function() {
       return false;
       return false;
     });
     });
 
 
-    // attachment
-    var $pageAttachmentList = $('.page-attachments ul');
-    $.get('/_api/attachments.list', {page_id: pageId}, function(res) {
-      if (!res.ok) {
-        return ;
-      }
-
-      var attachments = res.attachments;
-      if (attachments.length > 0) {
-        $.each(attachments, function(i, file) {
-          $pageAttachmentList.append(
-          '<li><a href="' + file.fileUrl + '">' + (file.originalName || file.fileName) + '</a> <span class="label label-default">' + file.fileFormat + '</span></li>'
-          );
-        })
-      } else {
-        $('.page-attachments').remove();
-      }
-    });
-
     // Like
     // Like
     var $likeButton = $('.like-button');
     var $likeButton = $('.like-button');
     var $likeCount = $('#like-count');
     var $likeCount = $('#like-count');

+ 8 - 2
resource/js/util/Crowi.js

@@ -7,6 +7,7 @@ import axios from 'axios'
 export default class Crowi {
 export default class Crowi {
   constructor(context, window) {
   constructor(context, window) {
     this.context = context;
     this.context = context;
+    this.csrfToken = context.csrfToken;
 
 
     this.location = window.location || {};
     this.location = window.location || {};
     this.document = window.document || {};
     this.document = window.document || {};
@@ -118,14 +119,19 @@ export default class Crowi {
   }
   }
 
 
   apiGet(path, params) {
   apiGet(path, params) {
-    return this.apiRequest('get', path, {params});
+    return this.apiRequest('get', path, {params: params});
   }
   }
 
 
   apiPost(path, params) {
   apiPost(path, params) {
+    if (!params._csrf) {
+      params._csrf = this.csrfToken;
+    }
+
     return this.apiRequest('post', path, params);
     return this.apiRequest('post', path, params);
   }
   }
 
 
   apiRequest(method, path, params) {
   apiRequest(method, path, params) {
+
     return new Promise((resolve, reject) => {
     return new Promise((resolve, reject) => {
       axios[method](`/_api${path}`, params)
       axios[method](`/_api${path}`, params)
       .then(res => {
       .then(res => {
@@ -133,7 +139,7 @@ export default class Crowi {
           resolve(res.data);
           resolve(res.data);
         } else {
         } else {
           // FIXME?
           // FIXME?
-          reject(new Error(res.data));
+          reject(new Error(res.error));
         }
         }
       }).catch(res => {
       }).catch(res => {
           // FIXME?
           // FIXME?

+ 16 - 1
resource/js/util/CrowiRenderer.js

@@ -4,7 +4,8 @@ import hljs from 'highlight.js';
 import MarkdownFixer from './PreProcessor/MarkdownFixer';
 import MarkdownFixer from './PreProcessor/MarkdownFixer';
 import Linker        from './PreProcessor/Linker';
 import Linker        from './PreProcessor/Linker';
 import ImageExpander from './PreProcessor/ImageExpander';
 import ImageExpander from './PreProcessor/ImageExpander';
-import Emoji         from './PreProcessor/Emoji';
+
+import Emoji         from './PostProcessor/Emoji';
 
 
 import Tsv2Table from './LangProcessor/Tsv2Table';
 import Tsv2Table from './LangProcessor/Tsv2Table';
 import Template from './LangProcessor/Template';
 import Template from './LangProcessor/Template';
@@ -18,6 +19,8 @@ export default class CrowiRenderer {
       new MarkdownFixer(),
       new MarkdownFixer(),
       new Linker(),
       new Linker(),
       new ImageExpander(),
       new ImageExpander(),
+    ];
+    this.postProcessors = [
       new Emoji(),
       new Emoji(),
     ];
     ];
 
 
@@ -41,6 +44,17 @@ export default class CrowiRenderer {
     return markdown;
     return markdown;
   }
   }
 
 
+  postProcess(html) {
+    for (let i = 0; i < this.postProcessors.length; i++) {
+      if (!this.postProcessors[i].process) {
+        continue;
+      }
+      html = this.postProcessors[i].process(html);
+    }
+
+    return html;
+  }
+
   codeRenderer(code, lang, escaped) {
   codeRenderer(code, lang, escaped) {
     let result = '', hl;
     let result = '', hl;
 
 
@@ -116,6 +130,7 @@ export default class CrowiRenderer {
 
 
     markdown = this.preProcess(markdown);
     markdown = this.preProcess(markdown);
     html = this.parseMarkdown(markdown);
     html = this.parseMarkdown(markdown);
+    html = this.postProcess(html);
 
 
     return html;
     return html;
   }
   }

+ 0 - 0
resource/js/util/PreProcessor/Emoji.js → resource/js/util/PostProcessor/Emoji.js


+ 7 - 7
webpack.config.js

@@ -1,5 +1,6 @@
 var path = require('path');
 var path = require('path');
 var webpack = require('webpack');
 var webpack = require('webpack');
+var UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
 
 
 var ManifestPlugin = require('webpack-manifest-plugin');
 var ManifestPlugin = require('webpack-manifest-plugin');
 
 
@@ -16,19 +17,18 @@ var config = {
     filename: "[name].[hash].js"
     filename: "[name].[hash].js"
   },
   },
   resolve: {
   resolve: {
-    modulesDirectories: [
+    modules: [
       './node_modules', './resource/thirdparty-js',
       './node_modules', './resource/thirdparty-js',
     ],
     ],
   },
   },
   module: {
   module: {
-    loaders: [
+    rules: [
       {
       {
         test: /.jsx?$/,
         test: /.jsx?$/,
-        loader: 'babel-loader',
         exclude: /node_modules/,
         exclude: /node_modules/,
-        query: {
-          presets: ['es2015', 'react']
-        }
+        use: [{
+          loader: 'babel-loader',
+        }]
       }
       }
     ]
     ]
   },
   },
@@ -42,7 +42,7 @@ if (process.env && process.env.NODE_ENV !== 'development') {
         'NODE_ENV': JSON.stringify('production')
         'NODE_ENV': JSON.stringify('production')
       }
       }
     }),
     }),
-    new webpack.optimize.UglifyJsPlugin({
+    new UglifyJsPlugin({
       compress:{
       compress:{
         warnings: false
         warnings: false
       }
       }

Некоторые файлы не были показаны из-за большого количества измененных файлов