You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by be...@apache.org on 2015/09/09 20:58:41 UTC

[1/2] fauxton commit: updated refs/heads/master to 8cd744a

Repository: couchdb-fauxton
Updated Branches:
  refs/heads/master 3da92763e -> 8cd744acb


http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/app/addons/documents/templates/string_edit_modal.html
----------------------------------------------------------------------
diff --git a/app/addons/documents/templates/string_edit_modal.html b/app/addons/documents/templates/string_edit_modal.html
deleted file mode 100644
index 1177795..0000000
--- a/app/addons/documents/templates/string_edit_modal.html
+++ /dev/null
@@ -1,30 +0,0 @@
-<!--
-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 class="modal hide fade">
-  <div class="modal-header">
-    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-    <h3>Edit text <span id="string-edit-header"></span></h3>
-  </div>
-  <div class="modal-body">
-    <div id="modal-error" class="hide alert alert-error"/>
-    <div id="string-editor-wrapper"><div id="string-editor-container" class="doc-code"></div></div>
-  </div>
-  <div class="modal-footer">
-    <button data-dismiss="modal" class="btn"><i class="icon fonticon-circle-x"></i> Cancel</button>
-    <button id="string-edit-save-btn" class="btn btn-success save"><i class="fonticon-circle-check"></i> Save</button>
-  </div>
-</div>
-
-

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/app/addons/documents/templates/upload_modal.html
----------------------------------------------------------------------
diff --git a/app/addons/documents/templates/upload_modal.html b/app/addons/documents/templates/upload_modal.html
deleted file mode 100644
index 4625826..0000000
--- a/app/addons/documents/templates/upload_modal.html
+++ /dev/null
@@ -1,42 +0,0 @@
-<!--
-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 class="modal hide fade">
-  <div class="modal-header">
-    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-    <h3>Upload an Attachment</h3>
-  </div>
-  <div class="modal-body">
-    <div id="modal-error" class="alert alert-error hide" style="font-size: 16px;"> </div>
-    <form id="file-upload" class="form" method="post">
-      <p class="help-block">
-      Please select the file you want to upload as an attachment to this document. 
-      Please note that this will result in the immediate creation of a new revision of the document, 
-      so it's not necessary to save the document after the upload.
-      </p>
-      <input id="_attachments" type="file" name="_attachments">
-      <input id="_rev" type="hidden" name="_rev" value="" >
-      <br/>
-    </form>
-
-    <div class="progress progress-info">
-      <div class="bar" style="width: 0%"></div>
-    </div>
-  </div>
-  <div class="modal-footer">
-    <button href="#" data-dismiss="modal" data-bypass="true" class="btn"><i class="icon fonticon-cancel-circled"></i> Cancel</button>
-    <button href="#" id="upload-btn" data-bypass="true" class="btn btn-success save"><i class="icon fonticon-ok-circled"></i> Upload</button>
-  </div>
-</div>
-

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/app/addons/documents/tests/nightwatch/createsDocument.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/tests/nightwatch/createsDocument.js b/app/addons/documents/tests/nightwatch/createsDocument.js
index e16de06..684233a 100644
--- a/app/addons/documents/tests/nightwatch/createsDocument.js
+++ b/app/addons/documents/tests/nightwatch/createsDocument.js
@@ -26,8 +26,11 @@ module.exports = {
       .clickWhenVisible('#new-all-docs-button a[href="#/database/' + newDatabaseName + '/new"]')
       .waitForElementPresent('#editor-container', waitTime, false)
       .verify.urlEquals(baseUrl + '/#/database/' + newDatabaseName + '/new')
+      .waitForElementPresent('.ace_layer.ace_cursor-layer.ace_hidden-cursors', waitTime, false)
+
+      //.pause(1000) // looks like auto-focus happens during the next execute() line, so this slows it down
       .execute('\
-        var editor = ace.edit("editor-container");\
+        var editor = ace.edit("doc-editor");\
         editor.gotoLine(2,10);\
         editor.removeWordRight();\
         editor.insert("' + newDocumentName + '");\

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/app/addons/documents/tests/nightwatch/deletesDocuments.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/tests/nightwatch/deletesDocuments.js b/app/addons/documents/tests/nightwatch/deletesDocuments.js
index 72a6a68..5a3a900 100644
--- a/app/addons/documents/tests/nightwatch/deletesDocuments.js
+++ b/app/addons/documents/tests/nightwatch/deletesDocuments.js
@@ -29,6 +29,8 @@ module.exports = {
       .waitForElementPresent('.control-select-all', waitTime, false)
       .clickWhenVisible('.control-delete')
       .acceptAlert()
+      .waitForElementVisible('.alert.alert-info', waitTime, false)
+
       .waitForElementVisible('label[for="checkbox-' + newDocumentName + '2' + '"]', waitTime, false)
       .clickWhenVisible('label[for="checkbox-' + newDocumentName + '2' + '"]', waitTime, false)
       .waitForElementPresent('.control-select-all', waitTime, false)

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/app/addons/documents/tests/nightwatch/mangoIndex.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/tests/nightwatch/mangoIndex.js b/app/addons/documents/tests/nightwatch/mangoIndex.js
index a47e319..dce4cb9 100644
--- a/app/addons/documents/tests/nightwatch/mangoIndex.js
+++ b/app/addons/documents/tests/nightwatch/mangoIndex.js
@@ -36,8 +36,8 @@ module.exports = {
         var editor = ace.edit("query-field");\
         editor.getSession().setValue(json);\
       ')
-      .execute('$(".save")[0].scrollIntoView();')
-      .clickWhenVisible('button.btn-success.save')
+      .execute('$("#create-index-btn")[0].scrollIntoView();')
+      .clickWhenVisible('#create-index-btn')
       .checkForStringPresent(newDatabaseName + '/_index', 'rocko-artischocko')
       .checkForStringPresent(newDatabaseName + '/_index', 'gans_gans_mango')
       .waitForElementPresent('.prettyprint', waitTime, false)

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/app/addons/documents/tests/nightwatch/mangoQuery.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/tests/nightwatch/mangoQuery.js b/app/addons/documents/tests/nightwatch/mangoQuery.js
index f173824..6801436 100644
--- a/app/addons/documents/tests/nightwatch/mangoQuery.js
+++ b/app/addons/documents/tests/nightwatch/mangoQuery.js
@@ -32,8 +32,8 @@ module.exports = {
         var editor = ace.edit("query-field");\
         editor.getSession().setValue(json);\
       ')
-      .execute('$(".save")[0].scrollIntoView();')
-      .clickWhenVisible('button.btn-success.save')
+      .execute('$("#create-index-btn")[0].scrollIntoView();')
+      .clickWhenVisible('#create-index-btn')
 
       .waitForElementPresent('.prettyprint', waitTime, false)
       .assert.containsText('#dashboard-lower-content', 'number')

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/app/addons/documents/tests/nightwatch/uploadAttachment.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/tests/nightwatch/uploadAttachment.js b/app/addons/documents/tests/nightwatch/uploadAttachment.js
index b0a5532..4005984 100644
--- a/app/addons/documents/tests/nightwatch/uploadAttachment.js
+++ b/app/addons/documents/tests/nightwatch/uploadAttachment.js
@@ -24,11 +24,10 @@ module.exports = {
       .createDocument('my_doc', newDatabaseName)
       .url(baseUrl + '/#/database/' + newDatabaseName + '/my_doc')
       .clickWhenVisible('.panel-button.upload')
-      .waitForElementVisible('input#_attachments', waitTime)
-      .setValue('input#_attachments', require('path').resolve(__dirname + '/uploadAttachment.js'))
+      .waitForElementVisible('input[name="_attachments"]', waitTime)
+      .setValue('input[name="_attachments"]', require('path').resolve(__dirname + '/uploadAttachment.js'))
       .clickWhenVisible('#upload-btn')
       .waitForElementVisible('#global-notification-id', waitTime, false)
-      .assert.attributeEquals('#_rev', 'value', '')
       .getText('#global-notification-id', function (result) {
         var data = result.value;
         this.verify.ok(data, 'Document saved successfully.');

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/app/addons/documents/tests/nightwatch/viewCreate.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/tests/nightwatch/viewCreate.js b/app/addons/documents/tests/nightwatch/viewCreate.js
index dcdfc48..f163ea5 100644
--- a/app/addons/documents/tests/nightwatch/viewCreate.js
+++ b/app/addons/documents/tests/nightwatch/viewCreate.js
@@ -26,9 +26,9 @@ module.exports = {
         var editor = ace.edit("map-function");\
         editor.getSession().setValue("function (doc) { emit(\'hasehase\'); }");\
       ')
-      .execute('$(".save")[0].scrollIntoView();')
-      .waitForElementPresent('button.btn.btn-success.save', waitTime, false)
-      .clickWhenVisible('button.btn.btn-success.save', waitTime, false)
+      .execute('$("#save-view")[0].scrollIntoView();')
+      .waitForElementPresent('#save-view', waitTime, false)
+      .clickWhenVisible('#save-view', waitTime, false)
       .checkForDocumentCreated('_design/test_design_doc-selenium-1')
       .waitForElementPresent('.prettyprint', waitTime, false)
       .waitForElementNotPresent('.loading-lines', waitTime, false)
@@ -50,9 +50,9 @@ module.exports = {
         var editor = ace.edit("map-function");\
         editor.getSession().setValue("function (doc) { emit(\'hasehase\'); }");\
       ')
-      .execute('$(".save")[0].scrollIntoView();')
-      .waitForElementPresent('button.btn.btn-success.save', waitTime, false)
-      .clickWhenVisible('button.btn.btn-success.save', waitTime, false)
+      .execute('$("#save-view")[0].scrollIntoView();')
+      .waitForElementPresent('#save-view', waitTime, false)
+      .clickWhenVisible('#save-view', waitTime, false)
       .checkForDocumentCreated('_design/test_design_doc-selenium-1')
       .waitForElementPresent('.prettyprint', waitTime, false)
       .waitForElementNotPresent('.loading-lines', waitTime, false)
@@ -78,8 +78,8 @@ module.exports = {
         var editor = ace.edit("map-function");\
         editor.getSession().setValue("function (doc) { emit(\'gansgans\'); }");\
       ')
-      .execute('$(".save")[0].scrollIntoView();')
-      .clickWhenVisible('button.btn-success.save')
+      .execute('$("#save-view")[0].scrollIntoView();')
+      .clickWhenVisible('#save-view')
       .checkForDocumentCreated('_design/test_design_doc-selenium-2')
       .waitForElementPresent('.prettyprint', waitTime, false)
       .waitForElementNotPresent('.loading-lines', waitTime, false)
@@ -107,8 +107,8 @@ module.exports = {
         var editor = ace.edit("map-function");\
         editor.getSession().setValue("function (doc) { emit(\'enteente\', 1); }");\
       ')
-      .execute('$(".save")[0].scrollIntoView();')
-      .clickWhenVisible('button.btn-success.save')
+      .execute('$("#save-view")[0].scrollIntoView();')
+      .clickWhenVisible('#save-view')
       .checkForDocumentCreated('_design/testdesigndoc/_view/test-new-view')
 
       .waitForElementPresent('.prettyprint', waitTime, false)

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/app/addons/documents/tests/nightwatch/viewCreateBadView.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/tests/nightwatch/viewCreateBadView.js b/app/addons/documents/tests/nightwatch/viewCreateBadView.js
index f4a693d..0555cf4 100644
--- a/app/addons/documents/tests/nightwatch/viewCreateBadView.js
+++ b/app/addons/documents/tests/nightwatch/viewCreateBadView.js
@@ -36,8 +36,8 @@ module.exports = {
         var editor = ace.edit("map-function");\
         editor.getSession().setValue("function (doc) { emit(\'boom\', doc._id); }");\
       ')
-      .execute('$(".save")[0].scrollIntoView();')
-      .clickWhenVisible('button.btn-success.save')
+      .execute('$("#save-view")[0].scrollIntoView();')
+      .clickWhenVisible('#save-view')
       .waitForAttribute('#global-notifications', 'textContent', function (docContents) {
         return (/_sum function requires/).test(docContents);
       })

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/app/addons/documents/tests/nightwatch/viewEdit.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/tests/nightwatch/viewEdit.js b/app/addons/documents/tests/nightwatch/viewEdit.js
index e77ba6b..96cf409 100644
--- a/app/addons/documents/tests/nightwatch/viewEdit.js
+++ b/app/addons/documents/tests/nightwatch/viewEdit.js
@@ -32,8 +32,8 @@ module.exports = {
         var editor = ace.edit("map-function");\
         editor.getSession().setValue("function (doc) { emit(\'hasehase5000\', 1); }");\
       ')
-      .execute('$(".save")[0].scrollIntoView();')
-      .clickWhenVisible('button.btn-success.save')
+      .execute('$("#save-view")[0].scrollIntoView();')
+      .clickWhenVisible('#save-view')
       .checkForStringPresent(viewUrl, 'hasehase5000')
       .waitForElementNotPresent('.loading-lines', waitTime, false)
       .waitForElementVisible('.prettyprint', waitTime, false)
@@ -62,9 +62,9 @@ module.exports = {
         editor.getSession().setValue("function (doc) { emit(\'hasehase6000\', 1); }");\
         editor._emit(\'blur\');\
       ')
-      .execute('$(".save")[0].scrollIntoView();')
+      .execute('$("#save-view")[0].scrollIntoView();')
 
-      .clickWhenVisible('button.btn-success.save')
+      .clickWhenVisible('#save-view')
 
       .checkForStringPresent(viewUrl, 'hasehase6000')
       .waitForElementNotPresent('.loading-lines', waitTime, false)
@@ -95,8 +95,8 @@ module.exports = {
         var editor = ace.edit("map-function");\
         editor.getSession().setValue("function (doc) { emit(\'newstub\', 2); }");\
       ')
-      .execute('$("button.save")[0].scrollIntoView();')
-      .clickWhenVisible('button.save', waitTime, false)
+      .execute('$("#save-view")[0].scrollIntoView();')
+      .clickWhenVisible('#save-view', waitTime, false)
       .checkForStringPresent(viewUrl, '40')
       .waitForElementNotPresent('.loading-lines', waitTime, false)
       .waitForElementVisible('.prettyprint', waitTime, false)

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/app/addons/documents/views-doceditor.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/views-doceditor.js b/app/addons/documents/views-doceditor.js
deleted file mode 100644
index 42c3d31..0000000
--- a/app/addons/documents/views-doceditor.js
+++ /dev/null
@@ -1,621 +0,0 @@
-// 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([
-  'app',
-
-  'api',
-  'addons/fauxton/components',
-  'addons/documents/resources',
-  'addons/databases/resources',
-
-  // Plugins
-  'plugins/prettify'
-],
-
-function (app, FauxtonAPI, Components, Documents, Databases, prettify) {
-
-  var Views = {};
-
-  Views.UploadModal = Components.ModalView.extend({
-    template: 'addons/documents/templates/upload_modal',
-
-    events: {
-      'click #upload-btn': 'uploadFile'
-    },
-
-    uploadFile: function (event) {
-      event.preventDefault();
-
-      var docRev = this.model.get('_rev'),
-          file = $('#_attachments')[0].files[0];
-
-      if (!docRev) {
-        return this.set_error_msg('The document needs to be saved before adding an attachment.');
-      }
-
-      if ($('input[type="file"]')[0].files.length === 0) {
-        return this.set_error_msg('Selected a file to be uploaded.');
-      }
-
-      this.$('#_rev').val(docRev);
-
-      $.ajax({
-        url: this.model.url() + '/' + file.name + '?rev=' + docRev,
-        xhrFields: {
-          withCredentials: true
-        },
-        type: 'PUT',
-        data: file,
-        processData: false,
-        contentType: file.type,
-        success: this.success,
-        error: function (resp) {
-          this.set_error_msg('Could not upload attachment: ' + JSON.parse(resp.responseText).reason);
-          return;
-        }.bind(this)
-      });
-    },
-
-    success: function (resp) {
-      var hideModal = this.hideModal,
-          $form = this.$('#file-upload');
-
-      FauxtonAPI.triggerRouteEvent('reRenderDoc');
-      //slight delay to make this transistion a little more fluid and less jumpy
-      setTimeout(function () {
-        this.$('#_attachments').val('');
-        this.$('#_rev').val('');
-        hideModal();
-        $('.modal-backdrop').remove();
-      }, 1000);
-    },
-
-    uploadProgress: function (event, position, total, percentComplete) {
-      this.$('.bar').css({width: percentComplete + '%'});
-    },
-
-    beforeSend: function () {
-      this.$('.progress').removeClass('hide');
-    },
-
-    _showModal: function () {
-      this.$('.bar').css({width: '0%'});
-      this.$('.progress').addClass('hide');
-    }
-  });
-
-  Views.StringEditModal = Components.ModalView.extend({
-    template: 'addons/documents/templates/string_edit_modal',
-
-    initialize: function () {
-      _.bindAll(this);
-    },
-
-    events: {
-      'click #string-edit-save-btn': 'saveString'
-    },
-
-    saveString: function (event) {
-      event.preventDefault();
-      var newStr = this.subEditor.getValue();
-      this.subEditor.editSaved();
-      this.editor.replaceCurrentLine(this.indent + this.hashKey + JSON.stringify(newStr) + this.comma + '\n');
-      this.hideModal();
-    },
-
-    _showModal: function () {
-      this.$('.bar').css({width: '0%'});
-      this.$('.progress').addClass('hide');
-      this.clear_error_msg();
-    },
-
-    openWin: function (editor, indent, hashKey, jsonString, comma) {
-      this.editor = editor;
-      this.indent = indent;
-      this.hashKey = hashKey;
-      this.$('#string-edit-header').text(hashKey);
-      this.subEditor.setValue(JSON.parse(jsonString));
-      /* make sure we don't have save warnings w/out change */
-      this.subEditor.editSaved();
-      this.comma = comma;
-      this.showModal();
-    },
-
-    afterRender: function () {
-      /* make sure we init only ONCE */
-      if (!this.subEditor) {
-        this.subEditor = new Components.Editor({
-          editorId: 'string-editor-container',
-          mode: 'plain'
-        });
-
-        this.subEditor.render().promise().then(function () {
-        /* optimize by disabling auto sizing (35 is the lines fitting into the pop-up) */
-          this.subEditor.configureFixedHeightEditor(35);
-        }.bind(this));
-      }
-    },
-
-    cleanup: function () {
-      if (this.subEditor) { this.subEditor.remove(); }
-    }
-
-  });
-
-  /* Doc Duplication modal */
-  Views.DuplicateDocModal = Components.ModalView.extend({
-    template: 'addons/documents/templates/duplicate_doc_modal',
-
-    initialize: function () {
-      _.bindAll(this);
-    },
-
-    events: {
-      'click #duplicate-btn':'duplicate',
-      'submit #doc-duplicate': 'duplicate'
-    },
-
-    duplicate: function (event) {
-      event.preventDefault();
-      var newId = this.$('#dup-id').val(),
-          isDDoc = newId.match(/^_design\//),
-          removeDDocID = newId.replace(/^_design\//, ''),
-          encodedID = isDDoc ? '_design/' + app.utils.safeURLName(removeDDocID) : app.utils.safeURLName(newId);
-
-      this.hideModal();
-      FauxtonAPI.triggerRouteEvent('duplicateDoc', encodedID);
-    },
-
-    _showModal: function () {
-      this.$('.bar').css({width: '0%'});
-      this.$('.progress').addClass('hide');
-      this.clear_error_msg();
-      this.$('.modal').modal();
-      // hack to get modal visible
-      $('.modal-backdrop').css('z-index', 1025);
-    },
-
-    showModal: function () {
-      var showModal = this._showModal,
-          setDefaultIdValue = this.setDefaultIdValue,
-          uuid = new FauxtonAPI.UUID();
-
-      uuid.fetch().then(function () {
-        setDefaultIdValue(uuid.next());
-        showModal();
-      });
-    },
-
-    setDefaultIdValue: function (id) {
-      this.$('#dup-id').val(id);
-    }
-  });
-
-  Views.CodeEditor = FauxtonAPI.View.extend({
-    template: 'addons/documents/templates/code_editor',
-    className: 'editor-content-page',
-    events: {
-      'click button.save-doc': 'saveDoc',
-      'click button.delete': 'destroy',
-      'click button.duplicate': 'duplicate',
-      'click button.upload': 'upload',
-      'click button.string-edit': 'stringEditing',
-      'click a.js-back': 'onClickGoBack',
-      'click .code-region': 'focusOnLastLine'
-    },
-    loaderStyles: {
-      color: '#ffffff',
-      opacity: 0.15
-    },
-
-    initialize: function (options) {
-      this.database = options.database;
-      _.bindAll(this);
-    },
-
-    onClickGoBack: function (e) {
-      e.preventDefault();
-      e.stopPropagation();
-
-      this.goBack();
-    },
-
-    goBack: function () {
-      FauxtonAPI.navigate(FauxtonAPI.urls('allDocs', 'app', this.database.id));
-    },
-
-    destroy: function () {
-      if (this.model.isNewDoc()) {
-        FauxtonAPI.addNotification({
-          msg: 'This document has not been saved yet.',
-          type: 'warning',
-          clear: true
-        });
-        return;
-      }
-      this.confirmDeleteModal.showModal();
-    },
-
-    deleteDocument: function () {
-      var database = this.model.database;
-
-      this.model.destroy().then(function () {
-        FauxtonAPI.addNotification({
-          msg: 'Your document has been successfully deleted.',
-          clear: true
-        });
-        FauxtonAPI.navigate(FauxtonAPI.urls('allDocs', 'app', database.id, '?limit=20'));
-      }, function () {
-        FauxtonAPI.addNotification({
-          msg: 'Failed to delete your document!',
-          type: 'error',
-          clear: true
-        });
-      });
-    },
-
-    upload: function (e) {
-      e.preventDefault();
-      if (this.model.isNewDoc()) {
-        FauxtonAPI.addNotification({
-          msg: 'Please save the document before uploading an attachment.',
-          type: 'warning',
-          clear: true
-        });
-        return;
-      }
-      this.uploadModal.showModal();
-    },
-
-    duplicate: function (e) {
-      if (this.model.isNewDoc()) {
-        FauxtonAPI.addNotification({
-          msg: 'Please save the document before duplicating it.',
-          type: 'warning',
-          clear: true
-        });
-        return;
-      }
-      e.preventDefault();
-      this.duplicateModal.showModal();
-    },
-
-    saveDoc: function (event) {
-      var that = this,
-        editor = this.editor,
-        validDoc = this.getDocFromEditor();
-
-      if (validDoc) {
-        FauxtonAPI.addNotification({msg: 'Saving document.'});
-
-        this.model.save().then(function () {
-          editor.editSaved();
-          FauxtonAPI.navigate(FauxtonAPI.urls('document', 'app', that.database.safeID(), that.model.id));
-        }).fail(function (xhr) {
-          var responseText = JSON.parse(xhr.responseText).reason;
-          FauxtonAPI.addNotification({
-            msg: 'Save failed: ' + responseText,
-            type: 'error',
-            fade: false,
-            clear: true
-          });
-        });
-      } else if (this.model.validationError && this.model.validationError === 'Cannot change a documents id.') {
-        FauxtonAPI.addNotification({
-          msg: 'Cannot save. Cannot change a documents _id, try Clone Document instead!',
-          type: 'error',
-          clear:  true
-        });
-        delete this.model.validationError;
-      } else {
-        FauxtonAPI.addNotification({
-          msg: 'Please fix the JSON errors and try saving again.',
-          type: 'error',
-          clear:  true
-        });
-      }
-    },
-
-    getDocFromEditor: function () {
-      if (!this.hasValidCode()) {
-        return false;
-      }
-      var json = JSON.parse(this.editor.getValue());
-      this.model.clear().set(json, {validate: true});
-      if (this.model.validationError) {
-        return false;
-      }
-      return this.model;
-    },
-
-    beforeRender: function () {
-      this.uploadModal = this.setView('#upload-modal', new Views.UploadModal({ model: this.model }));
-      this.duplicateModal = this.setView('#duplicate-modal', new Views.DuplicateDocModal({ model: this.model }));
-      this.confirmDeleteModal = this.setView('#delete-doc-modal', new Components.ConfirmationModal({
-        text: 'Are you sure you want to delete this document?',
-        action: this.deleteDocument
-      }));
-
-      // ensures it's initialized only once
-      this.stringEditModal = this.stringEditModal || this.setView('#string-edit-modal', new Views.StringEditModal());
-
-      var extensions = FauxtonAPI.getExtensions('DocEditor:icons');
-      _.each(extensions, function (View) {
-        this.insertView('.doc-editor-extension-icons', new View({ doc: this.model }));
-      }, this);
-    },
-
-    updateValues: function () {
-      if (this.model.changedAttributes()) {
-        FauxtonAPI.addNotification({
-          msg: 'Document saved successfully.',
-          type: 'success',
-          clear: true
-        });
-        this.editor.setValue(this.model.prettyJSON());
-      }
-    },
-
-    establish: function () {
-      var promise = this.model.fetch(),
-          deferred = $.Deferred(),
-          goBack = _.bind(this.goBack, this);
-
-      promise.then(function () {
-        deferred.resolve();
-      }, function (xhr, reason, msg) {
-        if (xhr.status === 404) {
-          FauxtonAPI.addNotification({
-            msg: 'The document does not exist',
-            type: 'error',
-            clear: true
-          });
-          goBack();
-        }
-        deferred.reject();
-      });
-
-      return deferred;
-    },
-
-    hasValidCode: function () {
-      var errors = this.editor.getAnnotations();
-      return errors.length === 0;
-    },
-
-    afterRender: function () {
-      this.listenTo(this.model, 'sync', this.updateValues);
-      this.editor = new Components.Editor({
-        editorId: 'editor-container',
-        isFullPageEditor: true,
-        forceMissingId: true,
-        commands: [{
-          name: 'save',
-          bindKey: {win: 'Ctrl-S',  mac: 'Ctrl-S'},
-          exec: function (editor) {
-            this.saveDoc();
-          },
-          readOnly: true // false if this command should not apply in readOnly mode
-        }]
-      });
-
-      this.editor.render();
-
-      var editor = this.editor;
-      var model = this.model;
-
-      // only start listening to editor once it has been rendered
-      this.editor.promise().then(function () {
-
-        this.listenTo(editor.editor, 'change', function (event) {
-          var changedDoc;
-          try {
-            changedDoc = JSON.parse(editor.getValue());
-          } catch (exception) {
-            //not complete doc. Cannot work with it
-            return;
-          }
-
-          var keyChecked = ['_id'];
-          if (model.get('_rev')) {
-            keyChecked.push('_rev');
-          }
-
-          // check the changedDoc has all the required standard keys
-          if (_.isEmpty(_.difference(keyChecked, _.keys(changedDoc)))) {
-            return;
-          }
-
-          editor.setReadOnly(true);
-          setTimeout(function () { editor.setReadOnly(false) ;}, 400);
-
-          // use extend so that _id stays at the top of the object with displaying the doc
-          changedDoc = _.extend({_id: model.id, _rev: model.get('_rev')}, changedDoc);
-          editor.setValue(JSON.stringify(changedDoc, null, '  '));
-          FauxtonAPI.addNotification({
-            type: 'error',
-            msg: "Cannot remove a document's id or revision.",
-            clear: true
-          });
-        });
-
-        var showHideEditDocString = _.bind(this.showHideEditDocString, this);
-        this.listenTo(editor.editor, 'changeSelection', function (event) {
-          showHideEditDocString(event);
-        });
-        this.listenTo(editor.editor.session, 'changeBackMarker', function (event) {
-          showHideEditDocString(event);
-        });
-
-        // place focus on the editor
-        editor.editor.focus();
-
-      }.bind(this));
-    },
-
-    focusOnLastLine: function (e) {
-      var clickedInEditor = $(e.target).closest('#editor-container');
-      if (clickedInEditor.length === 0) {
-        this.editor.editor.focus();
-        var session = this.editor.editor.getSession();
-        var count = session.getLength();
-        this.editor.editor.gotoLine(count, session.getLine(count - 1).length);
-      }
-    },
-
-    serialize: function () {
-      return {
-        doc: this.model,
-        attachments: this.getAttachments()
-      };
-    },
-
-    getAttachments: function () {
-      var attachments = this.model.get('_attachments');
-      if (!attachments) { return false; }
-
-      return _.map(attachments, function (att, key) {
-        return {
-          fileName: key,
-          size: att.length,
-          contentType: att.content_type,
-          url: this.model.url() + '/' + app.utils.safeURLName(key)
-        };
-      }, this);
-    },
-
-    determineStringEditMatch: function (event) {
-      var selStart = this.editor.getSelectionStart().row;
-      var selEnd = this.editor.getSelectionEnd().row;
-
-      // one JS(ON) string can't span more than one line - we edit one string, so ensure we don't select several lines
-      if (selStart >= 0 && selEnd >= 0 && selStart === selEnd && this.editor.isRowExpanded(selStart)) {
-        var editLine = this.editor.getLine(selStart),
-            editMatch = editLine.match(/^([ \t]*)(["|'][a-zA-Z0-9_]*["|']: )?(["|'].*["|'],?[ \t]*)$/);
-
-        if (editMatch) {
-          return editMatch;
-        }
-      }
-      return null;
-    },
-
-    showHideEditDocString: function (event) {
-      this.$('button.string-edit').attr('disabled', 'true');
-      if (!this.hasValidCode()) {
-        return false;
-      }
-      var editMatch = this.determineStringEditMatch(event);
-      if (editMatch) {
-        this.$('button.string-edit').removeAttr('disabled');
-        /* remove the following line (along with CSS) to go back to the toolbar: take the offset top of the editor, go down as many lines as we are positioned including fold and adjust by two pixels as the button is slightly larger than a line */
-        var positionFromTop = (this.$('#editor-container').offset().top - 2 + this.editor.getRowHeight() * this.editor.documentToScreenRow(this.editor.getSelectionStart().row)) - 62;
-        this.$('button.string-edit').css('top', positionFromTop + 'px');
-        return true;
-      }
-      return false;
-    },
-
-    stringEditing: function (event) {
-      event.preventDefault();
-      if (!this.hasValidCode()) {
-        return;
-      }
-      var editMatch = this.determineStringEditMatch(event);
-      if (editMatch) {
-        var indent = editMatch[1] || '',
-          hashKey = editMatch[2] || '',
-          editText = editMatch[3],
-          comma = '';
-        if (editText.substring(editText.length - 1) === ',') {
-          editText = editText.substring(0, editText.length - 1);
-          comma = ',';
-        }
-        this.stringEditModal.openWin(this.editor, indent, hashKey, editText, comma);
-      }
-    },
-
-    cleanup: function () {
-      this.editor && this.editor.remove();
-      $('#dashboard').off('click');
-    }
-  });
-
-
-  Views.StringEditModal = Components.ModalView.extend({
-    template: 'addons/documents/templates/string_edit_modal',
-
-    events: {
-      'click #string-edit-save-btn': 'saveString'
-    },
-
-    saveString: function (event) {
-      event.preventDefault();
-      var newStr = this.subEditor.getValue();
-      this.subEditor.editSaved();
-      this.editor.replaceCurrentLine(this.indent + this.hashKey + JSON.stringify(newStr) + this.comma + '\n');
-      this.hideModal();
-    },
-
-    _showModal: function () {
-      this.$('.bar').css({width: '0%'});
-      this.$('.progress').addClass('hide');
-      this.clear_error_msg();
-    },
-
-    openWin: function (editor, indent, hashKey, jsonString, comma) {
-      this.editor = editor;
-      this.indent = indent;
-      this.hashKey = hashKey;
-      this.$('#string-edit-header').text(hashKey);
-      this.subEditor.setValue(JSON.parse(jsonString));
-      /* make sure we don't have save warnings w/out change */
-      this.subEditor.editSaved();
-      this.comma = comma;
-      this.showModal();
-    },
-
-    afterRender: function () {
-      var that = this;
-      this.$('.modal').on('hide', function (e) {
-        if (that.subEditor.edited) {
-          if (!confirm("Close without saving changes?")) {
-            e.preventDefault();
-            return;
-          }
-        }
-        /* make sure we don't have save warnings w/out change */
-        that.subEditor.editSaved();
-      });
-      /* make sure we init only ONCE */
-      if (!this.subEditor) {
-        this.subEditor = new Components.Editor({
-          editorId: 'string-editor-container',
-          mode: 'plain'
-        });
-
-        this.subEditor.render().promise().then(function () {
-          /* optimize by disabling auto sizing (35 is the lines fitting into the pop-up) */
-          this.subEditor.configureFixedHeightEditor(35);
-        }.bind(this));
-      }
-    },
-
-    cleanup: function () {
-      if (this.subEditor) { this.subEditor.remove(); }
-    }
-  });
-
-
-  return Views;
-});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/app/addons/documents/views.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/views.js b/app/addons/documents/views.js
index fa9214b..24f8078 100644
--- a/app/addons/documents/views.js
+++ b/app/addons/documents/views.js
@@ -25,8 +25,7 @@ define([
   "plugins/prettify"
 ],
 
-function (app, FauxtonAPI, Components, Documents,
-  Databases, QueryOptions, QueryActions) {
+function (app, FauxtonAPI, Components, Documents, Databases, QueryOptions, QueryActions) {
 
   var Views = {};
 

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/app/addons/fauxton/components.js
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/components.js b/app/addons/fauxton/components.js
index 42c8102..00ae7f4 100644
--- a/app/addons/fauxton/components.js
+++ b/app/addons/fauxton/components.js
@@ -580,223 +580,6 @@ function (app, FauxtonAPI, ace, spin, ZeroClipboard) {
     }
   });
 
-  Components.Editor = FauxtonAPI.View.extend({
-    initialize: function (options) {
-      this.editorId = options.editorId;
-      this.mode = options.mode || "json";
-      this.commands = options.commands;
-      this.theme = options.theme || 'idle_fingers';
-      this.couchJSHINT = options.couchJSHINT;
-
-      // going forward we can use flexbox for all editors with a fixed height and drop all custom JS to adjust its height
-      this.setHeightWithJS = _.has(options, 'setHeightWithJS') ? options.setHeightWithJS : true;
-
-      this.edited = false;
-
-      // the full-page document editor does some extra work to adjust the total height of the editor based
-      // on available space. This setting ensures that only takes place there, and not with other editor locations
-      this.isFullPageEditor = options.isFullPageEditor || false;
-
-      var that = this;
-      this.onPageResize = _.debounce(function () {
-        if (that.isFullPageEditor) {
-          that.setAvailableEditorHeight();
-        }
-        that.setHeightToLineCount();
-        that.editor.resize(true);
-      }, 300);
-
-      $(window).on('resize.editor', this.onPageResize);
-      this.listenTo(FauxtonAPI.Events, FauxtonAPI.constants.EVENTS.NAVBAR_SIZE_CHANGED, this.onPageResize);
-
-      _.bindAll(this);
-    },
-
-    afterRender: function () {
-      this.editor = ace.edit(this.editorId);
-      this.setAvailableEditorHeight();
-      this.setHeightToLineCount();
-      this.editor.setTheme("ace/theme/" + this.theme);
-
-      if (this.mode != "plain") {
-        this.editor.getSession().setMode("ace/mode/" + this.mode);
-      }
-
-      this.editor.setShowPrintMargin(false);
-      this.editor.autoScrollEditorIntoView = true;
-      this.addCommands();
-
-      if (this.couchJSHINT) {
-        this.removeIncorrectAnnotations();
-      }
-
-      this.editor.getSession().setTabSize(2);
-
-      this.editor.getSession().on('change', function () {
-        this.setHeightToLineCount();
-        this.edited = true;
-      }.bind(this));
-
-      $(window).on('beforeunload.editor_' + this.editorId, function () {
-        if (this.edited) {
-          return 'Your changes have not been saved. Click cancel to return to the document.';
-        }
-      }.bind(this));
-
-      FauxtonAPI.beforeUnload('editor_' + this.editorId, function (deferred) {
-        if (this.edited) {
-          return 'Your changes have not been saved. Click cancel to return to the document.';
-        }
-      }.bind(this));
-    },
-
-    cleanup: function () {
-      $(window).off('beforeunload.editor_' + this.editorId);
-      $(window).off('resize.editor', this.onPageResize);
-      FauxtonAPI.removeBeforeUnload('editor_' + this.editorId);
-      this.editor.destroy();
-    },
-
-    // we need to track the possible available height of the editor to tell it how large it can grow vertically
-    setAvailableEditorHeight: function () {
-      this.availableEditorHeight = $('.code-region').height();
-    },
-
-    setHeightToLineCount: function () {
-      if (!this.setHeightWithJS) {
-        return;
-      }
-      var lines = this.editor.getSession().getDocument().getLength();
-
-      if (this.isFullPageEditor) {
-        var maxLines = this.getMaxAvailableLinesOnPage();
-        lines = lines < maxLines ? lines : maxLines;
-      }
-      this.editor.setOptions({
-        maxLines: lines
-      });
-    },
-
-    getMaxAvailableLinesOnPage: function () {
-      var singleLine = this.getRowHeight();
-      return Math.floor(this.availableEditorHeight  / singleLine);
-    },
-
-    getLines: function () {
-      return this.editor.getSession().getDocument().getLength();
-    },
-
-    addCommands: function () {
-      _.each(this.commands, function (command) {
-        this.editor.commands.addCommand(command);
-      }, this);
-    },
-
-    removeIncorrectAnnotations: function () {
-      var editor = this.editor,
-          isIgnorableError = this.isIgnorableError;
-
-      this.editor.getSession().on("changeAnnotation", function () {
-        var annotations = editor.getSession().getAnnotations();
-
-        var newAnnotations = _.reduce(annotations, function (annotations, error) {
-          if (!isIgnorableError(error.raw)) {
-            annotations.push(error);
-          }
-          return annotations;
-        }, []);
-
-        if (annotations.length !== newAnnotations.length) {
-          editor.getSession().setAnnotations(newAnnotations);
-        }
-      });
-    },
-
-    editSaved: function () {
-      this.edited = false;
-    },
-
-    setReadOnly: function (value) {
-      return this.editor.setReadOnly(value);
-    },
-
-    setValue: function (data, lineNumber) {
-      lineNumber = lineNumber ? lineNumber : -1;
-      this.editor.setValue(data, lineNumber);
-    },
-
-    getValue: function () {
-      return this.editor.getValue();
-    },
-
-    getAnnotations: function () {
-      return this.editor.getSession().getAnnotations();
-    },
-
-    hadValidCode: function () {
-      var errors = this.getAnnotations();
-      // By default CouchDB view functions don't pass lint
-      return _.every(errors, function (error) {
-        return this.isIgnorableError(error.raw);
-      }, this);
-    },
-
-    // List of JSHINT errors to ignore
-    // Gets around problem of anonymous functions not being a valid statement
-    excludedViewErrors: [
-      "Missing name in function declaration.",
-      "['{a}'] is better written in dot notation."
-    ],
-
-    isIgnorableError: function (msg) {
-      return _.contains(this.excludedViewErrors, msg);
-    },
-
-    configureFixedHeightEditor: function (numLines) {
-      this.editor.renderer.setVScrollBarAlwaysVisible(true);
-      this.editor.renderer.setHScrollBarAlwaysVisible(true);
-      /* customize the ace scrolling for static edit height */
-      this.editor.renderer.$autosize = function () {
-        this.$size.height = numLines * this.lineHeight;
-        this.desiredHeight = numLines * this.lineHeight;
-        this.container.style.height = this.desiredHeight + "px";
-        this.scrollBarV.setVisible(true);
-        this.scrollBarH.setVisible(true);
-      };
-    },
-
-    replaceCurrentLine: function (replacement) {
-      this.editor.getSelection().selectLine();
-      this.editor.insert(replacement);
-      this.editor.getSelection().moveCursorUp();
-    },
-
-    getLine: function (lineNum) {
-      return this.editor.session.getLine(lineNum);
-    },
-
-    getSelectionStart: function () {
-      return this.editor.getSelectionRange().start;
-    },
-
-    getSelectionEnd: function () {
-      return this.editor.getSelectionRange().end;
-    },
-
-    getRowHeight: function () {
-      return this.editor.renderer.layerConfig.lineHeight;
-    },
-
-    isRowExpanded: function (row) {
-      return !this.editor.getSession().isRowFolded(row);
-    },
-
-    documentToScreenRow: function (row) {
-      return this.editor.getSession().documentToScreenRow(row, 0);
-    }
-
-  });
-
 
   //Menu Drop down component. It takes links in this format and renders the Dropdown:
   // [{

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/app/addons/fauxton/components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/components.react.jsx b/app/addons/fauxton/components.react.jsx
index fa843c0..4d3776c 100644
--- a/app/addons/fauxton/components.react.jsx
+++ b/app/addons/fauxton/components.react.jsx
@@ -269,12 +269,63 @@ function (app, FauxtonAPI, React, ZeroClipboard) {
   });
 
 
+  // A super-simple replacement for window.confirm()
+  var ConfirmationModal = React.createClass({
+    propTypes: {
+      visible: React.PropTypes.bool.isRequired,
+      text: React.PropTypes.string.isRequired,
+      onClose: React.PropTypes.func.isRequired,
+      onSubmit: React.PropTypes.func.isRequired
+    },
+
+    getDefaultProps: function () {
+      return {
+        visible: false,
+        title: 'Please confirm',
+        text: '',
+        onClose: function () { },
+        onSubmit: function () { }
+      };
+    },
+
+    componentDidUpdate: function () {
+      var params = (this.props.visible) ? { show: true, backdrop: 'static', keyboard: true } : 'hide';
+      $(this.getDOMNode()).modal(params);
+
+      $(this.getDOMNode()).on('hidden.bs.modal', function () {
+        this.props.onClose();
+      }.bind(this));
+    },
+
+    render: function () {
+      return (
+        <div className="modal hide confirmation-modal fade" tabIndex="-1" data-js-visible={this.props.visible}>
+          <div className="modal-header">
+            <button type="button" className="close" onClick={this.props.onClose} aria-hidden="true">&times;</button>
+            <h3>{this.props.title}</h3>
+          </div>
+          <div className="modal-body">
+            <p>
+              {this.props.text}
+            </p>
+          </div>
+          <div className="modal-footer">
+            <button className="btn" onClick={this.props.onClose}><i className="icon fonticon-cancel-circled"></i> Cancel</button>
+            <button className="btn btn-success js-btn-success" onClick={this.props.onSubmit}><i className="fonticon-ok-circled"></i> Okay</button>
+          </div>
+        </div>
+      );
+    }
+  });
+
+
   return {
     Clipboard: Clipboard,
     ClipboardWithTextField: ClipboardWithTextField,
     CodeFormat: CodeFormat,
     Tray: Tray,
-    Pagination: Pagination
+    Pagination: Pagination,
+    ConfirmationModal: ConfirmationModal
   };
 
 });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/assets/less/code-editors.less
----------------------------------------------------------------------
diff --git a/assets/less/code-editors.less b/assets/less/code-editors.less
new file mode 100644
index 0000000..25edcf5
--- /dev/null
+++ b/assets/less/code-editors.less
@@ -0,0 +1,53 @@
+//  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.
+
+
+/* site-wide Ace Editor theme overrides */
+
+.ace_scrollbar-h {
+  right: 0 !important;
+  height: 15px !important;
+}
+
+body {
+  .ace-idle-fingers .ace_marker-layer .ace_selection {
+    background: #015F6F;
+  }
+  .ace-idle-fingers .ace_gutter {
+    background: #3A3A3A;
+  }
+  .ace-idle-fingers {
+    background-color: #4d4d4d;
+  }
+  .ace-idle-fingers .ace_cursor {
+    color: #ffffff;
+  }
+  .ace-idle-fingers.ace_multiselect .ace_selection.ace_start {
+    box-shadow: 0 0 3px 0 #4d4d4d;
+  }
+  .ace-idle-fingers .ace_marker-layer .ace_active-line {
+    background: #000000;
+    opacity: 0.4;
+  }
+  .ace-idle-fingers .ace_constant {
+    color: #72cdf4;
+  }
+  .ace-idle-fingers .ace_boolean {
+    color: #ff6532;
+  }
+  .ace-idle-fingers .ace_string {
+    color: #29be9d;
+  }
+  .ace-idle-fingers .ace_collab.ace_user1 {
+    color: #4d4d4d;
+  }
+}

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/assets/less/codeeditor.less
----------------------------------------------------------------------
diff --git a/assets/less/codeeditor.less b/assets/less/codeeditor.less
deleted file mode 100644
index a08e309..0000000
--- a/assets/less/codeeditor.less
+++ /dev/null
@@ -1,192 +0,0 @@
-//  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.
-
-@import "mixins.less";
-
-
-#dashboard.doc-editor-page {
-  background-color: #4d4d4d;
-
-  #dashboard-content {
-    padding-bottom: 0px;
-    top: 0px;
-  }
-}
-
-.doc-editor-page {
-  border-left: none;
-
-  .fixed-header {
-    height: 65px;
-    .box-shadow(none);
-  }
-
-  #breadcrumbs {
-    white-space: nowrap;
-  }
-
-  #doc-editor-actions-panel {
-    position: absolute;
-    top: 64px;
-    right: 0px;
-    left: 0px;
-    width: 100%;
-    background-color: #f1f1f1;
-    height: 61px;
-  }
-
-  #editor-container {
-    margin-top: 0px;
-    font-size: 13px;
-    line-height: 22px;
-    padding-bottom: 15px;
-  }
-
-  .doc-actions-left {
-    float: left;
-    padding: 9px 0px;
-    button {
-      margin: 0px 10px 0px 30px;
-    }
-    div {
-      display: inline-block;
-    }
-  }
-
-  .cancel-button {
-    font-size: 14px;
-  }
-
-  .bgEditorGutter {
-    width: 49px;
-    position: absolute;
-    top: 0px;
-    bottom: 0px;
-    background-color: #3b3b3b;
-  }
-
-  .panel-button {
-    border: 0px;
-    background-color: #f1f1f1;
-    padding: 11px;
-    color: #555555;
-
-    span:first-of-type {
-      margin-left: 4px;
-    }
-
-    .icon {
-      font-size: 18px;
-    }
-  }
-  .panel-section {
-    border-left: 1px solid #cccccc;
-    text-align: center;
-    padding: 9px 0px;
-    display: inline-block;
-
-    &.open .dropdown-toggle {
-      box-shadow: none;
-      background-color: white;
-    }
-  }
-  .alignRight {
-    text-align: right;
-    font-size: 0; // prevents whitespace gaps between elements
-  }
-  .row-fluid.content-area {
-    background-color: #4d4d4d;
-  }
-
-  // overriding the ace editor inline styles
-  .ace_gutter-layer {
-    min-width: 49px;
-  }
-  .ace_gutter-cell {
-    min-width: 49px;
-  }
-
-  .ace_marker-layer .ace_bracket {
-    margin: 0px;
-    border: 1px solid #999999;
-  }
-
-  // hide the labels on the buttons when the screen is shrunk too small,
-  @media screen and (max-width: 1000px) {
-    .panel-button span {
-      display: none;
-    }
-  }
-
-  // hides the API Url header link when the page is too small (prevents wrapping)
-  @media screen and (max-width: 835px) {
-    #api-navbar {
-      display: none;
-    }
-  }
-
-  #dashboard-content .code-region {
-    overflow-y: hidden;
-    position: absolute;
-    top: 125px;
-    height: auto;
-    width: 100%;
-    padding: 0;
-    left: 0;
-    right: 0;
-    bottom: 0;
-  }
-}
-
-.ace_scrollbar-h {
-  right: 0px !important;
-  height: 15px !important;
-}
-
-#editor-container {
-  height: 100% !important;
-}
-
-/* Ace theme overrides */
-body {
-  .ace-idle-fingers .ace_marker-layer .ace_selection {
-    background: #015F6F;
-  }
-  .ace-idle-fingers .ace_gutter {
-    background: #3A3A3A;
-  }
-  .ace-idle-fingers {
-    background-color: #4d4d4d;
-  }
-  .ace-idle-fingers .ace_cursor {
-    color: #ffffff;
-  }
-  .ace-idle-fingers.ace_multiselect .ace_selection.ace_start {
-    box-shadow: 0 0 3px 0 #4d4d4d;
-  }
-  .ace-idle-fingers .ace_marker-layer .ace_active-line {
-    background: #000000;
-    opacity: 0.4;
-  }
-  .ace-idle-fingers .ace_constant {
-    color: #72cdf4;
-  }
-  .ace-idle-fingers .ace_boolean {
-    color: #ff6532;
-  }
-  .ace-idle-fingers .ace_string {
-    color: #29be9d;
-  }
-  .ace-idle-fingers .ace_collab.ace_user1 {
-    color: #4d4d4d;
-  }
-}

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/assets/less/fauxton.less
----------------------------------------------------------------------
diff --git a/assets/less/fauxton.less b/assets/less/fauxton.less
index 14e7f07..477b0bf 100644
--- a/assets/less/fauxton.less
+++ b/assets/less/fauxton.less
@@ -16,7 +16,7 @@
 @import "layouts.less";
 @import "prettyprint.less";
 @import "icons.less";
-@import "codeeditor.less";
+@import "code-editors.less";
 @import "templates.less";
 @import "formstyles.less";
 @import "pagination.less";


[2/2] fauxton commit: updated refs/heads/master to 8cd744a

Posted by be...@apache.org.
Reactifying full page doc editor

This PR converts the Full Page Document Editor to React, including
all subcomponents. It drops the old Backbone version of the
code editor.

File uploads now have a progress bar and work locally.


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

Branch: refs/heads/master
Commit: 8cd744acb4f7bba17b3b578056f5e7e3199a2016
Parents: 3da9276
Author: Ben Keen <be...@gmail.com>
Authored: Mon Jun 8 15:43:59 2015 -0700
Committer: Ben Keen <be...@gmail.com>
Committed: Wed Sep 9 12:00:48 2015 -0700

----------------------------------------------------------------------
 .../components/assets/less/code-editor.less     |   7 +
 .../components/react-components.react.jsx       | 290 ++++++++-
 .../tests/stringEditModalSpec.react.jsx         |  75 +++
 .../documents/assets/less/doc-editor.less       | 181 ++++++
 app/addons/documents/assets/less/documents.less |   6 +-
 .../documents/assets/less/view-editor.less      |   1 -
 app/addons/documents/doc-editor/actions.js      | 239 +++++++
 app/addons/documents/doc-editor/actiontypes.js  |  31 +
 .../documents/doc-editor/components.react.jsx   | 498 +++++++++++++++
 app/addons/documents/doc-editor/stores.js       | 201 ++++++
 .../tests/doc-editor.componentsSpec.react.jsx   | 187 ++++++
 .../doc-editor/tests/doc-editor.storesSpec.js   |  81 +++
 .../documents/index-editor/components.react.jsx |  20 +-
 .../tests/viewIndex.componentsSpec.react.jsx    |  24 +-
 .../documents/mango/mango.components.react.jsx  |  11 +-
 app/addons/documents/routes-doc-editor.js       |  57 +-
 app/addons/documents/routes-documents.js        |   9 +-
 app/addons/documents/templates/code_editor.html |  78 ---
 .../templates/duplicate_doc_modal.html          |  36 --
 .../documents/templates/string_edit_modal.html  |  30 -
 .../documents/templates/upload_modal.html       |  42 --
 .../tests/nightwatch/createsDocument.js         |   5 +-
 .../tests/nightwatch/deletesDocuments.js        |   2 +
 .../documents/tests/nightwatch/mangoIndex.js    |   4 +-
 .../documents/tests/nightwatch/mangoQuery.js    |   4 +-
 .../tests/nightwatch/uploadAttachment.js        |   5 +-
 .../documents/tests/nightwatch/viewCreate.js    |  20 +-
 .../tests/nightwatch/viewCreateBadView.js       |   4 +-
 .../documents/tests/nightwatch/viewEdit.js      |  12 +-
 app/addons/documents/views-doceditor.js         | 621 -------------------
 app/addons/documents/views.js                   |   3 +-
 app/addons/fauxton/components.js                | 217 -------
 app/addons/fauxton/components.react.jsx         |  53 +-
 assets/less/code-editors.less                   |  53 ++
 assets/less/codeeditor.less                     | 192 ------
 assets/less/fauxton.less                        |   2 +-
 36 files changed, 1960 insertions(+), 1341 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/app/addons/components/assets/less/code-editor.less
----------------------------------------------------------------------
diff --git a/app/addons/components/assets/less/code-editor.less b/app/addons/components/assets/less/code-editor.less
index 522aa74..571882e 100644
--- a/app/addons/components/assets/less/code-editor.less
+++ b/app/addons/components/assets/less/code-editor.less
@@ -22,6 +22,13 @@
     background-color: #ffffff;
   }
 
+  &>div {
+    height: 100%;
+    &.zen-mode-controls {
+      height: 96px;
+    }
+  }
+
   .ace_editor {
     height: 100%;
     font-size: 15px;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/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 c64bd3d..3dc4b25 100644
--- a/app/addons/components/react-components.react.jsx
+++ b/app/addons/components/react-components.react.jsx
@@ -73,6 +73,7 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
     getDefaultProps: function () {
       return {
         id: 'code-editor',
+        className: '',
         defaultCode: '',
         title: '',
         docLink: '',
@@ -145,10 +146,8 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
     },
 
     exitZenMode: function (content) {
-      this.setState({
-        zenModeEnabled: false,
-        code: content
-      });
+      this.setState({ zenModeEnabled: false });
+      this.getEditor().setValue(content);
     },
 
     getEditor: function () {
@@ -163,9 +162,17 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
       this.setState({ code: code });
     },
 
+    update: function () {
+      this.getEditor().setValue(this.state.code);
+    },
+
     render: function () {
+      var classes = 'control-group';
+      if (this.props.className) {
+        classes += ' ' + this.props.className;
+      }
       return (
-        <div className="control-group">
+        <div className={classes}>
           <label>
             <strong>{this.props.title + ' '}</strong>
             {this.getDocIcon()}
@@ -189,7 +196,6 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
   });
 
 
-  // a generic Ace Editor component. This should be the only place in the app that instantiates an editor
   var CodeEditor = React.createClass({
     getDefaultProps: function () {
       return {
@@ -197,12 +203,17 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
         mode: 'javascript',
         theme: 'idle_fingers',
         fontSize: 13,
+
+        // this sets the default value for the editor. On the fly changes are stored in state in this component only. To
+        // change the editor content after initial construction use CodeEditor.setValue()
         defaultCode: '',
+
         showGutter: true,
         highlightActiveLine: true,
         showPrintMargin: false,
         autoScrollEditorIntoView: true,
         autoFocus: false,
+        stringEditModalEnabled: false,
 
         // these two options create auto-resizeable code editors, with a maximum number of lines
         setHeightToLineCount: false,
@@ -214,7 +225,7 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
         // notifies users that there is unsaved changes in the editor when navigating away from the page
         notifyUnsavedChanges: false,
 
-        // an optional array of ignorable Ace editors. Lets us filter out errors based on context
+        // an optional array of ignorable Ace errors. Lets us filter out errors based on context
         ignorableErrors: [],
 
         // un-Reacty, but the code editor is a self-contained component and it's helpful to be able to tie into
@@ -224,10 +235,15 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
       };
     },
 
-    // used purely to keep track of content changes. This is only reset via an explicit clearChanges() call
     getInitialState: function () {
       return {
-        originalCode: this.props.defaultCode
+        originalCode: this.props.defaultCode,
+
+        // these are all related to the (optional) string edit modal
+        stringEditModalVisible: false,
+        stringEditIconVisible: false,
+        stringEditIconStyle: {},
+        stringEditModalDefaultString: ''
       };
     },
 
@@ -248,11 +264,12 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
       this.editor.$blockScrolling = Infinity;
 
       if (shouldUpdateCode) {
-        this.setEditorValue(props.defaultCode);
+        this.setValue(props.defaultCode);
       }
 
       this.editor.setShowPrintMargin(props.showPrintMargin);
       this.editor.autoScrollEditorIntoView = props.autoScrollEditorIntoView;
+
       this.editor.setOption('highlightActiveLine', this.props.highlightActiveLine);
 
       if (this.props.setHeightToLineCount) {
@@ -267,6 +284,7 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
       this.editor.getSession().setMode('ace/mode/' + props.mode);
       this.editor.setTheme('ace/theme/' + props.theme);
       this.editor.setFontSize(props.fontSize);
+      this.editor.getSession().setTabSize(2);
       this.editor.getSession().setUseSoftTabs(true);
 
       if (this.props.autoFocus) {
@@ -283,6 +301,13 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
     setupEvents: function () {
       this.editor.on('blur', _.bind(this.onBlur, this));
       this.editor.on('change', _.bind(this.onContentChange, this));
+
+      if (this.props.stringEditModalEnabled) {
+        this.editor.on('changeSelection', _.bind(this.showHideEditStringGutterIcon, this));
+        this.editor.getSession().on('changeBackMarker', _.bind(this.showHideEditStringGutterIcon, this));
+        this.editor.getSession().on('changeScrollTop', _.bind(this.updateEditStringGutterIconPosition, this));
+      }
+
       if (this.props.notifyUnsavedChanges) {
         $(window).on('beforeunload.editor_' + this.props.id, _.bind(this.quitWarningMsg));
         FauxtonAPI.beforeUnload('editor_' + this.props.id, _.bind(this.quitWarningMsg, this));
@@ -302,7 +327,7 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
 
     quitWarningMsg: function () {
       if (this.hasChanged()) {
-        return 'Your changes have not been saved. Click cancel to return to the document.';
+        return 'Your changes have not been saved. Click Cancel to return to the document, or OK to proceed.';
       }
     },
 
@@ -324,6 +349,10 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
     componentDidMount: function () {
       this.setupAce(this.props, true);
       this.setupEvents();
+
+      if (this.props.autoFocus) {
+        this.editor.focus();
+      }
     },
 
     componentWillUnmount: function () {
@@ -332,8 +361,7 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
     },
 
     componentWillReceiveProps: function (nextProps) {
-      var codeChanged = !_.isEqual(nextProps.defaultCode, this.getValue());
-      this.setupAce(nextProps, codeChanged);
+      this.setupAce(nextProps, false);
     },
 
     getAnnotations: function () {
@@ -361,18 +389,100 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
       }.bind(this));
     },
 
-    // ------------------
-    // TODO two things to do after full page doc editor refactor:
-    // 1. rename to hasErrors()
-    hadValidCode: function () {
-      var errors = this.getAnnotations();
-      return _.every(errors, function (error) {
+    showHideEditStringGutterIcon: function (e) {
+      if (this.hasErrors() || !this.parseLineForStringMatch()) {
+        this.setState({ stringEditIconVisible: false });
+        return false;
+      }
+
+      this.setState({
+        stringEditIconVisible: true,
+        stringEditIconStyle: {
+          top: this.getGutterIconPosition()
+        }
+      });
+
+      return true;
+    },
+
+    updateEditStringGutterIconPosition: function () {
+      if (!this.state.stringEditIconVisible) {
+        return;
+      }
+      this.setState({
+        stringEditIconStyle: {
+          top: this.getGutterIconPosition()
+        }
+      });
+    },
+
+    getGutterIconPosition: function () {
+      var rowHeight = this.getRowHeight();
+      var scrollTop = this.editor.session.getScrollTop();
+      var positionFromTop = (rowHeight * this.documentToScreenRow(this.getSelectionStart().row)) - scrollTop;
+      return positionFromTop + 'px';
+    },
+
+    parseLineForStringMatch: function () {
+      var selStart = this.getSelectionStart().row;
+      var selEnd   = this.getSelectionEnd().row;
+
+      // one JS(ON) string can't span more than one line - we edit one string, so ensure we don't select several lines
+      if (selStart >= 0 && selEnd >= 0 && selStart === selEnd && this.isRowExpanded(selStart)) {
+        var editLine = this.getLine(selStart),
+            editMatch = editLine.match(/^([ \t]*)("[a-zA-Z0-9_]*["|']: )?(["|'].*",?[ \t]*)$/);
+
+        if (editMatch) {
+          return editMatch;
+        }
+      }
+      return false;
+    },
+
+    openStringEditModal: function () {
+      var matches = this.parseLineForStringMatch();
+      var string = matches[3];
+      var lastChar = string.length - 1;
+      if (string.substring(string.length - 1) === ',') {
+        lastChar = string.length - 2;
+      }
+      string = string.substring(1, lastChar);
+
+      this.setState({ stringEditModalVisible: true });
+      this.refs.stringEditModal.setValue(string);
+    },
+
+    saveStringEditModal: function (newString) {
+      // replace the string on the selected line
+      var line = this.parseLineForStringMatch();
+      var indent = line[1] || '',
+          key = line[2] || '',
+          originalString = line[3],
+          comma = '';
+      if (originalString.substring(originalString.length - 1) === ',') {
+        comma = ',';
+      }
+      this.replaceCurrentLine(indent + key + JSON.stringify(newString) + comma + '\n');
+      this.closeStringEditModal();
+    },
+
+    closeStringEditModal: function () {
+      this.setState({
+        stringEditModalVisible: false
+      });
+    },
+
+    hasErrors: function () {
+      return !_.every(this.getAnnotations(), function (error) {
         return this.isIgnorableError(error.raw);
       }, this);
     },
-    // ------------------
 
-    setEditorValue: function (code, lineNumber) {
+    setReadOnly: function (readonly) {
+      this.editor.setReadOnly(readonly);
+    },
+
+    setValue: function (code, lineNumber) {
       lineNumber = lineNumber ? lineNumber : -1;
       this.editor.setValue(code, lineNumber);
     },
@@ -385,18 +495,145 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
       return this;
     },
 
+    getLine: function (lineNum) {
+      return this.editor.session.getLine(lineNum);
+    },
+
+    getSelectionStart: function () {
+      return this.editor.getSelectionRange().start;
+    },
+
+    getSelectionEnd: function () {
+      return this.editor.getSelectionRange().end;
+    },
+
+    getRowHeight: function () {
+      return this.editor.renderer.layerConfig.lineHeight;
+    },
+
+    isRowExpanded: function (row) {
+      return !this.editor.getSession().isRowFolded(row);
+    },
+
+    documentToScreenRow: function (row) {
+      return this.editor.getSession().documentToScreenRow(row, 0);
+    },
+
+    replaceCurrentLine: function (replacement) {
+      this.editor.getSelection().selectLine();
+      this.editor.insert(replacement);
+      this.editor.getSelection().moveCursorUp();
+    },
+
+    render: function () {
+      return (
+        <div>
+          <div ref="ace" className="js-editor" id={this.props.id}></div>
+          <button ref="stringEditIcon" className="btn string-edit" title="Edit string" disabled={!this.state.stringEditIconVisible}
+            style={this.state.stringEditIconStyle} onClick={this.openStringEditModal}>
+            <i className="icon icon-edit"></i>
+          </button>
+          <StringEditModal
+            ref="stringEditModal"
+            visible={this.state.stringEditModalVisible}
+            onSave={this.saveStringEditModal}
+            onClose={this.closeStringEditModal} />
+        </div>
+      );
+    }
+  });
+
+
+  // this appears when the cursor is over a string. It shows an icon in the gutter that opens the modal.
+  var StringEditModal = React.createClass({
+
+    propTypes: {
+      visible: React.PropTypes.bool.isRequired,
+      onClose: React.PropTypes.func.isRequired,
+      onSave: React.PropTypes.func.isRequired
+    },
+
+    getDefaultProps: function () {
+      return {
+        visible: false,
+        onClose: function () { },
+        onSave: function () { }
+      };
+    },
+
+    componentDidUpdate: function () {
+      var params = (this.props.visible) ? { show: true, backdrop: 'static', keyboard: true } : 'hide';
+      $(this.getDOMNode()).modal(params);
+
+      $(this.getDOMNode()).on('shown.bs.modal', function () {
+        this.editor.focus();
+
+        // re-opening the modal to edit a second string doesn't update the content. This forces the editor to redraw
+        // to show the latest content each time it opens
+        this.editor.resize();
+        this.editor.renderer.updateFull();
+      }.bind(this));
+    },
+
+    // ensure that if the user clicks ESC to close the window, the store gets wind of it
+    componentDidMount: function () {
+      $(this.getDOMNode()).on('hidden.bs.modal', function () {
+        this.props.onClose();
+      }.bind(this));
+
+      this.editor = ace.edit(this.refs.stringEditor.getDOMNode());
+
+      // suppresses an Ace editor error
+      this.editor.$blockScrolling = Infinity;
+
+      this.editor.setShowPrintMargin(false);
+      this.editor.setOption('highlightActiveLine', true);
+      this.editor.setTheme('ace/theme/idle_fingers');
+    },
+
+    setValue: function (val) {
+      this.editor.setValue(val, -1);
+    },
+
+    componentWillUnmount: function () {
+      $(this.getDOMNode()).off('hidden.bs.modal shown.bs.modal');
+    },
+
+    closeModal: function () {
+      this.props.onClose();
+    },
+
+    save: function () {
+      this.props.onSave(this.editor.getValue());
+    },
+
     render: function () {
       return (
-        <div ref="ace" className="js-editor" id={this.props.id}></div>
+        <div className="modal hide fade string-editor-modal" tabIndex="-1">
+          <div className="modal-header">
+            <button type="button" className="close" onClick={this.closeModal} aria-hidden="true">&times;</button>
+            <h3>Edit text <span id="string-edit-header"></span></h3>
+          </div>
+          <div className="modal-body">
+            <div id="modal-error" className="hide alert alert-error"/>
+            <div id="string-editor-wrapper"><div ref="stringEditor" className="doc-code"></div></div>
+          </div>
+          <div className="modal-footer">
+            <button className="cancel-button btn" onClick={this.closeModal}><i className="icon fonticon-circle-x"></i> Cancel</button>
+            <button id="string-edit-save-btn" onClick={this.save} className="btn btn-success save">
+              <i className="fonticon-circle-check"></i> Save
+            </button>
+          </div>
+        </div>
       );
     }
   });
 
 
   // Zen mode editing has very few options:
-  // - It covers the full screen hiding everything else.
-  // - It has two themes: light & dark (choice stored in local storage)
-  // - It has no save option, but has a 1-1 map with a <CodeEditor /> element which gets updated when the user leaves
+  // - It covers the full screen, hiding everything else
+  // - Two themes: light & dark (choice stored in local storage)
+  // - No save option, but has a 1-1 map with a <CodeEditor /> element which gets updated when the user leaves
   // - [Escape] closes the mode, as does clicking the shrink icon at the top right
   var ZenModeOverlay = React.createClass({
     getDefaultProps: function () {
@@ -456,7 +693,7 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
       app.utils.localStorageSet('zenTheme', newTheme);
     },
 
-    setEditorValue: function (code, lineNumber) {
+    setValue: function (code, lineNumber) {
       lineNumber = lineNumber ? lineNumber : -1;
       this.editor.setValue(code, lineNumber);
     },
@@ -850,6 +1087,7 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
     StyledSelect: StyledSelect,
     CodeEditorPanel: CodeEditorPanel,
     CodeEditor: CodeEditor,
+    StringEditModal: StringEditModal,
     ZenModeOverlay: ZenModeOverlay,
     Beautify: Beautify,
     PaddedBorderedBox: PaddedBorderedBox,

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/app/addons/components/tests/stringEditModalSpec.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/components/tests/stringEditModalSpec.react.jsx b/app/addons/components/tests/stringEditModalSpec.react.jsx
new file mode 100644
index 0000000..a0b7539
--- /dev/null
+++ b/app/addons/components/tests/stringEditModalSpec.react.jsx
@@ -0,0 +1,75 @@
+// 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([
+  'api',
+  'addons/components/react-components.react',
+  'testUtils',
+  'react'
+], function (FauxtonAPI, ReactComponents, utils, React) {
+
+  var assert = utils.assert;
+  var TestUtils = React.addons.TestUtils;
+
+  describe('String Edit Modal', function () {
+    var container, modalEl;
+    var stub = function () { };
+
+    beforeEach(function () {
+      container = document.createElement('div');
+    });
+
+    afterEach(function () {
+      React.unmountComponentAtNode(container);
+    });
+
+    describe('event methods called', function () {
+      it('onClose called by top (x)', function () {
+        var spy = sinon.spy();
+        modalEl = TestUtils.renderIntoDocument(
+          <ReactComponents.StringEditModal visible={true} onClose={spy} onSave={stub} />,
+          container
+        );
+        TestUtils.Simulate.click($(modalEl.getDOMNode()).find('.close')[0]);
+        assert.ok(spy.calledOnce);
+      });
+
+      it('onClose called by cancel button', function () {
+        var spy = sinon.spy();
+        modalEl = TestUtils.renderIntoDocument(
+          <ReactComponents.StringEditModal visible={true} onClose={spy} onSave={stub} />,
+          container
+        );
+        TestUtils.Simulate.click($(modalEl.getDOMNode()).find('.cancel-button')[0]);
+        assert.ok(spy.calledOnce);
+      });
+    });
+
+    describe('setValue / onSave', function () {
+      it('setValue ensures same content returns on saving', function () {
+        var spy = sinon.spy();
+        modalEl = TestUtils.renderIntoDocument(
+          <ReactComponents.StringEditModal visible={true} onClose={stub} onSave={spy} />,
+          container
+        );
+
+        var string = "a string!";
+
+        modalEl.setValue(string);
+        TestUtils.Simulate.click($(modalEl.getDOMNode()).find('#string-edit-save-btn')[0]);
+        assert.ok(spy.calledOnce);
+        assert.ok(spy.calledWith(string));
+      });
+    });
+
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/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
new file mode 100644
index 0000000..3967a01
--- /dev/null
+++ b/app/addons/documents/assets/less/doc-editor.less
@@ -0,0 +1,181 @@
+// 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.
+
+@import "../../../../../assets/less/mixins.less";
+
+
+#dashboard.doc-editor-page {
+  background-color: #4d4d4d;
+
+  #dashboard-content {
+    padding-bottom: 0;
+    top: 0;
+  }
+}
+
+#editor-container {
+  height: 100%;
+  &>div:not(.loading-lines), &>div>div {
+    height: 100%;
+  }
+}
+
+.doc-editor-page {
+  border-left: none;
+
+  .loading-lines {
+    margin-top: 30px;
+    div {
+      background-color: white;
+    }
+  }
+
+  .fixed-header {
+    height: 65px;
+    .box-shadow(none);
+  }
+
+  #breadcrumbs {
+    white-space: nowrap;
+  }
+
+  #doc-editor-actions-panel {
+    position: absolute;
+    top: 64px;
+    right: 0;
+    left: 0;
+    width: 100%;
+    background-color: #f1f1f1;
+    height: 61px;
+  }
+
+  #editor-container {
+    margin-top: 0;
+    font-size: 13px;
+
+    .ace_editor {
+      line-height: 22px;
+    }
+  }
+
+  .doc-actions-left {
+    float: left;
+    padding: 9px 0;
+    button {
+      margin: 0 10px 0 30px;
+    }
+    div {
+      display: inline-block;
+    }
+  }
+
+  .cancel-button {
+    font-size: 14px;
+  }
+
+  .bgEditorGutter {
+    width: 49px;
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    background-color: #3b3b3b;
+  }
+
+  .panel-button {
+    border: 0;
+    background-color: #f1f1f1;
+    padding: 11px;
+    color: #555555;
+
+    span:first-of-type {
+      margin-left: 4px;
+    }
+
+    .icon {
+      font-size: 18px;
+    }
+  }
+  .panel-section {
+    border-left: 1px solid #cccccc;
+    text-align: center;
+    padding: 9px 0;
+    display: inline-block;
+
+    &.open .dropdown-toggle, &:active {
+      box-shadow: none;
+      background-color: inherit;
+    }
+  }
+  .alignRight {
+    text-align: right;
+    font-size: 0; // prevents whitespace gaps between elements
+  }
+  .row-fluid.content-area {
+    background-color: #4d4d4d;
+  }
+
+  // overriding the ace editor inline styles
+  .ace_gutter-layer {
+    min-width: 49px;
+  }
+  .ace_gutter-cell {
+    min-width: 49px;
+  }
+
+  .ace_marker-layer .ace_bracket {
+    margin: 0;
+    border: 1px solid #999999;
+  }
+
+  // hide the labels on the buttons when the screen is shrunk too small,
+  @media screen and (max-width: 1000px) {
+    .panel-button span {
+      display: none;
+    }
+  }
+
+  // hides the API Url header link when the page is too small (prevents wrapping)
+  @media screen and (max-width: 835px) {
+    #api-navbar {
+      display: none;
+    }
+  }
+
+  #dashboard-content .code-region {
+    overflow-y: hidden;
+    position: absolute;
+    top: 125px;
+    height: auto;
+    width: 100%;
+    padding: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+  }
+}
+
+.upload-file-modal input[type="file"] {
+  margin-top: 10px;
+  height: inherit;
+  line-height: inherit;
+}
+
+.view-attachments-section .dropdown-menu {
+  max-width: 540px;
+  text-align: left;
+  overflow: hidden;
+}
+
+#editor-container div.string-editor-modal {
+  height: inherit;
+  max-height: 600px;
+}

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/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 4d0725d..9ece2e5 100644
--- a/app/addons/documents/assets/less/documents.less
+++ b/app/addons/documents/assets/less/documents.less
@@ -18,7 +18,7 @@
 @import "changes.less";
 @import "sidenav.less";
 @import "index-results.less";
-
+@import "doc-editor.less";
 @import "header.less";
 
 button.beautify {
@@ -107,6 +107,10 @@ button.string-edit[disabled] {
 #string-editor-wrapper {
   height: 500px;
   width: 100%;
+
+  &>div {
+    height: 100%;
+  }
 }
 
 #keys-input {

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/app/addons/documents/assets/less/view-editor.less
----------------------------------------------------------------------
diff --git a/app/addons/documents/assets/less/view-editor.less b/app/addons/documents/assets/less/view-editor.less
index 8421eff..ed31d40 100644
--- a/app/addons/documents/assets/less/view-editor.less
+++ b/app/addons/documents/assets/less/view-editor.less
@@ -16,7 +16,6 @@
   .define-view {
     padding-bottom: 70px;
   }
-
   .define-view {
     .help-link {
       margin-left: 3px;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/app/addons/documents/doc-editor/actions.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/doc-editor/actions.js b/app/addons/documents/doc-editor/actions.js
new file mode 100644
index 0000000..04fc4a9
--- /dev/null
+++ b/app/addons/documents/doc-editor/actions.js
@@ -0,0 +1,239 @@
+// 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',
+  'api',
+  'addons/documents/doc-editor/actiontypes'
+],
+function (app, FauxtonAPI, ActionTypes) {
+
+  var xhr;
+
+  function initDocEditor (params) {
+    var doc = params.doc;
+
+    // ensure a clean slate
+    FauxtonAPI.dispatch({ type: ActionTypes.RESET_DOC });
+
+    doc.fetch().then(function () {
+      FauxtonAPI.dispatch({
+        type: ActionTypes.DOC_LOADED,
+        options: {
+          doc: doc
+        }
+      });
+
+      if (params.onLoaded) {
+        params.onLoaded();
+      }
+    }, function (xhr, reason, msg) {
+      if (xhr.status === 404) {
+        errorNotification('The document does not exist.');
+        FauxtonAPI.navigate(FauxtonAPI.urls('allDocs', 'app', params.database.id, ''));
+      }
+    });
+  }
+
+  function saveDoc (doc, isValidDoc, onSave) {
+    var databaseLink = doc.database.safeID();
+
+    if (isValidDoc) {
+      FauxtonAPI.addNotification({
+        msg: 'Saving document.',
+        clear: true
+      });
+
+      doc.save().then(function () {
+        onSave(doc.prettyJSON());
+        FauxtonAPI.navigate(FauxtonAPI.urls('document', 'app', databaseLink, doc.id));
+      }).fail(function (xhr) {
+        FauxtonAPI.addNotification({
+          msg: 'Save failed: ' + JSON.parse(xhr.responseText).reason,
+          type: 'error',
+          fade: false,
+          clear: true
+        });
+      });
+
+    } else if (doc.validationError && doc.validationError === 'Cannot change a documents id.') {
+      errorNotification('You cannot edit the _id of an existing document. Try this: Click \'Clone Document\', then change the _id on the clone before saving.');
+      delete doc.validationError;
+    } else {
+      errorNotification('Please fix the JSON errors and try saving again.');
+    }
+  }
+
+  function showDeleteDocModal () {
+    FauxtonAPI.dispatch({ type: ActionTypes.SHOW_DELETE_DOC_CONFIRMATION_MODAL });
+  }
+
+  function hideDeleteDocModal () {
+    FauxtonAPI.dispatch({ type: ActionTypes.HIDE_DELETE_DOC_CONFIRMATION_MODAL });
+  }
+
+  function deleteDoc (doc) {
+    var databaseName = doc.database.id;
+    doc.destroy().then(function () {
+      FauxtonAPI.addNotification({
+        msg: 'Your document has been successfully deleted.',
+        clear: true
+      });
+      FauxtonAPI.navigate(FauxtonAPI.urls('allDocs', 'app', databaseName, ''));
+    }, function () {
+      FauxtonAPI.addNotification({
+        msg: 'Failed to delete your document!',
+        type: 'error',
+        clear: true
+      });
+    });
+  }
+
+  function showCloneDocModal () {
+    FauxtonAPI.dispatch({ type: ActionTypes.SHOW_CLONE_DOC_MODAL });
+  }
+
+  function hideCloneDocModal () {
+    FauxtonAPI.dispatch({ type: ActionTypes.HIDE_CLONE_DOC_MODAL });
+  }
+
+  function cloneDoc (newId) {
+    var isDDoc = newId.match(/^_design\//),
+      removeDDocID = newId.replace(/^_design\//, ''),
+      encodedID = isDDoc ? '_design/' + app.utils.safeURLName(removeDDocID) : app.utils.safeURLName(newId);
+
+    this.hideCloneDocModal();
+    FauxtonAPI.triggerRouteEvent('duplicateDoc', encodedID);
+  }
+
+  function showUploadModal () {
+    FauxtonAPI.dispatch({ type: ActionTypes.SHOW_UPLOAD_MODAL });
+  }
+
+  function hideUploadModal () {
+    FauxtonAPI.dispatch({ type: ActionTypes.HIDE_UPLOAD_MODAL });
+  }
+
+  function uploadAttachment (params) {
+    if (params.files.length === 0) {
+      FauxtonAPI.dispatch({
+        type: ActionTypes.FILE_UPLOAD_ERROR,
+        options: {
+          error: 'Please select a file to be uploaded.'
+        }
+      });
+      return;
+    }
+
+    FauxtonAPI.dispatch({ type: ActionTypes.START_FILE_UPLOAD });
+
+    // store the xhr in parent scope to allow us to cancel any uploads if the user closes the modal
+    xhr = $.ajaxSettings.xhr();
+
+    var file = params.files[0];
+    $.ajax({
+      url: params.doc.url() + '/' + file.name + '?rev=' + params.rev,
+      type: 'PUT',
+      data: file,
+      contentType: file.type,
+      processData: false,
+      xhrFields: {
+        withCredentials: true
+      },
+      xhr: function () {
+        xhr.upload.onprogress = function (evt) {
+          var percentComplete = evt.loaded / evt.total * 100;
+          FauxtonAPI.dispatch({
+            type: ActionTypes.SET_FILE_UPLOAD_PERCENTAGE,
+            options: {
+              percent: percentComplete
+            }
+          });
+        };
+        return xhr;
+      },
+      success: function () {
+
+        // re-initialize the document editor. Only announce it's been updated when
+        initDocEditor({
+          doc: params.doc,
+          onLoaded: function () {
+            FauxtonAPI.dispatch({ type: ActionTypes.FILE_UPLOAD_SUCCESS });
+            FauxtonAPI.addNotification({
+              msg: 'Document saved successfully.',
+              type: 'success',
+              clear: true
+            });
+          }.bind(this)
+        });
+
+      },
+      error: function (resp) {
+        // cancelled uploads throw an ajax error but they don't contain a response. We don't want to publish an error
+        // event in those cases
+        if (_.isEmpty(resp.responseText)) {
+          return;
+        }
+        FauxtonAPI.dispatch({
+          type: ActionTypes.FILE_UPLOAD_ERROR,
+          options: {
+            error: JSON.parse(resp.responseText).reason
+          }
+        });
+      }
+    });
+  }
+
+  function cancelUpload () {
+    xhr.abort();
+  }
+
+  function resetUploadModal () {
+    FauxtonAPI.dispatch({ type: ActionTypes.RESET_UPLOAD_MODAL });
+  }
+
+
+  // helpers
+
+  function errorNotification (msg) {
+    FauxtonAPI.addNotification({
+      msg: msg,
+      type: 'error',
+      clear: true
+    });
+  }
+
+  return {
+    initDocEditor: initDocEditor,
+    saveDoc: saveDoc,
+
+    // clone doc
+    showCloneDocModal: showCloneDocModal,
+    hideCloneDocModal: hideCloneDocModal,
+    cloneDoc: cloneDoc,
+
+    // delete doc
+    showDeleteDocModal: showDeleteDocModal,
+    hideDeleteDocModal: hideDeleteDocModal,
+    deleteDoc: deleteDoc,
+
+    // upload modal
+    showUploadModal: showUploadModal,
+    hideUploadModal: hideUploadModal,
+    uploadAttachment: uploadAttachment,
+    cancelUpload: cancelUpload,
+    resetUploadModal: resetUploadModal
+  };
+
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/app/addons/documents/doc-editor/actiontypes.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/doc-editor/actiontypes.js b/app/addons/documents/doc-editor/actiontypes.js
new file mode 100644
index 0000000..e669155
--- /dev/null
+++ b/app/addons/documents/doc-editor/actiontypes.js
@@ -0,0 +1,31 @@
+// 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([], function () {
+  return {
+    RESET_DOC: 'RESET_DOC',
+    DOC_LOADED: 'DOC_LOADED',
+    SHOW_CLONE_DOC_MODAL: 'SHOW_CLONE_DOC_MODAL',
+    HIDE_CLONE_DOC_MODAL: 'HIDE_CLONE_DOC_MODAL',
+    SHOW_DELETE_DOC_CONFIRMATION_MODAL: 'SHOW_DELETE_DOC_CONFIRMATION_MODAL',
+    HIDE_DELETE_DOC_CONFIRMATION_MODAL: 'HIDE_DELETE_DOC_CONFIRMATION_MODAL',
+    SHOW_UPLOAD_MODAL: 'SHOW_UPLOAD_MODAL',
+    HIDE_UPLOAD_MODAL: 'HIDE_UPLOAD_MODAL',
+    RESET_UPLOAD_MODAL: 'RESET_UPLOAD_MODAL',
+    SHOW_STRING_EDIT_MODAL: 'SHOW_STRING_EDIT_MODAL',
+    HIDE_STRING_EDIT_MODAL: 'HIDE_STRING_EDIT_MODAL',
+    FILE_UPLOAD_SUCCESS: 'FILE_UPLOAD_SUCCESS',
+    FILE_UPLOAD_ERROR: 'FILE_UPLOAD_ERROR',
+    START_FILE_UPLOAD: 'START_FILE_UPLOAD',
+    SET_FILE_UPLOAD_PERCENTAGE: 'SET_FILE_UPLOAD_PERCENTAGE'
+  };
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/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
new file mode 100644
index 0000000..ddc7fa1
--- /dev/null
+++ b/app/addons/documents/doc-editor/components.react.jsx
@@ -0,0 +1,498 @@
+define([
+  'api',
+  'app',
+  'react',
+  'addons/documents/doc-editor/actions',
+  'addons/documents/doc-editor/stores',
+  'addons/fauxton/components.react',
+  'addons/components/react-components.react',
+  'helpers'
+], function (FauxtonAPI, app, React, Actions, Stores, FauxtonComponents, GeneralComponents, Helpers) {
+
+  var store = Stores.docEditorStore;
+
+
+  var DocEditorController = React.createClass({
+
+    getInitialState: function () {
+      return this.getStoreState();
+    },
+
+    getStoreState: function () {
+      return {
+        isLoading: store.isLoading(),
+        doc: store.getDoc(),
+        cloneDocModalVisible: store.isCloneDocModalVisible(),
+        uploadModalVisible: store.isUploadModalVisible(),
+        deleteDocModalVisible: store.isDeleteDocModalVisible(),
+        numFilesUploaded: store.getNumFilesUploaded(),
+      };
+    },
+
+    getDefaultProps: function () {
+      return {
+        database: {},
+        previousPage: '',
+        isNewDoc: false
+      };
+    },
+
+    getCodeEditor: function () {
+      if (this.state.isLoading) {
+        return (<GeneralComponents.LoadLines />);
+      }
+
+      var code = JSON.stringify(this.state.doc.attributes, null, '  ');
+      var editorCommands = [{
+        name: 'save',
+        bindKey: { win: 'Ctrl-S', mac: 'Ctrl-S' },
+        exec: this.saveDoc
+      }];
+
+      return (
+        <GeneralComponents.CodeEditor
+          id="doc-editor"
+          ref="docEditor"
+          defaultCode={code}
+          mode="json"
+          autoFocus={true}
+          editorCommands={editorCommands}
+          notifyUnsavedChanges={true}
+          stringEditModalEnabled={true}
+          change={this.onChangeDoc} />
+      );
+    },
+
+    onChangeDoc: function (doc) {
+      // super ugly, but necessary. This tells the user they can't delete the _id or _rev fields as they type and actually
+      // prevents it from being removed in the editable doc
+      var json;
+      try {
+        json = JSON.parse(doc);
+      } catch (exception) {
+        return;
+      }
+
+      var keyChecked = ['_id'];
+      if (this.state.doc.get('_rev')) {
+        keyChecked.push('_rev');
+      }
+      if (_.isEmpty(_.difference(keyChecked, _.keys(json)))) {
+        return;
+      }
+
+      this.getEditor().setReadOnly(true);
+      setTimeout(function () { this.getEditor().setReadOnly(false); }.bind(this), 400);
+
+      // extend ensures _id stays at the top of the editor doc
+      json = _.extend({ _id: this.state.doc.id, _rev: this.state.doc.get('_rev') }, json);
+      this.getEditor().setValue(JSON.stringify(json, null, '  '));
+
+      FauxtonAPI.addNotification({
+        type: 'error',
+        msg: "Cannot remove a document's id or revision.",
+        clear: true
+      });
+    },
+
+    componentDidMount: function () {
+      store.on('change', this.onChange, this);
+    },
+
+    componentWillUnmount: function () {
+      store.off('change', this.onChange);
+    },
+
+    // whenever a file is uploaded, reset the editor
+    componentWillUpdate: function (nextProps, nextState) {
+      if (this.state.numFilesUploaded !== nextState.numFilesUploaded) {
+        this.getEditor().setValue(JSON.stringify(nextState.doc.attributes, null, '  '));
+      }
+    },
+
+    onChange: function () {
+      if (this.isMounted()) {
+        this.setState(this.getStoreState());
+      }
+    },
+
+    saveDoc: function () {
+      Actions.saveDoc(this.state.doc, this.checkDocIsValid(), this.onSaveComplete);
+    },
+
+    onSaveComplete: function (json) {
+      this.getEditor().setValue(json);
+      this.getEditor().clearChanges();
+
+      // the save action updates the doc. This ensures the button row then shows the appropriate buttons
+      this.forceUpdate();
+    },
+
+    hideDeleteDocModal: function () {
+      Actions.hideDeleteDocModal();
+    },
+
+    deleteDoc: function () {
+      Actions.hideDeleteDocModal();
+      Actions.deleteDoc(this.state.doc);
+    },
+
+    getEditor: function () {
+      return (this.refs.docEditor) ? this.refs.docEditor.getEditor() : null;
+    },
+
+    checkDocIsValid: function () {
+      if (this.getEditor().hasErrors()) {
+        return false;
+      }
+      var json = JSON.parse(this.getEditor().getValue());
+      this.state.doc.clear().set(json, { validate: true });
+
+      return !this.state.doc.validationError;
+    },
+
+    clearChanges: function () {
+      this.refs.docEditor.clearChanges();
+    },
+
+    getButtonRow: function () {
+      if (this.props.isNewDoc) {
+        return false;
+      }
+      return (
+        <div>
+          <AttachmentsPanelButton doc={this.state.doc} isLoading={this.state.isLoading} />
+          <div className="doc-editor-extension-icons"></div>
+          <PanelButton 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>
+      );
+    },
+
+    render: function () {
+      return (
+        <div>
+          <div id="doc-editor-actions-panel">
+            <div className="doc-actions-left">
+              <button className="save-doc btn btn-success save" type="button" onClick={this.saveDoc}>
+                <i className="icon fonticon-ok-circled"></i> Save
+              </button>
+              <div>
+                <a href={this.props.previousPage} className="js-back cancel-button">Cancel</a>
+              </div>
+            </div>
+            <div className="alignRight">
+              {this.getButtonRow()}
+            </div>
+          </div>
+
+          <div className="code-region">
+            <div className="bgEditorGutter"></div>
+            <div id="editor-container" className="doc-code">{this.getCodeEditor()}</div>
+          </div>
+
+          <UploadModal
+            ref="uploadModal"
+            visible={this.state.uploadModalVisible}
+            doc={this.state.doc} />
+          <CloneDocModal
+            visible={this.state.cloneDocModalVisible}
+            onSubmit={this.clearChanges} />
+          <FauxtonComponents.ConfirmationModal
+            visible={this.state.deleteDocModalVisible}
+            text="Are you sure you want to delete this document?"
+            onClose={this.hideDeleteDocModal}
+            onSubmit={this.deleteDoc} />
+        </div>
+      );
+    }
+  });
+
+  var AttachmentsPanelButton = React.createClass({
+
+    propTypes: {
+      isLoading: React.PropTypes.bool.isRequired,
+      doc: React.PropTypes.object
+    },
+
+    getDefaultProps: function () {
+      return {
+        isLoading: true,
+        doc: {}
+      };
+    },
+
+    getAttachmentList: function () {
+      var docBaseURL = this.props.doc.url();
+      return _.map(this.props.doc.get('_attachments'), function (item, filename) {
+        var url = docBaseURL + '/' + app.utils.safeURLName(filename);
+        return (
+          <li key={filename}>
+            <a href={url} target="_blank" data-bypass="true"> <strong>{filename}</strong> -
+              <span>{item.content_type}, {Helpers.formatSize(item.length)}</span>
+            </a>
+          </li>
+        );
+      });
+    },
+
+    render: function () {
+      if (this.props.isLoading || !this.props.doc.get('_attachments')) {
+        return false;
+      }
+
+      return (
+        <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>{' '}
+            <span className="caret"></span>
+          </button>
+          <ul className="dropdown-menu" role="menu" aria-labelledby="view-attachments-menu">
+            {this.getAttachmentList()}
+          </ul>
+        </div>
+      );
+    }
+  });
+
+
+  var PanelButton = React.createClass({
+    propTypes: {
+      title: React.PropTypes.string.isRequired,
+      onClick: React.PropTypes.func.isRequired
+    },
+
+    getDefaultProps: function () {
+      return {
+        title: '',
+        iconClass: '',
+        onClick: function () { }
+      };
+    },
+
+    render: function () {
+      var iconClasses = 'icon ' + this.props.iconClass;
+      return (
+        <div className="panel-section">
+          <button className="panel-button upload" title={this.props.title} onClick={this.props.onClick}>
+            <i className={iconClasses}></i>
+            <span>{this.props.title}</span>
+          </button>
+        </div>
+      );
+    }
+  });
+
+
+  var UploadModal = React.createClass({
+    propTypes: {
+      visible: React.PropTypes.bool.isRequired,
+      doc: React.PropTypes.object
+    },
+
+    getInitialState: function () {
+      return this.getStoreState();
+    },
+
+    getStoreState: function () {
+      return {
+        inProgress: store.isUploadInProgress(),
+        loadPercentage: store.getUploadLoadPercentage(),
+        errorMessage: store.getFileUploadErrorMsg()
+      };
+    },
+
+    componentDidUpdate: function () {
+      var params = (this.props.visible) ? { show: true, backdrop: 'static', keyboard: true } : 'hide';
+      $(this.getDOMNode()).modal(params);
+    },
+
+    // ensure that if the user clicks ESC to close the window, the store gets wind of it
+    componentDidMount: function () {
+      $(this.getDOMNode()).on('hidden.bs.modal', function () {
+        Actions.hideUploadModal();
+      });
+    },
+
+    componentWillUnmount: function () {
+      $(this.getDOMNode()).off('hidden.bs.modal');
+    },
+
+    closeModal: function () {
+      if (this.state.inProgress) {
+        Actions.cancelUpload();
+      }
+      Actions.hideUploadModal();
+
+      // timeout needed to only clear it once the animate close effect is done, otherwise the user sees it reset
+      // as it closes, which looks bad
+      setTimeout(function () {
+        Actions.resetUploadModal();
+        this.refs.uploadForm.getDOMNode().reset();
+      }.bind(this), 1000);
+    },
+
+    upload: function () {
+      Actions.uploadAttachment({
+        doc: this.props.doc,
+        rev: this.props.doc.get('_rev'),
+        files: $(this.refs.attachments.getDOMNode())[0].files
+      });
+    },
+
+    render: function () {
+      var errorClasses = 'alert alert-error';
+      if (this.state.errorMessage === '') {
+        errorClasses += ' hide';
+      }
+      var loadIndicatorClasses = 'progress progress-info';
+      if (!this.state.inProgress) {
+        loadIndicatorClasses += ' hide';
+      }
+
+      return (
+        <div className="modal hide fade upload-file-modal" tabIndex="-1" data-js-visible={this.props.visible}>
+          <div className="modal-header">
+            <button type="button" className="close" onClick={this.closeModal} aria-hidden="true">&times;</button>
+            <h3>Upload Attachment</h3>
+          </div>
+          <div className="modal-body">
+            <div className={errorClasses}>{this.state.errorMessage}</div>
+
+            <div>
+              <form ref="uploadForm" className="form" method="post">
+                <p>
+                  Please select the file you want to upload as an attachment to this document. This creates a new
+                  revision of the document, so it's not necessary to save after uploading.
+                </p>
+                <input ref="attachments" type="file" name="_attachments" />
+                <br />
+              </form>
+
+              <div ref="loadIndicator" className={loadIndicatorClasses}>
+                <div className="bar" style={{ width: this.state.loadPercentage + '%'}}></div>
+              </div>
+            </div>
+
+          </div>
+          <div className="modal-footer">
+             <button href="#" data-bypass="true" className="btn" onClick={this.closeModal}>
+              <i className="icon fonticon-cancel-circled"></i> Cancel
+            </button>
+            <button href="#" id="upload-btn" data-bypass="true" className="btn btn-success save" onClick={this.upload}>
+              <i className="icon fonticon-ok-circled"></i> Upload
+            </button>
+          </div>
+        </div>
+      );
+    }
+  });
+
+
+  var CloneDocModal = React.createClass({
+    propTypes: {
+      visible: React.PropTypes.bool.isRequired
+    },
+
+    getInitialState: function () {
+      return {
+        uuid: null
+      };
+    },
+
+    cloneDoc: function () {
+      if (this.props.onSubmit) {
+        this.props.onSubmit();
+      }
+      Actions.cloneDoc(this.state.uuid);
+    },
+
+    componentDidUpdate: function () {
+      if (this.state.uuid === null) {
+        var uuid = new FauxtonAPI.UUID();
+        uuid.fetch().then(function () {
+          this.setState({ uuid: uuid.next() });
+        }.bind(this));
+        return;
+      }
+
+      var params = (this.props.visible) ? { show: true, backdrop: 'static', keyboard: true } : 'hide';
+      $(this.getDOMNode()).modal(params);
+      this.clearEvents();
+
+      // ensure that if the user clicks ESC to close the window, the store gets wind of it
+      $(this.getDOMNode()).on('hidden.bs.modal', function () {
+        Actions.hideCloneDocModal();
+      });
+
+      $(this.getDOMNode()).on('shown.bs.modal', function () {
+        this.focus();
+      }.bind(this));
+    },
+
+    focus: function () {
+      $(this.refs.newDocId.getDOMNode()).focus();
+    },
+
+    componentWillUnmount: function () {
+      this.clearEvents();
+    },
+
+    clearEvents: function () {
+      if (this.refs.newDocId) {
+        $(this.refs.newDocId.getDOMNode()).off('shown.bs.modal hidden.bs.modal');
+      }
+    },
+
+    closeModal: function () {
+      Actions.hideCloneDocModal();
+    },
+
+    docIDChange: function (e) {
+      this.setState({ uuid: e.target.value });
+    },
+
+    render: function () {
+      if (this.state.uuid === null) {
+        return false;
+      }
+
+      return (
+        <div className="modal hide fade clone-doc-modal" data-js-visible={this.props.visible} tabIndex="-1">
+          <div className="modal-header">
+            <button type="button" className="close" onClick={this.closeModal} aria-hidden="true">&times;</button>
+            <h3>Clone Document</h3>
+          </div>
+          <div className="modal-body">
+            <form className="form" method="post">
+              <p>
+                Set new document's ID:
+              </p>
+              <input ref="newDocId" type="text" className="input-block-level" onChange={this.docIDChange} value={this.state.uuid} />
+            </form>
+          </div>
+          <div className="modal-footer">
+            <button className="btn" onClick={this.closeModal}>
+              <i className="icon fonticon-cancel-circled"></i> Cancel
+            </button>
+            <button className="btn btn-success save" onClick={this.cloneDoc}>
+              <i className="fonticon-ok-circled"></i> Clone
+            </button>
+          </div>
+        </div>
+      );
+    }
+  });
+
+
+  return {
+    DocEditorController: DocEditorController,
+    AttachmentsPanelButton: AttachmentsPanelButton,
+    UploadModal: UploadModal,
+    CloneDocModal: CloneDocModal
+  };
+
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/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
new file mode 100644
index 0000000..7cdff19
--- /dev/null
+++ b/app/addons/documents/doc-editor/stores.js
@@ -0,0 +1,201 @@
+// 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([
+  'api',
+  'addons/documents/doc-editor/actiontypes'
+],
+
+function (FauxtonAPI, ActionTypes) {
+  var Stores = {};
+
+  Stores.DocEditorStore = FauxtonAPI.Store.extend({
+    initialize: function () {
+      this.reset();
+    },
+
+    reset: function () {
+      this._doc = null;
+      this._isLoading = true;
+      this._cloneDocModalVisible = false;
+      this._deleteDocModalVisible = false;
+      this._uploadModalVisible = false;
+
+      // file upload-related fields
+      this._numFilesUploaded = 0;
+      this._fileUploadErrorMsg = '';
+      this._uploadInProgress = false;
+      this._fileUploadLoadPercentage = 0;
+    },
+
+    isLoading: function () {
+      return this._isLoading;
+    },
+
+    docLoaded: function (options) {
+      this._isLoading = false;
+      this._doc = options.doc;
+    },
+
+    getDoc: function () {
+      return this._doc;
+    },
+
+    isCloneDocModalVisible: function () {
+      return this._cloneDocModalVisible;
+    },
+
+    showCloneDocModal: function () {
+      this._cloneDocModalVisible = true;
+    },
+
+    hideCloneDocModal: function () {
+      this._cloneDocModalVisible = false;
+    },
+
+    isDeleteDocModalVisible: function () {
+      return this._deleteDocModalVisible;
+    },
+
+    showDeleteDocModal: function () {
+      this._deleteDocModalVisible = true;
+    },
+
+    hideDeleteDocModal: function () {
+      this._deleteDocModalVisible = false;
+    },
+
+    isUploadModalVisible: function () {
+      return this._uploadModalVisible;
+    },
+
+    showUploadModal: function () {
+      this._uploadModalVisible = true;
+    },
+
+    hideUploadModal: function () {
+      this._uploadModalVisible = false;
+    },
+
+    getNumFilesUploaded: function () {
+      return this._numFilesUploaded;
+    },
+
+    getFileUploadErrorMsg: function () {
+      return this._fileUploadErrorMsg;
+    },
+
+    setFileUploadErrorMsg: function (error) {
+      this._uploadInProgress = false;
+      this._fileUploadLoadPercentage = 0;
+      this._fileUploadErrorMsg = error;
+    },
+
+    isUploadInProgress: function () {
+      return this._uploadInProgress;
+    },
+
+    getUploadLoadPercentage: function () {
+      return this._fileUploadLoadPercentage;
+    },
+
+    resetUploadModal: function () {
+      this._uploadInProgress = false;
+      this._fileUploadLoadPercentage = 0;
+      this._fileUploadErrorMsg = '';
+    },
+
+    startFileUpload: function () {
+      this._uploadInProgress = true;
+      this._fileUploadLoadPercentage = 0;
+      this._fileUploadErrorMsg = '';
+    },
+
+    dispatch: function (action) {
+      switch (action.type) {
+        case ActionTypes.RESET_DOC:
+          this.reset();
+        break;
+
+        case ActionTypes.DOC_LOADED:
+          this.docLoaded(action.options);
+          this.triggerChange();
+        break;
+
+        case ActionTypes.SHOW_CLONE_DOC_MODAL:
+          this.showCloneDocModal();
+          this.triggerChange();
+        break;
+
+        case ActionTypes.HIDE_CLONE_DOC_MODAL:
+          this.hideCloneDocModal();
+          this.triggerChange();
+        break;
+
+        case ActionTypes.SHOW_DELETE_DOC_CONFIRMATION_MODAL:
+          this.showDeleteDocModal();
+          this.triggerChange();
+        break;
+
+        case ActionTypes.HIDE_DELETE_DOC_CONFIRMATION_MODAL:
+          this.hideDeleteDocModal();
+          this.triggerChange();
+        break;
+
+        case ActionTypes.SHOW_UPLOAD_MODAL:
+          this.showUploadModal();
+          this.triggerChange();
+        break;
+
+        case ActionTypes.HIDE_UPLOAD_MODAL:
+          this.hideUploadModal();
+          this.triggerChange();
+        break;
+
+        case ActionTypes.FILE_UPLOAD_SUCCESS:
+          this._numFilesUploaded++;
+          this.triggerChange();
+        break;
+
+        case ActionTypes.FILE_UPLOAD_ERROR:
+          this.setFileUploadErrorMsg(action.options.error);
+          this.triggerChange();
+        break;
+
+        case ActionTypes.RESET_UPLOAD_MODAL:
+          this.resetUploadModal();
+          this.triggerChange();
+        break;
+
+        case ActionTypes.START_FILE_UPLOAD:
+          this.startFileUpload();
+          this.triggerChange();
+        break;
+
+        case ActionTypes.SET_FILE_UPLOAD_PERCENTAGE:
+          this._fileUploadLoadPercentage = action.options.percent;
+          this.triggerChange();
+        break;
+
+        default:
+        return;
+        // do nothing
+      }
+    }
+
+  });
+
+  Stores.docEditorStore = new Stores.DocEditorStore();
+  Stores.docEditorStore.dispatchToken = FauxtonAPI.dispatcher.register(Stores.docEditorStore.dispatch);
+
+  return Stores;
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/app/addons/documents/doc-editor/tests/doc-editor.componentsSpec.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/documents/doc-editor/tests/doc-editor.componentsSpec.react.jsx b/app/addons/documents/doc-editor/tests/doc-editor.componentsSpec.react.jsx
new file mode 100644
index 0000000..b1720f2
--- /dev/null
+++ b/app/addons/documents/doc-editor/tests/doc-editor.componentsSpec.react.jsx
@@ -0,0 +1,187 @@
+// 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([
+  'app',
+  'api',
+  'react',
+  'addons/documents/resources',
+  'addons/documents/doc-editor/components.react',
+  'addons/documents/doc-editor/stores',
+  'addons/documents/doc-editor/actions',
+  'addons/documents/doc-editor/actiontypes',
+  'testUtils'
+], function (app, FauxtonAPI, React, Documents, Components, Stores, Actions, ActionTypes, utils) {
+  FauxtonAPI.router = new FauxtonAPI.Router([]);
+
+  var assert = utils.assert;
+  var TestUtils = React.addons.TestUtils;
+  var docJSON = {
+    _id: '_design/test-doc',
+    views: {
+      'test-view': {
+        map: 'function () {};'
+      }
+    }
+  };
+
+  var docWithAttachmentsJSON = {
+    _id: '_design/test-doc',
+    _rev: '12345',
+    blah: {
+      whatever: {
+        something: 1
+      }
+    },
+    _attachments: {
+      "one.png": {
+        "content-type": "images/png",
+        "length": 100
+      },
+      "one.zip": {
+        "content-type": "application/zip",
+        "length": 111100
+      }
+    }
+  };
+
+  var database = {
+    safeID: function () { return 'id'; }
+  };
+
+
+  describe('DocEditorController', function () {
+    var container;
+
+    beforeEach(function () {
+      container = document.createElement('div');
+    });
+
+    afterEach(function () {
+      React.unmountComponentAtNode(container);
+    });
+
+    it('loading indicator appears on load', function () {
+      var el = TestUtils.renderIntoDocument(<Components.DocEditorController />, container);
+      assert.equal($(el.getDOMNode()).find('.loading-lines').length, 1);
+    });
+
+    it('new docs do not show the button row', function () {
+      var el = TestUtils.renderIntoDocument(<Components.DocEditorController isNewDoc={true} database={database} />, container);
+
+      var doc = new Documents.Doc(docJSON, { database: database });
+      FauxtonAPI.dispatch({
+        type: ActionTypes.DOC_LOADED,
+        options: {
+          doc: doc
+        }
+      });
+
+      assert.equal($(el.getDOMNode()).find('.loading-lines').length, 0);
+      assert.equal($(el.getDOMNode()).find('.icon-circle-arrow-up').length, 0);
+      assert.equal($(el.getDOMNode()).find('.icon-repeat').length, 0);
+      assert.equal($(el.getDOMNode()).find('.icon-trash').length, 0);
+    });
+
+    it('view attachments button does not appear with no attachments', function () {
+      var el = TestUtils.renderIntoDocument(<Components.DocEditorController database={database} />, container);
+
+      var doc = new Documents.Doc(docJSON, { database: database });
+      FauxtonAPI.dispatch({
+        type: ActionTypes.DOC_LOADED,
+        options: {
+          doc: doc
+        }
+      });
+      assert.equal($(el.getDOMNode()).find('.view-attachments-section').length, 0);
+    });
+
+    it('view attachments button shows up when the doc has attachments', function () {
+      var el = TestUtils.renderIntoDocument(<Components.DocEditorController database={database} />, container);
+
+      var doc = new Documents.Doc(docWithAttachmentsJSON, { database: database });
+      FauxtonAPI.dispatch({
+        type: ActionTypes.DOC_LOADED,
+        options: {
+          doc: doc
+        }
+      });
+      assert.equal($(el.getDOMNode()).find('.view-attachments-section').length, 1);
+    });
+
+    it('view attachments dropdown contains right number of docs', function () {
+      var el = TestUtils.renderIntoDocument(<Components.DocEditorController database={database} />, container);
+
+      var doc = new Documents.Doc(docWithAttachmentsJSON, { database: database });
+      FauxtonAPI.dispatch({
+        type: ActionTypes.DOC_LOADED,
+        options: {
+          doc: doc
+        }
+      });
+      assert.equal($(el.getDOMNode()).find('.view-attachments-section .dropdown-menu li').length, 2);
+    });
+
+    it('setting deleteDocModal=true in store shows modal', function () {
+      var el = TestUtils.renderIntoDocument(<Components.DocEditorController database={database} />, container);
+      var doc = new Documents.Doc(docWithAttachmentsJSON, { database: database });
+      FauxtonAPI.dispatch({
+        type: ActionTypes.DOC_LOADED,
+        options: {
+          doc: doc
+        }
+      });
+      assert.ok($(el.getDOMNode()).find('.confirmation-modal')[0].getAttribute('data-js-visible') == 'false');
+      Actions.showDeleteDocModal();
+      assert.ok($(el.getDOMNode()).find('.confirmation-modal')[0].getAttribute('data-js-visible') == 'true');
+    });
+
+    it('setting uploadDocModal=true in store shows modal', function () {
+      var el = TestUtils.renderIntoDocument(<Components.DocEditorController database={database} />, container);
+      var doc = new Documents.Doc(docWithAttachmentsJSON, { database: database });
+      FauxtonAPI.dispatch({
+        type: ActionTypes.DOC_LOADED,
+        options: {
+          doc: doc
+        }
+      });
+      assert.ok($(el.getDOMNode()).find('.upload-file-modal')[0].getAttribute('data-js-visible') == 'false');
+      Actions.showUploadModal();
+      assert.ok($(el.getDOMNode()).find('.upload-file-modal')[0].getAttribute('data-js-visible') == 'true');
+    });
+
+  });
+
+  describe("AttachmentsPanelButton", function () {
+    var container, doc;
+
+    beforeEach(function () {
+      doc = new Documents.Doc(docWithAttachmentsJSON, { database: database });
+      container = document.createElement('div');
+    });
+
+    afterEach(function () {
+      React.unmountComponentAtNode(container);
+    });
+
+    it('does not show up when loading', function () {
+      var el = TestUtils.renderIntoDocument(<Components.AttachmentsPanelButton isLoading={true} doc={doc} />, container);
+      assert.equal($(el.getDOMNode()).find('.panel-button').length, 0);
+    });
+
+    it('shows up after loading', function () {
+      var el = TestUtils.renderIntoDocument(<Components.AttachmentsPanelButton isLoading={false} doc={doc} />, container);
+      assert.equal($(el.getDOMNode()).find('.panel-button').length, 1);
+    });
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/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
new file mode 100644
index 0000000..081ab67
--- /dev/null
+++ b/app/addons/documents/doc-editor/tests/doc-editor.storesSpec.js
@@ -0,0 +1,81 @@
+// 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([
+  'app',
+  'api',
+  'addons/documents/doc-editor/stores',
+  'testUtils'
+], function (app, FauxtonAPI, Stores, utils) {
+  FauxtonAPI.router = new FauxtonAPI.Router([]);
+
+  var assert = utils.assert;
+
+  var store = Stores.docEditorStore;
+
+  describe('DocEditorStore', function () {
+    afterEach(function () {
+      store.reset();
+    });
+
+    it('defines sensible defaults', function () {
+      assert.equal(store.isLoading(), true);
+      assert.equal(store.isCloneDocModalVisible(), false);
+      assert.equal(store.isDeleteDocModalVisible(), false);
+      assert.equal(store.isUploadModalVisible(), false);
+      assert.equal(store.getNumFilesUploaded(), 0);
+      assert.equal(store.isUploadInProgress(), false);
+      assert.equal(store.getUploadLoadPercentage(), 0);
+    });
+
+    it('docLoaded() marks loading as complete', function () {
+      store.docLoaded({ doc: {} });
+      assert.equal(store.isLoading(), false);
+    });
+
+    it('showCloneDocModal / hideCloneDocModal', function () {
+      store.showCloneDocModal();
+      assert.equal(store.isCloneDocModalVisible(), true);
+      store.hideCloneDocModal();
+      assert.equal(store.isCloneDocModalVisible(), false);
+    });
+
+    it('showDeleteDocModal / hideCloneDocModal', function () {
+      store.showDeleteDocModal();
+      assert.equal(store.isDeleteDocModalVisible(), true);
+      store.hideDeleteDocModal();
+      assert.equal(store.isDeleteDocModalVisible(), false);
+    });
+
+    it('showCloneDocModal / hideCloneDocModal', function () {
+      store.showUploadModal();
+      assert.equal(store.isUploadModalVisible(), true);
+      store.hideUploadModal();
+      assert.equal(store.isUploadModalVisible(), false);
+    });
+
+    it('reset() resets all values', function () {
+      store.docLoaded({ doc: {} });
+      store.showCloneDocModal();
+      store.showDeleteDocModal();
+      store.showUploadModal();
+
+      store.reset();
+      assert.equal(store.isLoading(), true);
+      assert.equal(store.isCloneDocModalVisible(), false);
+      assert.equal(store.isDeleteDocModalVisible(), false);
+      assert.equal(store.isUploadModalVisible(), false);
+    });
+
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/app/addons/documents/index-editor/components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/documents/index-editor/components.react.jsx b/app/addons/documents/index-editor/components.react.jsx
index f7009a1..fd8f853 100644
--- a/app/addons/documents/index-editor/components.react.jsx
+++ b/app/addons/documents/index-editor/components.react.jsx
@@ -130,7 +130,7 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents)
 
     componentWillUnmount: function () {
       indexEditorStore.off('change', this.onChange);
-    },
+    }
 
   });
 
@@ -154,7 +154,6 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents)
       return _.map(this.state.reduceOptions, function (reduce, i) {
         return <option key={i} value={reduce}>{reduce}</option>;
       }, this);
-
     },
 
     getReduceValue: function () {
@@ -231,8 +230,7 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents)
 
     componentWillUnmount: function () {
       indexEditorStore.off('change', this.onChange);
-    },
-
+    }
   });
 
   var DeleteView = React.createClass({
@@ -310,20 +308,16 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents)
       indexEditorStore.off('change', this.onChange);
     },
 
-    hasValidCode: function () {
-      return _.every(['mapEditor', 'reduceEditor'], function (editorName) {
-        if (editorName === 'reduceEditor' && !indexEditorStore.hasCustomReduce()) {
-          return true;
-        }
-        var editor = this.refs[editorName].getEditor();
-        return editor.hadValidCode();
-      }, this);
+    hasErrors: function () {
+      var mapEditorErrors = this.refs.mapEditor.getEditor().hasErrors();
+      var customReduceErrors = (indexEditorStore.hasCustomReduce()) ? this.refs.reduceEditor.getEditor().hasErrors() : false;
+      return mapEditorErrors || customReduceErrors;
     },
 
     saveView: function (event) {
       event.preventDefault();
 
-      if (!this.hasValidCode()) {
+      if (this.hasErrors()) {
         FauxtonAPI.addNotification({
           msg:  'Please fix the Javascript errors and try again.',
           type: 'error',

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/app/addons/documents/index-editor/tests/viewIndex.componentsSpec.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/documents/index-editor/tests/viewIndex.componentsSpec.react.jsx b/app/addons/documents/index-editor/tests/viewIndex.componentsSpec.react.jsx
index 4423572..67df4e5 100644
--- a/app/addons/documents/index-editor/tests/viewIndex.componentsSpec.react.jsx
+++ b/app/addons/documents/index-editor/tests/viewIndex.componentsSpec.react.jsx
@@ -65,8 +65,6 @@ define([
       });
 
       it('returns null for none', function () {
-        var store = Stores.indexEditorStore;
-
         var designDoc = {
           _id: '_design/test-doc',
           views: {
@@ -84,8 +82,6 @@ define([
       });
 
       it('returns built in for built in reduce', function () {
-        var store = Stores.indexEditorStore;
-
         var designDoc = {
           _id: '_design/test-doc',
           views: {
@@ -221,40 +217,42 @@ define([
   });
 
   describe('Editor', function () {
-    var container, editorEl;
+    var container, editorEl, sandbox;
 
     beforeEach(function () {
       container = document.createElement('div');
       $('body').append('<div id="map-function"></div>');
       $('body').append('<div id="editor"></div>');
       editorEl = TestUtils.renderIntoDocument(<Views.Editor/>, container);
+      sandbox = sinon.sandbox.create();
     });
 
     afterEach(function () {
       React.unmountComponentAtNode(container);
+      sandbox.restore();
     });
 
     it('returns false on invalid map editor code', function () {
-      var stub = sinon.stub(editorEl.refs.mapEditor.getEditor(), 'hadValidCode');
+      var stub = sandbox.stub(editorEl.refs.mapEditor.getEditor(), 'hasErrors');
       stub.returns(false);
-      assert.notOk(editorEl.hasValidCode());
+      assert.notOk(editorEl.hasErrors());
     });
 
     it('returns true on valid map editor code', function () {
-      var stub = sinon.stub(editorEl.refs.mapEditor.getEditor(), 'hadValidCode');
+      var stub = sandbox.stub(editorEl.refs.mapEditor.getEditor(), 'hasErrors');
       stub.returns(true);
-      assert.ok(editorEl.hasValidCode());
+      assert.ok(editorEl.hasErrors());
     });
 
-    it('returns true on non-custom reduce', function () {
-      var stub = sinon.stub(Stores.indexEditorStore, 'hasCustomReduce');
+    it('returns false on non-custom reduce', function () {
+      var stub = sandbox.stub(Stores.indexEditorStore, 'hasCustomReduce');
       stub.returns(false);
-      assert.ok(editorEl.hasValidCode());
+      assert.notOk(editorEl.hasErrors());
     });
 
     it('calls changeViewName on view name change', function () {
       var viewName = 'new-name';
-      var spy = sinon.spy(Actions, 'changeViewName');
+      var spy = sandbox.spy(Actions, 'changeViewName');
       var el = $(editorEl.getDOMNode()).find('#index-name')[0];
       TestUtils.Simulate.change(el, {
         target: {

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/app/addons/documents/mango/mango.components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/documents/mango/mango.components.react.jsx b/app/addons/documents/mango/mango.components.react.jsx
index 13123ed..a451e9a 100644
--- a/app/addons/documents/mango/mango.components.react.jsx
+++ b/app/addons/documents/mango/mango.components.react.jsx
@@ -100,7 +100,7 @@ function (app, FauxtonAPI, React, Stores, Actions,
     runQuery: function (event) {
       event.preventDefault();
 
-      if (!this.getMangoEditor().hasValidCode()) {
+      if (this.getMangoEditor().hasErrors()) {
         FauxtonAPI.addNotification({
           msg:  'Please fix the Javascript errors and try again.',
           type: 'error',
@@ -155,7 +155,7 @@ function (app, FauxtonAPI, React, Stores, Actions,
             {this.getIndexBox()}
             <div className="padded-box">
               <div className="control-group">
-                <ConfirmButton text={this.props.confirmbuttonText} />
+                <ConfirmButton text={this.props.confirmbuttonText} id="create-index-btn" />
               </div>
             </div>
           </form>
@@ -216,9 +216,8 @@ function (app, FauxtonAPI, React, Stores, Actions,
       return this.refs.field.getEditor();
     },
 
-    hasValidCode: function () {
-      var editor = this.getEditor();
-      return editor.hadValidCode();
+    hasErrors: function () {
+      return this.getEditor().hasErrors();
     }
   });
 
@@ -267,7 +266,7 @@ function (app, FauxtonAPI, React, Stores, Actions,
     saveQuery: function (event) {
       event.preventDefault();
 
-      if (!this.getMangoEditor().hasValidCode()) {
+      if (this.getMangoEditor().hasErrors()) {
         FauxtonAPI.addNotification({
           msg:  'Please fix the Javascript errors and try again.',
           type: 'error',

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/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 bf5d28c..81e6e00 100644
--- a/app/addons/documents/routes-doc-editor.js
+++ b/app/addons/documents/routes-doc-editor.js
@@ -11,17 +11,16 @@
 // the License.
 
 define([
-  "app",
-  "api",
-
-  // Modules
-  "addons/documents/helpers",
-  "addons/documents/views",
-  "addons/documents/views-doceditor",
-  "addons/databases/base"
+  'app',
+  'api',
+  'addons/documents/helpers',
+  'addons/documents/resources',
+  'addons/databases/base',
+  'addons/documents/doc-editor/actions',
+  'addons/documents/doc-editor/components.react'
 ],
 
-function (app, FauxtonAPI, Helpers, Documents, DocEditor, Databases) {
+function (app, FauxtonAPI, Helpers, Documents, Databases, Actions, ReactComponents) {
 
 
   var DocEditorRouteObject = FauxtonAPI.RouteObject.extend({
@@ -29,22 +28,24 @@ function (app, FauxtonAPI, Helpers, Documents, DocEditor, Databases) {
     disableLoader: true,
     selectedHeader: 'Databases',
 
+    roles: ['fx_loggedIn'],
+
     initialize: function (route, masterLayout, options) {
       this.databaseName = options[0];
       this.docID = options[1] || 'new';
-
       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.wasCloned = false;
     },
 
     routes: {
-      'database/:database/:doc/code_editor': 'code_editor',
-      'database/:database/:doc': 'code_editor',
+      'database/:database/:doc/code_editor': 'codeEditor',
+      'database/:database/:doc': 'codeEditor',
       'database/:database/_design/:ddoc': 'showDesignDoc'
     },
 
     events: {
-      'route:reRenderDoc': 'reRenderDoc',
       'route:duplicateDoc': 'duplicateDoc'
     },
 
@@ -57,7 +58,7 @@ function (app, FauxtonAPI, Helpers, Documents, DocEditor, Databases) {
       ];
     },
 
-    code_editor: function (database, doc) {
+    codeEditor: function (database, doc) {
 
       // if either the database or document just changed, we need to get the latest doc/db info
       if (this.databaseName !== database) {
@@ -69,25 +70,22 @@ function (app, FauxtonAPI, Helpers, Documents, DocEditor, Databases) {
         this.doc = new Documents.Doc({ _id: this.docID }, { database: this.database });
       }
 
-      this.docView = this.setView('#dashboard-content', new DocEditor.CodeEditor({
-        model: this.doc,
+      Actions.initDocEditor({ doc: this.doc, database: this.database });
+      this.setComponent('#dashboard-content', ReactComponents.DocEditorController, {
         database: this.database,
-        previousPage: Helpers.getPreviousPageForDoc(this.database)
-      }));
+        isNewDoc: this.isNewDoc,
+        previousPage: '#/' + Helpers.getPreviousPageForDoc(this.database)
+      });
     },
 
     showDesignDoc: function (database, ddoc) {
-      this.code_editor(database, '_design/' + ddoc);
-    },
-
-    reRenderDoc: function () {
-      this.docView.forceRender();
+      this.codeEditor(database, '_design/' + ddoc);
     },
 
     duplicateDoc: function (newId) {
       var doc = this.doc,
-      docView = this.docView,
-      database = this.database;
+          database = this.database;
+
       this.docID = newId;
 
       var that = this;
@@ -95,7 +93,6 @@ function (app, FauxtonAPI, Helpers, Documents, DocEditor, Databases) {
         doc.set({ _id: newId });
         that.wasCloned = true;
 
-        docView.forceRender();
         FauxtonAPI.navigate('/database/' + database.safeID() + '/' + app.utils.safeURLName(newId), { trigger: true });
         FauxtonAPI.addNotification({
           msg: 'Document has been duplicated.'
@@ -115,14 +112,15 @@ function (app, FauxtonAPI, Helpers, Documents, DocEditor, Databases) {
     }
   });
 
+
   var NewDocEditorRouteObject = DocEditorRouteObject.extend({
     initialize: function (route, masterLayout, options) {
       var databaseName = options[0];
-
-      this.database = this.database || new Databases.Model({id: databaseName});
+      this.database = this.database || new Databases.Model({ id: databaseName });
       this.doc = new Documents.NewDoc(null, {
         database: this.database
       });
+      this.isNewDoc = true;
     },
 
     crumbs: function () {
@@ -134,7 +132,7 @@ function (app, FauxtonAPI, Helpers, Documents, DocEditor, Databases) {
     },
 
     routes: {
-      'database/:database/new': 'code_editor'
+      'database/:database/new': 'codeEditor'
     },
 
     selectedHeader: 'Databases'
@@ -145,4 +143,5 @@ function (app, FauxtonAPI, Helpers, Documents, DocEditor, Databases) {
     NewDocEditorRouteObject: NewDocEditorRouteObject,
     DocEditorRouteObject: DocEditorRouteObject
   };
+
 });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/app/addons/documents/routes-documents.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/routes-documents.js b/app/addons/documents/routes-documents.js
index 09621c3..7ed5f58 100644
--- a/app/addons/documents/routes-documents.js
+++ b/app/addons/documents/routes-documents.js
@@ -19,8 +19,6 @@ define([
   'addons/documents/views',
   'addons/documents/changes/components.react',
   'addons/documents/changes/actions',
-  'addons/documents/views-doceditor',
-
   'addons/databases/base',
   'addons/documents/resources',
   'addons/fauxton/components',
@@ -35,10 +33,9 @@ define([
   'addons/documents/designdocinfo/components.react'
 ],
 
-function (app, FauxtonAPI, BaseRoute, Documents, Changes, ChangesActions, DocEditor,
-  Databases, Resources, Components, PaginationStores, IndexResultsActions,
-  IndexResultsComponents, ReactPagination, ReactHeader, ReactActions, SidebarActions,
-  DesignDocInfoActions, DesignDocInfoComponents) {
+function (app, FauxtonAPI, BaseRoute, Documents, Changes, ChangesActions, Databases, Resources, Components,
+  PaginationStores, IndexResultsActions, IndexResultsComponents, ReactPagination, ReactHeader, ReactActions,
+  SidebarActions, DesignDocInfoActions, DesignDocInfoComponents) {
 
     var DocumentsRouteObject = BaseRoute.extend({
       layout: "with_tabs_sidebar",

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/app/addons/documents/templates/code_editor.html
----------------------------------------------------------------------
diff --git a/app/addons/documents/templates/code_editor.html b/app/addons/documents/templates/code_editor.html
deleted file mode 100644
index f341df3..0000000
--- a/app/addons/documents/templates/code_editor.html
+++ /dev/null
@@ -1,78 +0,0 @@
-<%/*
-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 id="doc-editor-actions-panel">
-  <div class="doc-actions-left">
-    <button class="save-doc btn btn-success save" type="button"><i class="icon fonticon-ok-circled"></i> Save</button>
-    <div>
-      <a href="#" class="js-back cancel-button">Cancel</a>
-    </div>
-  </div>
-
-  <div class="alignRight">
-    <button class="btn string-edit" title="Edit line" disabled="true"><i class="icon icon-edit"></i></button>
-
-    <% if (attachments) { %>
-    <div class="panel-section btn-group">
-      <button class="panel-button dropdown-toggle btn" data-bypass="true" data-toggle="dropdown" title="View Attachments"
-        id="view-attachments-menu">
-        <i class="icon fonticon-picture"></i>
-        <span>View Attachments</span>
-        <span class="caret"></span>
-      </button>
-
-      <ul class="dropdown-menu" role="menu" aria-labelledby="view-attachments-menu">
-        <%_.each(attachments, function (att) { %>
-        <li>
-          <a href="<%- att.url %>" target="_blank" data-bypass="true"> <strong> <%- att.fileName %> </strong> -
-            <span> <%- att.contentType %>, <%- formatSize(att.size)%> </span>
-          </a>
-        </li>
-        <% }) %>
-      </ul>
-    </div>
-    <% } %>
-
-    <div class="doc-editor-extension-icons"></div>
-
-    <div class="panel-section">
-      <button class="panel-button upload" title="Upload attachment">
-        <i class="icon icon-circle-arrow-up"></i>
-        <span>Upload Attachment</span>
-      </button>
-    </div>
-    <div class="panel-section">
-      <button class="panel-button duplicate" title="Clone document">
-        <i class="icon icon-repeat"></i>
-        <span>Clone Document</span>
-      </button>
-    </div>
-    <div class="panel-section">
-      <button class="panel-button delete" title="Delete">
-        <i class="icon icon-trash"></i>
-        <span>Delete</span>
-      </button>
-    </div>
-  </div>
-</div>
-
-<div class="code-region">
-  <div class="bgEditorGutter"></div>
-  <div id="editor-container" class="doc-code"><%- JSON.stringify(doc.attributes, null, "  ") %></div>
-</div>
-
-<div id="upload-modal"> </div>
-<div id="duplicate-modal"> </div>
-<div id="delete-doc-modal"> </div>
-<div id="string-edit-modal"> </div>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/8cd744ac/app/addons/documents/templates/duplicate_doc_modal.html
----------------------------------------------------------------------
diff --git a/app/addons/documents/templates/duplicate_doc_modal.html b/app/addons/documents/templates/duplicate_doc_modal.html
deleted file mode 100644
index bab4067..0000000
--- a/app/addons/documents/templates/duplicate_doc_modal.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<% /*
-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 class="modal hide fade">
-  <div class="modal-header">
-    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-    <h3>Clone Document</h3>
-  </div>
-  <div class="modal-body">
-    <div id="modal-error" class="hide alert alert-error"/>
-    <form id="doc-duplicate" class="form" method="post">
-      <p class="help-block">
-      Set new documents ID:
-      </p>
-      <input id="dup-id" type="text" class="input-block-level" >
-    </form>
-
-  </div>
-  <div class="modal-footer">
-    <button data-dismiss="modal" class="btn"><i class="icon fonticon-cancel-circled"></i> Cancel</button>
-    <button id="duplicate-btn" class="btn btn-success save"><i class="fonticon-ok-circled"></i> Clone</button>
-  </div>
-</div>
-
-