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

Merge pull request #736 from weseek/imprv/load-page-on-demand

Imprv/load page on demand
Yuki Takei 7 лет назад
Родитель
Сommit
e2e45dc058

+ 1 - 0
package.json

@@ -193,6 +193,7 @@
     "react-dropzone": "^7.0.1",
     "react-dropzone": "^7.0.1",
     "react-frame-component": "^4.0.0",
     "react-frame-component": "^4.0.0",
     "react-i18next": "=7.13.0",
     "react-i18next": "=7.13.0",
+    "react-waypoint": "^8.1.0",
     "reveal.js": "^3.5.0",
     "reveal.js": "^3.5.0",
     "sass-loader": "^7.1.0",
     "sass-loader": "^7.1.0",
     "simple-load-script": "^1.0.2",
     "simple-load-script": "^1.0.2",

+ 64 - 0
src/client/js/components/Page.jsx

@@ -0,0 +1,64 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import RevisionRenderer from './Page/RevisionRenderer';
+import HandsontableModal from './PageEditor/HandsontableModal';
+import MarkdownTable from '../models/MarkdownTable';
+import mtu from './PageEditor/MarkdownTableUtil';
+
+export default class Page extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      markdown: this.props.markdown,
+      currentTargetTableArea: null
+    };
+
+    this.saveHandlerForHandsontableModal = this.saveHandlerForHandsontableModal.bind(this);
+  }
+
+  setMarkdown(markdown) {
+    this.setState({ markdown });
+  }
+
+  /**
+   * launch HandsontableModal with data specified by arguments
+   * @param beginLineNumber
+   * @param endLineNumber
+   */
+  launchHandsontableModal(beginLineNumber, endLineNumber) {
+    const tableLines = this.state.markdown.split(/\r\n|\r|\n/).slice(beginLineNumber - 1, endLineNumber).join('\n');
+    this.setState({currentTargetTableArea: {beginLineNumber, endLineNumber}});
+    this.refs.handsontableModal.show(MarkdownTable.fromMarkdownString(tableLines));
+  }
+
+  saveHandlerForHandsontableModal(markdownTable) {
+    const newMarkdown = mtu.replaceMarkdownTableInMarkdown(markdownTable, this.state.markdown, this.state.currentTargetTableArea.beginLineNumber, this.state.currentTargetTableArea.endLineNumber);
+    this.props.onSaveWithShortcut(newMarkdown);
+    this.setState({currentTargetTableArea: null});
+  }
+
+  render() {
+    const isMobile = this.props.crowi.isMobile;
+
+    return <div className={isMobile ? 'page-mobile' : ''}>
+      <RevisionRenderer
+          crowi={this.props.crowi} crowiRenderer={this.props.crowiRenderer}
+          markdown={this.state.markdown}
+          pagePath={this.props.pagePath}
+      />
+      <HandsontableModal ref='handsontableModal' onSave={this.saveHandlerForHandsontableModal} />
+    </div>;
+  }
+}
+
+Page.propTypes = {
+  crowi: PropTypes.object.isRequired,
+  crowiRenderer: PropTypes.object.isRequired,
+  onSaveWithShortcut: PropTypes.func.isRequired,
+  markdown: PropTypes.string.isRequired,
+  pagePath: PropTypes.string.isRequired,
+  showHeadEditButton: PropTypes.bool,
+};

+ 114 - 0
src/client/js/components/Page/RevisionLoader.jsx

@@ -0,0 +1,114 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import Waypoint  from 'react-waypoint';
+
+import RevisionRenderer from './RevisionRenderer';
+
+/**
+ * Load data from server and render RevisionBody component
+ */
+export default class RevisionLoader extends React.Component {
+
+  constructor(props) {
+    super(props);
+    this.logger = require('@alias/logger')('growi:Page:RevisionLoader');
+
+    this.state = {
+      markdown: '',
+      isLoading: false,
+      isLoaded: false,
+      error: null,
+    };
+
+    this.loadData = this.loadData.bind(this);
+    this.onWaypointChange = this.onWaypointChange.bind(this);
+  }
+
+  componentWillMount() {
+    if (!this.props.lazy) {
+      this.loadData();
+    }
+  }
+
+  loadData() {
+    if (!this.state.isLoaded && !this.state.isLoading) {
+      this.setState({ isLoading: true });
+    }
+
+    const requestData = {
+      page_id: this.props.pageId,
+      revision_id: this.props.revisionId,
+    };
+
+    // load data with REST API
+    this.props.crowi.apiGet('/revisions.get', requestData)
+      .then(res => {
+        if (!res.ok) {
+          throw new Error(res.error);
+        }
+
+        this.setState({
+          markdown: res.revision.body,
+          error: null,
+        });
+      })
+      .catch(err => {
+        this.setState({ error: err });
+      })
+      .finally(() => {
+        this.setState({ isLoaded: true, isLoading: false });
+      });
+  }
+
+  onWaypointChange(event) {
+    if (event.currentPosition === Waypoint.above || event.currentPosition === Waypoint.inside) {
+      this.loadData();
+    }
+  }
+
+  render() {
+    // ----- before load -----
+    if (this.props.lazy && !this.state.isLoaded) {
+      return <Waypoint onPositionChange={this.onWaypointChange} bottomOffset='-100px'>
+        <div className="wiki"></div>
+      </Waypoint>;
+    }
+
+    // ----- loading -----
+    if (this.state.isLoading) {
+      return (
+        <div className="wiki">
+          <div className="text-muted text-center">
+            <i className="fa fa-2x fa-spinner fa-pulse mr-1"></i>
+          </div>
+        </div>
+      );
+    }
+
+    // ----- after load -----
+    let markdown = this.state.markdown;
+    if (this.state.error != null) {
+      markdown = `<span class="text-muted"><em>${this.state.error}</em></span>`;
+    }
+
+    return (
+      <RevisionRenderer
+          crowi={this.props.crowi} crowiRenderer={this.props.crowiRenderer}
+          pagePath={this.props.pagePath}
+          markdown={markdown}
+          highlightKeywords={this.props.highlightKeywords}
+      />
+    );
+  }
+}
+
+RevisionLoader.propTypes = {
+  crowi: PropTypes.object.isRequired,
+  crowiRenderer: PropTypes.object.isRequired,
+  pageId: PropTypes.string.isRequired,
+  pagePath: PropTypes.string.isRequired,
+  revisionId: PropTypes.string.isRequired,
+  lazy: PropTypes.bool,
+  highlightKeywords: PropTypes.string,
+};

+ 10 - 37
src/client/js/components/Page.js → src/client/js/components/Page/RevisionRenderer.jsx

@@ -1,29 +1,25 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-import RevisionBody from './Page/RevisionBody';
-import HandsontableModal from './PageEditor/HandsontableModal';
-import MarkdownTable from '../models/MarkdownTable';
-import mtu from './PageEditor/MarkdownTableUtil';
+import RevisionBody from './RevisionBody';
 
 
-export default class Page extends React.Component {
+export default class RevisionRenderer extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
     this.state = {
     this.state = {
       html: '',
       html: '',
-      markdown: '',
-      currentTargetTableArea: null
     };
     };
 
 
     this.renderHtml = this.renderHtml.bind(this);
     this.renderHtml = this.renderHtml.bind(this);
     this.getHighlightedBody = this.getHighlightedBody.bind(this);
     this.getHighlightedBody = this.getHighlightedBody.bind(this);
-    this.saveHandlerForHandsontableModal = this.saveHandlerForHandsontableModal.bind(this);
+
+    this.setMarkdown(this.props.markdown);
   }
   }
 
 
