Răsfoiți Sursa

Merge remote-tracking branch 'origin/master' into feat/zip-download

mizozobu 6 ani în urmă
părinte
comite
d1143193fa

+ 21 - 1
CHANGES.md

@@ -1,6 +1,26 @@
 # CHANGES
 # CHANGES
 
 
-## 3.5.10-RC
+## 3.5.12-RC
+
+* Improvement: Use Elasticsearch Alias
+* Fix: Global Notification doesn't work after updating Webhook URL
+* Fix: User Trigger Notification is not be sent when channel is not specified
+
+## 3.5.11
+
+* 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
 * Feature: Send Global Notification with Slack
 * Improvement: Show loading spinner when fetching page history data
 * Improvement: Show loading spinner when fetching page history data

+ 1 - 1
README.md

@@ -258,7 +258,7 @@ License
 =======
 =======
 
 
 * The MIT License (MIT)
 * The MIT License (MIT)
-* See LICENSE file.
+* See [LICENSE](https://github.com/weseek/growi/blob/master/LICENSE) and [THIRD-PARTY-NOTICES.md](https://github.com/weseek/growi/blob/master/THIRD-PARTY-NOTICES.md).
 
 
 
 
 [crowi]: https://github.com/crowi/crowi
 [crowi]: https://github.com/crowi/crowi

+ 11 - 0
THIRD-PARTY-NOTICES.md

@@ -16,6 +16,7 @@ https://github.com/weseek/growi.
 2. crowi/crowi (https://github.com/crowi/crowi)
 2. crowi/crowi (https://github.com/crowi/crowi)
 3. Microsoft/vscode (https://github.com/Microsoft/vscode)
 3. Microsoft/vscode (https://github.com/Microsoft/vscode)
 4. stephenhutchings/typicons.font (https://github.com/stephenhutchings/typicons.font)
 4. stephenhutchings/typicons.font (https://github.com/stephenhutchings/typicons.font)
+5. EmojiOne Version 3 (https://github.com/joypixels/emojione/tree/v3.1.1)
 
 
 
 
 License Notice for Apache License, Version 2.0 Derivative Works
 License Notice for Apache License, Version 2.0 Derivative Works
@@ -97,3 +98,13 @@ https://creativecommons.org/licenses/by-sa/3.0/
 ```
 ```
 Copyright (c) 2018 Stephen Hutchings
 Copyright (c) 2018 Stephen Hutchings
 ```
 ```
+
+
+License Notice for EmojiOne
+------------------------
+
+https://creativecommons.org/licenses/by/4.0/
+
+```
+author: "EmojiOne <ryan@emojione.com> (http://emojione.com)"
+```

+ 6 - 6
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "growi",
   "name": "growi",
-  "version": "3.5.11-RC",
+  "version": "3.5.12-RC",
   "description": "Team collaboration software using markdown",
   "description": "Team collaboration software using markdown",
   "tags": [
   "tags": [
     "wiki",
     "wiki",
@@ -84,7 +84,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",
@@ -215,7 +215,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",
@@ -225,11 +225,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

@@ -342,7 +342,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

@@ -340,7 +340,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';
@@ -107,6 +108,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() {

+ 11 - 15
src/server/crowi/index.js

@@ -323,18 +323,17 @@ Crowi.prototype.setupSearcher = async function() {
   const searcherUri = this.env.ELASTICSEARCH_URI
   const searcherUri = this.env.ELASTICSEARCH_URI
     || this.env.BONSAI_URL
     || this.env.BONSAI_URL
     || null;
     || null;
-  return new Promise(((resolve, reject) => {
-    if (searcherUri) {
-      try {
-        self.searcher = new (require(path.join(self.libDir, 'util', 'search')))(self, searcherUri);
-      }
-      catch (e) {
-        logger.error('Error on setup searcher', e);
-        self.searcher = null;
-      }
+
+  if (searcherUri) {
+    try {
+      self.searcher = new (require(path.join(self.libDir, 'util', 'search')))(self, searcherUri);
+      self.searcher.initIndices();
     }
     }
-    resolve();
-  }));
+    catch (e) {
+      logger.error('Error on setup searcher', e);
+      self.searcher = null;
+    }
+  }
 };
 };
 
 
 Crowi.prototype.setupMailer = async function() {
 Crowi.prototype.setupMailer = async function() {
@@ -349,10 +348,7 @@ Crowi.prototype.setupSlack = async function() {
   const self = this;
   const self = this;
 
 
   return new Promise(((resolve, reject) => {
   return new Promise(((resolve, reject) => {
-    if (this.slackNotificationService.hasSlackConfig()) {
-      self.slack = require('../util/slack')(self);
-    }
-
+    self.slack = require('../util/slack')(self);
     resolve();
     resolve();
   }));
   }));
 };
 };

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

+ 3 - 27
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,
     });
     });
 
 
@@ -1372,38 +1372,14 @@ module.exports = function(crowi, app) {
       return res.json(ApiResponse.error('ElasticSearch Integration is not set up.'));
       return res.json(ApiResponse.error('ElasticSearch Integration is not set up.'));
     }
     }
 
 
-    // first, delete index
-    try {
-      await search.deleteIndex();
-    }
-    catch (err) {
-      logger.warn('Delete index Error, but if it is initialize, its ok.', err);
-    }
-
-    // second, create index
-    try {
-      await search.buildIndex();
-    }
-    catch (err) {
-      logger.error('Error', err);
-      return res.json(ApiResponse.error(err));
-    }
-
     searchEvent.on('addPageProgress', (total, current, skip) => {
     searchEvent.on('addPageProgress', (total, current, skip) => {
       crowi.getIo().sockets.emit('admin:addPageProgress', { total, current, skip });
       crowi.getIo().sockets.emit('admin:addPageProgress', { total, current, skip });
     });
     });
     searchEvent.on('finishAddPage', (total, current, skip) => {
     searchEvent.on('finishAddPage', (total, current, skip) => {
       crowi.getIo().sockets.emit('admin:finishAddPage', { total, current, skip });
       crowi.getIo().sockets.emit('admin:finishAddPage', { total, current, skip });
     });
     });
-    // add all page
-    search
-      .addAllPages()
-      .then(() => {
-        debug('Data is successfully indexed. ------------------ ✧✧');
-      })
-      .catch((err) => {
-        logger.error('Error', err);
-      });
+
+    await search.buildIndex();
 
 
     return res.json(ApiResponse.success());
     return res.json(ApiResponse.success());
   };
   };

+ 13 - 13
src/server/routes/comment.js

@@ -140,22 +140,22 @@ module.exports = function(crowi, app) {
     // slack notification
     // slack notification
     if (slackNotificationForm.isSlackEnabled) {
     if (slackNotificationForm.isSlackEnabled) {
       const user = await User.findUserByUsername(req.user.username);
       const user = await User.findUserByUsername(req.user.username);
-      const channels = slackNotificationForm.slackChannels;
+      const channelsStr = slackNotificationForm.slackChannels || null;
 
 
-      if (channels) {
-        page.updateSlackChannel(channels).catch((err) => {
-          logger.error('Error occured in updating slack channels: ', err);
-        });
+      page.updateSlackChannel(channelsStr).catch((err) => {
+        logger.error('Error occured in updating slack channels: ', err);
+      });
 
 
-        const promises = channels.split(',').map((chan) => {
-          return crowi.slack.postComment(createdComment, user, chan, path);
-        });
+      const channels = channelsStr != null ? channelsStr.split(',') : [null];
 
 
-        Promise.all(promises)
-          .catch((err) => {
-            logger.error('Error occured in sending slack notification: ', err);
-          });
-      }
+      const promises = channels.map((chan) => {
+        return crowi.slack.postComment(createdComment, user, chan, path);
+      });
+
+      Promise.all(promises)
+        .catch((err) => {
+          logger.error('Error occured in sending slack notification: ', err);
+        });
     }
     }
   };
   };
 
 

+ 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);
       }
       }

