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

Merge pull request #1178 from weseek/master

release v3.5.11
Yuki Takei 6 лет назад
Родитель
Сommit
b2039a9dee

+ 16 - 1
CHANGES.md

@@ -1,7 +1,22 @@
 # CHANGES
 # CHANGES
 
 
-## 3.5.10-RC
+## 3.5.11-RC
 
 
+* Fix: HackMD Editor shows 404 error when HackMD redirect to fqdn URI
+    * 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
+* Support: Upgrade libs
+    * env-cmd
+    * sass-loader
+    * webpack
+    * webpack-cli
+    * webpack-merge
+
+## 3.5.10
+
+* Feature: Send Global Notification with Slack
 * Improvement: Show loading spinner when fetching page history data
 * Improvement: Show loading spinner when fetching page history data
 * Improvement: Hierarchical page link when the page is in /Trash
 * Improvement: Hierarchical page link when the page is in /Trash
 * Fix: Code Highlight Theme does not change
 * Fix: Code Highlight Theme does not change

+ 6 - 6
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "growi",
   "name": "growi",
-  "version": "3.5.10-RC",
+  "version": "3.5.11-RC",
   "description": "Team collaboration software using markdown",
   "description": "Team collaboration software using markdown",
   "tags": [
   "tags": [
     "wiki",
     "wiki",
@@ -83,7 +83,7 @@
     "diff": "^4.0.1",
     "diff": "^4.0.1",
     "elasticsearch": "^16.0.0",
     "elasticsearch": "^16.0.0",
     "entities": "=1.1.2",
     "entities": "=1.1.2",
-    "env-cmd": "^9.0.1",
+    "env-cmd": "^10.0.1",
     "esa-nodejs": "^0.0.7",
     "esa-nodejs": "^0.0.7",
     "escape-string-regexp": "^2.0.0",
     "escape-string-regexp": "^2.0.0",
     "express": "^4.16.1",
     "express": "^4.16.1",
@@ -214,7 +214,7 @@
     "react-waypoint": "^9.0.0",
     "react-waypoint": "^9.0.0",
     "replacestream": "^4.0.3",
     "replacestream": "^4.0.3",
     "reveal.js": "^3.5.0",
     "reveal.js": "^3.5.0",
-    "sass-loader": "^7.1.0",
+    "sass-loader": "^8.0.0",
     "simple-load-script": "^1.0.2",
     "simple-load-script": "^1.0.2",
     "socket.io-client": "^2.0.3",
     "socket.io-client": "^2.0.3",
     "style-loader": "^1.0.0",
     "style-loader": "^1.0.0",
@@ -224,11 +224,11 @@
     "throttle-debounce": "^2.0.0",
     "throttle-debounce": "^2.0.0",
     "toastr": "^2.1.2",
     "toastr": "^2.1.2",
     "unstated": "^2.1.1",
     "unstated": "^2.1.1",
-    "webpack": "^4.29.3",
+    "webpack": "^4.39.3",
     "webpack-assets-manifest": "^3.1.1",
     "webpack-assets-manifest": "^3.1.1",
     "webpack-bundle-analyzer": "^3.0.2",
     "webpack-bundle-analyzer": "^3.0.2",
-    "webpack-cli": "^3.2.3",
-    "webpack-merge": "^4.2.1"
+    "webpack-cli": "^3.3.7",
+    "webpack-merge": "^4.2.2"
   },
   },
   "_moduleAliases": {
   "_moduleAliases": {
     "@root": ".",
     "@root": ".",

+ 1 - 1
resource/locales/en-US/translation.json

@@ -341,7 +341,7 @@
   "template": {
   "template": {
     "modal_label": {
     "modal_label": {
       "Create/Edit Template Page": "Create/Edit Template Page",
       "Create/Edit Template Page": "Create/Edit Template Page",
-      "Create template under": "Create template page under: <code>%s</code>"
+      "Create template under": "Create template page under:<br /><code><small>%s</small></code>"
     },
     },
     "option_label": {
     "option_label": {
       "create/edit": "Create/Edit Template page..",
       "create/edit": "Create/Edit Template page..",

+ 1 - 1
resource/locales/ja/translation.json

@@ -339,7 +339,7 @@
   "template": {
   "template": {
     "modal_label": {
     "modal_label": {
       "Create/Edit Template Page": "テンプレートページの作成/編集",
       "Create/Edit Template Page": "テンプレートページの作成/編集",
-      "Create template under": "<code>%s</code> にテンプレートページを作成"
+      "Create template under": "<code><small>%s</small></code><br />にテンプレートページを作成"
     },
     },
     "option_label": {
     "option_label": {
       "select": "テンプレートタイプを選択してください",
       "select": "テンプレートタイプを選択してください",

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

+ 1 - 1
src/server/routes/admin.js

@@ -440,7 +440,7 @@ module.exports = function(crowi, app) {
 
 
     const result = await User.findUsersWithPagination({
     const result = await User.findUsersWithPagination({
       page,
       page,
-      select: User.USER_PUBLIC_FIELDS,
+      select: `${User.USER_PUBLIC_FIELDS} lastLoginAt`,
       populate: User.IMAGE_POPULATION,
       populate: User.IMAGE_POPULATION,
     });
     });
 
 

+ 2 - 2
src/server/routes/hackmd.js

@@ -157,8 +157,8 @@ module.exports = function(crowi, app) {
       // when redirect
       // when redirect
       if (status === 302) {
       if (status === 302) {
         // extract page id on HackMD
         // extract page id on HackMD
-        const pagePathOnHackmd = headers.location; // e.g. '/NC7bSRraT1CQO1TO7wjCPw'
-        const pageIdOnHackmd = pagePathOnHackmd.substr(1); //        strip the head '/'
+        const pathnameOnHackmd = new URL(headers.location, hackmdUri).pathname; // e.g. '/NC7bSRraT1CQO1TO7wjCPw'
+        const pageIdOnHackmd = pathnameOnHackmd.substr(1); //                      strip the head '/'
 
 
         page = await Page.registerHackmdPage(page, pageIdOnHackmd);
         page = await Page.registerHackmdPage(page, pageIdOnHackmd);
       }
       }

+ 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) {

+ 0 - 1
src/server/views/admin/users.html

@@ -88,7 +88,6 @@
       </div><!-- /.modal -->
       </div><!-- /.modal -->
       {% endif %}
       {% endif %}
 
 
-      {# FIXME とりあえずクソ実装。React化はやくしたいなー(チラッチラッ #}
       <div class="modal fade" id="admin-password-reset-modal">
       <div class="modal fade" id="admin-password-reset-modal">
         <div class="modal-dialog">
         <div class="modal-dialog">
           <div class="modal-content">
           <div class="modal-content">

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

Разница между файлами не показана из-за своего большого размера
+ 378 - 230
yarn.lock


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