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

Merge pull request #666 from weseek/imprv/645-revision-history

Imprv/645 revision history
Yuki Takei 7 лет назад
Родитель
Сommit
65e304692a

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

@@ -46,8 +46,10 @@
 
   "Unportalize": "Unportalize",
 
-  "View this version": "View this version",
+  "Go to this version": "View this version",
   "View diff": "View diff",
+  "No diff": "No diff",
+  "Shrink versions that have no diffs": "Shrink versions that have no diffs",
 
   "User ID": "User ID",
   "Home": "Home",

+ 4 - 2
resource/locales/ja/translation.json

@@ -44,8 +44,10 @@
 
   "Unportalize": "ポータル解除",
 
-  "View this version": "このバージョンを見る",
-  "View diff": "差分を見る",
+  "Go to this version": "このバージョンを見る",
+  "View diff": "差分を表示",
+  "No diff": "差分なし",
+  "Shrink versions that have no diffs": "差分のないバージョンをコンパクトに表示する",
 
   "User ID": "ユーザーID",
   "Home": "ホーム",

+ 4 - 1
src/client/js/app.js

@@ -558,5 +558,8 @@ socket.on('page:editingWithHackmd', function(data) {
 
 // うわーもうー (commented by Crowi team -- 2018.03.23 Yuki Takei)
 $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', function() {
-  ReactDOM.render(<PageHistory pageId={pageId} crowi={crowi} />, document.getElementById('revision-history'));
+  ReactDOM.render(
+    <I18nextProvider i18n={i18n}>
+      <PageHistory pageId={pageId} crowi={crowi} />
+    </I18nextProvider>, document.getElementById('revision-history'));
 });

+ 6 - 1
src/client/js/components/PageHistory.js

@@ -1,9 +1,10 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+import { translate } from 'react-i18next';
 
 import PageRevisionList from './PageHistory/PageRevisionList';
 