+ 16 - 5
src/server/routes/page.js

@@ -100,14 +100,25 @@ module.exports = function(crowi, app) {
 
 
   // user notification
   // user notification
   // TODO create '/service/user-notification' module
   // TODO create '/service/user-notification' module
-  async function notifyToSlackByUser(page, user, slackChannels, updateOrCreate, previousRevision) {
-    await page.updateSlackChannel(slackChannels)
+  /**
+   *
+   * @param {Page} page
+   * @param {User} user
+   * @param {string} slackChannelsStr comma separated string. e.g. 'general,channel1,channel2'
+   * @param {boolean} updateOrCreate
+   * @param {string} previousRevision
+   */
+  async function notifyToSlackByUser(page, user, slackChannelsStr, updateOrCreate, previousRevision) {
+    await page.updateSlackChannel(slackChannelsStr)
       .catch((err) => {
       .catch((err) => {
         logger.error('Error occured in updating slack channels: ', err);
         logger.error('Error occured in updating slack channels: ', err);
       });
       });
 
 
+
     if (slackNotificationService.hasSlackConfig()) {
     if (slackNotificationService.hasSlackConfig()) {
-      const promises = slackChannels.split(',').map((chan) => {
+      const slackChannels = slackChannelsStr != null ? slackChannelsStr.split(',') : [null];
+
+      const promises = slackChannels.map((chan) => {
         return crowi.slack.postPage(page, user, chan, updateOrCreate, previousRevision);
         return crowi.slack.postPage(page, user, chan, updateOrCreate, previousRevision);
       });
       });
 
 
@@ -615,7 +626,7 @@ module.exports = function(crowi, app) {
     }
     }
 
 
     // user notification
     // user notification
-    if (isSlackEnabled && slackChannels != null) {
+    if (isSlackEnabled) {
       await notifyToSlackByUser(createdPage, req.user, slackChannels, 'create', false);
       await notifyToSlackByUser(createdPage, req.user, slackChannels, 'create', false);
     }
     }
   };
   };
@@ -702,7 +713,7 @@ module.exports = function(crowi, app) {
     }
     }
 
 
     // user notification
     // user notification
-    if (isSlackEnabled && slackChannels != null) {
+    if (isSlackEnabled) {
       await notifyToSlackByUser(page, req.user, slackChannels, 'update', previousRevision);
       await notifyToSlackByUser(page, req.user, slackChannels, 'update', previousRevision);
     }
     }
   };
   };

