You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ro...@apache.org on 2016/04/21 19:38:45 UTC

[1/2] fauxton commit: updated refs/heads/master to 3f46050

Repository: couchdb-fauxton
Updated Branches:
  refs/heads/master 978ae690f -> 3f46050d3


conflict-solving: first pass on revision browser

This adds the first iteration of a revision browsing tool for
conflicts that is able to diff documents and to select
conflicting revs as a winner.

Additional changes:

 - adjust button colors
 - add a helper to create an animal db, which also contains the
   zebra doc, which has a conflict from replication

Testing instructions:

`npm run create:animaldb` creates a fresh version of the animaldb
with a conflicting doc, the `zebra`.

PR: #670
PR-URL: https://github.com/apache/couchdb-fauxton/pull/670
Reviewed-By: Benjamin Keen <be...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/3f46050d
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/3f46050d
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/3f46050d

Branch: refs/heads/master
Commit: 3f46050d37d2c429dc0bd6e9c13476214e82ab5a
Parents: 319ce99
Author: Robert Kowalski <ro...@apache.org>
Authored: Fri Mar 18 12:38:26 2016 +0000
Committer: Robert Kowalski <ro...@apache.org>
Committed: Thu Apr 21 19:38:38 2016 +0200

----------------------------------------------------------------------
 .../components/react-components.react.jsx       |  24 +-
 .../documents/assets/less/doc-editor.less       |   4 +
 app/addons/documents/assets/less/documents.less |  13 +-
 .../documents/assets/less/index-results.less    |  19 +-
 .../documents/assets/less/revision-browser.less |  98 ++++
 app/addons/documents/base.js                    |  18 +
 .../documents/doc-editor/components.react.jsx   |  39 +-
 app/addons/documents/doc-editor/stores.js       |   9 +
 .../doc-editor/tests/doc-editor.storesSpec.js   |  13 +-
 .../index-results.components.react.jsx          |  12 +-
 .../rev-browser/rev-browser.actions.js          | 168 +++++++
 .../rev-browser/rev-browser.actiontypes.js      |  20 +
 .../rev-browser.components.react.jsx            | 442 +++++++++++++++++++
 .../documents/rev-browser/rev-browser.stores.js | 123 ++++++
 .../documents/rev-browser/tests/fixtures.js     |  72 +++
 .../tests/rev-browser.actionsSpec.js            |  94 ++++
 app/addons/documents/routes-doc-editor.js       | 122 ++---
 app/addons/documents/routes.js                  |   2 +-
 app/addons/documents/shared-resources.js        |   7 +-
 .../documents/tests/nightwatch/revBrowser.js    |  59 +++
 .../tests/nightwatch/tableViewConflicts.js      |   6 +-
 assets/less/fauxton.less                        |  14 +
 assets/less/formstyles.less                     |  15 +
 bin/create-animal-db                            |  13 +
 package.json                                    |   7 +-
 test/animal-db.json                             |  13 +
 test/create-animal-db.js                        | 154 +++++++
 .../custom-commands/createAnimalDb.js           |  34 ++
 .../populateDatabaseWithConflicts.js            |  18 +-
 29 files changed, 1537 insertions(+), 95 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/app/addons/components/react-components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/components/react-components.react.jsx b/app/addons/components/react-components.react.jsx