-  componentWillMount() {
-    this.renderHtml(this.props.markdown, this.props.highlightKeywords);
+  componentWillReceiveProps(nextProps) {
+    this.renderHtml(nextProps.markdown, this.props.highlightKeywords);
   }
   }
 
 
   setMarkdown(markdown) {
   setMarkdown(markdown) {
@@ -52,27 +48,9 @@ export default class Page extends React.Component {
     return returnBody;
     return returnBody;
   }
   }
 
 
-  /**
-   * launch HandsontableModal with data specified by arguments
-   * @param beginLineNumber
-   * @param endLineNumber
-   */
-  launchHandsontableModal(beginLineNumber, endLineNumber) {
-    const tableLines = this.state.markdown.split(/\r\n|\r|\n/).slice(beginLineNumber - 1, endLineNumber).join('\n');
-    this.setState({currentTargetTableArea: {beginLineNumber, endLineNumber}});
-    this.refs.handsontableModal.show(MarkdownTable.fromMarkdownString(tableLines));
-  }
-
-  saveHandlerForHandsontableModal(markdownTable) {
-    const newMarkdown = mtu.replaceMarkdownTableInMarkdown(markdownTable, this.state.markdown, this.state.currentTargetTableArea.beginLineNumber, this.state.currentTargetTableArea.endLineNumber);
-    this.props.onSaveWithShortcut(newMarkdown);
-    this.setState({currentTargetTableArea: null});
-  }
-
   renderHtml(markdown, highlightKeywords) {
   renderHtml(markdown, highlightKeywords) {
     let context = {
     let context = {
       markdown,
       markdown,
-      dom: this.revisionBodyElement,
       currentPagePath: this.props.pagePath,
       currentPagePath: this.props.pagePath,
     };
     };
 
 
@@ -89,7 +67,7 @@ export default class Page extends React.Component {
       })
       })
       .then(() => interceptorManager.process('prePostProcess', context))
       .then(() => interceptorManager.process('prePostProcess', context))
       .then(() => {
       .then(() => {
-        context.parsedHTML = crowiRenderer.postProcess(context.parsedHTML, context.dom);
+        context.parsedHTML = crowiRenderer.postProcess(context.parsedHTML);
 
 
         // highlight
         // highlight
         if (highlightKeywords != null) {
         if (highlightKeywords != null) {
@@ -108,27 +86,22 @@ export default class Page extends React.Component {
 
 
   render() {
   render() {
     const config = this.props.crowi.getConfig();
     const config = this.props.crowi.getConfig();
-    const isMobile = this.props.crowi.isMobile;
     const isMathJaxEnabled = !!config.env.MATHJAX;
     const isMathJaxEnabled = !!config.env.MATHJAX;
 
 
-    return <div className={isMobile ? 'page-mobile' : ''}>
+    return (
       <RevisionBody
       <RevisionBody
           html={this.state.html}
           html={this.state.html}
-          inputRef={el => this.revisionBodyElement = el}
           isMathJaxEnabled={isMathJaxEnabled}
           isMathJaxEnabled={isMathJaxEnabled}
           renderMathJaxOnInit={true}
           renderMathJaxOnInit={true}
       />
       />
-      <HandsontableModal ref='handsontableModal' onSave={this.saveHandlerForHandsontableModal} />
-    </div>;
+    );
   }
   }
 }
 }
 
 
-Page.propTypes = {
+RevisionRenderer.propTypes = {
   crowi: PropTypes.object.isRequired,
   crowi: PropTypes.object.isRequired,
   crowiRenderer: PropTypes.object.isRequired,
   crowiRenderer: PropTypes.object.isRequired,
-  onSaveWithShortcut: PropTypes.func.isRequired,
   markdown: PropTypes.string.isRequired,
   markdown: PropTypes.string.isRequired,
   pagePath: PropTypes.string.isRequired,
   pagePath: PropTypes.string.isRequired,
-  showHeadEditButton: PropTypes.bool,
   highlightKeywords: PropTypes.string,
   highlightKeywords: PropTypes.string,
 };
 };

+ 2 - 4
src/client/js/components/PageComment/Comment.js

@@ -74,7 +74,6 @@ export default class Comment extends React.Component {
     const isMathJaxEnabled = !!config.env.MATHJAX;
     const isMathJaxEnabled = !!config.env.MATHJAX;
     return (
     return (
       <RevisionBody html={this.state.html}
       <RevisionBody html={this.state.html}
-          inputRef={el => this.revisionBodyElement = el}
           isMathJaxEnabled={isMathJaxEnabled}
           isMathJaxEnabled={isMathJaxEnabled}
           renderMathJaxOnInit={true}
           renderMathJaxOnInit={true}
           additionalClassName="comment" />
           additionalClassName="comment" />
@@ -82,9 +81,8 @@ export default class Comment extends React.Component {
   }
   }
 
 
   renderHtml(markdown) {
   renderHtml(markdown) {
-    var context = {
+    const context = {
       markdown,
       markdown,
-      dom: this.revisionBodyElement,
     };
     };
 
 
     const crowiRenderer = this.props.crowiRenderer;
     const crowiRenderer = this.props.crowiRenderer;
@@ -101,7 +99,7 @@ export default class Comment extends React.Component {
       })
       })
       .then(() => interceptorManager.process('prePostProcess', context))
       .then(() => interceptorManager.process('prePostProcess', context))
       .then(() => {
       .then(() => {
-        context.parsedHTML = crowiRenderer.postProcess(context.parsedHTML, context.dom);
+        context.parsedHTML = crowiRenderer.postProcess(context.parsedHTML);
       })
       })
       .then(() => interceptorManager.process('postPostProcess', context))
       .then(() => interceptorManager.process('postPostProcess', context))
       .then(() => interceptorManager.process('preRenderCommentHtml', context))
       .then(() => interceptorManager.process('preRenderCommentHtml', context))

+ 2 - 4
src/client/js/components/PageComment/CommentForm.jsx

@@ -129,15 +129,13 @@ export default class CommentForm extends React.Component {
   getCommentHtml() {
   getCommentHtml() {
     return (
     return (
       <CommentPreview
       <CommentPreview
-        html={this.state.html}
-        inputRef={el => this.previewElement = el}/>
+        html={this.state.html} />
     );
     );
   }
   }
 
 
   renderHtml(markdown) {
   renderHtml(markdown) {
     const context = {
     const context = {
       markdown,
       markdown,
-      dom: this.previewElement,
     };
     };
 
 
     const growiRenderer = this.growiRenderer;
     const growiRenderer = this.growiRenderer;
@@ -154,7 +152,7 @@ export default class CommentForm extends React.Component {
       })
       })
       .then(() => interceptorManager.process('prePostProcess', context))
       .then(() => interceptorManager.process('prePostProcess', context))
       .then(() => {
       .then(() => {
-        context.parsedHTML = growiRenderer.postProcess(context.parsedHTML, context.dom);
+        context.parsedHTML = growiRenderer.postProcess(context.parsedHTML);
       })
       })
       .then(() => interceptorManager.process('postPostProcess', context))
       .then(() => interceptorManager.process('postPostProcess', context))
       .then(() => interceptorManager.process('preRenderCommentPreviewHtml', context))
       .then(() => interceptorManager.process('preRenderCommentPreviewHtml', context))

+ 1 - 2
src/client/js/components/PageEditor.js

@@ -263,7 +263,6 @@ export default class PageEditor extends React.Component {
     // render html
     // render html
     const context = {
     const context = {
       markdown: this.state.markdown,
       markdown: this.state.markdown,
-      dom: this.previewElement,
       currentPagePath: decodeURIComponent(location.pathname)
       currentPagePath: decodeURIComponent(location.pathname)
     };
     };
 
 
@@ -281,7 +280,7 @@ export default class PageEditor extends React.Component {
       })
       })
       .then(() => interceptorManager.process('prePostProcess', context))
       .then(() => interceptorManager.process('prePostProcess', context))
       .then(() => {
       .then(() => {
-        context.parsedHTML = growiRenderer.postProcess(context.parsedHTML, context.dom);
+        context.parsedHTML = growiRenderer.postProcess(context.parsedHTML);
       })
       })
       .then(() => interceptorManager.process('postPostProcess', context))
       .then(() => interceptorManager.process('postPostProcess', context))
       .then(() => interceptorManager.process('preRenderPreviewHtml', context))
       .then(() => interceptorManager.process('preRenderPreviewHtml', context))

+ 4 - 4
src/client/js/components/SearchPage/SearchResultList.js

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 
 
 import GrowiRenderer from '../../util/GrowiRenderer';
 import GrowiRenderer from '../../util/GrowiRenderer';
 
 
-import Page from '../Page.js';
+import RevisionLoader from '../Page/RevisionLoader';
 
 
 export default class SearchResultList extends React.Component {
 export default class SearchResultList extends React.Component {
 
 
@@ -15,15 +15,15 @@ export default class SearchResultList extends React.Component {
 
 
   render() {
   render() {
     const resultList = this.props.pages.map((page) => {
     const resultList = this.props.pages.map((page) => {
-      const pageBody = page.revision.body;
       return (
       return (
         <div id={page._id} key={page._id} className="search-result-page">
         <div id={page._id} key={page._id} className="search-result-page">
           <h2><a href={page.path}>{page.path}</a></h2>
           <h2><a href={page.path}>{page.path}</a></h2>
-          <Page
+          <RevisionLoader
             crowi={this.props.crowi}
             crowi={this.props.crowi}
             crowiRenderer={this.growiRenderer}
             crowiRenderer={this.growiRenderer}
-            markdown={pageBody}
+            pageId={page._id}
             pagePath={page.path}
             pagePath={page.path}
+            revisionId={page.revision}
             highlightKeywords={this.props.searchingKeyword}
             highlightKeywords={this.props.searchingKeyword}
           />
           />
         </div>
         </div>

+ 14 - 9
src/client/js/legacy/crowi.js

@@ -17,7 +17,7 @@ require('jquery.cookie');
 require('bootstrap-select');
 require('bootstrap-select');
 
 
 import GrowiRenderer from '../util/GrowiRenderer';
 import GrowiRenderer from '../util/GrowiRenderer';
-import Page from '../components/Page';
+import RevisionLoader from '../components/Page/RevisionLoader';
 
 
 require('./thirdparty-js/agile-admin');
 require('./thirdparty-js/agile-admin');
 
 
@@ -525,7 +525,7 @@ $(function() {
 
 
   // for list page
   // for list page
   let growiRendererForTimeline = null;
   let growiRendererForTimeline = null;
-  $('a[data-toggle="tab"][href="#view-timeline"]').on('show.bs.tab', function() {
+  $('a[data-toggle="tab"][href="#view-timeline"]').on('shown.bs.tab', function() {
     const isShown = $('#view-timeline').data('shown');
     const isShown = $('#view-timeline').data('shown');
 
 
     if (growiRendererForTimeline == null) {
     if (growiRendererForTimeline == null) {
@@ -535,16 +535,21 @@ $(function() {
     if (isShown == 0) {
     if (isShown == 0) {
       $('#view-timeline .timeline-body').each(function() {
       $('#view-timeline .timeline-body').each(function() {
         const id = $(this).attr('id');
         const id = $(this).attr('id');
-        const contentId = '#' + id + ' > script';
         const revisionBody = '#' + id + ' .revision-body';
         const revisionBody = '#' + id + ' .revision-body';
         const revisionBodyElem = document.querySelector(revisionBody);
         const revisionBodyElem = document.querySelector(revisionBody);
         /* eslint-disable no-unused-vars */
         /* eslint-disable no-unused-vars */
         const revisionPath = '#' + id + ' .revision-path';
         const revisionPath = '#' + id + ' .revision-path';
         /* eslint-enable */
         /* eslint-enable */
-        const pagePath = document.getElementById(id).getAttribute('data-page-path');
-        const markdown = entities.decodeHTML($(contentId).html());
-
-        ReactDOM.render(<Page crowi={crowi} crowiRenderer={growiRendererForTimeline} markdown={markdown} pagePath={pagePath} />, revisionBodyElem);
+        const timelineElm = document.getElementById(id);
+        const pageId = timelineElm.getAttribute('data-page-id');
+        const pagePath = timelineElm.getAttribute('data-page-path');
+        const revisionId = timelineElm.getAttribute('data-revision');
+
+        ReactDOM.render(
+          <RevisionLoader lazy={true}
+            crowi={crowi} crowiRenderer={growiRendererForTimeline}
+            pageId={pageId} pagePath={pagePath} revisionId={revisionId} />,
+          revisionBodyElem);
       });
       });
 
 
       $('#view-timeline').data('shown', 1);
       $('#view-timeline').data('shown', 1);
@@ -840,6 +845,6 @@ window.addEventListener('keydown', (event) => {
 });
 });
 
 
 // adjust min-height of page for print temporarily
 // adjust min-height of page for print temporarily
-window.onbeforeprint = function () {
-  $("#page-wrapper").css("min-height", "0px");
+window.onbeforeprint = function() {
+  $('#page-wrapper').css('min-height', '0px');
 };
 };

+ 2 - 2
src/client/js/util/GrowiRenderer.js

@@ -151,12 +151,12 @@ export default class GrowiRenderer {
     return this.md.render(markdown);
     return this.md.render(markdown);
   }
   }
 
 
-  postProcess(html, dom) {
+  postProcess(html) {
     for (let i = 0; i < this.postProcessors.length; i++) {
     for (let i = 0; i < this.postProcessors.length; i++) {
       if (!this.postProcessors[i].process) {
       if (!this.postProcessors[i].process) {
         continue;
         continue;
       }
       }
-      html = this.postProcessors[i].process(html, dom);
+      html = this.postProcessors[i].process(html);
     }
     }
 
 
     return html;
     return html;

+ 3 - 5
src/server/models/page.js

@@ -624,7 +624,7 @@ module.exports = function(crowi) {
     return await findListFromBuilderAndViewer(builder, currentUser, opt);
     return await findListFromBuilderAndViewer(builder, currentUser, opt);
   };
   };
 
 
-  pageSchema.statics.findListByPageIds = async function(ids, user, option) {
+  pageSchema.statics.findListByPageIds = async function(ids, option) {
     const User = crowi.model('User');
     const User = crowi.model('User');
 
 
     const opt = Object.assign({}, option);
     const opt = Object.assign({}, option);
@@ -632,16 +632,14 @@ module.exports = function(crowi) {
 
 
     builder.addConditionToExcludeRedirect();
     builder.addConditionToExcludeRedirect();
     builder.addConditionToPagenate(opt.offset, opt.limit);
     builder.addConditionToPagenate(opt.offset, opt.limit);
-    builder.populateDataToShowRevision(User.USER_PUBLIC_FIELDS);  // TODO omit this line after fixing GC-1323
-                                                                  // https://weseek.myjetbrains.com/youtrack/issue/GC-1323
 
 
     const totalCount = await builder.query.exec('count');
     const totalCount = await builder.query.exec('count');
-    const q = builder.query;
+    const q = builder.query
+      .populate({ path: 'lastUpdateUser', model: 'User', select: User.USER_PUBLIC_FIELDS });
     const pages = await q.exec('find');
     const pages = await q.exec('find');
 
 
     const result = { pages, totalCount, offset: opt.offset, limit: opt.limit };
     const result = { pages, totalCount, offset: opt.offset, limit: opt.limit };
     return result;
     return result;
-
   };
   };
 
 
 
 

+ 0 - 16
src/server/models/revision.js

@@ -40,22 +40,6 @@ module.exports = function(crowi) {
       });
       });
   };
   };
 
 
-  revisionSchema.statics.findRevision = function(id) {
-    const Revision = this;
-
-    return new Promise(function(resolve, reject) {
-      Revision.findById(id)
-        .populate('author')
-        .exec(function(err, data) {
-          if (err) {
-            return reject(err);
-          }
-
-          return resolve(data);
-        });
-    });
-  };
-
   revisionSchema.statics.findRevisions = function(ids) {
   revisionSchema.statics.findRevisions = function(ids) {
     const Revision = this,
     const Revision = this,
       User = crowi.model('User');
       User = crowi.model('User');

+ 0 - 8
src/server/routes/page.js

@@ -142,14 +142,6 @@ module.exports = function(crowi, app) {
       result.pages.pop();
       result.pages.pop();
     }
     }
 
 
-    // populate for timeline
-    if (Config.isEnabledTimeline(config)) {
-      await Page.populate(result.pages, {
-        path: 'revision',
-        model: 'Revision',
-      });
-    }
-
     renderVars.viewConfig = {
     renderVars.viewConfig = {
       seener_threshold: SEENER_THRESHOLD,
       seener_threshold: SEENER_THRESHOLD,
     };
     };

+ 29 - 20
src/server/routes/revision.js

@@ -1,12 +1,13 @@
 module.exports = function(crowi, app) {
 module.exports = function(crowi, app) {
   'use strict';
   'use strict';
 
 
-  var debug = require('debug')('growi:routes:revision')
-    , Page = crowi.model('Page')
-    , Revision = crowi.model('Revision')
-    , ApiResponse = require('../util/apiResponse')
-    , actions = {}
-  ;
+  const debug = require('debug')('growi:routes:revision');
+  const logger = require('@alias/logger')('growi:routes:revision');
+  const Page = crowi.model('Page');
+  const Revision = crowi.model('Revision');
+  const ApiResponse = require('../util/apiResponse');
+
+  const actions = {};
   actions.api = {};
   actions.api = {};
 
 
   /**
   /**
@@ -14,23 +15,31 @@ module.exports = function(crowi, app) {
    * @apiName GetRevision
    * @apiName GetRevision
    * @apiGroup Revision
    * @apiGroup Revision
    *
    *
+   * @apiParam {String} page_id Page Id.
    * @apiParam {String} revision_id Revision Id.
    * @apiParam {String} revision_id Revision Id.
    */
    */
-  actions.api.get = function(req, res) {
-    var revisionId = req.query.revision_id;
+  actions.api.get = async function(req, res) {
+    const pageId = req.query.page_id;
+    const revisionId = req.query.revision_id;
 
 
-    Revision
-      .findRevision(revisionId)
-      .then(function(revisionData) {
-        var result = {
-          revision: revisionData,
-        };
-        return res.json(ApiResponse.success(result));
-      })
-      .catch(function(err) {
-        debug('Error revisios.get', err);
-        return res.json(ApiResponse.error(err));
-      });
+    if (!pageId || !revisionId) {
+      return res.json(ApiResponse.error('Parameter page_id and revision_id are required.'));
+    }
+
+    // check whether accessible
+    const isAccessible = await Page.isAccessiblePageByViewer(pageId, req.user);
+    if (!isAccessible) {
+      return res.json(ApiResponse.error('Current user is not accessible to this page.'));
+    }
+
+    try {
+      const revision = await Revision.findById(revisionId);
+      return res.json(ApiResponse.success({ revision }));
+    }
+    catch (err) {
+      logger.error('Error revisios.get', err);
+      return res.json(ApiResponse.error(err));
+    }
   };
   };
 
 
   /**
   /**

+ 39 - 31
src/server/util/search.js

@@ -343,8 +343,6 @@ SearchClient.prototype.addAllPages = async function() {
  * }
  * }
  */
  */
 SearchClient.prototype.search = async function(query) {
 SearchClient.prototype.search = async function(query) {
-  let self = this;
-
   // for debug
   // for debug
   if (process.env.NODE_ENV === 'development') {
   if (process.env.NODE_ENV === 'development') {
     const result = await this.client.indices.validateQuery({
     const result = await this.client.indices.validateQuery({
@@ -356,28 +354,19 @@ SearchClient.prototype.search = async function(query) {
     logger.info('ES returns explanations: ', result.explanations);
     logger.info('ES returns explanations: ', result.explanations);
   }
   }
 
 
-  return new Promise(function(resolve, reject) {
-    self.client
-      .search(query)
-      .then(function(data) {
-        let result = {
-          meta: {
-            took: data.took,
-            total: data.hits.total,
-            results: data.hits.hits.length,
-          },
-          data: data.hits.hits.map(function(elm) {
-            return { _id: elm._id, _score: elm._score, _source: elm._source };
-          }),
-        };
-
-        resolve(result);
-      })
-      .catch(function(err) {
-        logger.error('Search error', err);
-        reject(err);
-      });
-  });
+  const result = await this.client.search(query);
+
+  return {
+    meta: {
+      took: result.took,
+      total: result.hits.total,
+      results: result.hits.hits.length,
+    },
+    data: result.hits.hits.map(function(elm) {
+      return { _id: elm._id, _score: elm._score, _source: elm._source };
+    }),
+  };
+
 };
 };
 
 
 SearchClient.prototype.createSearchQuerySortedByUpdatedAt = function(option) {
 SearchClient.prototype.createSearchQuerySortedByUpdatedAt = function(option) {
@@ -538,7 +527,21 @@ SearchClient.prototype.appendCriteriaForPathFilter = function(query, path) {
   });
   });
 };
 };
 
 
-SearchClient.prototype.filterPagesByViewer = function(query, user, userGroups) {
+SearchClient.prototype.filterPagesByViewer = async function(query, user, userGroups) {
+  const Config = this.crowi.model('Config');
+  const config = this.crowi.getConfig();
+
+  // determine User condition
+  const hidePagesRestrictedByOwner = Config.hidePagesRestrictedByOwnerInList(config);
+  user = hidePagesRestrictedByOwner ? user : null;
+
+  // determine UserGroup condition
+  const hidePagesRestrictedByGroup = Config.hidePagesRestrictedByGroupInList(config);
+  if (hidePagesRestrictedByGroup && user != null) {
+    const UserGroupRelation = this.crowi.model('UserGroupRelation');
+    userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
+  }
+
   query = this.initializeBoolQuery(query);
   query = this.initializeBoolQuery(query);
 
 
   const Page = this.crowi.model('Page');
   const Page = this.crowi.model('Page');
@@ -578,7 +581,12 @@ SearchClient.prototype.filterPagesByViewer = function(query, user, userGroups) {
     );
     );
   }
   }
 
 
-  if (userGroups != null && userGroups.length > 0) {
+  if (userGroups == null) {
+    grantConditions.push(
+      { term: { grant: GRANT_USER_GROUP } },
+    );
+  }
+  else if (userGroups.length > 0) {
     const userGroupIds = userGroups.map(group => group._id.toString() );
     const userGroupIds = userGroups.map(group => group._id.toString() );
     grantConditions.push(
     grantConditions.push(
       { bool: {
       { bool: {
@@ -646,7 +654,7 @@ SearchClient.prototype.appendFunctionScore = function(query) {
   };
   };
 };
 };
 
 
-SearchClient.prototype.searchKeyword = function(keyword, user, userGroups, option) {
+SearchClient.prototype.searchKeyword = async function(keyword, user, userGroups, option) {
   const from = option.offset || null;
   const from = option.offset || null;
   const size = option.limit || null;
   const size = option.limit || null;
   const type = option.type || null;
   const type = option.type || null;
@@ -654,7 +662,7 @@ SearchClient.prototype.searchKeyword = function(keyword, user, userGroups, optio
   this.appendCriteriaForKeywordContains(query, keyword);
   this.appendCriteriaForKeywordContains(query, keyword);
 
 
   this.filterPagesByType(query, type);
   this.filterPagesByType(query, type);
-  this.filterPagesByViewer(query, user, userGroups);
+  await this.filterPagesByViewer(query, user, userGroups);
 
 
   this.appendResultSize(query, from, size);
   this.appendResultSize(query, from, size);
 
 
@@ -663,11 +671,11 @@ SearchClient.prototype.searchKeyword = function(keyword, user, userGroups, optio
   return this.search(query);
   return this.search(query);
 };
 };
 
 
-SearchClient.prototype.searchByPath = function(keyword, prefix) {
+SearchClient.prototype.searchByPath = async function(keyword, prefix) {
   // TODO path 名だけから検索
   // TODO path 名だけから検索
 };
 };
 
 
-SearchClient.prototype.searchKeywordUnderPath = function(keyword, path, user, userGroups, option) {
+SearchClient.prototype.searchKeywordUnderPath = async function(keyword, path, user, userGroups, option) {
   const from = option.offset || null;
   const from = option.offset || null;
   const size = option.limit || null;
   const size = option.limit || null;
   const type = option.type || null;
   const type = option.type || null;
@@ -676,7 +684,7 @@ SearchClient.prototype.searchKeywordUnderPath = function(keyword, path, user, us
   this.appendCriteriaForPathFilter(query, path);
   this.appendCriteriaForPathFilter(query, path);
 
 
   this.filterPagesByType(query, type);
   this.filterPagesByType(query, type);
-  this.filterPagesByViewer(query, user, userGroups);
+  await this.filterPagesByViewer(query, user, userGroups);
 
 
   this.appendResultSize(query, from, size);
   this.appendResultSize(query, from, size);
 
 

+ 1 - 1
src/server/views/widget/page_list_and_timeline.html

@@ -26,7 +26,7 @@
     {% if isEnabledTimeline() %}
     {% if isEnabledTimeline() %}
     <div class="tab-pane m-t-30" id="view-timeline" data-shown=0>
     <div class="tab-pane m-t-30" id="view-timeline" data-shown=0>
       {% for page in pages %}
       {% for page in pages %}
-      <div class="timeline-body" id="id-{{ page.id }}" data-page-path="{{ page.path }}">
+      <div class="timeline-body" id="id-{{ page.id }}" data-page-id="{{ page.id }}" data-page-path="{{ page.path }}" data-revision="{{ page.revision.toString() }}">
         <div class="panel panel-timeline">
         <div class="panel panel-timeline">
           <div class="panel-heading"><a href="{{ page.path }}">{{ decodeURIComponent(page.path) }}</a></div>
           <div class="panel-heading"><a href="{{ page.path }}">{{ decodeURIComponent(page.path) }}</a></div>
           <div class="panel-body">
           <div class="panel-body">

+ 1 - 1
src/server/views/widget/page_list_and_timeline_kibela.html

@@ -26,7 +26,7 @@
     {% if isEnabledTimeline() %}
     {% if isEnabledTimeline() %}
     <div class="tab-pane m-t-30" id="view-timeline" data-shown=0>
     <div class="tab-pane m-t-30" id="view-timeline" data-shown=0>
       {% for page in pages %}
       {% for page in pages %}
-      <div class="timeline-body" id="id-{{ page.id }}" data-page-path="{{ page.path }}">
+      <div class="timeline-body" id="id-{{ page.id }}" data-page-id="{{ page.id }}" data-page-path="{{ page.path }}" data-revision="{{ page.revision._id }}">
         <div class="panel panel-timeline">
         <div class="panel panel-timeline">
           <div class="panel-heading"><a href="{{ page.path }}">{{ decodeURIComponent(page.path) }}</a></div>
           <div class="panel-heading"><a href="{{ page.path }}">{{ decodeURIComponent(page.path) }}</a></div>
           <div class="panel-body">
           <div class="panel-body">

+ 23 - 7
yarn.lock

@@ -2189,6 +2189,10 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0:
   version "1.1.0"
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
   resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
 
 
+"consolidated-events@^1.1.0 || ^2.0.0":
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/consolidated-events/-/consolidated-events-2.0.2.tgz#da8d8f8c2b232831413d9e190dc11669c79f4a91"
+
 constants-browserify@^1.0.0:
 constants-browserify@^1.0.0:
   version "1.0.0"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
   resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
@@ -7251,6 +7255,13 @@ prop-types-extra@^1.0.1:
   dependencies:
   dependencies:
     warning "^3.0.0"
     warning "^3.0.0"
 
 
+prop-types@^15.0.0, prop-types@^15.6.2:
+  version "15.6.2"
+  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
+  dependencies:
+    loose-envify "^1.3.1"
+    object-assign "^4.1.1"
+
 prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0:
 prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0:
   version "15.6.0"
   version "15.6.0"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856"
@@ -7267,13 +7278,6 @@ prop-types@^15.6.1:
     loose-envify "^1.3.1"
     loose-envify "^1.3.1"
     object-assign "^4.1.1"
     object-assign "^4.1.1"
 
 
-prop-types@^15.6.2:
-  version "15.6.2"
-  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
-  dependencies:
-    loose-envify "^1.3.1"
-    object-assign "^4.1.1"
-
 proxy-addr@~2.0.2:
 proxy-addr@~2.0.2:
   version "2.0.2"
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.2.tgz#6571504f47bb988ec8180253f85dd7e14952bdec"
   resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.2.tgz#6571504f47bb988ec8180253f85dd7e14952bdec"
@@ -7499,6 +7503,10 @@ react-i18next@=7.13.0:
     html-parse-stringify2 "2.0.1"
     html-parse-stringify2 "2.0.1"
     prop-types "^15.6.0"
     prop-types "^15.6.0"
 
 
+react-is@^16.6.3:
+  version "16.6.3"
+  resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.6.3.tgz#d2d7462fcfcbe6ec0da56ad69047e47e56e7eac0"
+
 react-onclickoutside@^6.1.1:
 react-onclickoutside@^6.1.1:
   version "6.7.0"
   version "6.7.0"
   resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.7.0.tgz#997a4d533114c9a0a104913638aa26afc084f75c"
   resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.7.0.tgz#997a4d533114c9a0a104913638aa26afc084f75c"
@@ -7538,6 +7546,14 @@ react-transition-group@^2.0.0, react-transition-group@^2.2.0:
     prop-types "^15.5.8"
     prop-types "^15.5.8"
     warning "^3.0.0"
     warning "^3.0.0"
 
 
+react-waypoint@^8.1.0:
+  version "8.1.0"
+  resolved "https://registry.yarnpkg.com/react-waypoint/-/react-waypoint-8.1.0.tgz#91d926a2fd1be4cbd0351cb8c3d494fac0ef1699"
+  dependencies:
+    consolidated-events "^1.1.0 || ^2.0.0"
+    prop-types "^15.0.0"
+    react-is "^16.6.3"
+
 react@^16.4.1:
 react@^16.4.1:
   version "16.4.1"
   version "16.4.1"
   resolved "https://registry.yarnpkg.com/react/-/react-16.4.1.tgz#de51ba5764b5dbcd1f9079037b862bd26b82fe32"
   resolved "https://registry.yarnpkg.com/react/-/react-16.4.1.tgz#de51ba5764b5dbcd1f9079037b862bd26b82fe32"