Browse Source

Merge pull request #1176 from weseek/fix/timeline

Fix/timeline
Yuki Takei 6 years ago
parent
commit
a1415e0aaf

+ 2 - 0
CHANGES.md

@@ -4,6 +4,8 @@
 
 
 * Fix: HackMD Editor shows 404 error when HackMD redirect to fqdn URI
 * Fix: HackMD Editor shows 404 error when HackMD redirect to fqdn URI
     * Introduced by 3.5.8
     * Introduced by 3.5.8
+* Fix: Timeline doesn't work
+    * Introduced by 3.5.1
 * Fix: Last Login field does not shown in /admin/user
 * Fix: Last Login field does not shown in /admin/user
 * Support: Upgrade libs
 * Support: Upgrade libs
     * env-cmd
     * env-cmd

+ 2 - 0
src/client/js/app.jsx

@@ -19,6 +19,7 @@ import PageEditorByHackmd from './components/PageEditorByHackmd';
 import Page from './components/Page';
 import Page from './components/Page';
 import PageHistory from './components/PageHistory';
 import PageHistory from './components/PageHistory';
 import PageComments from './components/PageComments';
 import PageComments from './components/PageComments';
+import PageTimeline from './components/PageTimeline';
 import CommentEditorLazyRenderer from './components/PageComment/CommentEditorLazyRenderer';
 import CommentEditorLazyRenderer from './components/PageComment/CommentEditorLazyRenderer';
 import PageAttachment from './components/PageAttachment';
 import PageAttachment from './components/PageAttachment';
 import PageStatusAlert from './components/PageStatusAlert';
 import PageStatusAlert from './components/PageStatusAlert';
@@ -106,6 +107,7 @@ if (pageContainer.state.pageId != null) {
     'page-editor-with-hackmd': <PageEditorByHackmd />,
     'page-editor-with-hackmd': <PageEditorByHackmd />,
     'page-comments-list': <PageComments />,
     'page-comments-list': <PageComments />,
     'page-attachment':  <PageAttachment />,
     'page-attachment':  <PageAttachment />,
+    'page-timeline':  <PageTimeline />,
     'page-comment-write':  <CommentEditorLazyRenderer />,
     'page-comment-write':  <CommentEditorLazyRenderer />,
     'revision-toc': <TableOfContents />,
     'revision-toc': <TableOfContents />,
     'like-button': <LikeButton pageId={pageContainer.state.pageId} isLiked={pageContainer.state.isLiked} />,
     'like-button': <LikeButton pageId={pageContainer.state.pageId} isLiked={pageContainer.state.isLiked} />,

+ 5 - 2
src/client/js/components/Page/RevisionLoader.jsx

@@ -57,6 +57,10 @@ class RevisionLoader extends React.Component {
       markdown: res.revision.body,
       markdown: res.revision.body,
       error: null,
       error: null,
     });
     });
+
+    if (this.props.onRevisionLoaded != null) {
+      this.props.onRevisionLoaded(res.revision);
+    }
   }
   }
 
 
   onWaypointChange(event) {
   onWaypointChange(event) {
@@ -95,7 +99,6 @@ class RevisionLoader extends React.Component {
     return (
     return (
       <RevisionRenderer
       <RevisionRenderer
         growiRenderer={this.props.growiRenderer}
         growiRenderer={this.props.growiRenderer}
-        pagePath={this.props.pagePath}
         markdown={markdown}
         markdown={markdown}
         highlightKeywords={this.props.highlightKeywords}
         highlightKeywords={this.props.highlightKeywords}
       />
       />
@@ -116,9 +119,9 @@ RevisionLoader.propTypes = {
 
 
   growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
   growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
   pageId: PropTypes.string.isRequired,
   pageId: PropTypes.string.isRequired,
-  pagePath: PropTypes.string.isRequired,
   revisionId: PropTypes.string.isRequired,
   revisionId: PropTypes.string.isRequired,
   lazy: PropTypes.bool,
   lazy: PropTypes.bool,
+  onRevisionLoaded: PropTypes.func,
   highlightKeywords: PropTypes.string,
   highlightKeywords: PropTypes.string,
 };
 };
 
 

+ 136 - 0
src/client/js/components/PageTimeline.jsx

@@ -0,0 +1,136 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { withTranslation } from 'react-i18next';
+
+import * as entities from 'entities';
+
+import AppContainer from '../services/AppContainer';
+import { createSubscribedElement } from './UnstatedUtils';
+
+import RevisionLoader from './Page/RevisionLoader';
+
+
+class PageTimeline extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    const { appContainer } = this.props;
+
+    this.state = {
+      isEnabled: appContainer.getConfig().isEnabledTimeline,
+      isInitialized: false,
+
+      // TODO: remove after when timeline is implemented with React and inject data with props
+      pages: this.props.pages,
+    };
+
+  }
+
+  componentWillMount() {
+    if (!this.state.isEnabled) {
+      return;
+    }
+
+    const { appContainer } = this.props;
+
+    // initialize GrowiRenderer
+    this.growiRenderer = appContainer.getRenderer('timeline');
+
+    this.initBsTab();
+  }
+
+  /**
+   * initialize Bootstrap Tab event for 'shown.bs.tab'
+   * TODO: remove this method after implement with React
+   */
+  initBsTab() {
+    $('a[data-toggle="tab"][href="#view-timeline"]').on('shown.bs.tab', () => {
+      if (this.state.isInitialized) {
+        return;
+      }
+
+      const pageIdsElm = document.getElementById('page-timeline-data');
+
+      if (pageIdsElm == null || pageIdsElm.text.length === 0) {
+        return;
+      }
+
+      const pages = this.extractDataFromDom();
+
+      this.setState({
+        isInitialized: true,
+        pages,
+      });
+    });
+  }
+
+  /**
+   * extract page data from DOM
+   * TODO: remove this method after implement with React
+   */
+  extractDataFromDom() {
+    const pageIdsElm = document.getElementById('page-timeline-data');
+
+    if (pageIdsElm == null || pageIdsElm.text.length === 0) {
+      return null;
+    }
+
+    let pages = JSON.parse(pageIdsElm.text);
+    // decode path
+    pages = pages.map((page) => {
+      page.path = decodeURIComponent(entities.decodeHTML(page.path));
+      return page;
+    });
+
+    return pages;
+  }
+
+  render() {
+    if (!this.state.isEnabled) {
+      return <React.Fragment></React.Fragment>;
+    }
+
+    const { pages } = this.state;
+
+    if (pages == null) {
+      return <React.Fragment></React.Fragment>;
+    }
+
+    return pages.map((page) => {
+      return (
+        <div className="timeline-body" key={`key-${page.id}`}>
+          <div className="panel panel-timeline">
+            <div className="panel-heading"><a href={page.path}>{page.path}</a></div>
+            <div className="panel-body">
+              <RevisionLoader
+                lazy
+                growiRenderer={this.growiRenderer}
+                pageId={page.id}
+                revisionId={page.revision}
+              />
+            </div>
+          </div>
+        </div>
+      );
+    });
+
+  }
+
+}
+
+PageTimeline.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pages: PropTypes.arrayOf(PropTypes.object),
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const PageTimelineWrapper = (props) => {
+  return createSubscribedElement(PageTimeline, props, [AppContainer]);
+};
+
+export default withTranslation()(PageTimelineWrapper);

+ 0 - 46
src/client/js/legacy/crowi.js

@@ -1,15 +1,7 @@
 /* eslint-disable react/jsx-filename-extension */
 /* eslint-disable react/jsx-filename-extension */
 
 
-import React from 'react';
-import ReactDOM from 'react-dom';
-
-import { Provider } from 'unstated';
-
 import { pathUtils } from 'growi-commons';
 import { pathUtils } from 'growi-commons';
 
 
-import GrowiRenderer from '../util/GrowiRenderer';
-import RevisionLoader from '../components/Page/RevisionLoader';
-
 const entities = require('entities');
 const entities = require('entities');
 const escapeStringRegexp = require('escape-string-regexp');
 const escapeStringRegexp = require('escape-string-regexp');
 require('jquery.cookie');
 require('jquery.cookie');
@@ -487,44 +479,6 @@ $(() => {
     $link.html(path.replace(new RegExp(pattern), `<strong>${shortPath}$1</strong>`));
     $link.html(path.replace(new RegExp(pattern), `<strong>${shortPath}$1</strong>`));
   });
   });
 
 
