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 <ben.k...@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 <robertkowal...@apache.org> Authored: Fri Mar 18 12:38:26 2016 +0000 Committer: Robert Kowalski <robertkowal...@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: {