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

Merge branch 'master' into wip-v1.6.1

Sotaro KARASAWA 9 лет назад
Родитель
Сommit
54f52f00ef
42 измененных файлов с 869 добавлено и 784 удалено
  1. 1 0
      .babelrc
  2. 21 1
      lib/models/attachment.js
  3. 25 2
      lib/routes/attachment.js
  4. 3 5
      lib/views/page.html
  5. 63 20
      local_modules/crowi-fileupload-aws/index.js
  6. 3 3
      local_modules/crowi-fileupload-local/index.js
  7. 201 645
      npm-shrinkwrap.json
  8. 10 6
      package.json
  9. 23 0
      resource/css/_attachments.scss
  10. 9 0
      resource/css/_user.scss
  11. 6 2
      resource/css/crowi.scss
  12. 15 4
      resource/js/app.js
  13. 10 3
      resource/js/components/Common/Icon.js
  14. 69 0
      resource/js/components/Common/Modal.js
  15. 4 3
      resource/js/components/Common/UserDate.js
  16. 3 5
      resource/js/components/HeaderSearchBox.js
  17. 5 4
      resource/js/components/HeaderSearchBox/SearchForm.js
  18. 4 3
      resource/js/components/HeaderSearchBox/SearchSuggest.js
  19. 3 2
      resource/js/components/Page/PageBody.js
  20. 2 1
      resource/js/components/Page/PagePath.js
  21. 114 0
      resource/js/components/PageAttachment.js
  22. 58 0
      resource/js/components/PageAttachment/Attachment.js
  23. 81 0
      resource/js/components/PageAttachment/DeleteAttachmentModal.js
  24. 30 0
      resource/js/components/PageAttachment/PageAttachmentList.js
  25. 3 2
      resource/js/components/PageHistory.js
  26. 4 3
      resource/js/components/PageHistory/PageRevisionList.js
  27. 3 2
      resource/js/components/PageHistory/Revision.js
  28. 4 3
      resource/js/components/PageHistory/RevisionDiff.js
  29. 3 2
      resource/js/components/PageList/ListView.js
  30. 3 2
      resource/js/components/PageList/Page.js
  31. 2 1
      resource/js/components/PageList/PageListMeta.js
  32. 2 1
      resource/js/components/PageList/PagePath.js
  33. 12 22
      resource/js/components/PageListSearch.js
  34. 4 5
      resource/js/components/SearchPage.js
  35. 2 1
      resource/js/components/SearchPage/SearchForm.js
  36. 5 4
      resource/js/components/SearchPage/SearchResult.js
  37. 3 2
      resource/js/components/SearchPage/SearchResultList.js
  38. 3 1
      resource/js/components/SeenUserList/UserList.js
  39. 41 0
      resource/js/components/User/User.js
  40. 3 2
      resource/js/components/User/UserPicture.js
  41. 0 19
      resource/js/crowi.js
  42. 9 3
      resource/js/util/Crowi.js

+ 1 - 0
.babelrc

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

+ 21 - 1
lib/models/attachment.js

@@ -127,7 +127,27 @@ module.exports = function(crowi) {
     // TODO
     // TODO
     var forceUpdate = forceUpdate || false;
     var forceUpdate = forceUpdate || false;
 
 
-    return fileUploader.findDeliveryFile(attachment);
+    return fileUploader.findDeliveryFile(attachment._id, attachment.filePath);
+  };
+
+  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);

+ 25 - 2
lib/routes/attachment.js

@@ -43,7 +43,7 @@ module.exports = function(crowi, app) {
         //debug('error', err);
         //debug('error', err);
       });
       });
     }).catch((err) => {
     }).catch((err) => {
-      debug('err', err);
+      //debug('err', err);
       // not found
       // not found
       return res.status(404).sendFile(crowi.publicDir + '/images/file-not-found.png');
       return res.status(404).sendFile(crowi.publicDir + '/images/file-not-found.png');
     });
     });
@@ -173,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;

+ 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 %}

+ 63 - 20
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,8 +88,8 @@ module.exports = function(crowi) {
     return url;
     return url;
   };
   };
 
 
-  lib.findDeliveryFile = function (attachment) {
-    var cacheFile = lib.createCacheFileName(attachment);
+  lib.findDeliveryFile = function (fileId, filePath) {
+    var cacheFile = lib.createCacheFileName(fileId);
 
 
     return new Promise((resolve, reject) => {
     return new Promise((resolve, reject) => {
       debug('find delivery file', cacheFile);
       debug('find delivery file', cacheFile);
@@ -71,11 +97,10 @@ module.exports = function(crowi) {
         return resolve(cacheFile);
         return resolve(cacheFile);
       }
       }
 
 
-      var fs = require('fs');
       var loader = require('https');
       var loader = require('https');
 
 
       var fileStream = fs.createWriteStream(cacheFile);
       var fileStream = fs.createWriteStream(cacheFile);
-      var fileUrl = lib.generateUrl(attachment.filePath);
+      var fileUrl = lib.generateUrl(filePath);
       debug('Load attachement file into local cache file', fileUrl, cacheFile);
       debug('Load attachement file into local cache file', fileUrl, cacheFile);
       var request = loader.get(fileUrl, function(response) {
       var request = loader.get(fileUrl, function(response) {
         response.pipe(fileStream, { end: false });
         response.pipe(fileStream, { end: false });
@@ -87,15 +112,33 @@ module.exports = function(crowi) {
     });
     });
   };
   };
 
 
+  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
   // private
-  lib.createCacheFileName = function(attachment) {
-    return crowi.cacheDir + '/attachment-' + attachment._id;
+  lib.createCacheFileName = function(fileId) {
+    return path.join(crowi.cacheDir, `attachment-${fileId}`);
   };
   };
 
 
   // private
   // private
   lib.shouldUpdateCacheFile = function(filePath) {
   lib.shouldUpdateCacheFile = function(filePath) {
-    var fs = require('fs');
-
     try {
     try {
       var stats = fs.statSync(filePath);
       var stats = fs.statSync(filePath);
 
 

+ 3 - 3
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,8 +54,8 @@ module.exports = function(crowi) {
     return path.join('/uploads', filePath);
     return path.join('/uploads', filePath);
   };
   };
 
 
-  lib.findDeliveryFile = function (attachment) {
-    return Promise.resolve(lib.generateUrl(attachment.filePath));
+  lib.findDeliveryFile = function (fileId, filePath) {
+    return Promise.resolve(lib.generateUrl(filePath));
   };
   };
 
 
   return lib;
   return lib;

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


+ 10 - 6
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.3.0",
-    "babel-preset-es2015": "~6.22.0",
-    "babel-preset-react": "~6.23.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",
@@ -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",

+ 23 - 0
resource/css/_attachments.scss

@@ -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 PageComment  from './components/PageComment';
 //import PageComment  from './components/PageComment';
 
 
@@ -17,12 +18,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();
 
 
@@ -30,9 +39,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 = {

+ 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();
-      }
-    });
-
     // bookmark
     // bookmark
     var $bookmarkButton = $('#bookmark-button');
     var $bookmarkButton = $('#bookmark-button');
     $.get('/_api/bookmarks.get', {page_id: pageId}, function(res) {
     $.get('/_api/bookmarks.get', {page_id: pageId}, function(res) {

+ 9 - 3
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,22 +119,27 @@ 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 => {
         if (res.data.ok) {
         if (res.data.ok) {
           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?

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