-export default class PageHistory extends React.Component {
+class PageHistory extends React.Component {
 
   constructor(props) {
     super(props);
@@ -119,6 +120,7 @@ export default class PageHistory extends React.Component {
     return (
       <div>
         <PageRevisionList
+          t={this.props.t}
           revisions={this.state.revisions}
           diffOpened={this.state.diffOpened}
           getPreviousRevision={this.getPreviousRevision}
@@ -130,6 +132,9 @@ export default class PageHistory extends React.Component {
 }
 
 PageHistory.propTypes = {
+  t: PropTypes.func.isRequired,               // i18next
   pageId: PropTypes.string,
   crowi: PropTypes.object.isRequired,
 };
+
+export default translate()(PageHistory);

+ 0 - 57
src/client/js/components/PageHistory/PageRevisionList.js

@@ -1,57 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import Revision     from './Revision';
-import RevisionDiff from './RevisionDiff';
-
-export default class PageRevisionList extends React.Component {
-
-  render() {
-    const revisions = this.props.revisions,
-      revisionCount = this.props.revisions.length;
-
-    const revisionList = this.props.revisions.map((revision, idx) => {
-      const revisionId = revision._id
-        , revisionDiffOpened = this.props.diffOpened[revisionId] || false;
-
-
-      let previousRevision;
-      if (idx+1 < revisionCount) {
-        previousRevision = revisions[idx + 1];
-      }
-      else {
-        previousRevision = revision; // if it is the first revision, show full text as diff text
-      }
-
-      return (
-        <div className="revision-hisory-outer" key={'revision-history-' + revisionId}>
-          <Revision
-            revision={revision}
-            revisionDiffOpened={revisionDiffOpened}
-            onDiffOpenClicked={this.props.onDiffOpenClicked}
-            key={'revision-history-rev-' + revisionId}
-            />
-          <RevisionDiff
-            revisionDiffOpened={revisionDiffOpened}
-            currentRevision={revision}
-            previousRevision={previousRevision}
-            key={'revision-diff-' + revisionId}
-          />
-        </div>
-      );
-    });
-
-    return (
-      <div className="revision-history-list">
-        {revisionList}
-      </div>
-    );
-  }
-}
-
-PageRevisionList.propTypes = {
-  revisions: PropTypes.array,
-  diffOpened: PropTypes.object,
-  onDiffOpenClicked: PropTypes.func.isRequired,
-};
-

+ 111 - 0
src/client/js/components/PageHistory/PageRevisionList.jsx

@@ -0,0 +1,111 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import Revision     from './Revision';
+import RevisionDiff from './RevisionDiff';
+
+export default class PageRevisionList extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      isCompactNodiffRevisions: true,
+    };
+
+    this.cbCompactizeChangeHandler = this.cbCompactizeChangeHandler.bind(this);
+  }
+
+  cbCompactizeChangeHandler() {
+    this.setState({ isCompactNodiffRevisions: !this.state.isCompactNodiffRevisions });
+  }
+
+  /**
+   * render a row (Revision component and RevisionDiff component)
+   * @param {Revison} revision
+   * @param {Revision} previousRevision
+   * @param {boolean} hasDiff whether revision has difference to previousRevision
+   * @param {boolean} isContiguousNodiff true if the current 'hasDiff' and one of previous row is both false
+   */
+  renderRow(revision, previousRevision, hasDiff, isContiguousNodiff) {
+    const revisionId = revision._id;
+    const revisionDiffOpened = this.props.diffOpened[revisionId] || false;
+
+    const classNames = ['revision-history-outer'];
+    if (isContiguousNodiff) {
+      classNames.push('revision-history-outer-contiguous-nodiff');
+    }
+
+    return (
+      <div className={classNames.join(' ')} key={`revision-history-${revisionId}`}>
+        <Revision
+          t={this.props.t}
+          revision={revision}
+          revisionDiffOpened={revisionDiffOpened}
+          hasDiff={hasDiff}
+          isCompactNodiffRevisions={this.state.isCompactNodiffRevisions}
+          onDiffOpenClicked={this.props.onDiffOpenClicked}
+          key={`revision-history-rev-${revisionId}`}
+          />
+        { hasDiff &&
+          <RevisionDiff
+            revisionDiffOpened={revisionDiffOpened}
+            currentRevision={revision}
+            previousRevision={previousRevision}
+            key={`revision-deff-${revisionId}`}
+          />
+        }
+      </div>
+    );
+  }
+
+  render() {
+    const { t } = this.props;
+
+    const revisions = this.props.revisions,
+      revisionCount = this.props.revisions.length;
+
+    let hasDiffPrev;
+
+    const revisionList = this.props.revisions.map((revision, idx) => {
+      let previousRevision;
+      if (idx+1 < revisionCount) {
+        previousRevision = revisions[idx + 1];
+      }
+      else {
+        previousRevision = revision; // if it is the first revision, show full text as diff text
+      }
+
+      const hasDiff = revision.hasDiffToPrev !== false; // set 'true' if undefined for backward compatibility
+      const isContiguousNodiff = !hasDiff && !hasDiffPrev;
+
+      hasDiffPrev = hasDiff;
+
+      return this.renderRow(revision, previousRevision, hasDiff, isContiguousNodiff);
+    });
+
+    const classNames = ['revision-history-list'];
+    if (this.state.isCompactNodiffRevisions) {
+      classNames.push('revision-history-list-compact');
+    }
+
+    return <React.Fragment>
+      <div className='checkbox checkbox-info pull-right'>
+        <input id='cbCompactize' type='checkbox' value={true} checked={this.state.isCompactNodiffRevisions} onChange={this.cbCompactizeChangeHandler}></input>
+        <label htmlFor='cbCompactize'>{ t('Shrink versions that have no diffs') }</label>
+      </div>
+      <div className="clearfix"></div>
+      <div className={classNames.join(' ')}>
+        {revisionList}
+      </div>
+    </React.Fragment>;
+  }
+}
+
+PageRevisionList.propTypes = {
+  t: PropTypes.func.isRequired,               // i18next
+  revisions: PropTypes.array,
+  diffOpened: PropTypes.object,
+  onDiffOpenClicked: PropTypes.func.isRequired,
+};
+

+ 0 - 65
src/client/js/components/PageHistory/Revision.js

@@ -1,65 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import UserDate     from '../Common/UserDate';
-import UserPicture  from '../User/UserPicture';
-
-export default class Revision extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this._onDiffOpenClicked = this._onDiffOpenClicked.bind(this);
-  }
-
-  componentDidMount() {
-  }
-
-  _onDiffOpenClicked() {
-    this.props.onDiffOpenClicked(this.props.revision);
-  }
-
-  render() {
-    const revision = this.props.revision;
-    const author = revision.author;
-
-    let pic = '';
-    if (typeof author === 'object') {
-      pic = <UserPicture user={author} />;
-    }
-
-    const iconClass = this.props.revisionDiffOpened ? 'caret caret-opened' : 'caret';
-    return (
-      <div className="revision-history-main d-flex">
-        <div className="m-t-5">
-          {pic}
-        </div>
-        <div className="m-l-10">
-          <div className="revision-history-author">
-            <strong>{author.username}</strong>
-          </div>
-          <div className="revision-history-meta">
-            <p>
-              <UserDate dateTime={revision.createdAt} />
-            </p>
-            <p>
-              <a className="diff-view" onClick={this._onDiffOpenClicked}>
-                <i className={iconClass}></i> View diff
-              </a>
-              <a href={'?revision=' + revision._id } className="m-l-10">
-                <i className="icon-login"></i> Go to this version
-              </a>
-            </p>
-          </div>
-        </div>
-      </div>
-    );
-  }
-}
-
-Revision.propTypes = {
-  revision: PropTypes.object,
-  revisionDiffOpened: PropTypes.bool.isRequired,
-  onDiffOpenClicked: PropTypes.func.isRequired,
-};
-

+ 113 - 0
src/client/js/components/PageHistory/Revision.jsx