index dcf9a75..bf30b52 100644
--- a/app/addons/components/react-components.react.jsx
+++ b/app/addons/components/react-components.react.jsx
@@ -1119,12 +1119,21 @@ define([
 
   var ConfirmButton = React.createClass({
     propTypes: {
-      showIcon: React.PropTypes.bool
+      showIcon: React.PropTypes.bool,
+      id: React.PropTypes.string,
+      customIcon: React.PropTypes.string,
+      style: React.PropTypes.object,
+      buttonType: React.PropTypes.string,
+      'data-id': React.PropTypes.string,
     },
 
     getDefaultProps: function () {
       return {
-        showIcon: true
+        showIcon: true,
+        customIcon: 'fonticon-ok-circled',
+        buttonType: 'btn-success',
+        style: {},
+        'data-id': null
       };
     },
 
@@ -1133,13 +1142,20 @@ define([
         return null;
       }
       return (
-        <i className="icon fonticon-ok-circled" />
+        <i className={"icon " + this.props.customIcon} />
       );
     },
 
     render: function () {
       return (
-        <button onClick={this.props.onClick} type="submit" className="btn btn-success save" id={this.props.id}>
+        <button
+          onClick={this.props.onClick}
+          type="submit"
+          data-id={this.props['data-id']}
+          className={'btn save ' + this.props.buttonType}
+          id={this.props.id}
+          style={this.props.style}
+        >
           {this.getIcon()}
           {this.props.text}
         </button>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/app/addons/documents/assets/less/doc-editor.less
----------------------------------------------------------------------
diff --git a/app/addons/documents/assets/less/doc-editor.less b/app/addons/documents/assets/less/doc-editor.less
index 82ef9d2..9565e14 100644
--- a/app/addons/documents/assets/less/doc-editor.less
+++ b/app/addons/documents/assets/less/doc-editor.less
@@ -105,6 +105,10 @@
     .icon {
       font-size: 18px;
     }
+
+    .button-text {
+      padding-right: 5px;
+    }
   }
   .panel-section {
     border-left: 1px solid #cccccc;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/app/addons/documents/assets/less/documents.less
----------------------------------------------------------------------
diff --git a/app/addons/documents/assets/less/documents.less b/app/addons/documents/assets/less/documents.less
index 787b775..99d616c 100644
--- a/app/addons/documents/assets/less/documents.less
+++ b/app/addons/documents/assets/less/documents.less
@@ -20,6 +20,7 @@
 @import "index-results.less";
 @import "doc-editor.less";
 @import "header.less";
+@import "revision-browser";
 
 .two-sides-toggle-button {
   font-size: 15px;
@@ -27,14 +28,18 @@
 
   button.btn {
     padding: 10px 15px;
+    background-color: #fff;
+    color: #888;
     &:hover {
-      background-color: @brandPrimary;
-      color: white;
+      background-color: #e73d34;
+      color: #fff;
     }
     &.active {
-      color: @brandPrimary;
+      background-color: #f1f1f1;
+      color: #af2d24;
       &:hover {
-        background-color: white;
+        background-color: #f1f1f1;
+        color: #af2d24;
       }
     }
   }

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/app/addons/documents/assets/less/index-results.less
----------------------------------------------------------------------
diff --git a/app/addons/documents/assets/less/index-results.less b/app/addons/documents/assets/less/index-results.less
index df11f72..39c3bde 100644
--- a/app/addons/documents/assets/less/index-results.less
+++ b/app/addons/documents/assets/less/index-results.less
@@ -103,7 +103,11 @@
     margin: 0 0 0 8px;
   }
   .tableview-conflict {
-    color: #FF0000;
+    color: #F00;
+  }
+  .icon-code-fork {
+    padding-right: 2px;
+    color: #F00;
   }
   .tableview-el-last {
     width: 75px;
@@ -115,6 +119,7 @@
   thead input {
     max-width: 138px;
     width: 100%;
+    overflow: visible;
   }
 
   .table-dropdown-item {
@@ -142,9 +147,14 @@
     height: 29px;
   }
 
+  th {
+    overflow: visible;
+  }
+
   .table-container-autocomplete .table-select-wrapper {
     width: inherit;
-    position: fixed;
+    overflow: visible;
+    min-height: 300px;
   }
 
   .Select div.Select-control {
@@ -179,6 +189,11 @@
     box-shadow: transparent;
   }
 
+  .Select .Select-menu {
+    min-height: 291px;
+    background-color: #333333;
+  }
+
 }
 
 .document-result-screen {

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/app/addons/documents/assets/less/revision-browser.less
----------------------------------------------------------------------
diff --git a/app/addons/documents/assets/less/revision-browser.less b/app/addons/documents/assets/less/revision-browser.less
new file mode 100644
index 0000000..a3f04df
--- /dev/null
+++ b/app/addons/documents/assets/less/revision-browser.less
@@ -0,0 +1,98 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+div#dashboard-content div.revision-wrapper {
+  padding: 0 0 15px 0;
+  margin-top: 60px;
+
+  .rev-subtree-selector {
+    font-family: monospace;
+    margin-bottom: 20px;
+  }
+  .left-area {
+    text-align: center;
+  }
+  .ours-rev {
+    font-size: 16px;
+    color: #fff;
+    text-align: center;
+    margin-top: 15px;
+  }
+
+  .revision-browser-controls {
+    color: #fff;
+    margin: 30px 0;
+  }
+
+  .revision-browser-controls .Select .Select-control {
+    border-radius: inherit;
+    border-color: #ccc;
+  }
+
+  .revision-browser-controls .Select .is-focused:not(.is-open) > .Select-control {
+    box-shadow: transparent;
+    border-color: #ccc;
+  }
+
+  .revision-browser-controls .Select .Select-menu-outer {
+    border-radius: inherit;
+  }
+
+  .revision-browser-controls .Select div.Select-placeholder {
+    color: #9d261d;
+  }
+
+  .revision-view-controls {
+    border-top: 1px solid grey;
+    text-align: center;
+  }
+
+  .revision-split-area {
+    padding: 20px 15px;
+    .display-flex();
+    color: #fff;
+  }
+
+  .conflicting-revs-dropdown {
+    max-width: 370px;
+    .display-flex();
+    margin-left: -17px;
+  }
+
+  .revision-split-area pre {
+    border: none;
+    background-color: transparent;
+  }
+  .revision-diff-area {
+    color: #fff;
+    margin-left: 60px;
+  }
+  .revision-diff-area .jsondiffpatch-unchanged, .revision-diff-area .jsondiffpatch-unchanged pre {
+    color: #fff;
+    background-color: transparent;
+    border: none;
+  }
+  .revision-diff-area .jsondiffpatch-added, .revision-diff-area .jsondiffpatch-deleted {
+    color: #000;
+  }
+  .revision-diff-area .jsondiffpatch-modified .jsondiffpatch-left-value pre,
+  .jsondiffpatch-textdiff-deleted,
+  .jsondiffpatch-deleted .jsondiffpatch-property-name,
+  .jsondiffpatch-deleted pre {
+    text-decoration: none;
+  }
+
+  .two-sides-toggle-button {
+    z-index: 0;
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/app/addons/documents/base.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/base.js b/app/addons/documents/base.js
index 79233e9..3de66d0 100644
--- a/app/addons/documents/base.js
+++ b/app/addons/documents/base.js
@@ -40,6 +40,24 @@ function (app, FauxtonAPI, Documents) {
     }
   });
 
+  FauxtonAPI.registerUrls('bulk_docs', {
+    server: function (id, query) {
+      return app.host + '/' + id + '/_bulk_docs' + getQueryParam(query);
+    },
+    app: function (id, query) {
+      return 'database/' + id + '/_bulk_docs' + getQueryParam(query);
+    },
+    apiurl: function (id, query) {
+      return window.location.origin + '/' + id + '/_bulk_docs' + getQueryParam(query);
+    }
+  });
+
+  FauxtonAPI.registerUrls('revision-browser', {
+    app: function (id, doc) {
+      return 'database/' + id + '/' + doc + '/conflicts';
+    }
+  });
+
   FauxtonAPI.registerUrls( 'designDocs', {
     server: function (id, designDoc) {
       return app.host + '/' + id + '/' + designDoc + '/_info';

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/app/addons/documents/doc-editor/components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/documents/doc-editor/components.react.jsx b/app/addons/documents/doc-editor/components.react.jsx
index 116a7b7..188e8c5 100644
--- a/app/addons/documents/doc-editor/components.react.jsx
+++ b/app/addons/documents/doc-editor/components.react.jsx
@@ -1,3 +1,16 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+
 define([
   '../../../core/api',
   '../../../app',
@@ -14,6 +27,7 @@ define([
   var store = Stores.docEditorStore;
   var Modal = ReactBootstrap.Modal;
 
+
   var DocEditorController = React.createClass({
 
     getInitialState: function () {
@@ -27,7 +41,8 @@ define([
         cloneDocModalVisible: store.isCloneDocModalVisible(),
         uploadModalVisible: store.isUploadModalVisible(),
         deleteDocModalVisible: store.isDeleteDocModalVisible(),
-        numFilesUploaded: store.getNumFilesUploaded()
+        numFilesUploaded: store.getNumFilesUploaded(),
+        conflictCount: store.getDocConflictCount()
       };
     },
 
@@ -135,7 +150,14 @@ define([
         <div>
           <AttachmentsPanelButton doc={this.state.doc} isLoading={this.state.isLoading} />
           <div className="doc-editor-extension-icons">{this.getExtensionIcons()}</div>
-          <PanelButton title="Upload Attachment" iconClass="icon-circle-arrow-up" onClick={Actions.showUploadModal} />
+
+          {this.state.conflictCount ? <PanelButton
+            title={`Conflicts (${this.state.conflictCount})`}
+            iconClass="icon-columns"
+            className="conflicts"
+            onClick={() => { FauxtonAPI.navigate(FauxtonAPI.urls('revision-browser', 'app', this.props.database.safeID(), this.state.doc.id));}}/> : null}
+
+          <PanelButton className="upload" title="Upload Attachment" iconClass="icon-circle-arrow-up" onClick={Actions.showUploadModal} />
           <PanelButton title="Clone Document" iconClass="icon-repeat" onClick={Actions.showCloneDocModal} />
           <PanelButton title="Delete" iconClass="icon-trash" onClick={Actions.showDeleteDocModal} />
         </div>
@@ -164,6 +186,7 @@ define([
           <div className="code-region">
             <div className="bgEditorGutter"></div>
             <div id="editor-container" className="doc-code">{this.getCodeEditor()}</div>
+
           </div>
 
           <UploadModal
@@ -225,8 +248,8 @@ define([
         <div className="panel-section view-attachments-section btn-group">
           <button className="panel-button dropdown-toggle btn" data-bypass="true" data-toggle="dropdown" title="View Attachments"
             id="view-attachments-menu">
-            <i className="icon fonticon-picture"></i>
-            <span>View Attachments</span>{' '}
+            <i className="icon icon-paper-clip"></i>
+            <span className="button-text">View Attachments</span>
             <span className="caret"></span>
           </button>
           <ul className="dropdown-menu" role="menu" aria-labelledby="view-attachments-menu">
@@ -241,14 +264,16 @@ define([
   var PanelButton = React.createClass({
     propTypes: {
       title: React.PropTypes.string.isRequired,
-      onClick: React.PropTypes.func.isRequired
+      onClick: React.PropTypes.func.isRequired,
+      className: React.PropTypes.string
     },
 
     getDefaultProps: function () {
       return {
         title: '',
         iconClass: '',
-        onClick: function () { }
+        onClick: function () { },
+        className: ''
       };
     },
 
@@ -256,7 +281,7 @@ define([
       var iconClasses = 'icon ' + this.props.iconClass;
       return (
         <div className="panel-section">
-          <button className="panel-button upload" title={this.props.title} onClick={this.props.onClick}>
+          <button className={`panel-button ${this.props.className}`} title={this.props.title} onClick={this.props.onClick}>
             <i className={iconClasses}></i>
             <span>{this.props.title}</span>
           </button>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/app/addons/documents/doc-editor/stores.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/doc-editor/stores.js b/app/addons/documents/doc-editor/stores.js
index 4f628b1..2635ce5 100644
--- a/app/addons/documents/doc-editor/stores.js
+++ b/app/addons/documents/doc-editor/stores.js
@@ -35,14 +35,22 @@ function (FauxtonAPI, ActionTypes) {
       this._fileUploadErrorMsg = '';
       this._uploadInProgress = false;
       this._fileUploadLoadPercentage = 0;
+
+      this._docConflictCount = null;
     },
 
     isLoading: function () {
       return this._isLoading;
     },
 
+    getDocConflictCount: function () {
+      return this._docConflictCount;
+    },
+
     docLoaded: function (options) {
       this._isLoading = false;
+      this._docConflictCount = options.doc.get('_conflicts') ? options.doc.get('_conflicts').length : 0;
+      options.doc.unset('_conflicts');
       this._doc = options.doc;
     },
 
@@ -186,6 +194,7 @@ function (FauxtonAPI, ActionTypes) {
           this.triggerChange();
         break;
 
+
         default:
         return;
         // do nothing

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/app/addons/documents/doc-editor/tests/doc-editor.storesSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/doc-editor/tests/doc-editor.storesSpec.js b/app/addons/documents/doc-editor/tests/doc-editor.storesSpec.js
index 2490f06..69b790c 100644
--- a/app/addons/documents/doc-editor/tests/doc-editor.storesSpec.js
+++ b/app/addons/documents/doc-editor/tests/doc-editor.storesSpec.js
@@ -14,13 +14,16 @@ define([
   '../../../../app',
   '../../../../core/api',
   '../stores',
+
+  '../../resources',
   '../../../../../test/mocha/testUtils',
-], function (app, FauxtonAPI, Stores, utils) {
+], function (app, FauxtonAPI, Stores, Documents, utils) {
   FauxtonAPI.router = new FauxtonAPI.Router([]);
 
-  var assert = utils.assert;
+  const assert = utils.assert;
+  const store = Stores.docEditorStore;
 
-  var store = Stores.docEditorStore;
+  const doc = new Documents.Doc({id: 'foo'}, {database: 'bar'});
 
   describe('DocEditorStore', function () {
     afterEach(function () {
@@ -38,7 +41,7 @@ define([
     });
 
     it('docLoaded() marks loading as complete', function () {
-      store.docLoaded({ doc: {} });
+      store.docLoaded({ doc: doc });
       assert.equal(store.isLoading(), false);
     });
 
@@ -64,7 +67,7 @@ define([
     });
 
     it('reset() resets all values', function () {
-      store.docLoaded({ doc: {} });
+      store.docLoaded({ doc: doc });
       store.showCloneDocModal();
       store.showDeleteDocModal();
       store.showUploadModal();

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/app/addons/documents/index-results/index-results.components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/documents/index-results/index-results.components.react.jsx b/app/addons/documents/index-results/index-results.components.react.jsx
index 6a88da3..86393ac 100644
--- a/app/addons/documents/index-results/index-results.components.react.jsx
+++ b/app/addons/documents/index-results/index-results.components.react.jsx
@@ -133,13 +133,13 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, Documents, Fauxto
     },
 
     getAdditionalInfoRow: function (el) {
-      var attachmentCount = Object.keys(el._attachments || {}).length;
-      var attachmentIndicator = null;
-      var textAttachments = null;
+      const attachmentCount = Object.keys(el._attachments || {}).length;
+      let attachmentIndicator = null;
+      let textAttachments = null;
 
-      var conflictCount = Object.keys(el._conflicts || {}).length;
-      var conflictIndicator = null;
-      var textConflicts = null;
+      const conflictCount = Object.keys(el._conflicts || {}).length;
+      let conflictIndicator = null;
+      let textConflicts = null;
 
 
       if (attachmentCount) {

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/app/addons/documents/rev-browser/rev-browser.actions.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/rev-browser/rev-browser.actions.js b/app/addons/documents/rev-browser/rev-browser.actions.js
new file mode 100644
index 0000000..29c9b2b
--- /dev/null
+++ b/app/addons/documents/rev-browser/rev-browser.actions.js
@@ -0,0 +1,168 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+/* global FormData */
+
+define([
+  '../../../app',
+  '../../../core/api',
+  './rev-browser.actiontypes',
+  'visualizeRevTree/lib/getTree',
+  'pouchdb'
+],
+(app, FauxtonAPI, ActionTypes, getTree, PouchDB) => {
+
+  let db;
+
+  function initDiffEditor (dbName, docId) {
+    const url = FauxtonAPI.urls('databaseBaseURL', 'server', dbName);
+    db = PouchDB(url);
+
+    // XXX: we need spec compliant promise support and get rid of jQ "deferreds"
+    const d1 = $.Deferred();
+    const d2 = $.Deferred();
+    $.when(d1, d2).done((tree, doc) => {
+      const conflictingRevs = getConflictingRevs(tree.paths, tree.winner, Object.keys(tree.deleted));
+      const initialRev = conflictingRevs[0];
+
+      if (!initialRev) {
+        return dispatchData(tree, doc, conflictingRevs, null, dbName);
+      }
+
+      db.get(doc._id, {rev: initialRev})
+        .then((conflictDoc) => {
+          dispatchData(tree, doc, conflictingRevs, conflictDoc, dbName);
+        });
+    });
+
+    db.get(docId)
+      .then(d2.resolve);
+
+    getTree(db, docId)
+      .then(d1.resolve);
+  }
+
+  function getConflictingRevs (paths, winner, deleted) {
+
+    return paths.reduce((acc, el) => {
+      if (el[0] !== winner) {
+        acc.push(el[0]);
+      }
+
+      return acc;
+    }, [])
+    .filter((el) => {
+      return deleted.indexOf(el) === -1;
+    });
+  }
+
+  function dispatchData (tree, doc, conflictingRevs, conflictDoc, databaseName) {
+    FauxtonAPI.dispatch({
+      type: ActionTypes.REV_BROWSER_REV_TREE_LOADED,
+      options: {
+        tree: tree,
+        doc: doc,
+        conflictDoc: conflictDoc,
+        conflictingRevs: conflictingRevs,
+        databaseName: databaseName
+      }
+    });
+  }
+
+  function toggleDiffView (enableDiff) {
+    FauxtonAPI.dispatch({
+      type: ActionTypes.REV_BROWSER_DIFF_ENABLE_DIFF_VIEW,
+      options: {
+        enableDiff: enableDiff
+      }
+    });
+  }
+
+  function chooseLeaves (doc, revTheirs) {
+    db.get(doc._id, {rev: revTheirs})
+      .then((res) => {
+        dispatchDocsToDiff(doc, res);
+      });
+  }
+
+  function dispatchDocsToDiff (doc, theirs) {
+    FauxtonAPI.dispatch({
+      type: ActionTypes.REV_BROWSER_DIFF_DOCS_READY,
+      options: {
+        theirs: theirs,
+        ours: doc
+      }
+    });
+  }
+
+  function showConfirmModal (show, docToWin) {
+    FauxtonAPI.dispatch({
+      type: ActionTypes.REV_BROWSER_SHOW_CONFIRM_MODAL,
+      options: {
+        show: show,
+        docToWin: docToWin
+      }
+    });
+  }
+
+  function selectRevAsWinner (databaseName, docId, paths, revToWin) {
+    const revsToDelete = getConflictingRevs(paths, revToWin, []);
+    const payload = buildBulkDeletePayload(docId, revsToDelete);
+
+    $.ajax({
+      url: FauxtonAPI.urls('bulk_docs', 'server', databaseName, ''),
+      type: 'POST',
+      contentType: 'application/json; charset=UTF-8',
+      data: JSON.stringify(payload),
+      success: () => {
+        FauxtonAPI.addNotification({
+          msg: 'Conflicts successfully solved.',
+          clear: true
+        });
+        showConfirmModal(false, null);
+        FauxtonAPI.navigate(FauxtonAPI.urls('allDocs', 'app', databaseName, ''));
+      },
+      error: (resp) => {
+        FauxtonAPI.addNotification({
+          msg: 'Failed to delete clean up conflicts!',
+          type: 'error',
+          clear: true
+        });
+      }
+    });
+  }
+
+  function buildBulkDeletePayload (docId, revs) {
+    const list = revs.map((rev) => {
+      return {
+        "_id": docId,
+        "_rev": rev,
+        "_deleted": true
+      };
+    });
+
+    return { "docs": list };
+  }
+
+  return {
+    getConflictingRevs: getConflictingRevs,
+    selectRevAsWinner: selectRevAsWinner,
+    buildBulkDeletePayload: buildBulkDeletePayload,
+    chooseLeaves: chooseLeaves,
+    dispatchDocsToDiff: dispatchDocsToDiff,
+    initDiffEditor: initDiffEditor,
+    dispatchData: dispatchData,
+    toggleDiffView: toggleDiffView,
+    showConfirmModal: showConfirmModal
+  };
+
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/app/addons/documents/rev-browser/rev-browser.actiontypes.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/rev-browser/rev-browser.actiontypes.js b/app/addons/documents/rev-browser/rev-browser.actiontypes.js
new file mode 100644
index 0000000..ddbba16
--- /dev/null
+++ b/app/addons/documents/rev-browser/rev-browser.actiontypes.js
@@ -0,0 +1,20 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+define([], () => {
+  return {
+    REV_BROWSER_REV_TREE_LOADED: 'REV_TREE_LOADED',
+    REV_BROWSER_DIFF_DOCS_READY: 'REV_BROWSER_DIFF_DOCS_READY',
+    REV_BROWSER_DIFF_ENABLE_DIFF_VIEW: 'REV_BROWSER_DIFF_ENABLE_DIFF_VIEW',
+    REV_BROWSER_SHOW_CONFIRM_MODAL: 'REV_BROWSER_SHOW_CONFIRM_MODAL'
+  };
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/app/addons/documents/rev-browser/rev-browser.components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/documents/rev-browser/rev-browser.components.react.jsx b/app/addons/documents/rev-browser/rev-browser.components.react.jsx
new file mode 100644
index 0000000..66e68a1
--- /dev/null
+++ b/app/addons/documents/rev-browser/rev-browser.components.react.jsx
@@ -0,0 +1,442 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+
+define([
+  '../../../core/api',
+  '../../../app',
+  'react',
+  'react-dom',
+  './rev-browser.actions',
+  './rev-browser.stores',
+  '../../components/react-components.react',
+
+  'react-bootstrap',
+  'react-select',
+  'jsondiffpatch',
+  'jsondiffpatch/src/formatters/html',
+
+  'brace',
+
+  'react-select/less/default.less',
+  'jsondiffpatch/public/formatters-styles/html.css'
+], (FauxtonAPI, app, React, ReactDOM, RevActions, RevStores, ReactComponents,
+    ReactBootstrap, ReactSelect, jdp, jdpformatters, ace) => {
+
+  const storageKeyDeleteConflictsModal = 'deleteConflictsHideModal';
+
+  const store = RevStores.revBrowserStore;
+  const ConfirmButton = ReactComponents.ConfirmButton;
+
+  const ButtonGroup = ReactBootstrap.ButtonGroup;
+  const Button = ReactBootstrap.Button;
+  const Modal = ReactBootstrap.Modal;
+
+  require('brace/ext/static_highlight');
+  const highlight = ace.acequire('ace/ext/static_highlight');
+
+  require('brace/mode/json');
+  const JavaScriptMode = ace.acequire('ace/mode/json').Mode;
+
+  require('brace/theme/idle_fingers');
+  const theme = ace.acequire('ace/theme/idle_fingers');
+
+
+  class DiffyController extends React.Component {
+
+    constructor (props) {
+      super(props);
+
+      this.state = this.getStoreState();
+    }
+
+    getStoreState () {
+
+      return {
+        tree: store.getRevTree(),
+        ours: store.getOurs(),
+        theirs: store.getTheirs(),
+        conflictingRevs: store.getConflictingRevs(),
+        dropdownData: store.getDropdownData(),
+        isDiffViewEnabled: store.getIsDiffViewEnabled(),
+        databaseName: store.getDatabaseName()
+      };
+    }
+
+    componentDidMount () {
+      store.on('change', this.onChange, this);
+    }
+
+    componentWillUnmount () {
+      store.off('change', this.onChange);
+    }
+
+    onChange () {
+      this.setState(this.getStoreState());
+    }
+
+    toggleDiffView (enableDiff) {
+      RevActions.toggleDiffView(enableDiff);
+    }
+
+    render () {
+      const {tree, ours, theirs, dropdownData, conflictingRevs, isDiffViewEnabled} = this.state;
+
+      if (!tree) {
+        return null;
+      }
+
+      // no conflicts happened for this doc
+      if (!theirs || !conflictingRevs.length) {
+        return <div style={{textAlign: 'center', color: '#fff'}}><h2>No conflicts</h2></div>;
+      }
+
+      return (
+        <div className="revision-wrapper scrollable">
+          <RevisionBrowserControls {...this.state} />
+          <div className="revision-view-controls">
+            <ButtonGroup className="two-sides-toggle-button">
+              <Button
+                style={{width: '120px'}}
+                className={isDiffViewEnabled ? 'active' : ''}
+                onClick={this.toggleDiffView.bind(this, true)}
+              >
+                <i className="icon-columns" /> Diff
+              </Button>
+              <Button
+                style={{width: '120px'}}
+                className={isDiffViewEnabled ? '' : 'active'}
+                onClick={this.toggleDiffView.bind(this, false)}
+              >
+                <i className="icon-file-text" /> Document
+              </Button>
+            </ButtonGroup>
+          </div>
+
+          {isDiffViewEnabled ?
+            <RevisionDiffArea ours={ours} theirs={theirs} /> :
+            <SplitScreenArea ours={ours} theirs={theirs} /> }
+        </div>
+      );
+    }
+  };
+
+
+  class SplitScreenArea extends React.Component {
+
+    constructor (props) {
+      super(props);
+    }
+
+    componentDidUpdate () {
+      this.hightlightAfterRender();
+    }
+
+    componentDidMount () {
+      this.hightlightAfterRender();
+    }
+
+    hightlightAfterRender () {
+      const format = (input) => { return JSON.stringify(input, null, '  '); };
+
+      const jsmode = new JavaScriptMode();
+      const left = ReactDOM.findDOMNode(this.refs.revLeftOurs);
+      const right = ReactDOM.findDOMNode(this.refs.revRightTheirs);
+
+      const leftRes = highlight.render(format(this.props.ours), jsmode, theme, 0, true);
+      left.innerHTML = leftRes.html;
+      const rightRes = highlight.render(format(this.props.theirs), jsmode, theme, 0, true);
+      right.innerHTML = rightRes.html;
+    }
+
+    render () {
+      const {ours, theirs} = this.props;
+
+      if (!ours || !theirs) {
+        return <div></div>;
+      }
+
+      return (
+        <div className="revision-split-area">
+          <div data-id="ours" style={{width: '50%'}}>
+            <div style={{marginBottom: '20px'}}>{ours._rev} (Server-Selected Rev)</div>
+            <pre ref="revLeftOurs"></pre>
+          </div>
+
+          <div data-id="theirs" style={{width: '50%'}}>
+            <div style={{marginBottom: '20px'}}>{theirs._rev}</div>
+            <pre ref="revRightTheirs"></pre>
+          </div>
+        </div>
+      );
+    }
+  }
+
+  const RevisionDiffArea = ({ours, theirs}) => {
+    if (!ours || !theirs) {
+      return <div></div>;
+    }
+
+    const delta = jdp.diff(ours, theirs);
+    const html = jdpformatters.format(delta, ours);
+
+    return (
+      <div className="revision-diff-area">
+        <div
+          style={{marginTop: '30px'}}
+          dangerouslySetInnerHTML={{__html: html}}
+        >
+        </div>
+      </div>
+    );
+  };
+  RevisionDiffArea.propTypes = {
+    ours: React.PropTypes.object,
+    theirs: React.PropTypes.object,
+    currentRev: React.PropTypes.string
+  };
+
+
+  const ConflictingRevisionsDropDown = ({options, selected, onRevisionClick, onBackwardClick, onForwardClick}) => {
+    return (
+      <div className="conflicting-revs-dropdown">
+        <BackForwardControls backward onClick={onBackwardClick} />
+        <div style={{width: '345px', margin: '0 5px'}}>
+          <ReactSelect
+            name="form-field-name"
+            value={selected}
+            options={options}
+            clearable={false}
+            onChange={onRevisionClick} />
+        </div>
+        <BackForwardControls forward onClick={onForwardClick} />
+      </div>
+    );
+  };
+  ConflictingRevisionsDropDown.propTypes = {
+    options: React.PropTypes.array.isRequired,
+    selected: React.PropTypes.string.isRequired,
+    onRevisionClick: React.PropTypes.func.isRequired,
+    onBackwardClick: React.PropTypes.func.isRequired,
+    onForwardClick: React.PropTypes.func.isRequired,
+  };
+
+  class RevisionBrowserControls extends React.Component {
+
+    constructor (props) {
+      super(props);
+
+      this.state = {showModal: false};
+    }
+
+    onRevisionClick (revTheirs) {
+
+      RevActions.chooseLeaves(this.props.ours, revTheirs.value);
+    }
+
+    onForwardClick () {
+      const conflictingRevs = this.props.conflictingRevs;
+      const index = conflictingRevs.indexOf(this.props.theirs._rev);
+
+      const next = conflictingRevs[index + 1];
+
+      if (!next) {
+        return;
+      }
+
+      RevActions.chooseLeaves(this.props.ours, next);
+    }
+
+    onBackwardClick () {
+      const conflictingRevs = this.props.conflictingRevs;
+      const index = conflictingRevs.indexOf(this.props.theirs._rev);
+
+      const next = conflictingRevs[index - 1];
+
+      if (!next) {
+        return;
+      }
+
+      RevActions.chooseLeaves(this.props.ours, next);
+    }
+
+    selectAsWinner (docToWin, doNotShowModalAgain) {
+      if (doNotShowModalAgain) {
+        app.utils.localStorageSet(storageKeyDeleteConflictsModal, true);
+      }
+
+      RevActions.selectRevAsWinner(this.props.databaseName, docToWin._id, this.props.tree.paths, docToWin._rev);
+    }
+
+    onSelectAsWinnerClick (docToWin) {
+      if (app.utils.localStorageGet(storageKeyDeleteConflictsModal) !== true) {
+        RevActions.showConfirmModal(true, docToWin);
+        return;
+      }
+
+      this.selectAsWinner(docToWin);
+    }
+
+    render () {
+      const {tree, conflictingRevs} = this.props;
+      const cellStyle = {paddingRight: '30px'};
+
+      return (
+        <div className="revision-browser-controls">
+          <ConfirmModal onConfirm={this.selectAsWinner.bind(this)} />
+          <table style={{margin: '10px 60px', width: '100%'}}>
+            <tbody>
+              <tr style={{height: '60px'}}>
+                <td style={cellStyle}>Server-Selected Rev: </td>
+                <td style={cellStyle}>
+                  <div style={{lineHeight: '36px', height: '36px', width: '337px', color: '#000', backgroundColor: '#ffbbbb'}}>
+                    <b style={{paddingLeft: '10px'}}>{tree.winner}</b>
+                  </div>
+                </td>
+                <td>
+                  <ConfirmButton
+                    onClick={this.onSelectAsWinnerClick.bind(this, this.props.ours)}
+                    style={{marginRight: '10px', width: '220px'}}
+                    text="Delete Other Conflicts"
+                    buttonType="btn-info"
+                    customIcon="icon-trophy" />
+                </td>
+              </tr>
+              <tr style={{height: '60px'}}>
+                <td style={cellStyle}>Conflicting Revisions: </td>
+                <td style={cellStyle}>
+                  <ConflictingRevisionsDropDown
+                    onRevisionClick={this.onRevisionClick.bind(this)}
+                    onForwardClick={this.onForwardClick.bind(this)}
+                    onBackwardClick={this.onBackwardClick.bind(this)}
+                    options={this.props.dropdownData}
+                    selected={this.props.theirs._rev} />
+                </td>
+                <td>
+                  <ConfirmButton
+                    data-id="button-select-theirs"
+                    onClick={this.onSelectAsWinnerClick.bind(this, this.props.theirs)}
+                    style={{marginRight: '10px', width: '220px'}}
+                    text="Select as Winner"
+                    buttonType="btn-info"
+                    customIcon="icon-trophy" />
+                </td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+
+      );
+    }
+  }
+  RevisionBrowserControls.propTypes = {
+    tree: React.PropTypes.object.isRequired,
+    ours: React.PropTypes.object.isRequired,
+    conflictingRevs: React.PropTypes.array.isRequired,
+  };
+
+  class ConfirmModal extends React.Component {
+
+    constructor (props) {
+      super(props);
+
+      this.state = this.getStoreState();
+    }
+
+    getStoreState () {
+      return {
+        show: store.getShowConfirmModal(),
+        docToWin: store.getDocToWin(),
+        checked: false
+      };
+    }
+
+    componentDidMount () {
+      store.on('change', this.onChange, this);
+    }
+
+    componentWillUnmount () {
+      store.off('change', this.onChange);
+    }
+
+    onChange () {
+      this.setState(this.getStoreState());
+    }
+
+    close () {
+      RevActions.showConfirmModal(false, null);
+    }
+
+    onDeleteConflicts () {
+      const hideModal = this.state.checked;
+      this.props.onConfirm(this.state.docToWin, hideModal);
+    }
+
+    render () {
+      return (
+        <Modal dialogClassName="delete-conflicts-modal" show={this.state.show} onHide={this.close}>
+          <Modal.Header closeButton={false}>
+            <Modal.Title>Solve Conflicts</Modal.Title>
+          </Modal.Header>
+          <Modal.Body>
+            <p>
+            <i className="icon-warning-sign"></i> Do you want to delete all conflicting revisions for this document?
+            </p>
+
+
+          </Modal.Body>
+          <Modal.Footer>
+            <div style={{float: 'left', marginTop: '10px'}}>
+              <label>
+                <input
+                  style={{margin: '0 5px 3px 0'}}
+                  onChange={() => { this.setState({checked: !this.state.checked }); }}
+                  type="checkbox" />
+                  Do not show this warning message again
+              </label>
+            </div>
+            <a
+              style={{marginRight: '10px', cursor: 'pointer'}}
+              onClick={this.close}
+              data-bypass="true"
+            >
+              Cancel
+            </a>
+
+            <ConfirmButton
+              onClick={this.onDeleteConflicts.bind(this)}
+              text="Delete Revisions"
+              buttonType="btn-danger" />
+          </Modal.Footer>
+        </Modal>
+      );
+    }
+  };
+  ConfirmModal.propTypes = {
+    onConfirm: React.PropTypes.func.isRequired,
+  };
+
+  const BackForwardControls = ({onClick, forward, backward}) => {
+    const icon = forward ? 'fonticon-right-open' : 'fonticon-left-open';
+    const style = {height: '20px', width: '11px', marginTop: '7px'};
+
+    return <div style={style} className={icon} onClick={onClick}></div>;
+  };
+  BackForwardControls.propTypes = {
+    onClick: React.PropTypes.func.isRequired,
+  };
+
+  return {
+    DiffyController: DiffyController
+  };
+
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/app/addons/documents/rev-browser/rev-browser.stores.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/rev-browser/rev-browser.stores.js b/app/addons/documents/rev-browser/rev-browser.stores.js
new file mode 100644
index 0000000..28a6dfd
--- /dev/null
+++ b/app/addons/documents/rev-browser/rev-browser.stores.js
@@ -0,0 +1,123 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+define([
+  '../../../core/api',
+  './rev-browser.actiontypes'
+], (FauxtonAPI, ActionTypes) => {
+
+  const Stores = {};
+
+  Stores.RevBrowserStore = FauxtonAPI.Store.extend({
+    initialize: function () {
+      this.reset();
+    },
+
+    reset: function () {
+      this._revTree = null;
+
+      this._ours = null;
+      this._theirs = null;
+
+      this._dropDownData = null;
+      this._isDiffViewEnabled = true;
+
+      this._databaseName = null;
+
+      this._showConfirmModal = false;
+      this._docToWin = null;
+    },
+
+    prepareDropdownData: function (revs) {
+      return revs.map((el) => {
+
+        return { value: el, label: el };
+      });
+    },
+
+    getRevTree: function () {
+      return this._revTree;
+    },
+
+    getDatabaseName: function () {
+      return this._databaseName;
+    },
+
+    getOurs: function () {
+      return this._ours;
+    },
+
+    getTheirs: function () {
+      return this._theirs;
+    },
+
+    getConflictingRevs: function () {
+      return this._conflictingRevs;
+    },
+
+    getDropdownData: function () {
+      return this._dropDownData;
+    },
+
+    getIsDiffViewEnabled: function () {
+      return this._isDiffViewEnabled;
+    },
+
+    getShowConfirmModal: function () {
+      return this._showConfirmModal;
+    },
+
+    getDocToWin: function () {
+      return this._docToWin;
+    },
+
+    dispatch: function (action) {
+      switch (action.type) {
+        case ActionTypes.REV_BROWSER_REV_TREE_LOADED:
+          this._revTree = action.options.tree;
+          this._ours = action.options.doc;
+          this._conflictingRevs = action.options.conflictingRevs;
+          this._theirs = action.options.conflictDoc;
+
+          this._dropDownData = this.prepareDropdownData(this._conflictingRevs);
+
+          this._databaseName = action.options.databaseName;
+        break;
+
+        case ActionTypes.REV_BROWSER_DIFF_DOCS_READY:
+          this._theirs = action.options.theirs;
+        break;
+
+        case ActionTypes.REV_BROWSER_DIFF_ENABLE_DIFF_VIEW:
+          this._isDiffViewEnabled = action.options.enableDiff;
+        break;
+
+        case ActionTypes.REV_BROWSER_SHOW_CONFIRM_MODAL:
+          this._showConfirmModal = action.options.show;
+          this._docToWin = action.options.docToWin;
+        break;
+
+        default:
+        return;
+        // do nothing
+      }
+
+      this.triggerChange();
+    }
+
+  });
+
+  Stores.revBrowserStore = new Stores.RevBrowserStore();
+  Stores.revBrowserStore.dispatchToken = FauxtonAPI.dispatcher.register(Stores.revBrowserStore.dispatch);
+
+  return Stores;
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/app/addons/documents/rev-browser/tests/fixtures.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/rev-browser/tests/fixtures.js b/app/addons/documents/rev-browser/tests/fixtures.js
new file mode 100644
index 0000000..f8f2e3a
--- /dev/null
+++ b/app/addons/documents/rev-browser/tests/fixtures.js
@@ -0,0 +1,72 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+define([], () => {
+
+  const twoPaths = {
+    "paths": [
+      [
+        "4-2868f2429e2211f74e656663f39b0cb8",
+        "3-b1a15f62533e8d3344504855c7c006f7",
+        "2-3016a16f8d02b6062c0f85af048974df",
+        "1-a2701a97f75439f13e9062ad8a9e2b9c"
+      ],
+      [
+        "6-9831e318304c35efafa6faa57a54809f",
+        "5-8eadb1a781b835cce132a339250bba53",
+        "4-3c1720cc9f559444f7e717a070f8eaec",
+        "3-b1a15f62533e8d3344504855c7c006f7",
+        "2-3016a16f8d02b6062c0f85af048974df",
+        "1-a2701a97f75439f13e9062ad8a9e2b9c"
+      ]
+    ],
+    "deleted": {},
+    "winner": "6-9831e318304c35efafa6faa57a54809f"
+  };
+
+  const threePaths = {
+    "paths": [
+      [
+        "5-5555f2429e2211f74e656663f39b0cb8",
+        "4-2868f2429e2211f74e656663f39b0cb8",
+        "3-b1a15f62533e8d3344504855c7c006f7",
+        "2-3016a16f8d02b6062c0f85af048974df",
+        "1-a2701a97f75439f13e9062ad8a9e2b9c"
+      ],
+      [
+        "7-1309b41d34787f7ba95280802f327dc2",
+        "6-9831e318304c35efafa6faa57a54809f",
+        "5-8eadb1a781b835cce132a339250bba53",
+        "4-3c1720cc9f559444f7e717a070f8eaec",
+        "3-b1a15f62533e8d3344504855c7c006f7",
+        "2-3016a16f8d02b6062c0f85af048974df",
+        "1-a2701a97f75439f13e9062ad8a9e2b9c"
+      ],
+      [
+        "7-1f1bb5806f33c8922277ea053d6fc4ed",
+        "6-9831e318304c35efafa6faa57a54809f",
+        "5-8eadb1a781b835cce132a339250bba53",
+        "4-3c1720cc9f559444f7e717a070f8eaec",
+        "3-b1a15f62533e8d3344504855c7c006f7",
+        "2-3016a16f8d02b6062c0f85af048974df",
+        "1-a2701a97f75439f13e9062ad8a9e2b9c"
+      ]
+    ],
+    "deleted": {},
+    "winner": "7-1f1bb5806f33c8922277ea053d6fc4ed"
+  };
+
+  return {
+    twoPaths: twoPaths,
+    threePaths: threePaths
+  };
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/app/addons/documents/rev-browser/tests/rev-browser.actionsSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/rev-browser/tests/rev-browser.actionsSpec.js b/app/addons/documents/rev-browser/tests/rev-browser.actionsSpec.js
new file mode 100644
index 0000000..f43962e
--- /dev/null
+++ b/app/addons/documents/rev-browser/tests/rev-browser.actionsSpec.js
@@ -0,0 +1,94 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+define([
+  '../../../../core/api',
+  '../rev-browser.actions',
+  './fixtures',
+
+  '../../../../../test/mocha/testUtils'
+], (FauxtonAPI, RevActions, fixtures, utils) => {
+
+  const assert = utils.assert;
+
+  describe('RevActions', () => {
+
+
+    it('getConflictingRevs gets the revisions which are obsolete, winner', () => {
+
+      const res = RevActions.getConflictingRevs(
+        fixtures.threePaths.paths,
+        "7-1f1bb5806f33c8922277ea053d6fc4ed",
+        Object.keys({})
+      );
+
+      const expected = [
+        "5-5555f2429e2211f74e656663f39b0cb8",
+        "7-1309b41d34787f7ba95280802f327dc2"
+      ];
+
+      assert.deepEqual(expected, res);
+    });
+
+    it('getConflictingRevs gets the revisions which are obsolete, sidetrack with a lot lower rev', () => {
+
+      const res = RevActions.getConflictingRevs(
+        fixtures.threePaths.paths,
+        "5-5555f2429e2211f74e656663f39b0cb8",
+        Object.keys({})
+      );
+
+      const expected = [
+        "7-1309b41d34787f7ba95280802f327dc2",
+        "7-1f1bb5806f33c8922277ea053d6fc4ed"
+      ];
+
+      assert.deepEqual(expected, res);
+    });
+
+    it('getConflictingRevs filters out deleted revisions', () => {
+
+      const res = RevActions.getConflictingRevs(
+        fixtures.threePaths.paths,
+        "5-5555f2429e2211f74e656663f39b0cb8",
+        Object.keys({ '7-1f1bb5806f33c8922277ea053d6fc4ed': true })
+      );
+
+      const expected = [
+        "7-1309b41d34787f7ba95280802f327dc2"
+      ];
+
+      assert.deepEqual(expected, res);
+    });
+
+    it('buildBulkDeletePayload prepares the payload for bulkdocs', () => {
+
+      const data = [
+        "7-1309b41d34787f7ba95280802f327dc2",
+        "6-9831e318304c35efafa6faa57a54809f",
+        "5-8eadb1a781b835cce132a339250bba53",
+        "4-3c1720cc9f559444f7e717a070f8eaec",
+        "7-1f1bb5806f33c8922277ea053d6fc4ed"
+      ];
+
+      const res = RevActions.buildBulkDeletePayload('fooId', data);
+
+      assert.deepEqual([
+        { "_id": "fooId", "_rev": "7-1309b41d34787f7ba95280802f327dc2", "_deleted": true },
+        { "_id": "fooId", "_rev": "6-9831e318304c35efafa6faa57a54809f", "_deleted": true },
+        { "_id": "fooId", "_rev": "5-8eadb1a781b835cce132a339250bba53", "_deleted": true },
+        { "_id": "fooId", "_rev": "4-3c1720cc9f559444f7e717a070f8eaec", "_deleted": true },
+        { "_id": "fooId", "_rev": "7-1f1bb5806f33c8922277ea053d6fc4ed", "_deleted": true },
+      ], res.docs);
+    });
+  });
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/app/addons/documents/routes-doc-editor.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/routes-doc-editor.js b/app/addons/documents/routes-doc-editor.js
index e04d57f..0702a40 100644
--- a/app/addons/documents/routes-doc-editor.js
+++ b/app/addons/documents/routes-doc-editor.js
@@ -17,13 +17,57 @@ define([
   './resources',
   '../databases/base',
   './doc-editor/actions',
-  './doc-editor/components.react'
+  './doc-editor/components.react',
+
+  './rev-browser/rev-browser.actions',
+  './rev-browser/rev-browser.components.react'
 ],
 
-function (app, FauxtonAPI, Helpers, Documents, Databases, Actions, ReactComponents) {
+(app, FauxtonAPI, Helpers, Documents, Databases, Actions, ReactComponents,
+RevBrowserActions, RevBrowserComponents) => {
+
+
+  const RevBrowserRouteObject = FauxtonAPI.RouteObject.extend({
+    layout: 'doc_editor',
+    disableLoader: true,
+    selectedHeader: 'Databases',
+    roles: ['fx_loggedIn'],
+
+    routes: {
+      'database/:database/:doc/conflicts': 'revisionBrowser'
+    },
+
+    initialize: function (route, masterLayout, options) {
+      const databaseName = options[0];
+
+      this.docId = options[1];
+      this.database = this.database || new Databases.Model({ id: databaseName });
+      this.doc = new Documents.Doc({ _id: this.docId }, { database: this.database });
+    },
 
+    crumbs: function () {
+      const previousPage = Helpers.getPreviousPageForDoc(this.database, this.wasCloned);
+      const docUrl = FauxtonAPI.urls('document', 'app', this.database.safeID(), this.docId);
+
+      return [
+        { type: 'back', link: previousPage },
+        { name: this.docId + ' > Conflicts', link: '#' }
+      ];
+    },
+
+    apiUrl: function () {
+      return [this.doc.url('apiurl'), this.doc.documentation()];
+    },
+
+    revisionBrowser: function (databaseName, docId) {
+      RevBrowserActions.showConfirmModal(false, null);
+      RevBrowserActions.initDiffEditor(databaseName, docId);
+      this.setComponent('#dashboard-content', RevBrowserComponents.DiffyController);
+    }
+
+  });
 
-  var DocEditorRouteObject = FauxtonAPI.RouteObject.extend({
+  const DocEditorRouteObject = FauxtonAPI.RouteObject.extend({
     layout: 'doc_editor',
     disableLoader: true,
     selectedHeader: 'Databases',
@@ -32,17 +76,17 @@ function (app, FauxtonAPI, Helpers, Documents, Databases, Actions, ReactComponen
 
     initialize: function (route, masterLayout, options) {
       this.databaseName = options[0];
-      this.docID = options[1] || 'new';
+      this.docId = options[1];
       this.database = this.database || new Databases.Model({ id: this.databaseName });
-      this.doc = new Documents.Doc({ _id: this.docID }, { database: this.database });
-      this.isNewDoc = false;
+      this.doc = new Documents.NewDoc(null, { database: this.database });
       this.wasCloned = false;
     },
 
     routes: {
       'database/:database/:doc/code_editor': 'codeEditor',
+      'database/:database/_design/:ddoc': 'showDesignDoc',
       'database/:database/:doc': 'codeEditor',
-      'database/:database/_design/:ddoc': 'showDesignDoc'
+      'database/:database/new': 'codeEditor'
     },
 
     events: {
@@ -50,29 +94,34 @@ function (app, FauxtonAPI, Helpers, Documents, Databases, Actions, ReactComponen
     },
 
     crumbs: function () {
-      var previousPage = Helpers.getPreviousPageForDoc(this.database, this.wasCloned);
 
+      if (this.docId) {
+        let previousPage = Helpers.getPreviousPageForDoc(this.database, this.wasCloned);
+
+        return [
+          { type: 'back', link: previousPage },
+          { name: this.docId, link: '#' }
+        ];
+      }
+
+      let previousPage = Helpers.getPreviousPageForDoc(this.database);
       return [
         { type: 'back', link: previousPage },
-        { name: this.docID, link: '#' }
+        { name: 'New Document', link: '#' }
       ];
     },
 
-    codeEditor: function (database, doc) {
+    codeEditor: function (databaseName, docId) {
+      this.database = new Databases.Model({ id: databaseName });
 
-      // if either the database or document just changed, we need to get the latest doc/db info
-      if (this.databaseName !== database) {
-        this.databaseName = database;
-        this.database = new Databases.Model({ id: this.databaseName });
+      if (docId) {
+        this.doc = new Documents.Doc({ _id: docId }, { database: this.database, fetchConflicts: true });
       }
-      if (this.docID !== doc) {
-        this.docID = doc;
-        this.doc = new Documents.Doc({ _id: this.docID }, { database: this.database });
-       }
+
       Actions.initDocEditor({ doc: this.doc, database: this.database });
       this.setComponent('#dashboard-content', ReactComponents.DocEditorController, {
         database: this.database,
-        isNewDoc: this.isNewDoc,
+        isNewDoc: docId ? false : true,
         previousPage: '#/' + Helpers.getPreviousPageForDoc(this.database)
       });
     },
@@ -112,40 +161,9 @@ function (app, FauxtonAPI, Helpers, Documents, Databases, Actions, ReactComponen
   });
 
 
-  var NewDocEditorRouteObject = DocEditorRouteObject.extend({
-    initialize: function (route, masterLayout, options) {
-      var databaseName = options[0];
-      this.database = this.database || new Databases.Model({ id: databaseName });
-      this.doc = new Documents.NewDoc(null, {
-        database: this.database
-      });
-      this.isNewDoc = true;
-      this.docID = null;
-    },
-
-    apiUrl: function () {
-      return [this.doc.url('apiurl'), this.doc.documentation()];
-    },
-
-    crumbs: function () {
-      var previousPage = Helpers.getPreviousPageForDoc(this.database);
-      return [
-        { type: 'back', link: previousPage },
-        { name: 'New Document', link: '#' }
-      ];
-    },
-
-    routes: {
-      'database/:database/new': 'codeEditor'
-    },
-
-    selectedHeader: 'Databases'
-  });
-
-
   return {
-    NewDocEditorRouteObject: NewDocEditorRouteObject,
-    DocEditorRouteObject: DocEditorRouteObject
+    DocEditorRouteObject: DocEditorRouteObject,
+    RevBrowserRouteObject: RevBrowserRouteObject
   };
 
 });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/app/addons/documents/routes.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/routes.js b/app/addons/documents/routes.js
index 8eb8bff..f994780 100644
--- a/app/addons/documents/routes.js
+++ b/app/addons/documents/routes.js
@@ -22,7 +22,7 @@ define([
 function (Documents, DocumentsRouteObject, docEditor, IndexEditorRouteObject, Mango) {
   Documents.RouteObjects = [
     docEditor.DocEditorRouteObject,
-    docEditor.NewDocEditorRouteObject,
+    docEditor.RevBrowserRouteObject,
     DocumentsRouteObject,
     IndexEditorRouteObject,
     Mango.MangoIndexEditorAndQueryEditor

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/app/addons/documents/shared-resources.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/shared-resources.js b/app/addons/documents/shared-resources.js
index d603d53..0dc3346 100644
--- a/app/addons/documents/shared-resources.js
+++ b/app/addons/documents/shared-resources.js
@@ -38,7 +38,8 @@ define([
         id = '';
       }
 
-      return FauxtonAPI.urls('document', context, this.getDatabase().safeID(), id);
+      const query = this.fetchConflicts ? '?conflicts=true' : '';
+      return FauxtonAPI.urls('document', context, this.getDatabase().safeID(), id, query);
     },
 
     initialize: function (_attrs, options) {
@@ -47,6 +48,10 @@ define([
       } else if (options.database) {
         this.database = options.database;
       }
+
+      if (options.fetchConflicts) {
+        this.fetchConflicts = true;
+      }
     },
 
     // HACK: the doc needs to know about the database, but it may be

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/app/addons/documents/tests/nightwatch/revBrowser.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/tests/nightwatch/revBrowser.js b/app/addons/documents/tests/nightwatch/revBrowser.js
new file mode 100644
index 0000000..cd1f289
--- /dev/null
+++ b/app/addons/documents/tests/nightwatch/revBrowser.js
@@ -0,0 +1,59 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+module.exports = {
+  'is able to show two docs next to each other, and diff them' : function (client) {
+    /*jshint multistr: true */
+    const waitTime = client.globals.maxWaitTime;
+    const newDatabaseName = 'animaldb';
+    const baseUrl = client.globals.test_settings.launch_url;
+
+    client
+      .createAnimalDb()
+      .checkForDocumentCreated('zebra', null, newDatabaseName)
+
+      .loginToGUI()
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/zebra')
+
+      .clickWhenVisible('button.conflicts')
+
+      .waitForElementVisible('.revision-diff-area', waitTime, false)
+
+      .assert.containsText('.revision-diff-area', '"black & white"')
+      .assert.containsText('.revision-diff-area', '"white"')
+
+      .clickWhenVisible('.two-sides-toggle-button button:last-child')
+
+      .waitForElementVisible('.revision-split-area', waitTime, false)
+
+      .assert.containsText('.revision-split-area [data-id="ours"]', '"black & white"')
+      .assert.containsText('.revision-split-area [data-id="theirs"]', '"white"')
+
+
+      .clickWhenVisible('[data-id="button-select-theirs"]')
+      .clickWhenVisible('.modal-footer input[type="checkbox"]')
+      .clickWhenVisible('.modal-footer button.btn-danger')
+
+      .clickWhenVisible('[data-id="zebra"] a')
+
+      .waitForElementVisible('.panel-section', waitTime, false)
+      .assert.elementNotPresent('button.conflicts')
+
+      .url(baseUrl + '/#/database/' + newDatabaseName + '?include_docs=true&conflicts=true')
+
+      .getText('body', function (result) {
+        this.verify.ok(result.value.indexOf('"color": "white"') !== -1, 'check if doc version was promoted')
+      })
+
+    .end();
+  }
+};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/app/addons/documents/tests/nightwatch/tableViewConflicts.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/tests/nightwatch/tableViewConflicts.js b/app/addons/documents/tests/nightwatch/tableViewConflicts.js
index c0be7c3..b5740bf 100644
--- a/app/addons/documents/tests/nightwatch/tableViewConflicts.js
+++ b/app/addons/documents/tests/nightwatch/tableViewConflicts.js
@@ -13,9 +13,9 @@
 module.exports = {
 
   'Shows how many conflicts have appeared': function (client) {
-    var waitTime = client.globals.maxWaitTime,
-        newDatabaseName = client.globals.testDatabaseName,
-        baseUrl = client.globals.test_settings.launch_url;
+    const waitTime = client.globals.maxWaitTime;
+    const newDatabaseName = client.globals.testDatabaseName;
+    const baseUrl = client.globals.test_settings.launch_url;
 
     client
       .populateDatabaseWithConflicts(newDatabaseName)

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/assets/less/fauxton.less
----------------------------------------------------------------------
diff --git a/assets/less/fauxton.less b/assets/less/fauxton.less
index 3f971d7..ef6a79e 100644
--- a/assets/less/fauxton.less
+++ b/assets/less/fauxton.less
@@ -594,6 +594,20 @@ footer.pagination-footer {
   line-height: 30px;
 }
 
+.modal-footer {
+  background-color: transparent;
+  border-top: none;
+  color: #666;
+}
+
+.modal-footer a {
+  color: #666;
+}
+
+.modal-header {
+  border-bottom: 1px solid #666;
+}
+
 .simple-header {
   font-weight: 400;
   font-size: 15pt;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/assets/less/formstyles.less
----------------------------------------------------------------------
diff --git a/assets/less/formstyles.less b/assets/less/formstyles.less
index 4db0f85..c04ffb7 100644
--- a/assets/less/formstyles.less
+++ b/assets/less/formstyles.less
@@ -77,6 +77,21 @@ select {
 .btn-primary {
   background: @brandPrimary;
 }
+.btn.btn-danger {
+  background-color: #f00;
+  color: #fff;
+}
+.btn.btn-danger:hover {
+  background-color: #e73d34;
+  color: #fff;
+}
+.btn.btn-info, .btn-secondary {
+  background-color: #0082BF;
+  color: #fff;
+}
+.btn.btn-info:hover, .btn-secondary:hover {
+  background-color: #E73D34;
+}
 
 .btn-primary a:visited {
   color: #fff;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/bin/create-animal-db
----------------------------------------------------------------------
diff --git a/bin/create-animal-db b/bin/create-animal-db
new file mode 100755
index 0000000..bc81593
--- /dev/null
+++ b/bin/create-animal-db
@@ -0,0 +1,13 @@
+#!/usr/bin/env node
+
+// deletes the old animaldb, creates a new, fresh one,
+// with conflicts for the zebra doc
+
+
+const url = 'http://localhost:5984/';
+
+createAnimalDb = require('../test/create-animal-db.js');
+
+createAnimalDb(url, () => {
+  console.log('created :)');
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/package.json
----------------------------------------------------------------------
diff --git a/package.json b/package.json
index 5635f6e..48d5bc9 100644
--- a/package.json
+++ b/package.json
@@ -63,6 +63,7 @@
     "http-proxy": "^1.13.2",
     "imports-loader": "^0.6.5",
     "jquery": "^2.2.0",
+    "jsondiffpatch": "^0.1.41",
     "less": "^2.3.1",
     "less-loader": "^2.2.3",
     "lodash": "^3.10.1",
@@ -74,7 +75,7 @@
     "react": "^15.0.1",
     "react-addons-css-transition-group": "^15.0.1",
     "react-addons-test-utils": "^15.0.1",
-    "react-autocomplete": "^0.1.4",
+    "pouchdb": "^5.3.1",
     "react-bootstrap": "^0.28.5",
     "react-dom": "^15.0.1",
     "react-select": "^1.0.0-beta12",
@@ -87,6 +88,7 @@
     "url-loader": "^0.5.7",
     "urls": "~0.0.3",
     "velocity-animate": "^1.2.3",
+    "visualizeRevTree": "git+https://github.com/neojski/visualizeRevTree.git#gh-pages",
     "webpack": "^1.12.12",
     "webpack-dev-server": "^1.14.1",
     "zeroclipboard": "^2.2.0"
@@ -104,7 +106,8 @@
     "dev": "node ./devserver.js",
     "nightwatch": "grunt nightwatch",
     "start": "node ./bin/fauxton",
-    "prepublish": "node version-check.js && grunt release"
+    "prepublish": "node version-check.js && grunt release",
+    "create:animaldb": "./bin/create-animal-db"
   },
   "repository": {
     "type": "git",

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/test/animal-db.json
----------------------------------------------------------------------
diff --git a/test/animal-db.json b/test/animal-db.json
new file mode 100644
index 0000000..19e3ff5
--- /dev/null
+++ b/test/animal-db.json
@@ -0,0 +1,13 @@
+[
+{"_id":"aardvark","min_weight":40,"max_weight":65,"min_length":1,"max_length":2.2,"latin_name":"Orycteropus afer","wiki_page":"http://en.wikipedia.org/wiki/Aardvark","class":"mammal","diet":"omnivore"},
+{"_id":"badger","wiki_page":"http://en.wikipedia.org/wiki/Badger","min_weight":7,"max_weight":30,"min_length":0.6,"max_length":0.9,"latin_name":"Meles meles","class":"mammal","diet":"omnivore"},
+{"_id":"elephant","wiki_page":"http://en.wikipedia.org/wiki/African_elephant","min_weight":4700,"max_weight":6050,"min_length":3.2,"max_length":4,"class":"mammal","diet":"herbivore"},
+{"_id":"flamingo","min_weight":1,"min_length":1.2,"max_weight":2.8,"max_length":1.45,"wiki_page":"https://en.wikipedia.org/wiki/American_flamingo","class":"aves","diet":"omnivore"},
+{"_id":"giraffe","min_weight":830,"min_length":5,"max_weight":1600,"max_length":6,"wiki_page":"http://en.wikipedia.org/wiki/Giraffe","class":"mammal","diet":"herbivore"},
+{"_id":"kookaburra","min_length":0.28,"max_length":0.42,"wiki_page":"http://en.wikipedia.org/wiki/Kookaburra","class":"bird","diet":"carnivore","latin_name":"Dacelo novaeguineae"},
+{"_id":"lemur","wiki_page":"http://en.wikipedia.org/wiki/Ring-tailed_lemur","min_weight":2.2,"max_weight":2.2,"min_length":0.95,"max_length":1.1,"class":"mammal","diet":"omnivore"},
+{"_id":"llama","min_weight":130,"max_weight":200,"min_length":1.7,"max_length":1.8,"latin_name":"Lama glama","wiki_page":"http://en.wikipedia.org/wiki/Llama","class":"mammal","diet":"herbivore"},
+{"_id":"panda","wiki_page":"http://en.wikipedia.org/wiki/Panda","min_weight":75,"max_weight":115,"min_length":1.2,"max_length":1.8,"class":"mammal","diet":"carnivore"},
+{"_id":"snipe","min_weight":0.08,"max_weight":0.14,"min_length":0.25,"max_length":0.27,"latin_name":"Gallinago gallinago","wiki_page":"http://en.wikipedia.org/wiki/Common_Snipe","class":"bird","diet":"omnivore"},
+{"_id":"zebra","wiki_page":"http://en.wikipedia.org/wiki/Plains_zebra","min_length":2,"max_length":2.5,"min_weight":175,"max_weight":387,"class":"mammal","diet":"herbivore"}
+]

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/test/create-animal-db.js
----------------------------------------------------------------------
diff --git a/test/create-animal-db.js b/test/create-animal-db.js
new file mode 100644
index 0000000..65baf1a
--- /dev/null
+++ b/test/create-animal-db.js
@@ -0,0 +1,154 @@
+const request = require('request');
+const async = require('async');
+
+const animals = require('../test/animal-db.json');
+
+const conflictingDoc = 'zebra';
+
+module.exports = createAnimalDb;
+
+function createAnimalDb (url, cb) {
+
+  deleteDatabase('animaldb', () => {
+    createAnimalDb();
+  });
+
+
+  function deleteDatabase (db, cb) {
+    request({
+      uri: `${url}/${db}`,
+      method: 'DELETE',
+      json: true,
+      body: {}
+    }, (err, res, body) => {
+      if (err) {
+        throw err;
+      }
+
+      cb();
+    });
+  }
+
+  function createAnimalDb () {
+    request({
+      uri: `${url}/animaldb/`,
+      method: 'PUT',
+      json: true,
+      body: {}
+    }, (err, res, body) => {
+      if (err) {
+        throw err;
+      }
+
+      bulkLoadDocs();
+    });
+  }
+
+  function bulkLoadDocs () {
+    request({
+      uri: `${url}/animaldb/_bulk_docs`,
+      method: 'POST',
+      json: true,
+      body: {
+        docs: animals
+      }
+    }, (err, res, body) => {
+      if (err) {
+        throw err;
+      }
+
+      async.waterfall([
+        (cb) => {
+          replicate(`${url}/animaldb`, `${url}/animaldb-copy`, true, cb);
+        },
+        (cb) => {
+          replicate(`${url}/animaldb`, `${url}/animaldb-copy-2`, true, cb);
+        },
+        (cb) => {
+          alterDocs(cb);
+        },
+        (cb) => {
+          replicate(`${url}/animaldb-copy`, `${url}/animaldb`, false, cb);
+        },
+        (cb) => {
+          replicate(`${url}/animaldb-copy-2`, `${url}/animaldb`, false, cb);
+        },
+        (cb) => {
+          deleteDatabase('animaldb-copy', cb);
+        },
+        (cb) => {
+          deleteDatabase('animaldb-copy-2', cb);
+        },
+      ], (err, result) => {
+        cb();
+      });
+    });
+  }
+
+  function replicate (source, target, createTarget, cb) {
+    request({
+      uri: `${url}/_replicate`,
+      method: 'POST',
+      json: true,
+      body: {
+        source: source,
+        target: target,
+        create_target: createTarget
+      }
+    }, (err, res, body) => {
+      if (err) {
+        throw err;
+      }
+
+      cb(null);
+
+    });
+  }
+
+  function getRev (db, cb) {
+    request({
+      uri: `${url}/${db}/${conflictingDoc}`,
+      json: true
+    }, (err, res, body) => {
+      cb(null, body._rev);
+    });
+  }
+
+  function updateDoc (db, data, cb) {
+
+    getRev(db, (err, rev) => {
+      alterDoc(db, data, rev, cb);
+    });
+  }
+
+  function alterDoc (db, data, rev, cb) {
+    data._rev = rev;
+
+    request({
+      uri: `${url}/${db}/${conflictingDoc}`,
+      json: true,
+      method: 'PUT',
+      body: data
+    }, (err, res, body) => {
+      console.log(body);
+      cb(null);
+    });
+  }
+
+  function alterDocs (cb) {
+
+    updateDoc('animaldb', {
+      color: 'black & white'
+    }, () => {
+
+      updateDoc('animaldb-copy', {
+        color: 'white'
+      }, () => {
+        updateDoc('animaldb-copy-2', {
+          color: 'green'
+        }, cb);
+      });
+    });
+}
+
+}

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/test/nightwatch_tests/custom-commands/createAnimalDb.js
----------------------------------------------------------------------
diff --git a/test/nightwatch_tests/custom-commands/createAnimalDb.js b/test/nightwatch_tests/custom-commands/createAnimalDb.js
new file mode 100644
index 0000000..d21c255
--- /dev/null
+++ b/test/nightwatch_tests/custom-commands/createAnimalDb.js
@@ -0,0 +1,34 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+const util = require('util');
+const events = require('events');
+const helpers = require('../helpers/helpers.js');
+
+const createAnimalDbHelper = require('../../create-animal-db.js');
+function CreateAnimalDb () {
+  events.EventEmitter.call(this);
+}
+
+// inherit from node's event emitter
+util.inherits(CreateAnimalDb, events.EventEmitter);
+
+CreateAnimalDb.prototype.command = function (databaseName) {
+
+  createAnimalDbHelper(this.client.options.db_url, () => {
+    this.emit('complete');
+  });
+
+  return this;
+};
+
+module.exports = CreateAnimalDb;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3f46050d/test/nightwatch_tests/custom-commands/populateDatabaseWithConflicts.js
----------------------------------------------------------------------
diff --git a/test/nightwatch_tests/custom-commands/populateDatabaseWithConflicts.js b/test/nightwatch_tests/custom-commands/populateDatabaseWithConflicts.js
index b252d14..38efec8 100644
--- a/test/nightwatch_tests/custom-commands/populateDatabaseWithConflicts.js
+++ b/test/nightwatch_tests/custom-commands/populateDatabaseWithConflicts.js
@@ -10,10 +10,10 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-var util = require('util'),
-    events = require('events'),
-    helpers = require('../helpers/helpers.js'),
-    request = require('request');
+const util = require('util');
+const events = require('events');
+const helpers = require('../helpers/helpers.js');
+const request = require('request');
 
 function PopulateDatabaseWithConflicts () {
   events.EventEmitter.call(this);
@@ -22,8 +22,10 @@ function PopulateDatabaseWithConflicts () {
 util.inherits(PopulateDatabaseWithConflicts, events.EventEmitter);
 
 PopulateDatabaseWithConflicts.prototype.command = function (databaseName) {
-  var nano = helpers.getNanoInstance(),
-      database = nano.use(databaseName);
+  const nano = helpers.getNanoInstance(this.client.options.db_url);
+  const database = nano.use(databaseName);
+  const dbUrl = this.client.options.db_url;
+
 
   database.insert({
     hat: 'flamingo'
@@ -35,7 +37,7 @@ PopulateDatabaseWithConflicts.prototype.command = function (databaseName) {
 
   function createConflictingDoc (err, cb) {
     request({
-      uri: helpers.test_settings.db_url + '/' + databaseName + '/conflictingdoc',
+      uri: dbUrl + '/' + databaseName + '/conflictingdoc',
       method: 'PUT',
       json: true,
       body: {
@@ -49,7 +51,7 @@ PopulateDatabaseWithConflicts.prototype.command = function (databaseName) {
         );
       }
       request({
-        uri: helpers.test_settings.db_url + '/' + databaseName + '/conflictingdoc?new_edits=false',
+        uri: dbUrl + '/' + databaseName + '/conflictingdoc?new_edits=false',
         method: 'PUT',
         json: true,
         body: {


[2/2] fauxton commit: updated refs/heads/master to 3f46050

Posted by ro...@apache.org.
show conflicts / conflict-count in table-view

display a small indicator in table view, given a doc has conflicts

PR: #670
PR-URL: https://github.com/apache/couchdb-fauxton/pull/670
Reviewed-By: Benjamin Keen <be...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/319ce992
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/319ce992
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/319ce992

Branch: refs/heads/master
Commit: 319ce9924f60adad6f9e61af363a43f51ad70171
Parents: 978ae69
Author: Robert Kowalski <ro...@apache.org>
Authored: Tue Mar 15 14:37:37 2016 +0000
Committer: Robert Kowalski <ro...@apache.org>
Committed: Thu Apr 21 19:38:38 2016 +0200

----------------------------------------------------------------------
 .../documents/assets/less/index-results.less    |  5 +-
 app/addons/documents/header/header.actions.js   |  2 +
 .../index-results.components.react.jsx          | 37 +++++++---
 .../tests/nightwatch/tableViewConflicts.js      | 37 ++++++++++
 .../populateDatabaseWithConflicts.js            | 73 ++++++++++++++++++++
 5 files changed, 144 insertions(+), 10 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/319ce992/app/addons/documents/assets/less/index-results.less
----------------------------------------------------------------------
diff --git a/app/addons/documents/assets/less/index-results.less b/app/addons/documents/assets/less/index-results.less
index dc46c6d..df11f72 100644
--- a/app/addons/documents/assets/less/index-results.less
+++ b/app/addons/documents/assets/less/index-results.less
@@ -102,8 +102,11 @@
   .tableview-checkbox-cell input {
     margin: 0 0 0 8px;
   }
+  .tableview-conflict {
+    color: #FF0000;
+  }
   .tableview-el-last {
-    width: 50px;
+    width: 75px;
   }
   .tableview-el-copy {
     width: 35px;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/319ce992/app/addons/documents/header/header.actions.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/header/header.actions.js b/app/addons/documents/header/header.actions.js
index fcc7e7f..282d20b 100644
--- a/app/addons/documents/header/header.actions.js
+++ b/app/addons/documents/header/header.actions.js
@@ -26,8 +26,10 @@ function (app, FauxtonAPI, ActionTypes, ActionsQueryOptions) {
 
       if (state) {
         delete params.include_docs;
+        delete params.conflicts;
       } else {
         params.include_docs = true;
+        params.conflicts = true;
       }
 
       app.utils.localStorageSet('include_docs_bulkdocs', bulkDocsCollection.toJSON());

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/319ce992/app/addons/documents/index-results/index-results.components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/documents/index-results/index-results.components.react.jsx b/app/addons/documents/index-results/index-results.components.react.jsx
index 612eeb4..6a88da3 100644
--- a/app/addons/documents/index-results/index-results.components.react.jsx
+++ b/app/addons/documents/index-results/index-results.components.react.jsx
@@ -132,21 +132,40 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, Documents, Fauxto
       );
     },
 
-    getAttachmentRow: function (el) {
+    getAdditionalInfoRow: function (el) {
       var attachmentCount = Object.keys(el._attachments || {}).length;
-      var paperClip = null;
-      var text = null;
+      var attachmentIndicator = null;
+      var textAttachments = null;
+
+      var conflictCount = Object.keys(el._conflicts || {}).length;
+      var conflictIndicator = null;
+      var textConflicts = null;
+
 
       if (attachmentCount) {
-        text = attachmentCount === 1 ? attachmentCount + ' Attachment' : attachmentCount + ' Attachments';
-        paperClip = (
-          <div><i className="icon fonticon-paperclip"></i> {attachmentCount}</div>
+        textAttachments = attachmentCount === 1 ? attachmentCount + ' Attachment' : attachmentCount + ' Attachments';
+        attachmentIndicator = (
+          <div style={{display: 'inline', marginLeft: '5px'}} title={textAttachments}>
+            <i className="icon fonticon-paperclip"></i>{attachmentCount}
+          </div>
+        );
+      }
+
+      if (conflictCount) {
+        textConflicts = conflictCount === 1 ? conflictCount + ' Conflict' : conflictCount + ' Conflicts';
+        conflictIndicator = (
+          <div className="tableview-conflict" data-conflicts-indicator style={{display: 'inline'}} title={textConflicts}>
+            <i
+              style={{fontSize: '17px'}}
+              className="icon icon-code-fork"></i>{conflictCount}
+          </div>
         );
       }
 
       return (
-        <td title={text} className="tableview-el-last">
-          {paperClip}
+        <td className="tableview-el-last">
+          {conflictIndicator}
+          {attachmentIndicator}
         </td>
       );
     },
@@ -182,7 +201,7 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, Documents, Fauxto
           {this.getCopyButton(docContent)}
           {this.maybeGetSpecialField(el, i)}
           {this.getRowContents(el, i)}
-          {this.getAttachmentRow(docContent)}
+          {this.getAdditionalInfoRow(docContent)}
         </tr>
       );
     }

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/319ce992/app/addons/documents/tests/nightwatch/tableViewConflicts.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/tests/nightwatch/tableViewConflicts.js b/app/addons/documents/tests/nightwatch/tableViewConflicts.js
new file mode 100644
index 0000000..c0be7c3
--- /dev/null
+++ b/app/addons/documents/tests/nightwatch/tableViewConflicts.js
@@ -0,0 +1,37 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+module.exports = {
+
+  'Shows how many conflicts have appeared': function (client) {
+    var waitTime = client.globals.maxWaitTime,
+        newDatabaseName = client.globals.testDatabaseName,
+        baseUrl = client.globals.test_settings.launch_url;
+
+    client
+      .populateDatabaseWithConflicts(newDatabaseName)
+      .checkForDocumentCreated('outfit1')
+      .loginToGUI()
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+
+      .clickWhenVisible('.alternative-header .two-sides-toggle-button button:last-child')
+      .waitForElementVisible('.table', client.globals.maxWaitTime, false)
+
+      .clickWhenVisible('.control-toggle-include-docs')
+
+      .waitForElementVisible('.table-container-autocomplete', client.globals.maxWaitTime, false)
+
+      .assert.visible('.table [data-conflicts-indicator="true"]')
+
+      .end();
+  }
+};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/319ce992/test/nightwatch_tests/custom-commands/populateDatabaseWithConflicts.js
----------------------------------------------------------------------
diff --git a/test/nightwatch_tests/custom-commands/populateDatabaseWithConflicts.js b/test/nightwatch_tests/custom-commands/populateDatabaseWithConflicts.js
new file mode 100644
index 0000000..b252d14
--- /dev/null
+++ b/test/nightwatch_tests/custom-commands/populateDatabaseWithConflicts.js
@@ -0,0 +1,73 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+var util = require('util'),
+    events = require('events'),
+    helpers = require('../helpers/helpers.js'),
+    request = require('request');
+
+function PopulateDatabaseWithConflicts () {
+  events.EventEmitter.call(this);
+}
+
+util.inherits(PopulateDatabaseWithConflicts, events.EventEmitter);
+
+PopulateDatabaseWithConflicts.prototype.command = function (databaseName) {
+  var nano = helpers.getNanoInstance(),
+      database = nano.use(databaseName);
+
+  database.insert({
+    hat: 'flamingo'
+  }, 'outfit1', function () {
+    createConflictingDoc(null, function () {
+      this.emit('complete');
+    }.bind(this));
+  }.bind(this));
+
+  function createConflictingDoc (err, cb) {
+    request({
+      uri: helpers.test_settings.db_url + '/' + databaseName + '/conflictingdoc',
+      method: 'PUT',
+      json: true,
+      body: {
+        id: 'conflictingdoc',
+        rocko: 'dances'
+      }
+    }, function (err, res, body) {
+      if (err) {
+        console.log(
+          'Error in nano populateDatabase Function: ' + err.message
+        );
+      }
+      request({
+        uri: helpers.test_settings.db_url + '/' + databaseName + '/conflictingdoc?new_edits=false',
+        method: 'PUT',
+        json: true,
+        body: {
+          _rev: '4-afae890a0310210db079b0f49fb2569d',
+          rocko: 'jumps'
+        }
+      }, function (err, res, body) {
+        if (err) {
+          console.log('Error in nano populateDatabase Function: ' +
+            err.message);
+        }
+
+        cb && cb();
+      });
+    });
+  }
+
+  return this;
+};
+
+module.exports = PopulateDatabaseWithConflicts;