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

add view to show comparing revision changes

Ryu Sato 5 лет назад
Родитель
Сommit
240f7f90b7

+ 3 - 0
resource/locales/en_US/translation.json

@@ -318,6 +318,9 @@
     "outdated": "Page is updated someone and now outdated.",
     "outdated": "Page is updated someone and now outdated.",
     "user_not_admin": "Only admin user can delete completely"
     "user_not_admin": "Only admin user can delete completely"
   },
   },
+  "page_compare_revision": {
+    "comparing_changes": "Comparing changes"
+  },
   "modal_rename": {
   "modal_rename": {
     "label": {
     "label": {
       "Move/Rename page": "Move/Rename page",
       "Move/Rename page": "Move/Rename page",

+ 3 - 0
resource/locales/ja_JP/translation.json

@@ -320,6 +320,9 @@
     "outdated": "ページが他のユーザーによって更新されました。",
     "outdated": "ページが他のユーザーによって更新されました。",
     "user_not_admin": "権限のあるユーザーのみが完全削除できます"
     "user_not_admin": "権限のあるユーザーのみが完全削除できます"
   },
   },
+  "page_compare_revision": {
+    "comparing_changes": "リビジョン比較"
+  },
   "modal_rename": {
   "modal_rename": {
     "label": {
     "label": {
       "Move/Rename page": "ページを移動/名前変更する",
       "Move/Rename page": "ページを移動/名前変更する",

+ 4 - 1
resource/locales/zh_CN/translation.json

@@ -298,7 +298,10 @@
 		"already_exists": "新建页面已存在",
 		"already_exists": "新建页面已存在",
 		"outdated": "页面已被某人更新,现在已过时。",
 		"outdated": "页面已被某人更新,现在已过时。",
 		"user_not_admin": "仅管理员用户可以完全删除"
 		"user_not_admin": "仅管理员用户可以完全删除"
-	},
+  },
+  "page_compare_revision": {
+    "comparing_changes": "比较变化"
+  },
 	"modal_rename": {
 	"modal_rename": {
 		"label": {
 		"label": {
 			"Move/Rename page": "页面 移动/重命名",
 			"Move/Rename page": "页面 移动/重命名",

+ 3 - 1
src/client/js/app.jsx

@@ -38,6 +38,7 @@ import GrowiSubNavigationSwitcher from './components/Navbar/GrowiSubNavigationSw
 import NavigationContainer from './services/NavigationContainer';
 import NavigationContainer from './services/NavigationContainer';
 import PageContainer from './services/PageContainer';
 import PageContainer from './services/PageContainer';
 import PageHistoryContainer from './services/PageHistoryContainer';
 import PageHistoryContainer from './services/PageHistoryContainer';
+import RevisionCompareContainer from './services/RevisionCompareContainer';
 import CommentContainer from './services/CommentContainer';
 import CommentContainer from './services/CommentContainer';
 import EditorContainer from './services/EditorContainer';
 import EditorContainer from './services/EditorContainer';
 import TagContainer from './services/TagContainer';
 import TagContainer from './services/TagContainer';
@@ -56,12 +57,13 @@ const socketIoContainer = appContainer.getContainer('SocketIoContainer');
 const navigationContainer = new NavigationContainer(appContainer);
 const navigationContainer = new NavigationContainer(appContainer);
 const pageContainer = new PageContainer(appContainer);
 const pageContainer = new PageContainer(appContainer);
 const pageHistoryContainer = new PageHistoryContainer(appContainer, pageContainer);
 const pageHistoryContainer = new PageHistoryContainer(appContainer, pageContainer);
+const revisionCompareContainer = new RevisionCompareContainer(appContainer, pageContainer);
 const commentContainer = new CommentContainer(appContainer);
 const commentContainer = new CommentContainer(appContainer);
 const editorContainer = new EditorContainer(appContainer, defaultEditorOptions, defaultPreviewOptions);
 const editorContainer = new EditorContainer(appContainer, defaultEditorOptions, defaultPreviewOptions);
 const tagContainer = new TagContainer(appContainer);
 const tagContainer = new TagContainer(appContainer);
 const personalContainer = new PersonalContainer(appContainer);
 const personalContainer = new PersonalContainer(appContainer);
 const injectableContainers = [
 const injectableContainers = [
-  appContainer, socketIoContainer, navigationContainer, pageContainer, pageHistoryContainer, commentContainer, editorContainer, tagContainer, personalContainer,
+  appContainer, socketIoContainer, navigationContainer, pageContainer, pageHistoryContainer, revisionCompareContainer, commentContainer, editorContainer, tagContainer, personalContainer,
 ];
 ];
 
 
 logger.info('unstated containers have been initialized');
 logger.info('unstated containers have been initialized');

+ 11 - 2
src/client/js/components/Page.jsx

@@ -11,6 +11,7 @@ import MarkdownTable from '../models/MarkdownTable';
 
 
 import LinkEditModal from './PageEditor/LinkEditModal';
 import LinkEditModal from './PageEditor/LinkEditModal';
 import RevisionRenderer from './Page/RevisionRenderer';
 import RevisionRenderer from './Page/RevisionRenderer';
+import RevisionCompare from './RevisionCompare';
 import HandsontableModal from './PageEditor/HandsontableModal';
 import HandsontableModal from './PageEditor/HandsontableModal';
 import DrawioModal from './PageEditor/DrawioModal';
 import DrawioModal from './PageEditor/DrawioModal';
 import mtu from './PageEditor/MarkdownTableUtil';
 import mtu from './PageEditor/MarkdownTableUtil';
@@ -131,11 +132,19 @@ class Page extends React.Component {
     const { appContainer, pageContainer } = this.props;
     const { appContainer, pageContainer } = this.props;
     const { isMobile } = appContainer;
     const { isMobile } = appContainer;
     const isLoggedIn = appContainer.currentUser != null;
     const isLoggedIn = appContainer.currentUser != null;
-    const { markdown } = pageContainer.state;
+    const { markdown, compareRevisionIds } = pageContainer.state;
+
+    const renderer = (!compareRevisionIds || compareRevisionIds.length == 0) ?
+                       <RevisionRenderer
+                           growiRenderer={this.growiRenderer}
+                           markdown={markdown} />
+                     : <RevisionCompare
+                         fromRevisionId={compareRevisionIds.length >= 1 ? compareRevisionIds[0] : ""}
+                         toRevisionId={compareRevisionIds.length >= 2 ? compareRevisionIds[1] : ""} />;
 
 
     return (
     return (
       <div className={`mb-5 ${isMobile ? 'page-mobile' : ''}`}>
       <div className={`mb-5 ${isMobile ? 'page-mobile' : ''}`}>
-        <RevisionRenderer growiRenderer={this.growiRenderer} markdown={markdown} />
+        { renderer }
 
 
         { isLoggedIn && (
         { isLoggedIn && (
           <>
           <>

+ 56 - 0
src/client/js/components/RevisionCompare.jsx

@@ -0,0 +1,56 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { withUnstatedContainers } from './UnstatedUtils';
+import RevisionCompareContainer from '../services/RevisionCompareContainer';
+import RevisionDiff from './PageHistory/RevisionDiff';
+
+class PageCompare extends React.Component {
+  componentWillMount() {
+    const { revisionCompareContainer, fromRevisionId, toRevisionId } = this.props;
+
+    revisionCompareContainer.fetchPageRevisionBody(fromRevisionId, toRevisionId);
+  }
+
+  render() {
+    const { t, revisionCompareContainer } = this.props;
+
+    const fromRev = revisionCompareContainer.state.fromRevision;
+    const toRev = revisionCompareContainer.state.toRevision;
+    const showDiff = (fromRev && toRev);
+
+    return (
+      <div id="revision-compare-content">
+        <div>{ t('page_compare_revision.comparing_changes') }</div>
+        <div class="card card-compare">
+          <div class="card-body">
+          { fromRev && fromRev._id }<i class="icon-arrow-right-circle mx-1"></i>{ toRev && toRev._id }
+          </div>
+        </div>
+        { showDiff &&
+          <RevisionDiff
+            revisionDiffOpened={ true }
+            previousRevision={ fromRev }
+            currentRevision={ toRev }
+          />
+        }
+      </div>
+    );
+  }
+
+}
+
+/**
+ * Wrapper component for using unstated
+ */
+const PageCompareWrapper = withUnstatedContainers(PageCompare, [RevisionCompareContainer]);
+
+PageCompare.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  revisionCompareContainer: PropTypes.instanceOf(RevisionCompareContainer).isRequired,
+  fromRevisionId: PropTypes.string,
+  toRevisionId: PropTypes.string,
+};
+
+export default withTranslation()(PageCompareWrapper);

+ 1 - 0
src/client/js/services/PageContainer.js

@@ -75,6 +75,7 @@ export default class PageContainer extends Container {
       templateTagData: mainContent.getAttribute('data-template-tags') || null,
       templateTagData: mainContent.getAttribute('data-template-tags') || null,
       shareLinksNumber: mainContent.getAttribute('data-share-links-number'),
       shareLinksNumber: mainContent.getAttribute('data-share-links-number'),
       shareLinkId: JSON.parse(mainContent.getAttribute('data-share-link-id') || null),
       shareLinkId: JSON.parse(mainContent.getAttribute('data-share-link-id') || null),
+      compareRevisionIds: JSON.parse(mainContent.getAttribute('data-compare-revision-ids') || null),
 
 
       // latest(on remote) information
       // latest(on remote) information
       remoteRevisionId: revisionId,
       remoteRevisionId: revisionId,

+ 64 - 0
src/client/js/services/RevisionCompareContainer.js

@@ -0,0 +1,64 @@
+import { Container } from 'unstated';
+import loggerFactory from '@alias/logger';
+
+import { toastError } from '../util/apiNotification';
+
+const logger = loggerFactory('growi:PageHistoryContainer');
+
+/**
+ * Service container for personal settings page (RevisionCompare.jsx)
+ * @extends {Container} unstated Container
+ */
+export default class RevisionCompareContainer extends Container {
+
+  constructor(appContainer, pageContainer) {
+    super();
+
+    this.appContainer = appContainer;
+    this.pageContainer = pageContainer;
+
+    this.state = {
+      errMessage: null,
+
+      fromRevision: null,
+      toRevision: null,
+    }
+
+    this.fetchPageRevisionBody = this.fetchPageRevisionBody.bind(this);
+  }
+
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'RevisionCompareContainer';
+  }
+
+  /**
+   * fetch page revision body by revision_id in argument
+   * @param {string} fromRevisionId
+   * @param {string} toRevisionId
+   */
+  async fetchPageRevisionBody(fromRevisionId, toRevisionId) {
+    const { pageId, shareLinkId } = this.pageContainer.state;
+    try {
+      const revsAll = [
+        { id: fromRevisionId, key: "fromRevision" },
+        { id: toRevisionId,   key: "toRevision" },
+      ];
+      const revs = revsAll.filter(it => it && it.id);
+      for(let it of revs) {
+        const res = await this.appContainer.apiv3Get(`/revisions/${it.id}`, { pageId, shareLinkId });
+        const state = {}
+        state[it.key] = res.data.revision;
+        this.setState(state);
+      }
+    }
+    catch (err) {
+      toastError(err);
+      this.setState({ errorMessage: err.message });
+      logger.error(err);
+    }
+  }
+
+}

+ 10 - 3
src/server/routes/page.js

@@ -225,12 +225,13 @@ module.exports = function(crowi, app) {
     }
     }
   }
   }
 
 
-  function addRenderVarsForPage(renderVars, page) {
+  function addRenderVarsForPage(renderVars, page, compareRevisionIds = []) {
     renderVars.page = page;
     renderVars.page = page;
     renderVars.revision = page.revision;
     renderVars.revision = page.revision;
     renderVars.pageIdOnHackmd = page.pageIdOnHackmd;
     renderVars.pageIdOnHackmd = page.pageIdOnHackmd;
     renderVars.revisionHackmdSynced = page.revisionHackmdSynced;
     renderVars.revisionHackmdSynced = page.revisionHackmdSynced;
     renderVars.hasDraftOnHackmd = page.hasDraftOnHackmd;
     renderVars.hasDraftOnHackmd = page.hasDraftOnHackmd;
+    renderVars.compareRevisionIds = compareRevisionIds;
 
 
     if (page.creator != null) {
     if (page.creator != null) {
       renderVars.page.creator = renderVars.page.creator.toObject();
       renderVars.page.creator = renderVars.page.creator.toObject();
@@ -364,6 +365,7 @@ module.exports = function(crowi, app) {
   async function showPageForGrowiBehavior(req, res, next) {
   async function showPageForGrowiBehavior(req, res, next) {
     const path = getPathFromRequest(req);
     const path = getPathFromRequest(req);
     const revisionId = req.query.revision;
     const revisionId = req.query.revision;
+    const compare = req.query.compare;
     const layoutName = configManager.getConfig('crowi', 'customize:layout');
     const layoutName = configManager.getConfig('crowi', 'customize:layout');
 
 
     let page = await Page.findByPathAndViewer(path, req.user);
     let page = await Page.findByPathAndViewer(path, req.user);
@@ -393,9 +395,11 @@ module.exports = function(crowi, app) {
       page = await page.seen(req.user);
       page = await page.seen(req.user);
     }
     }
 
 
+    const compareRevisionIds = compare ? compare.split('...') : [];
+
     // populate
     // populate
     page = await page.populateDataToShowRevision();
     page = await page.populateDataToShowRevision();
-    addRenderVarsForPage(renderVars, page);
+    addRenderVarsForPage(renderVars, page, compareRevisionIds);
     addRenderVarsForScope(renderVars, page);
     addRenderVarsForScope(renderVars, page);
 
 
     await addRenderVarsForSlack(renderVars, page);
     await addRenderVarsForSlack(renderVars, page);
@@ -452,6 +456,7 @@ module.exports = function(crowi, app) {
   actions.showSharedPage = async function(req, res, next) {
   actions.showSharedPage = async function(req, res, next) {
     const { linkId } = req.params;
     const { linkId } = req.params;
     const revisionId = req.query.revision;
     const revisionId = req.query.revision;
+    const compare = req.query.compare;
 
 
     const layoutName = configManager.getConfig('crowi', 'customize:layout');
     const layoutName = configManager.getConfig('crowi', 'customize:layout');
 
 
@@ -485,9 +490,11 @@ module.exports = function(crowi, app) {
 
 
     page.initLatestRevisionField(revisionId);
     page.initLatestRevisionField(revisionId);
 
 
+    const compareRevisionIds = compare ? compare.split('...') : [];
+
     // populate
     // populate
     page = await page.populateDataToShowRevision();
     page = await page.populateDataToShowRevision();
-    addRenderVarsForPage(renderVars, page);
+    addRenderVarsForPage(renderVars, page, compareRevisionIds);
     addRenderVarsForScope(renderVars, page);
     addRenderVarsForScope(renderVars, page);
 
 
     await interceptorManager.process('beforeRenderPage', req, res, renderVars);
     await interceptorManager.process('beforeRenderPage', req, res, renderVars);

+ 1 - 0
src/server/views/widget/page_content.html

@@ -29,6 +29,7 @@
   data-page-count-of-seen-users="{{ page.seenUsers.length|default(0) }}"
   data-page-count-of-seen-users="{{ page.seenUsers.length|default(0) }}"
   data-share-links-number="{% if page %}{{ sharelinksNumber }}{% endif %}"
   data-share-links-number="{% if page %}{{ sharelinksNumber }}{% endif %}"
   data-share-link-id="{% if sharelink %}{{ sharelink._id|json }}{% endif %}"
   data-share-link-id="{% if sharelink %}{{ sharelink._id|json }}{% endif %}"
+  data-compare-revision-ids="{% if compareRevisionIds %}{{ compareRevisionIds|json }}{% else %}null{% endif %}"
   >
   >
 {% else %}
 {% else %}
 <div id="content-main" class="content-main d-flex"
 <div id="content-main" class="content-main d-flex"