+ 83 - 13
src/server/util/search.js

@@ -44,6 +44,7 @@ function SearchClient(crowi, esUri) {
   const uri = this.parseUri(this.esUri);
   const uri = this.parseUri(this.esUri);
   this.host = uri.host;
   this.host = uri.host;
   this.indexName = uri.indexName;
   this.indexName = uri.indexName;
+  this.aliasName = `${this.indexName}-alias`;
 
 
   this.client = new elasticsearch.Client({
   this.client = new elasticsearch.Client({
     host: this.host,
     host: this.host,
@@ -117,17 +118,86 @@ SearchClient.prototype.parseUri = function(uri) {
   };
   };
 };
 };
 
 
-SearchClient.prototype.buildIndex = function(uri) {
-  return this.client.indices.create({
-    index: this.indexName,
-    body: require(this.mappingFile),
-  });
+SearchClient.prototype.initIndices = async function() {
+  await this.checkESVersion();
+
+  const { client, indexName, aliasName } = this;
+
+  const tmpIndexName = `${indexName}-tmp`;
+
+  // remove tmp index
+  const isExistsTmpIndex = await client.indices.exists({ index: tmpIndexName });
+  if (isExistsTmpIndex) {
+    await client.indices.delete({ index: tmpIndexName });
+  }
+
+  // create index
+  const isExistsIndex = await client.indices.exists({ index: indexName });
+  if (!isExistsIndex) {
+    await this.createIndex(indexName);
+  }
+
+  // create alias
+  const isExistsAlias = await client.indices.existsAlias({ name: aliasName, index: indexName });
+  if (!isExistsAlias) {
+    await client.indices.putAlias({
+      name: aliasName,
+      index: indexName,
+    });
+  }
 };
 };
 
 
-SearchClient.prototype.deleteIndex = function(uri) {
-  return this.client.indices.delete({
-    index: this.indexName,
+SearchClient.prototype.createIndex = async function(index) {
+  const body = require(this.mappingFile);
+  return this.client.indices.create({ index, body });
+};
+
+SearchClient.prototype.buildIndex = async function(uri) {
+  await this.initIndices();
+
+  const { client, indexName } = this;
+
+  const aliasName = `${indexName}-alias`;
+  const tmpIndexName = `${indexName}-tmp`;
+
+  // reindex to tmp index
+  await this.createIndex(tmpIndexName);
+  await client.reindex({
+    body: {
+      source: { index: indexName },
+      dest: { index: tmpIndexName },
+    },
+  });
+
+  // update alias
+  await client.indices.updateAliases({
+    body: {
+      actions: [
+        { add: { alias: aliasName, index: tmpIndexName } },
+        { remove: { alias: aliasName, index: indexName } },
+      ],
+    },
   });
   });
+
+  // flush index
+  await client.indices.delete({
+    index: indexName,
+  });
+  await this.createIndex(indexName);
+  await this.addAllPages();
+
+  // update alias
+  await client.indices.updateAliases({
+    body: {
+      actions: [
+        { add: { alias: aliasName, index: indexName } },
+        { remove: { alias: aliasName, index: tmpIndexName } },
+      ],
+    },
+  });
+
+  // remove tmp index
+  await client.indices.delete({ index: tmpIndexName });
 };
 };
 
 
 /**
 /**
@@ -162,7 +232,7 @@ SearchClient.prototype.prepareBodyForUpdate = function(body, page) {
 
 
   const command = {
   const command = {
     update: {
     update: {
-      _index: this.indexName,
+      _index: this.aliasName,
       _type: 'pages',
       _type: 'pages',
       _id: page._id.toString(),
       _id: page._id.toString(),
     },
     },
@@ -194,7 +264,7 @@ SearchClient.prototype.prepareBodyForCreate = function(body, page) {
 
 
   const command = {
   const command = {
     index: {
     index: {
-      _index: this.indexName,
+      _index: this.aliasName,
       _type: 'pages',
       _type: 'pages',
       _id: page._id.toString(),
       _id: page._id.toString(),
     },
     },
@@ -226,7 +296,7 @@ SearchClient.prototype.prepareBodyForDelete = function(body, page) {
 
 
   const command = {
   const command = {
     delete: {
     delete: {
-      _index: this.indexName,
+      _index: this.aliasName,
       _type: 'pages',
       _type: 'pages',
       _id: page._id.toString(),
       _id: page._id.toString(),
     },
     },
@@ -399,7 +469,7 @@ SearchClient.prototype.createSearchQuerySortedByUpdatedAt = function(option) {
 
 
   // default is only id field, sorted by updated_at
   // default is only id field, sorted by updated_at
   const query = {
   const query = {
-    index: this.indexName,
+    index: this.aliasName,
     type: 'pages',
     type: 'pages',
     body: {
     body: {
       sort: [{ updated_at: { order: 'desc' } }],
       sort: [{ updated_at: { order: 'desc' } }],
@@ -420,7 +490,7 @@ SearchClient.prototype.createSearchQuerySortedByScore = function(option) {
 
 
   // sort by score
   // sort by score
   const query = {
   const query = {
-    index: this.indexName,
+    index: this.aliasName,
     type: 'pages',
     type: 'pages',
     body: {
     body: {
       sort: [{ _score: { order: 'desc' } }],
       sort: [{ _score: { order: 'desc' } }],

+ 3 - 3
src/server/util/slack.js

@@ -132,7 +132,7 @@ module.exports = function(crowi) {
     }
     }
 
 
     const message = {
     const message = {
-      channel: `#${channel}`,
+      channel: (channel != null) ? `#${channel}` : undefined,
       username: appTitle,
       username: appTitle,
       text: getSlackMessageTextForPage(page.path, page.id, user, updateType),
       text: getSlackMessageTextForPage(page.path, page.id, user, updateType),
       attachments: [attachment],
       attachments: [attachment],
@@ -159,7 +159,7 @@ module.exports = function(crowi) {
     }
     }
 
 
     const message = {
     const message = {
-      channel: `#${channel}`,
+      channel: (channel != null) ? `#${channel}` : undefined,
       username: appTitle,
       username: appTitle,
       text: getSlackMessageTextForComment(path, String(comment.page), user),
       text: getSlackMessageTextForComment(path, String(comment.page), user),
       attachments: [attachment],
       attachments: [attachment],
@@ -185,7 +185,7 @@ module.exports = function(crowi) {
     };
     };
 
 
     const message = {
     const message = {
-      channel: `#${slackChannel}`,
+      channel: (slackChannel != null) ? `#${slackChannel}` : undefined,
       username: appTitle,
       username: appTitle,
       text: messageBody,
       text: messageBody,
       attachments: [attachment],
       attachments: [attachment],

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

Fișier diff suprimat deoarece este prea mare
+ 369 - 237
yarn.lock


Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff