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;