-  // for list page
-  let growiRendererForTimeline = null;
-  $('a[data-toggle="tab"][href="#view-timeline"]').on('shown.bs.tab', () => {
-    const isShown = $('#view-timeline').data('shown');
-
-    if (growiRendererForTimeline == null) {
-      growiRendererForTimeline = GrowiRenderer.generate('timeline');
-    }
-
-    if (isShown === 0) {
-      $('#view-timeline .timeline-body').each(function() {
-        const id = $(this).attr('id');
-        const revisionBody = `#${id} .revision-body`;
-        const revisionBodyElem = document.querySelector(revisionBody);
-        const revisionPath = `#${id} .revision-path`; // eslint-disable-line no-unused-vars
-        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(
-          <Provider inject={[appContainer]}>
-            <RevisionLoader
-              lazy
-              growiRenderer={growiRendererForTimeline}
-              pageId={pageId}
-              pagePath={pagePath}
-              revisionId={revisionId}
-            />
-          </Provider>,
-          revisionBodyElem,
-        );
-      });
-
-      $('#view-timeline').data('shown', 1);
-    }
-  });
-
   if (pageId) {
   if (pageId) {
     // for Crowi Template LangProcessor
     // for Crowi Template LangProcessor
     $('.template-create-button', $('#revision-body')).on('click', function() {
     $('.template-create-button', $('#revision-body')).on('click', function() {

+ 1 - 0
src/server/models/config.js

@@ -176,6 +176,7 @@ module.exports = function(crowi) {
       isEnabledLinebreaks: crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
       isEnabledLinebreaks: crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
       isEnabledLinebreaksInComments: crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
       isEnabledLinebreaksInComments: crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
       isEnabledXssPrevention: crowi.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
       isEnabledXssPrevention: crowi.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
+      isEnabledTimeline: crowi.configManager.getConfig('crowi', 'customize:isEnabledTimeline'),
       xssOption: crowi.configManager.getConfig('markdown', 'markdown:xss:option'),
       xssOption: crowi.configManager.getConfig('markdown', 'markdown:xss:option'),
       tagWhiteList: crowi.xssService.getTagWhiteList(),
       tagWhiteList: crowi.xssService.getTagWhiteList(),
       attrWhiteList: crowi.xssService.getAttrWhiteList(),
       attrWhiteList: crowi.xssService.getAttrWhiteList(),

+ 13 - 0
src/server/util/swigFunctions.js

@@ -1,7 +1,10 @@
 module.exports = function(crowi, app, req, locals) {
 module.exports = function(crowi, app, req, locals) {
   const debug = require('debug')('growi:lib:swigFunctions');
   const debug = require('debug')('growi:lib:swigFunctions');
   const stringWidth = require('string-width');
   const stringWidth = require('string-width');
+  const entities = require('entities');
+
   const { pathUtils } = require('growi-commons');
   const { pathUtils } = require('growi-commons');
+
   const Page = crowi.model('Page');
   const Page = crowi.model('Page');
   const User = crowi.model('User');
   const User = crowi.model('User');
   const {
   const {
@@ -174,6 +177,16 @@ module.exports = function(crowi, app, req, locals) {
     return `/user/${user.username}`;
     return `/user/${user.username}`;
   };
   };
 
 
+  locals.pagesDataForTimeline = function(pages) {
+    return pages.map((page) => {
+      return {
+        id: page.id,
+        path: entities.encodeHTML(page.path),
+        revision: page.revision,
+      };
+    });
+  };
+
   locals.css = {
   locals.css = {
     grant(pageData) {
     grant(pageData) {
       if (!pageData) {
       if (!pageData) {

+ 4 - 13
src/server/views/widget/page_list_and_timeline.html

@@ -24,20 +24,11 @@
 
 
     {# timeline view #}
     {# timeline view #}
     {% if getConfig('crowi', 'customize:isEnabledTimeline') %}
     {% if getConfig('crowi', 'customize:isEnabledTimeline') %}
-    <div class="tab-pane m-t-30" id="view-timeline" data-shown=0>
-      {% for page in pages %}
-      <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-heading"><a href="{{ page.path }}">{{ decodeURIComponent(page.path) }}</a></div>
-          <div class="panel-body">
-            <div class="revision-body wiki"></div>
-          </div>
-        </div>
-        <script type="text/template">{{ page.revision.body.toString() | encodeHTML }}</script>
+      <div class="tab-pane m-t-30" id="view-timeline">
+        <script type="text/template" id="page-timeline-data">{{ JSON.stringify(pagesDataForTimeline(pages)) }}</script>
+        {# render React Component PageTimeline #}
+        <div id="page-timeline"></div>
       </div>
       </div>
-      <hr>
-      {% endfor %}
-    </div>
     {% endif %}
     {% endif %}
   </div>
   </div>
 </div>
 </div>

+ 4 - 13
src/server/views/widget/page_list_and_timeline_kibela.html

@@ -24,20 +24,11 @@
 
 
     {# timeline view #}
     {# timeline view #}
     {% if getConfig('crowi', 'customize:isEnabledTimeline') %}
     {% if getConfig('crowi', 'customize:isEnabledTimeline') %}
-    <div class="tab-pane m-t-30" id="view-timeline" data-shown=0>
-      {% for page in pages %}
-      <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-heading"><a href="{{ page.path }}">{{ decodeURIComponent(page.path) }}</a></div>
-          <div class="panel-body">
-            <div class="revision-body wiki"></div>
-          </div>
-        </div>
-        <script type="text/template">{{ page.revision.body.toString() | encodeHTML }}</script>
+      <div class="tab-pane m-t-30" id="view-timeline">
+        <script type="text/template" id="page-timeline-data">{{ JSON.stringify(pagesDataForTimeline(pages)) }}</script>
+        {# render React Component PageTimeline #}
+        <div id="page-timeline"></div>
       </div>
       </div>
-      <hr>
-      {% endfor %}
-    </div>
     {% endif %}
     {% endif %}
   </div>
   </div>
 </div>
 </div>