@@ -0,0 +1,113 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import UserDate     from '../Common/UserDate';
+import UserPicture  from '../User/UserPicture';
+
+export default class Revision extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this._onDiffOpenClicked = this._onDiffOpenClicked.bind(this);
+  }
+
+  componentDidMount() {
+  }
+
+  _onDiffOpenClicked() {
+    this.props.onDiffOpenClicked(this.props.revision);
+  }
+
+  renderSimplifiedNodiff(revision) {
+    const { t } = this.props;
+
+    const author = revision.author;
+
+    let pic = '';
+    if (typeof author === 'object') {
+      pic = <UserPicture user={author} size='sm' />;
+    }
+
+    return (
+      <div className="revision-history-main revision-history-main-nodiff my-1 d-flex align-items-center">
+        <div className="picture-container">
+          {pic}
+        </div>
+        <div className="m-l-10">
+          <div className="revision-history-meta">
+            <span className="text-muted small">
+              <UserDate dateTime={revision.createdAt} /> ({ t('No diff') })
+            </span>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  renderFull(revision) {
+    const { t } = this.props;
+
+    const author = revision.author;
+
+    let pic = '';
+    if (typeof author === 'object') {
+      pic = <UserPicture user={author} size='lg' />;
+    }
+
+    const iconClass = this.props.revisionDiffOpened ? 'caret caret-opened' : 'caret';
+    return (
+      <div className="revision-history-main d-flex mt-3">
+        <div className="m-t-5">
+          {pic}
+        </div>
+        <div className="m-l-10">
+          <div className="revision-history-author">
+            <strong>{author.username}</strong>
+          </div>
+          <div className="revision-history-meta">
+            <p>
+              <UserDate dateTime={revision.createdAt} />
+            </p>
+            <p>
+              <span className='d-inline-block' style={{ minWidth: '90px' }}>
+                { !this.props.hasDiff &&
+                  <span className='text-muted'>{ t('No diff') }</span>
+                }
+                { this.props.hasDiff &&
+                  <a className="diff-view" onClick={this._onDiffOpenClicked}>
+                    <i className={iconClass}></i> { t('View diff') }
+                  </a>
+                }
+              </span>
+              <a href={'?revision=' + revision._id } className="m-l-10">
+                <i className="icon-login"></i> { t('Go to this version') }
+              </a>
+            </p>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  render() {
+    const revision = this.props.revision;
+
+    if (this.props.isCompactNodiffRevisions && !this.props.hasDiff) {
+      return this.renderSimplifiedNodiff(revision);
+    }
+    else {
+      return this.renderFull(revision);
+    }
+  }
+}
+
+Revision.propTypes = {
+  t: PropTypes.func.isRequired,               // i18next
+  revision: PropTypes.object,
+  revisionDiffOpened: PropTypes.bool.isRequired,
+  hasDiff: PropTypes.bool.isRequired,
+  isCompactNodiffRevisions: PropTypes.bool.isRequired,
+  onDiffOpenClicked: PropTypes.func.isRequired,
+};
+

+ 19 - 2
src/client/styles/scss/_page.scss

@@ -85,9 +85,14 @@
 .main-container .main .content-main .revision-history { // {{{
 
   .revision-history-list {
-    .revision-hisory-outer {
+    .revision-history-outer {
+      // add border-top except of first element
+      &:not(:first-of-type) {
+        border-top: 1px solid $border;
+      }
+
       .revision-history-main {
-        .picture {
+        .picture-lg {
           width: 32px;
           height: 32px;
         }
@@ -106,6 +111,12 @@
           }
         }
       }
+      .revision-history-main-nodiff {
+        .picture-container {
+          min-width: 32px;
+          text-align: center; // centering .picture
+        }
+      }
       .revision-history-diff {
         padding-left: 40px;
       }
@@ -116,6 +127,12 @@
       list-style: none;
     }
   }
+  // compacted list
+  .revision-history-list-compact {
+    .revision-history-outer-contiguous-nodiff {
+      border-top: unset !important; // force unset border
+    }
+  }
 
   // adjust
   // this is for diff2html. hide page name from diff view

+ 6 - 2
src/server/models/revision.js

@@ -15,7 +15,8 @@ module.exports = function(crowi) {
     }},
     format: { type: String, default: 'markdown' },
     author: { type: ObjectId, ref: 'User' },
-    createdAt: { type: Date, default: Date.now }
+    createdAt: { type: Date, default: Date.now },
+    hasDiffToPrev: { type: Boolean },
   });
 
   /*
@@ -80,7 +81,7 @@ module.exports = function(crowi) {
 
   revisionSchema.statics.findRevisionIdList = function(path) {
     return this.find({path: path})
-      .select('_id author createdAt')
+      .select('_id author createdAt hasDiffToPrev')
       .sort({createdAt: -1})
       .exec();
   };
@@ -135,6 +136,9 @@ module.exports = function(crowi) {
     newRevision.format = format;
     newRevision.author = user._id;
     newRevision.createdAt = Date.now();
+    if (pageData.revision != null) {
+      newRevision.hasDiffToPrev = body !== pageData.revision.body;
+    }
 
     return newRevision;
   };