couchdb-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From benk...@apache.org
Subject [09/10] fauxton commit: updated refs/heads/master to 3ff6ff6
Date Fri, 18 Mar 2016 18:56:55 GMT
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/documents/sidebar/sidebar.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/documents/sidebar/sidebar.react.jsx b/app/addons/documents/sidebar/sidebar.react.jsx
index a96b647..c8f6146 100644
--- a/app/addons/documents/sidebar/sidebar.react.jsx
+++ b/app/addons/documents/sidebar/sidebar.react.jsx
@@ -15,22 +15,32 @@ define([
   'api',
   'react',
   'react-dom',
-  'addons/documents/sidebar/stores',
+  'addons/documents/sidebar/stores.react',
   'addons/documents/sidebar/actions',
-
-
   'addons/components/react-components.react',
   'addons/components/stores',
   'addons/components/actions',
+  'addons/documents/index-editor/actions',
+  'addons/documents/index-editor/components.react',
+  'addons/fauxton/components.react',
+  'addons/documents/views',
   'addons/documents/helpers',
+  'libs/react-bootstrap',
   'plugins/prettify'
 ],
 
-function (app, FauxtonAPI, React, ReactDOM, Stores, Actions,
-  Components, ComponentsStore, ComponentsActions, DocumentHelper) {
+function (app, FauxtonAPI, React, ReactDOM, Stores, Actions, Components, ComponentsStore, ComponentsActions,
+  IndexEditorActions, IndexEditorComponents, GeneralComponents, DocumentViews, DocumentHelper, ReactBootstrap) {
+
+  var DeleteDBModal = DocumentViews.Views.DeleteDBModal;
 
   var store = Stores.sidebarStore;
   var LoadLines = Components.LoadLines;
+  var DesignDocSelector = IndexEditorComponents.DesignDocSelector;
+  var OverlayTrigger = ReactBootstrap.OverlayTrigger;
+  var Popover = ReactBootstrap.Popover;
+  var Modal = ReactBootstrap.Modal;
+  var ConfirmationModal = GeneralComponents.ConfirmationModal;
 
   var DeleteDatabaseModal = Components.DeleteDatabaseModal;
   var deleteDbModalStore = ComponentsStore.deleteDbModalStore;
@@ -110,38 +120,105 @@ function (app, FauxtonAPI, React, ReactDOM, Stores, Actions,
         </ul>
       );
     }
-
   });
 
+
   var IndexSection = React.createClass({
 
     propTypes: {
       urlNamespace: React.PropTypes.string.isRequired,
-      databaseName: React.PropTypes.string.isRequired,
+      indexLabel: React.PropTypes.string.isRequired,
+      database: React.PropTypes.object.isRequired,
       designDocName: React.PropTypes.string.isRequired,
       items: React.PropTypes.array.isRequired,
       isExpanded: React.PropTypes.bool.isRequired,
-      selectedIndex: React.PropTypes.string.isRequired
+      selectedIndex: React.PropTypes.string.isRequired,
+      onDelete: React.PropTypes.func.isRequired,
+      onClone: React.PropTypes.func.isRequired
+    },
+
+    getInitialState: function () {
+      return {
+        placement: 'bottom'
+      };
+    },
+
+    // this dynamically changes the placement of the menu (top/bottom) to prevent it going offscreen and causing some
+    // unsightly shifting
+    setPlacement: function (rowId) {
+      var rowTop = document.getElementById(rowId).getBoundingClientRect().top;
+      var toggleHeight = 150; // the height of the menu overlay, arrow, view row
+      var placement = (rowTop + toggleHeight > window.innerHeight) ? 'top' : 'bottom';
+      this.setState({ placement: placement });
     },
 
     createItems: function () {
-      return _.map(this.props.items, function (index, key) {
-        var href = FauxtonAPI.urls(this.props.urlNamespace, 'app', this.props.databaseName, this.props.designDocName);
-        var className = (this.props.selectedIndex === index) ? 'active' : '';
+
+      // sort the indexes alphabetically
+      var sortedItems = this.props.items.sort();
+
+      return _.map(sortedItems, function (indexName, index) {
+        var href = FauxtonAPI.urls(this.props.urlNamespace, 'app', this.props.database.id, this.props.designDocName);
+        var className = (this.props.selectedIndex === indexName) ? 'active' : '';
 
         return (
-          <li className={className} key={key}>
+          <li className={className} key={index}>
             <a
-              id={this.props.designDocName + '_' + index}
-              href={"#/" + href + index}
+              id={this.props.designDocName + '_' + indexName}
+              href={"#/" + href + indexName}
               className="toggle-view">
-              {index}
+              {indexName}
             </a>
+            <OverlayTrigger
+              ref={"indexMenu-" + index}
+              trigger="click"
+              onEnter={this.setPlacement.bind(this, this.props.designDocName + '_' + indexName)}
+              placement={this.state.placement}
+              rootClose={true}
+              overlay={
+                <Popover id="index-menu-component-popover">
+                  <ul>
+                    <li onClick={this.indexAction.bind(this, 'edit', { indexName: indexName, onEdit: this.props.onEdit })}>
+                      <span className="fonticon fonticon-file-code-o"></span>
+                      Edit
+                    </li>
+                    <li onClick={this.indexAction.bind(this, 'clone', { indexName: indexName, onClone: this.props.onClone })}>
+                      <span className="fonticon fonticon-files-o"></span>
+                      Clone
+                    </li>
+                    <li onClick={this.indexAction.bind(this, 'delete', { indexName: indexName, onDelete: this.props.onDelete })}>
+                      <span className="fonticon fonticon-trash"></span>
+                      Delete
+                    </li>
+                  </ul>
+                </Popover>
+              }>
+              <span className="index-menu-toggle fonticon fonticon-wrench2"></span>
+            </OverlayTrigger>
           </li>
         );
       }, this);
     },
 
+    indexAction: function (action, params, e) {
+      e.preventDefault();
+
+      // ensures the menu gets closed. The hide() on the ref doesn't consistently close it
+      $('body').trigger('click');
+
+      switch (action) {
+        case 'delete':
+          Actions.showDeleteIndexModal(params.indexName, this.props.designDocName, this.props.indexLabel, params.onDelete);
+        break;
+        case 'clone':
+          Actions.showCloneIndexModal(params.indexName, this.props.designDocName, this.props.indexLabel, params.onClone);
+        break;
+        case 'edit':
+          params.onEdit(this.props.database.id, this.props.designDocName, params.indexName);
+        break;
+      }
+    },
+
     toggle: function (e) {
       e.preventDefault();
       var newToggleState = !this.props.isExpanded;
@@ -186,6 +263,7 @@ function (app, FauxtonAPI, React, ReactDOM, Stores, Actions,
 
   var DesignDoc = React.createClass({
     propTypes: {
+      database: React.PropTypes.object.isRequired,
       sidebarListTypes: React.PropTypes.array.isRequired,
       isExpanded: React.PropTypes.bool.isRequired,
       selectedNavInfo: React.PropTypes.object.isRequired,
@@ -206,7 +284,11 @@ function (app, FauxtonAPI, React, ReactDOM, Stores, Actions,
         newList.unshift({
           selector: 'views',
           name: 'Views',
-          urlNamespace: 'view'
+          urlNamespace: 'view',
+          indexLabel: 'view',
+          onDelete: IndexEditorActions.deleteView,
+          onClone: IndexEditorActions.cloneView,
+          onEdit: IndexEditorActions.gotoEditViewPage
         });
         this.setState({ updatedSidebarListTypes: newList });
       }
@@ -227,9 +309,13 @@ function (app, FauxtonAPI, React, ReactDOM, Stores, Actions,
             icon={index.icon}
             isExpanded={expanded}
             urlNamespace={index.urlNamespace}
+            indexLabel={index.indexLabel}
+            onEdit={index.onEdit}
+            onDelete={index.onDelete}
+            onClone={index.onClone}
             selectedIndex={selectedIndex}
             toggle={this.props.toggle}
-            databaseName={this.props.databaseName}
+            database={this.props.database}
             designDocName={this.props.designDocName}
             key={key}
             title={index.name}
@@ -248,8 +334,7 @@ function (app, FauxtonAPI, React, ReactDOM, Stores, Actions,
     },
 
     getNewButtonLinks: function () {
-      var databaseName = this.props.databaseName;
-      var newUrlPrefix = FauxtonAPI.urls('databaseBaseURL', 'app', databaseName);
+      var newUrlPrefix = FauxtonAPI.urls('databaseBaseURL', 'app', this.props.database.id);
       var designDocName = this.props.designDocName;
 
       var addNewLinks = _.reduce(FauxtonAPI.getExtensions('sidebar:links'), function (menuLinks, link) {
@@ -261,7 +346,7 @@ function (app, FauxtonAPI, React, ReactDOM, Stores, Actions,
         return menuLinks;
       }, [{
         title: 'New View',
-        url: '#' + FauxtonAPI.urls('new', 'addView', databaseName, designDocName),
+        url: '#' + FauxtonAPI.urls('new', 'addView', this.props.database.id, designDocName),
         icon: 'fonticon-plus-circled'
       }]);
 
@@ -281,7 +366,7 @@ function (app, FauxtonAPI, React, ReactDOM, Stores, Actions,
         toggleBodyClassNames += ' in';
       }
       var designDocName = this.props.designDocName;
-      var designDocMetaUrl = FauxtonAPI.urls('designDocs', 'app', this.props.databaseName, designDocName);
+      var designDocMetaUrl = FauxtonAPI.urls('designDocs', 'app', this.props.database.id, designDocName);
       var metadataRowClass = (this.props.selectedNavInfo.designDocSection === 'metadata') ? 'active' : '';
 
       return (
@@ -344,7 +429,7 @@ function (app, FauxtonAPI, React, ReactDOM, Stores, Actions,
             key={key}
             designDoc={designDoc}
             designDocName={ddName}
-            databaseName={this.props.databaseName} />
+            database={this.props.database} />
         );
       }.bind(this));
     },
@@ -361,13 +446,32 @@ function (app, FauxtonAPI, React, ReactDOM, Stores, Actions,
   var SidebarController = React.createClass({
     getStoreState: function () {
       return {
-        databaseName: store.getDatabaseName(),
+        database: store.getDatabase(),
         selectedNav: store.getSelected(),
         designDocs: store.getDesignDocs(),
+        designDocList: store.getDesignDocList(),
+        availableDesignDocIds: store.getAvailableDesignDocs(),
         toggledSections: store.getToggledSections(),
         isLoading: store.isLoading(),
         database: store.getDatabase(),
-        deleteDbModalProperties: deleteDbModalStore.getShowDeleteDatabaseModal()
+        deleteDbModalProperties: deleteDbModalStore.getShowDeleteDatabaseModal(),
+
+        deleteIndexModalVisible: store.isDeleteIndexModalVisible(),
+        deleteIndexModalText: store.getDeleteIndexModalText(),
+        deleteIndexModalOnSubmit: store.getDeleteIndexModalOnSubmit(),
+        deleteIndexModalIndexName: store.getDeleteIndexModalIndexName(),
+        deleteIndexModalDesignDoc: store.getDeleteIndexDesignDoc(),
+
+        cloneIndexModalVisible: store.isCloneIndexModalVisible(),
+        cloneIndexModalTitle: store.getCloneIndexModalTitle(),
+        cloneIndexModalSelectedDesignDoc: store.getCloneIndexModalSelectedDesignDoc(),
+        cloneIndexModalNewDesignDocName: store.getCloneIndexModalNewDesignDocName(),
+        cloneIndexModalOnSubmit: store.getCloneIndexModalOnSubmit(),
+        cloneIndexDesignDocProp: store.getCloneIndexDesignDocProp(),
+        cloneIndexModalNewIndexName: store.getCloneIndexModalNewIndexName(),
+        cloneIndexSourceIndexName: store.getCloneIndexModalSourceIndexName(),
+        cloneIndexSourceDesignDocName: store.getCloneIndexModalSourceDesignDocName(),
+        cloneIndexModalIndexLabel: store.getCloneIndexModalIndexLabel()
       };
     },
 
@@ -395,33 +499,173 @@ function (app, FauxtonAPI, React, ReactDOM, Stores, Actions,
       ComponentsActions.showDeleteDatabaseModal(payload);
     },
 
+    // handles deleting of any index regardless of type. The delete handler and all relevant info is set when the user
+    // clicks the delete action for a particular index
+    deleteIndex: function () {
+
+      // if the user is currently on the index that's being deleted, pass that info along to the delete handler. That can
+      // be used to redirect the user to somewhere appropriate
+      var isOnIndex = this.state.selectedNav.navItem === 'designDoc' &&
+                      ('_design/' + this.state.selectedNav.designDocName) === this.state.deleteIndexModalDesignDoc.id &&
+                      this.state.selectedNav.indexName === this.state.deleteIndexModalIndexName;
+
+      this.state.deleteIndexModalOnSubmit({
+        isOnIndex: isOnIndex,
+        indexName: this.state.deleteIndexModalIndexName,
+        designDoc: this.state.deleteIndexModalDesignDoc,
+        designDocs: this.state.designDocs,
+        database: this.state.database
+      });
+    },
+
+    cloneIndex: function () {
+      this.state.cloneIndexModalOnSubmit({
+        sourceIndexName: this.state.cloneIndexSourceIndexName,
+        sourceDesignDocName: this.state.cloneIndexSourceDesignDocName,
+        targetDesignDocName: this.state.cloneIndexModalSelectedDesignDoc,
+        newDesignDocName: this.state.cloneIndexModalNewDesignDocName,
+        newIndexName: this.state.cloneIndexModalNewIndexName,
+        designDocs: this.state.designDocs,
+        database: this.state.database,
+        onComplete: Actions.hideCloneIndexModal
+      });
+    },
+
     render: function () {
       if (this.state.isLoading) {
         return <LoadLines />;
       }
+
       return (
         <nav className="sidenav">
           <MainSidebar
             selectedNavItem={this.state.selectedNav.navItem}
-            databaseName={this.state.databaseName} />
+            databaseName={this.state.database.id} />
           <DesignDocList
             selectedNav={this.state.selectedNav}
             toggle={Actions.toggleContent}
             toggledSections={this.state.toggledSections}
-            designDocs={this.state.designDocs}
-            databaseName={this.state.databaseName} />
-
+            designDocs={this.state.designDocList}
+            database={this.state.database} />
           <DeleteDatabaseModal
             showHide={this.showDeleteDatabaseModal}
             modalProps={this.state.deleteDbModalProperties} />
+
+          {/* the delete and clone index modals handle all index types, hence the props all being pulled from the store */}
+          <ConfirmationModal
+            title="Confirm Deletion"
+            visible={this.state.deleteIndexModalVisible}
+            text={this.state.deleteIndexModalText}
+            onClose={Actions.hideDeleteIndexModal}
+            onSubmit={this.deleteIndex} />
+          <CloneIndexModal
+            visible={this.state.cloneIndexModalVisible}
+            title={this.state.cloneIndexModalTitle}
+            close={Actions.hideCloneIndexModal}
+            submit={this.cloneIndex}
+            designDocArray={this.state.availableDesignDocIds}
+            selectedDesignDoc={this.state.cloneIndexModalSelectedDesignDoc}
+            newDesignDocName={this.state.cloneIndexModalNewDesignDocName}
+            newIndexName={this.state.cloneIndexModalNewIndexName}
+            indexLabel={this.state.cloneIndexModalIndexLabel} />
         </nav>
       );
     }
   });
 
+
+  var CloneIndexModal = React.createClass({
+    propTypes: {
+      visible: React.PropTypes.bool.isRequired,
+      title: React.PropTypes.string,
+      close: React.PropTypes.func.isRequired,
+      submit: React.PropTypes.func.isRequired,
+      designDocArray: React.PropTypes.array.isRequired,
+      selectedDesignDoc: React.PropTypes.string.isRequired,
+      newDesignDocName: React.PropTypes.string.isRequired,
+      newIndexName: React.PropTypes.string.isRequired,
+      indexLabel: React.PropTypes.string.isRequired
+    },
+
+    getDefaultProps: function () {
+      return {
+        title: 'Clone Index',
+        visible: false
+      };
+    },
+
+    submit: function () {
+      if (!this.refs.designDocSelector.validate()) {
+        return;
+      }
+      if (this.props.newIndexName === '') {
+        FauxtonAPI.addNotification({
+          msg: 'Please enter the new index name.',
+          type: 'error',
+          clear: true
+        });
+        return;
+      }
+      this.props.submit();
+    },
+
+    close: function (e) {
+      if (e) {
+        e.preventDefault();
+      }
+      this.props.close();
+    },
+
+    setNewIndexName: function (e) {
+      Actions.setNewCloneIndexName(e.target.value);
+    },
+
+    render: function () {
+      return (
+        <Modal dialogClassName="clone-index-modal" show={this.props.visible} onHide={this.close}>
+          <Modal.Header closeButton={true}>
+            <Modal.Title>{this.props.title}</Modal.Title>
+          </Modal.Header>
+          <Modal.Body>
+
+            <form className="form" method="post" onSubmit={this.submit}>
+              <p>
+                Select the design document where the cloned {this.props.indexLabel} will be created, and then enter
+                a name for the cloned {this.props.indexLabel}.
+              </p>
+
+              <div className="row">
+                <DesignDocSelector
+                  ref="designDocSelector"
+                  designDocList={this.props.designDocArray}
+                  selectedDesignDocName={this.props.selectedDesignDoc}
+                  newDesignDocName={this.props.newDesignDocName}
+                  onSelectDesignDoc={Actions.selectDesignDoc}
+                  onChangeNewDesignDocName={Actions.updateNewDesignDocName} />
+              </div>
+
+              <div className="clone-index-name-row">
+                <label className="new-index-title-label" htmlFor="new-index-name">{this.props.indexLabel} Name</label>
+                <input type="text" id="new-index-name" value={this.props.newIndexName} onChange={this.setNewIndexName}
+                   placeholder="Enter new view name" />
+              </div>
+            </form>
+
+          </Modal.Body>
+          <Modal.Footer>
+            <button onClick={this.submit} data-bypass="true" className="btn btn-success save">
+              <i className="icon fonticon-ok-circled" /> Clone {this.props.indexLabel}</button>
+            <a href="#" className="cancel-link" onClick={this.close} data-bypass="true">Cancel</a>
+          </Modal.Footer>
+        </Modal>
+      );
+    }
+  });
+
   return {
     SidebarController: SidebarController,
-    DesignDoc: DesignDoc
+    DesignDoc: DesignDoc,
+    CloneIndexModal: CloneIndexModal
   };
 
 });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/documents/sidebar/stores.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/sidebar/stores.js b/app/addons/documents/sidebar/stores.js
deleted file mode 100644
index f351fcd..0000000
--- a/app/addons/documents/sidebar/stores.js
+++ /dev/null
@@ -1,191 +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/documents/sidebar/actiontypes'
-],
-
-function (app, FauxtonAPI, ActionTypes) {
-  var Stores = {};
-
-  Stores.SidebarStore = FauxtonAPI.Store.extend({
-
-    initialize: function () {
-      this._selected = {
-        navItem: 'all-docs',
-        designDocName: '',
-        designDocSection: '', // metadata / name of index group ("Views", etc.)
-        indexName: ''
-      };
-      this._loading = true;
-      this._toggledSections = {};
-    },
-
-    newOptions: function (options) {
-      this._database = options.database;
-      this._designDocs = options.designDocs;
-      this._loading = false;
-
-      // this can be expanded in future as we need. Right now it can only set a top-level nav item ('all docs',
-      // 'permissions' etc.) and not a nested page
-      if (options.selectedNavItem) {
-        this._selected = {
-          navItem: options.selectedNavItem,
-          designDocName: '',
-          designDocSection: '',
-          indexName: ''
-        };
-      }
-    },
-
-    isLoading: function () {
-      return this._loading;
-    },
-
-    getDatabase: function () {
-      if (this.isLoading()) {
-        return {};
-      }
-      return this._database;
-    },
-
-    // used to toggle both design docs, and any index groups within them
-    toggleContent: function (designDoc, indexGroup) {
-      if (!this._toggledSections[designDoc]) {
-        this._toggledSections[designDoc] = {
-          visible: true,
-          indexGroups: {}
-        };
-        return;
-      }
-
-      if (indexGroup) {
-        return this.toggleIndexGroup(designDoc, indexGroup);
-      }
-
-      this._toggledSections[designDoc].visible = !this._toggledSections[designDoc].visible;
-    },
-
-    toggleIndexGroup: function (designDoc, indexGroup) {
-      var expanded = this._toggledSections[designDoc].indexGroups[indexGroup];
-
-      if (_.isUndefined(expanded)) {
-        this._toggledSections[designDoc].indexGroups[indexGroup] = true;
-        return;
-      }
-
-      this._toggledSections[designDoc].indexGroups[indexGroup] = !expanded;
-    },
-
-    isVisible: function (designDoc, indexGroup) {
-      if (!this._toggledSections[designDoc]) {
-        return false;
-      }
-      if (indexGroup) {
-        return this._toggledSections[designDoc].indexGroups[indexGroup];
-      }
-      return this._toggledSections[designDoc].visible;
-    },
-
-    getSelected: function () {
-      return this._selected;
-    },
-
-    setSelected: function (params) {
-      this._selected = {
-        navItem: params.navItem,
-        designDocName: params.designDocName,
-        designDocSection: params.designDocSection,
-        indexName: params.indexName
-      };
-
-      if (params.designDocName) {
-        if (!_.has(this._toggledSections, params.designDocName)) {
-          this._toggledSections[params.designDocName] = { visible: true, indexGroups: {} };
-        }
-        this._toggledSections[params.designDocName].visible = true;
-
-        if (params.designDocSection) {
-          this._toggledSections[params.designDocName].indexGroups[params.designDocSection] = true;
-        }
-      }
-    },
-
-    getToggledSections: function () {
-      return this._toggledSections;
-    },
-
-    getDatabaseName: function () {
-      if (this.isLoading()) {
-        return '';
-      }
-      return this._database.safeID();
-    },
-
-    getDesignDocs: function () {
-      if (this.isLoading()) {
-        return {};
-      }
-      var docs = this._designDocs.toJSON();
-
-      docs = _.filter(docs, function (doc) {
-        if (_.has(doc.doc, 'language')) {
-          return doc.doc.language !== 'query';
-        }
-        return true;
-      });
-
-      return docs.map(function (doc) {
-        doc.safeId = app.utils.safeURLName(doc._id.replace(/^_design\//, ""));
-        return _.extend(doc, doc.doc);
-      });
-    },
-
-    dispatch: function (action) {
-      switch (action.type) {
-        case ActionTypes.SIDEBAR_SET_SELECTED_NAV_ITEM:
-          this.setSelected(action.options);
-        break;
-
-        case ActionTypes.SIDEBAR_NEW_OPTIONS:
-          this.newOptions(action.options);
-        break;
-
-        case ActionTypes.SIDEBAR_TOGGLE_CONTENT:
-          this.toggleContent(action.designDoc, action.indexGroup);
-        break;
-
-        case ActionTypes.SIDEBAR_FETCHING:
-          this._loading = true;
-        break;
-
-        case ActionTypes.SIDEBAR_REFRESH:
-        break;
-
-        default:
-        return;
-        // do nothing
-      }
-
-      this.triggerChange();
-    }
-
-  });
-
-  Stores.sidebarStore = new Stores.SidebarStore();
-  Stores.sidebarStore.dispatchToken = FauxtonAPI.dispatcher.register(Stores.sidebarStore.dispatch);
-
-  return Stores;
-
-});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/documents/sidebar/stores.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/documents/sidebar/stores.react.jsx b/app/addons/documents/sidebar/stores.react.jsx
new file mode 100644
index 0000000..0b8c031
--- /dev/null
+++ b/app/addons/documents/sidebar/stores.react.jsx
@@ -0,0 +1,345 @@
+// 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/sidebar/actiontypes'
+],
+
+function (app, FauxtonAPI, React, ActionTypes) {
+  var Stores = {};
+
+  Stores.SidebarStore = FauxtonAPI.Store.extend({
+
+    initialize: function () {
+      this.reset();
+    },
+
+    reset: function () {
+      this._designDocs = new Backbone.Collection();
+      this._selected = {
+        navItem: 'all-docs',
+        designDocName: '',
+        designDocSection: '', // 'metadata' / name of index group ("Views", etc.)
+        indexName: ''
+      };
+      this._loading = true;
+      this._toggledSections = {};
+
+      this._deleteIndexModalVisible = false;
+      this._deleteIndexModalDesignDocName = '';
+      this._deleteIndexModalText = '';
+      this._deleteIndexModalIndexName = '';
+      this._deleteIndexModalOnSubmit = function () { };
+
+      this._cloneIndexModalVisible = false;
+      this._cloneIndexDesignDocProp = '';
+      this._cloneIndexModalTitle = '';
+      this._cloneIndexModalSelectedDesignDoc = '';
+      this._cloneIndexModalNewDesignDocName = '';
+      this._cloneIndexModalNewIndexName = '';
+      this._cloneIndexModalIndexLabel = '';
+      this._cloneIndexModalOnSubmit = function () { };
+    },
+
+    newOptions: function (options) {
+      this._database = options.database;
+      this._designDocs = options.designDocs;
+      this._loading = false;
+
+      // this can be expanded in future as we need. Right now it can only set a top-level nav item ('all docs',
+      // 'permissions' etc.) and not a nested page
+      if (options.selectedNavItem) {
+        this._selected = {
+          navItem: options.selectedNavItem,
+          designDocName: '',
+          designDocSection: '',
+          indexName: ''
+        };
+      }
+    },
+
+    updatedDesignDocs: function (designDocs) {
+      this._designDocs = designDocs;
+    },
+
+    isDeleteIndexModalVisible: function () {
+      return this._deleteIndexModalVisible;
+    },
+
+    getDeleteIndexModalText: function () {
+      return this._deleteIndexModalText;
+    },
+
+    getDeleteIndexModalOnSubmit: function () {
+      return this._deleteIndexModalOnSubmit;
+    },
+
+    isLoading: function () {
+      return this._loading;
+    },
+
+    getDatabase: function () {
+      if (this.isLoading()) {
+        return {};
+      }
+      return this._database;
+    },
+
+    // used to toggle both design docs, and any index groups within them
+    toggleContent: function (designDoc, indexGroup) {
+      if (!this._toggledSections[designDoc]) {
+        this._toggledSections[designDoc] = {
+          visible: true,
+          indexGroups: {}
+        };
+        return;
+      }
+
+      if (indexGroup) {
+        return this.toggleIndexGroup(designDoc, indexGroup);
+      }
+
+      this._toggledSections[designDoc].visible = !this._toggledSections[designDoc].visible;
+    },
+
+    toggleIndexGroup: function (designDoc, indexGroup) {
+      var expanded = this._toggledSections[designDoc].indexGroups[indexGroup];
+
+      if (_.isUndefined(expanded)) {
+        this._toggledSections[designDoc].indexGroups[indexGroup] = true;
+        return;
+      }
+
+      this._toggledSections[designDoc].indexGroups[indexGroup] = !expanded;
+    },
+
+    isVisible: function (designDoc, indexGroup) {
+      if (!this._toggledSections[designDoc]) {
+        return false;
+      }
+      if (indexGroup) {
+        return this._toggledSections[designDoc].indexGroups[indexGroup];
+      }
+      return this._toggledSections[designDoc].visible;
+    },
+
+    getSelected: function () {
+      return this._selected;
+    },
+
+    setSelected: function (params) {
+      this._selected = {
+        navItem: params.navItem,
+        designDocName: params.designDocName,
+        designDocSection: params.designDocSection,
+        indexName: params.indexName
+      };
+
+      if (params.designDocName) {
+        if (!_.has(this._toggledSections, params.designDocName)) {
+          this._toggledSections[params.designDocName] = { visible: true, indexGroups: {} };
+        }
+        this._toggledSections[params.designDocName].visible = true;
+
+        if (params.designDocSection) {
+          this._toggledSections[params.designDocName].indexGroups[params.designDocSection] = true;
+        }
+      }
+    },
+
+    getToggledSections: function () {
+      return this._toggledSections;
+    },
+
+    getDatabaseName: function () {
+      if (this.isLoading()) {
+        return '';
+      }
+      return this._database.safeID();
+    },
+
+    getDesignDocs: function () {
+      return this._designDocs;
+    },
+
+    // returns a simple array of design doc IDs
+    getAvailableDesignDocs: function () {
+      var availableDocs = this.getDesignDocs().filter(function (doc) {
+        return !doc.isMangoDoc();
+      });
+      return _.map(availableDocs, function (doc) {
+        return doc.id;
+      });
+    },
+
+    getDesignDocList: function () {
+      if (this.isLoading()) {
+        return {};
+      }
+      var docs = this._designDocs.toJSON();
+
+      docs = _.filter(docs, function (doc) {
+        if (_.has(doc.doc, 'language')) {
+          return doc.doc.language !== 'query';
+        }
+        return true;
+      });
+
+      return docs.map(function (doc) {
+        doc.safeId = app.utils.safeURLName(doc._id.replace(/^_design\//, ""));
+        return _.extend(doc, doc.doc);
+      });
+    },
+
+    showDeleteIndexModal: function (params) {
+      this._deleteIndexModalIndexName = params.indexName;
+      this._deleteIndexModalDesignDocName = params.designDocName;
+      this._deleteIndexModalVisible = true;
+      this._deleteIndexModalText = (<div>Are you sure you want to delete the <code>{this._deleteIndexModalIndexName}</code> {params.indexLabel}?</div>);
+      this._deleteIndexModalOnSubmit = params.onDelete;
+    },
+
+    getDeleteIndexModalIndexName: function () {
+      return this._deleteIndexModalIndexName;
+    },
+
+    getDeleteIndexDesignDoc: function () {
+      var designDoc = this._designDocs.find(function (ddoc) {
+        return '_design/' + this._deleteIndexModalDesignDocName === ddoc.id;
+      }, this);
+
+      return (designDoc) ? designDoc.dDocModel() : null;
+    },
+
+    isCloneIndexModalVisible: function () {
+      return this._cloneIndexModalVisible;
+    },
+
+    getCloneIndexModalTitle: function () {
+      return this._cloneIndexModalTitle;
+    },
+
+    showCloneIndexModal: function (params) {
+      this._cloneIndexModalIndexLabel = params.indexLabel;
+      this._cloneIndexModalTitle = params.cloneIndexModalTitle;
+      this._cloneIndexModalSourceIndexName = params.sourceIndexName;
+      this._cloneIndexModalSourceDesignDocName = params.sourceDesignDocName;
+      this._cloneIndexModalSelectedDesignDoc = '_design/' + params.sourceDesignDocName;
+      this._cloneIndexDesignDocProp = '';
+      this._cloneIndexModalVisible = true;
+      this._cloneIndexModalOnSubmit = params.onSubmit;
+    },
+
+    getCloneIndexModalIndexLabel: function () {
+      return this._cloneIndexModalIndexLabel;
+    },
+
+    getCloneIndexModalOnSubmit: function () {
+      return this._cloneIndexModalOnSubmit;
+    },
+
+    getCloneIndexModalSourceIndexName: function () {
+      return this._cloneIndexModalSourceIndexName;
+    },
+
+    getCloneIndexModalSourceDesignDocName: function () {
+      return this._cloneIndexModalSourceDesignDocName;
+    },
+
+    getCloneIndexDesignDocProp: function () {
+      return this._cloneIndexDesignDocProp;
+    },
+
+    getCloneIndexModalSelectedDesignDoc: function () {
+      return this._cloneIndexModalSelectedDesignDoc;
+    },
+
+    getCloneIndexModalNewDesignDocName: function () {
+      return this._cloneIndexModalNewDesignDocName;
+    },
+
+    getCloneIndexModalNewIndexName: function () {
+      return this._cloneIndexModalNewIndexName;
+    },
+
+    dispatch: function (action) {
+      switch (action.type) {
+        case ActionTypes.SIDEBAR_SET_SELECTED_NAV_ITEM:
+          this.setSelected(action.options);
+        break;
+
+        case ActionTypes.SIDEBAR_NEW_OPTIONS:
+          this.newOptions(action.options);
+        break;
+
+        case ActionTypes.SIDEBAR_TOGGLE_CONTENT:
+          this.toggleContent(action.designDoc, action.indexGroup);
+        break;
+
+        case ActionTypes.SIDEBAR_FETCHING:
+          this._loading = true;
+        break;
+
+        case ActionTypes.SIDEBAR_REFRESH:
+        break;
+
+        case ActionTypes.SIDEBAR_SHOW_DELETE_INDEX_MODAL:
+          this.showDeleteIndexModal(action.options);
+        break;
+
+        case ActionTypes.SIDEBAR_HIDE_DELETE_INDEX_MODAL:
+          this._deleteIndexModalVisible = false;
+        break;
+
+        case ActionTypes.SIDEBAR_SHOW_CLONE_INDEX_MODAL:
+          this.showCloneIndexModal(action.options);
+        break;
+
+        case ActionTypes.SIDEBAR_HIDE_CLONE_INDEX_MODAL:
+          this._cloneIndexModalVisible = false;
+        break;
+
+        case ActionTypes.SIDEBAR_CLONE_MODAL_DESIGN_DOC_CHANGE:
+          this._cloneIndexModalSelectedDesignDoc = action.options.value;
+        break;
+
+        case ActionTypes.SIDEBAR_CLONE_MODAL_DESIGN_DOC_NEW_NAME_UPDATED:
+          this._cloneIndexModalNewDesignDocName = action.options.value;
+        break;
+
+        case ActionTypes.SIDEBAR_CLONE_MODAL_UPDATE_INDEX_NAME:
+          this._cloneIndexModalNewIndexName = action.options.value;
+        break;
+
+        case ActionTypes.SIDEBAR_UPDATED_DESIGN_DOCS:
+          this.updatedDesignDocs(action.options.designDocs);
+        break;
+
+        default:
+        return;
+        // do nothing
+      }
+
+      this.triggerChange();
+    }
+
+  });
+
+  Stores.sidebarStore = new Stores.SidebarStore();
+  Stores.sidebarStore.dispatchToken = FauxtonAPI.dispatcher.register(Stores.sidebarStore.dispatch);
+
+  return Stores;
+
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/documents/sidebar/tests/sidebar.componentsSpec.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/documents/sidebar/tests/sidebar.componentsSpec.react.jsx b/app/addons/documents/sidebar/tests/sidebar.componentsSpec.react.jsx
index 68436f8..81695f8 100644
--- a/app/addons/documents/sidebar/tests/sidebar.componentsSpec.react.jsx
+++ b/app/addons/documents/sidebar/tests/sidebar.componentsSpec.react.jsx
@@ -23,6 +23,7 @@ define([
 
   describe('DesignDoc', function () {
     var container;
+    var database = { id: 'db' };
 
     var selectedNavInfo = {
       navItem: 'all-docs',
@@ -40,80 +41,72 @@ define([
     });
 
     it('confirm only single sub-option is shown by default (metadata link)', function () {
-      var stub = function () { return true; };
       var el = TestUtils.renderIntoDocument(<DesignDoc
-        toggle={stub}
+        database={database}
+        toggle={function () {}}
         sidebarListTypes={[]}
-        contentVisible={true}
-        isVisible={stub}
-        designDoc={{}}
+        isExpanded={true}
         selectedNavInfo={selectedNavInfo}
-        designDocName="id"
-        databaseName="db-name" />, container);
+        toggledSections={{}}
+        designDoc={{ customProp: { one: 'something' } }}
+      />, container);
+
       var subOptions = $(ReactDOM.findDOMNode(el)).find('.accordion-body li');
       assert.equal(subOptions.length, 1);
    });
 
     it('confirm design doc sidebar extensions appear', function () {
-      var stub = function () { return true; };
       var el = TestUtils.renderIntoDocument(<DesignDoc
-        toggle={stub}
-        contentVisible={true}
-        isVisible={stub}
+        database={database}
+        toggle={function () {}}
         sidebarListTypes={[{
           selector: 'customProp',
           name: 'Search Indexes',
           icon: 'icon-here',
           urlNamespace: 'whatever'
         }]}
-        designDoc={{
-          customProp: {
-            one: 'something'
-          }
-        }}
+        isExpanded={true}
         selectedNavInfo={selectedNavInfo}
-        designDocName="id"
-        databaseName="db-name" />, container);
+        toggledSections={{}}
+        designDoc={{ customProp: { one: 'something' } }}
+      />, container);
+
       var subOptions = $(ReactDOM.findDOMNode(el)).find('.accordion-body li');
       assert.equal(subOptions.length, 3); // 1 for "Metadata" row, 1 for Type List row ("search indexes") and one for the index itself
     });
 
     it('confirm design doc sidebar extensions do not appear when they have no content', function () {
-      var stub = function () { return true; };
       var el = TestUtils.renderIntoDocument(<DesignDoc
-        toggle={stub}
+        database={database}
+        toggle={function () {}}
         sidebarListTypes={[{
           selector: 'customProp',
           name: 'Search Indexes',
           icon: 'icon-here',
           urlNamespace: 'whatever'
         }]}
-        contentVisible={true}
-        isVisible={stub}
+        isExpanded={true}
         selectedNavInfo={selectedNavInfo}
         designDoc={{}} // note that this is empty
-        designDocName="id"
-        databaseName="db-name" />, container);
+      />, container);
+
       var subOptions = $(ReactDOM.findDOMNode(el)).find('.accordion-body li');
       assert.equal(subOptions.length, 1);
     });
 
     it('confirm doc metadata page is highlighted if selected', function () {
-      var stub = function () { return true; };
       var el = TestUtils.renderIntoDocument(<DesignDoc
-        toggle={stub}
+        database={database}
+        toggle={function () {}}
         sidebarListTypes={[]}
-        contentVisible={true}
-        isVisible={stub}
+        isExpanded={true}
         selectedNavInfo={{
           navItem: 'designDoc',
           designDocName: 'id',
           designDocSection: 'metadata',
           indexName: ''
         }}
-        designDoc={{}}
-        designDocName="id"
-        databaseName="db-name" />, container);
+        designDoc={{}} />, container);
 
       assert.equal($(ReactDOM.findDOMNode(el)).find('.accordion-body li.active a').html(), 'Metadata');
     });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/documents/sidebar/tests/sidebar.storesSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/sidebar/tests/sidebar.storesSpec.js b/app/addons/documents/sidebar/tests/sidebar.storesSpec.js
index 3012820..b2c1f1e 100644
--- a/app/addons/documents/sidebar/tests/sidebar.storesSpec.js
+++ b/app/addons/documents/sidebar/tests/sidebar.storesSpec.js
@@ -12,7 +12,7 @@
 
 define([
   'api',
-  'addons/documents/sidebar/stores',
+  'addons/documents/sidebar/stores.react',
   'addons/documents/sidebar/actiontypes',
   'testUtils'
 ], function (FauxtonAPI, Stores, ActionTypes, testUtils) {

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/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 9ae5edb..1799fed 100644
--- a/app/addons/documents/tests/nightwatch/deletesDocuments.js
+++ b/app/addons/documents/tests/nightwatch/deletesDocuments.js
@@ -68,7 +68,7 @@ module.exports = {
       .clickWhenVisible('#header-dropdown-menu a', waitTime, false)
       .waitForElementPresent('#header-dropdown-menu  a[href*="new_view"]', waitTime, false)
       .clickWhenVisible('#header-dropdown-menu a[href*="new_view"]', waitTime, false)
-      .waitForElementPresent('.editor-wrapper', waitTime, false)
+      .waitForElementPresent('.index-cancel-link', waitTime, false)
       .waitForElementPresent('#new-ddoc', waitTime, false)
       .setValue('#new-ddoc', 'sidebar-update')
       .clearValue('#index-name')

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/documents/tests/nightwatch/doubleEmitResults.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/tests/nightwatch/doubleEmitResults.js b/app/addons/documents/tests/nightwatch/doubleEmitResults.js
index 2878744..d83ecb2 100644
--- a/app/addons/documents/tests/nightwatch/doubleEmitResults.js
+++ b/app/addons/documents/tests/nightwatch/doubleEmitResults.js
@@ -22,7 +22,7 @@ module.exports = {
     .loginToGUI()
     .populateDatabase(newDatabaseName)
     .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/testdesigndoc/_view/stubview')
-    .waitForElementPresent('.editor-wrapper', waitTime, false)
+    .waitForElementPresent('.clearfix', waitTime, false)
     .waitForElementPresent('.doc-row', waitTime, false)
     .execute(function () {
       return $('.doc-row').length;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/documents/tests/nightwatch/previousButton.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/tests/nightwatch/previousButton.js b/app/addons/documents/tests/nightwatch/previousButton.js
index 8fe2476..39ef44c 100644
--- a/app/addons/documents/tests/nightwatch/previousButton.js
+++ b/app/addons/documents/tests/nightwatch/previousButton.js
@@ -11,26 +11,8 @@
 // the License.
 
 module.exports = {
-  'View: Navigate previous navigates to _all_docs': function (client) {
-    var waitTime = client.globals.maxWaitTime,
-        newDatabaseName = client.globals.testDatabaseName,
-        baseUrl = client.globals.test_settings.launch_url;
-
-    client
-      .populateDatabase(newDatabaseName, 3)
-      .loginToGUI()
-      .url(baseUrl + '/#/database/' + newDatabaseName + '/_changes')
-      .clickWhenVisible('#nav-header-keyview')
-      .clickWhenVisible('#nav-design-function-keyviewviews a')
-      .clickWhenVisible('#keyview_keyview')
-      .clickWhenVisible('.breadcrumb-back-link .fonticon-left-open')
-      .assert.urlContains('_all_docs')
-    .end();
-  },
-
   'Mango: Navigate back to _all_docs': function (client) {
-    var waitTime = client.globals.maxWaitTime,
-        newDatabaseName = client.globals.testDatabaseName,
+    var newDatabaseName = client.globals.testDatabaseName,
         baseUrl = client.globals.test_settings.launch_url;
 
     client

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/documents/tests/nightwatch/viewClone.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/tests/nightwatch/viewClone.js b/app/addons/documents/tests/nightwatch/viewClone.js
new file mode 100644
index 0000000..9127c25
--- /dev/null
+++ b/app/addons/documents/tests/nightwatch/viewClone.js
@@ -0,0 +1,37 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+module.exports = {
+
+  'Clones a view': function (client) {
+    var waitTime = client.globals.maxWaitTime,
+        newDatabaseName = client.globals.testDatabaseName,
+        baseUrl = client.globals.test_settings.launch_url;
+
+    client
+      .createDatabase(newDatabaseName)
+      .populateDatabase(newDatabaseName)
+      .loginToGUI()
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/testdesigndoc/_view/stubview')
+      .waitForElementPresent('.prettyprint', waitTime, false)
+      .assert.containsText('.prettyprint', 'stub')
+      .clickWhenVisible('.index-list .active span', waitTime, true)
+      .clickWhenVisible('.popover-content .fonticon-files-o', waitTime, true)
+      .waitForElementVisible('#new-index-name', waitTime, true)
+      .setValue('#new-index-name', 'cloned-view')
+      .clickWhenVisible('.clone-index-modal .btn-success', waitTime, true)
+
+      // now wait for the sidebar to be updated with the new view
+      .waitForElementVisible('#testdesigndoc_cloned-view', waitTime, true)
+      .end();
+  }
+};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/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 26c549b..4eacb91 100644
--- a/app/addons/documents/tests/nightwatch/viewCreate.js
+++ b/app/addons/documents/tests/nightwatch/viewCreate.js
@@ -56,9 +56,9 @@ module.exports = {
       .checkForDocumentCreated('_design/test_design_doc-selenium-3')
       .waitForElementPresent('.prettyprint', waitTime, false)
       .waitForElementNotPresent('.loading-lines', waitTime, false)
+
+      // page now automatically redirects user to results of View. Confirm the new doc is present.
       .assert.containsText('.prettyprint', 'hasehase')
-      .back()
-      .waitForElementPresent('.watermark-logo', waitTime, false)
     .end();
   },
 
@@ -110,16 +110,6 @@ module.exports = {
       .execute('$("#save-view")[0].scrollIntoView();')
       .clickWhenVisible('#save-view')
       .checkForDocumentCreated('_design/testdesigndoc/_view/test-new-view')
-
-      .waitForElementPresent('.prettyprint', waitTime, false)
-      .waitForElementNotPresent('.loading-lines', waitTime, false)
-      //go back to all docs
-      .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
-      .clickWhenVisible('#nav-header-testdesigndoc', waitTime, false)
-      .clickWhenVisible('#nav-design-function-testdesigndocviews a', waitTime, false)
-      .execute('$("#testdesigndoc_test-new-view")[0].scrollIntoView();')
-      .clickWhenVisible('#testdesigndoc_test-new-view', waitTime, false)
-      .execute('$(".save")[0].scrollIntoView();')
       .waitForElementPresent('.prettyprint', waitTime, false)
       .waitForElementNotPresent('.loading-lines', waitTime, false)
       .assert.containsText('.prettyprint', 'enteente')
@@ -131,7 +121,6 @@ function openDifferentDropdownsAndClick (client, dropDownElement) {
   var modifier = dropDownElement.slice(1);
   var waitTime = client.globals.maxWaitTime;
   var newDatabaseName = client.globals.testDatabaseName;
-  var newDocumentName = 'create_view_doc' + modifier;
   var baseUrl = client.globals.test_settings.launch_url;
 
   return client
@@ -143,5 +132,5 @@ function openDifferentDropdownsAndClick (client, dropDownElement) {
     .clickWhenVisible(dropDownElement + ' a', waitTime, false)
     .waitForElementPresent(dropDownElement + ' a[href*="new_view"]', waitTime, false)
     .clickWhenVisible(dropDownElement + ' a[href*="new_view"]', waitTime, false)
-    .waitForElementPresent('.editor-wrapper', waitTime, false);
+    .waitForElementPresent('.index-cancel-link', waitTime, false);
 }

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/documents/tests/nightwatch/viewDelete.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/tests/nightwatch/viewDelete.js b/app/addons/documents/tests/nightwatch/viewDelete.js
new file mode 100644
index 0000000..0911066
--- /dev/null
+++ b/app/addons/documents/tests/nightwatch/viewDelete.js
@@ -0,0 +1,40 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+module.exports = {
+
+  'Deletes a view': function (client) {
+    var waitTime = client.globals.maxWaitTime,
+        newDatabaseName = client.globals.testDatabaseName,
+        baseUrl = client.globals.test_settings.launch_url;
+
+    client
+      .createDatabase(newDatabaseName)
+      .populateDatabase(newDatabaseName)
+      .loginToGUI()
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/testdesigndoc/_view/stubview')
+      .waitForElementPresent('.prettyprint', waitTime, false)
+      .assert.containsText('.prettyprint', 'stub')
+
+      // confirm the sidebar shows the testdesigndoc design doc
+      .waitForElementVisible('#testdesigndoc', waitTime, true)
+
+      .clickWhenVisible('.index-list .active span', waitTime, true)
+      .clickWhenVisible('.popover-content .fonticon-trash', waitTime, true)
+      .waitForElementVisible('.confirmation-modal .js-btn-success', waitTime, true)
+      .clickWhenVisible('.confirmation-modal .js-btn-success', waitTime, true)
+
+      // now wait for the sidebar to have removed the design doc
+      .waitForElementNotPresent('#testdesigndoc', waitTime, true)
+      .end();
+  }
+};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/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 ef9526f..20c0f5f 100644
--- a/app/addons/documents/tests/nightwatch/viewEdit.js
+++ b/app/addons/documents/tests/nightwatch/viewEdit.js
@@ -12,32 +12,38 @@
 
 module.exports = {
 
-  'Edits a design doc - set new index name': function (client) {
-    /*jshint multistr: true */
+  'Edits a design doc - renames index': function (client) {
     var waitTime = client.globals.maxWaitTime,
         newDatabaseName = client.globals.testDatabaseName,
         baseUrl = client.globals.test_settings.launch_url;
 
-    var viewUrl = newDatabaseName + '/_design/testdesigndoc/_view/hasenindex5000?limit=6&reduce=false';
     client
+      .deleteDatabase(newDatabaseName)
       .createDatabase(newDatabaseName)
       .populateDatabase(newDatabaseName)
       .loginToGUI()
-      .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/testdesigndoc/_view/stubview')
-      .waitForElementPresent('.prettyprint', waitTime, false)
-      .assert.containsText('.prettyprint', 'stub')
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/testdesigndoc/_view/stubview/edit')
+      .waitForElementPresent('.index-cancel-link', waitTime, true)
+      .waitForElementNotPresent('.spinner', waitTime, true)
+      .waitForElementNotPresent('.loading-lines', waitTime, true)
+      .waitForElementVisible('#index-name', waitTime, true)
+      .waitForElementPresent('.breadcrumb .js-lastelement', waitTime, false)
+      .waitForAttribute('.breadcrumb .js-lastelement', 'textContent', function (docContents) {
+        var regExp = new RegExp(newDatabaseName);
+        return regExp.test(docContents);
+      })
+
+      .waitForAttribute('#index-name', 'value', function (val) {
+        return val === 'stubview';
+      })
       .clearValue('#index-name')
       .setValue('#index-name', 'hasenindex5000')
-      .execute('\
-        var editor = ace.edit("map-function");\
-        editor.getSession().setValue("function (doc) { emit(\'hasehase5000\', 1); }");\
-      ')
+
       .execute('$("#save-view")[0].scrollIntoView();')
       .clickWhenVisible('#save-view')
-      .checkForStringPresent(viewUrl, 'hasehase5000')
-      .waitForElementNotPresent('.loading-lines', waitTime, false)
-      .waitForElementVisible('.prettyprint', waitTime, false)
-      .assert.containsText('.prettyprint', 'hasehase5000')
+
+      // confirm the new index name is present
+      .waitForElementVisible('#testdesigndoc_hasenindex5000', waitTime, false)
     .end();
   },
 
@@ -50,12 +56,25 @@ module.exports = {
     var viewUrl = newDatabaseName + '/_design/testdesigndoc/_view/stubview?limit=6&reduce=false';
 
     client
+      .deleteDatabase(newDatabaseName)
       .createDatabase(newDatabaseName)
       .populateDatabase(newDatabaseName)
       .loginToGUI()
-      .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/testdesigndoc/_view/stubview')
-      .waitForElementPresent('.prettyprint', waitTime, false)
-      .assert.containsText('.prettyprint', 'stub')
+
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/testdesigndoc/_view/stubview/edit')
+      .waitForElementPresent('.index-cancel-link', waitTime, true)
+      .waitForElementNotPresent('.spinner', waitTime, true)
+      .waitForElementNotPresent('.loading-lines', waitTime, true)
+      .waitForElementVisible('#index-name', waitTime, true)
+      .waitForElementPresent('.breadcrumb .js-lastelement', waitTime, false)
+      .waitForAttribute('.breadcrumb .js-lastelement', 'textContent', function (docContents) {
+        var regExp = new RegExp(newDatabaseName);
+        return regExp.test(docContents);
+      })
+
+      .waitForAttribute('#index-name', 'value', function (val) {
+        return val === 'stubview';
+      })
 
       .execute('\
         var editor = ace.edit("map-function");\
@@ -63,12 +82,18 @@ module.exports = {
         editor._emit(\'blur\');\
       ')
       .execute('$("#save-view")[0].scrollIntoView();')
-
       .clickWhenVisible('#save-view')
 
       .checkForStringPresent(viewUrl, 'hasehase6000')
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/testdesigndoc/_view/stubview')
       .waitForElementNotPresent('.loading-lines', waitTime, false)
+      .waitForElementNotPresent('.spinner', waitTime, false)
       .waitForElementVisible('.prettyprint', waitTime, false)
+      .waitForElementPresent('.breadcrumb .js-lastelement', waitTime, false)
+      .waitForAttribute('.breadcrumb .js-lastelement', 'textContent', function (docContents) {
+        var regExp = new RegExp(newDatabaseName);
+        return regExp.test(docContents);
+      })
       .waitForAttribute('#doc-list', 'textContent', function (docContents) {
         return (/hasehase6000/).test(docContents);
       })
@@ -83,6 +108,7 @@ module.exports = {
       dropDownElement = '#header-dropdown-menu';
 
     client
+      .deleteDatabase(newDatabaseName)
       .createDatabase(newDatabaseName)
       .populateDatabase(newDatabaseName)
       .loginToGUI()
@@ -92,6 +118,14 @@ module.exports = {
       .waitForElementPresent(dropDownElement, waitTime, false)
       .clickWhenVisible(dropDownElement + ' a')
       .clickWhenVisible(dropDownElement + ' a[href*="new_view"]')
+      .waitForElementNotPresent('.spinner', waitTime, true)
+      .waitForElementNotPresent('.loading-lines', waitTime, true)
+      .waitForElementPresent('.breadcrumb .js-lastelement', waitTime, false)
+      .waitForAttribute('.breadcrumb .js-lastelement', 'textContent', function (docContents) {
+        var regExp = new RegExp(newDatabaseName);
+        return regExp.test(docContents);
+      })
+
       .waitForElementVisible('#new-ddoc', waitTime, false)
       .setValue('#new-ddoc', 'view1-name')
       .clearValue('#index-name')
@@ -105,7 +139,6 @@ module.exports = {
       .execute('$("#save-view")[0].scrollIntoView();')
       .clickWhenVisible('#save-view')
       .checkForDocumentCreated('_design/view1-name')
-      .waitForElementPresent('.btn.btn-danger.delete', waitTime, false)
 
       // create the second view
       .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
@@ -113,6 +146,14 @@ module.exports = {
       .clickWhenVisible(dropDownElement + ' a')
       .clickWhenVisible(dropDownElement + ' a[href*="new_view"]')
       .waitForElementVisible('#new-ddoc', waitTime, false)
+      .waitForElementNotPresent('.spinner', waitTime, true)
+      .waitForElementNotPresent('.loading-lines', waitTime, true)
+      .waitForElementPresent('.breadcrumb .js-lastelement', waitTime, false)
+      .waitForAttribute('.breadcrumb .js-lastelement', 'textContent', function (docContents) {
+        var regExp = new RegExp(newDatabaseName);
+        return regExp.test(docContents);
+      })
+
       .setValue('#new-ddoc', 'view2-name')
       .clearValue('#index-name')
       .setValue('#index-name', 'view2')
@@ -125,15 +166,19 @@ module.exports = {
       .execute('$("#save-view")[0].scrollIntoView();')
       .clickWhenVisible('#save-view')
       .checkForDocumentCreated('_design/view2-name')
-      .waitForElementPresent('.btn.btn-danger.delete', waitTime, false)
-
-      // go back to the all docs page to ensure a page reload when we return to the Edit View page
-      .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
-      .waitForElementPresent(dropDownElement, waitTime, false)
 
       // now redirect back to first view and confirm the fields are all populated properly
-      .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/view1-name/_view/view1')
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/view1-name/_view/view1/edit')
+
+      .waitForElementNotPresent('.spinner', waitTime, true)
+      .waitForElementNotPresent('.loading-lines', waitTime, true)
       .waitForElementVisible('#save-view', waitTime, false)
+      .waitForElementPresent('.breadcrumb .js-lastelement', waitTime, false)
+      .waitForAttribute('.breadcrumb .js-lastelement', 'textContent', function (docContents) {
+        var regExp = new RegExp(newDatabaseName);
+        return regExp.test(docContents);
+      })
+
       .execute(function () {
         var editor = window.ace.edit("map-function");
         return editor.getSession().getValue();
@@ -143,34 +188,56 @@ module.exports = {
       .end();
   },
 
-  'Query Options are kept after a new reduce method is chosen': function (client) {
-    /*jshint multistr: true */
+  'Editing a view and putting it into a new design doc removes it from the old design doc': function (client) {
     var waitTime = client.globals.maxWaitTime,
-        newDatabaseName = client.globals.testDatabaseName,
-        baseUrl = client.globals.test_settings.launch_url;
-
-    var viewUrl = newDatabaseName + '/_design/testdesigndoc/_view/stubview?reduce=true&group_level=0';
+      newDatabaseName = client.globals.testDatabaseName,
+      baseUrl = client.globals.test_settings.launch_url;
 
     client
+      .deleteDatabase(newDatabaseName)
       .createDatabase(newDatabaseName)
       .populateDatabase(newDatabaseName)
       .loginToGUI()
-      .url(baseUrl + '/#/database/' + viewUrl)
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/testdesigndoc/_view/stubview')
       .waitForElementPresent('.prettyprint', waitTime, false)
-      .assert.containsText('.prettyprint', '20')
-      .clickWhenVisible('#reduce-function-selector option[value="_sum"]')
-      .execute('\
-        var editor = ace.edit("map-function");\
-        editor.getSession().setValue("function (doc) { emit(\'newstub\', 2); }");\
-      ')
-      .execute('$("#save-view")[0].scrollIntoView();')
-      .clickWhenVisible('#save-view', waitTime, false)
-      .checkForStringPresent(viewUrl, '40')
-      .waitForElementNotPresent('.loading-lines', waitTime, false)
-      .waitForElementVisible('.prettyprint', waitTime, false)
-      .waitForAttribute('.prettyprint', 'textContent', function (docContents) {
-        return (/40/).test(docContents);
+
+      // confirm the sidebar shows the testdesigndoc design doc
+      .waitForElementVisible('#testdesigndoc', waitTime, true)
+
+      .waitForElementPresent('.breadcrumb .js-lastelement', waitTime, false)
+      .waitForAttribute('.breadcrumb .js-lastelement', 'textContent', function (docContents) {
+        var regExp = new RegExp(newDatabaseName);
+        return regExp.test(docContents);
       })
-    .end();
+
+      // now edit the view and move it into a brand new design doc
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/testdesigndoc/_view/stubview/edit')
+      .waitForElementPresent('.breadcrumb .js-lastelement', waitTime, false)
+      .waitForAttribute('.breadcrumb .js-lastelement', 'textContent', function (docContents) {
+        var regExp = new RegExp(newDatabaseName);
+        return regExp.test(docContents);
+      })
+
+      .waitForElementPresent('.index-cancel-link', waitTime, true)
+      .waitForElementVisible('select#ddoc', waitTime, true)
+      .waitForElementNotPresent('.spinner', waitTime, true)
+      .waitForElementNotPresent('.loading-lines', waitTime, true)
+
+      .setValue('select#ddoc', 'new-doc')
+
+      // needed to get React to update + show the new design doc field
+      .click('body')
+
+      .waitForElementPresent('#new-ddoc', waitTime, true)
+      .execute('$("#new-ddoc")[0].scrollIntoView();')
+      .setValue('#new-ddoc', 'brand-new-ddoc')
+      .execute('$("#save-view")[0].scrollIntoView();')
+      .clickWhenVisible('#save-view')
+
+      // now wait for the old design doc to be gone, and the new one to have shown up
+      .waitForElementNotPresent('#testdesigndoc', waitTime, true)
+      .waitForElementPresent('#brand-new-ddoc', waitTime, true)
+      .end();
   }
+
 };

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/documents/tests/nightwatch/viewQueryOptions.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/tests/nightwatch/viewQueryOptions.js b/app/addons/documents/tests/nightwatch/viewQueryOptions.js
index 424dd18..2ec943c 100644
--- a/app/addons/documents/tests/nightwatch/viewQueryOptions.js
+++ b/app/addons/documents/tests/nightwatch/viewQueryOptions.js
@@ -25,10 +25,10 @@ module.exports = {
       .clickWhenVisible('#byKeys', waitTime, false)
       .setValue('#keys-input', '["document_1"]')
       .clickWhenVisible('#query-options .btn-success')
-      .waitForElementNotPresent('#right-content [data-id="document_2"]', waitTime, false)
-      .assert.elementNotPresent('#right-content [data-id="document_2"]')
-      .assert.elementNotPresent('#right-content [data-id="document_0"]')
-      .assert.elementPresent('#right-content [data-id="document_1"]')
+      .waitForElementNotPresent('#doc-list [data-id="document_2"]', waitTime, false)
+      .assert.elementNotPresent('#doc-list [data-id="document_2"]')
+      .assert.elementNotPresent('#doc-list [data-id="document_0"]')
+      .assert.elementPresent('#doc-list [data-id="document_1"]')
     .end();
   },
 
@@ -46,9 +46,9 @@ module.exports = {
       .clickWhenVisible('#byKeys', waitTime, false)
       .setValue('#keys-input', '["document_1",\n"document_2"]')
       .clickWhenVisible('#query-options .btn-success')
-      .waitForElementNotPresent('#right-content [data-id="document_0"]', waitTime, false)
-      .assert.elementNotPresent('#right-content [data-id="document_0"]')
-      .assert.elementPresent('#right-content [data-id="document_1"]')
+      .waitForElementNotPresent('#doc-list [data-id="document_0"]', waitTime, false)
+      .assert.elementNotPresent('#doc-list [data-id="document_0"]')
+      .assert.elementPresent('#doc-list [data-id="document_1"]')
       .end();
   }
 

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/fauxton/components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/components.react.jsx b/app/addons/fauxton/components.react.jsx
index 3d54e58..d18bdb1 100644
--- a/app/addons/fauxton/components.react.jsx
+++ b/app/addons/fauxton/components.react.jsx
@@ -353,7 +353,10 @@ function (app, FauxtonAPI, React, ReactDOM, ZeroClipboard, ReactBootstrap) {
   var ConfirmationModal = React.createClass({
     propTypes: {
       visible: React.PropTypes.bool.isRequired,
-      text: React.PropTypes.string.isRequired,
+      text: React.PropTypes.oneOfType([
+        React.PropTypes.string,
+        React.PropTypes.element
+      ]).isRequired,
       onClose: React.PropTypes.func.isRequired,
       onSubmit: React.PropTypes.func.isRequired
     },
@@ -377,15 +380,17 @@ function (app, FauxtonAPI, React, ReactDOM, ZeroClipboard, ReactBootstrap) {
     },
 
     render: function () {
+      var content = <p>{this.props.text}</p>;
+      if (!_.isString(this.props.text)) {
+        content = this.props.text;
+      }
       return (
         <Modal dialogClassName="confirmation-modal" show={this.props.visible} onHide={this.close}>
           <Modal.Header closeButton={true}>
             <Modal.Title>{this.props.title}</Modal.Title>
           </Modal.Header>
           <Modal.Body>
-            <p>
-              {this.props.text}
-            </p>
+            {content}
           </Modal.Body>
           <Modal.Footer>
             <button className="btn btn-success js-btn-success" onClick={this.props.onSubmit}>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/addons/permissions/routes.js
----------------------------------------------------------------------
diff --git a/app/addons/permissions/routes.js b/app/addons/permissions/routes.js
index a92666b..6f578c2 100644
--- a/app/addons/permissions/routes.js
+++ b/app/addons/permissions/routes.js
@@ -85,15 +85,10 @@ function (app, FauxtonAPI, Databases, Resources, Actions, Permissions, BaseRoute
     },
 
     cleanup: function () {
-      if (this.pageContent) {
-        this.removeView('#dashboard-content');
-      }
       if (this.leftheader) {
         this.removeView('#breadcrumbs');
       }
-      if (this.sidebar) {
-        this.removeView('#sidebar');
-      }
+      this.removeComponent('#sidebar-content');
       this.stopListening(FauxtonAPI.Events, 'lookaheadTray:update', this.onSelectDatabase);
     }
   });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/app/core/api.js
----------------------------------------------------------------------
diff --git a/app/core/api.js b/app/core/api.js
index d925aa0..2f806bd 100644
--- a/app/core/api.js
+++ b/app/core/api.js
@@ -98,6 +98,14 @@ function (FauxtonAPI, Layout, Router, RouteObject, utils, Store, constants, Flux
     return url;
   };
 
+  // out-the-box Fauxton has only Views, but scripts extending Fauxton may introduce others (search indexes, geospatial
+  // indexes, etc). This returns an array of the special design doc property names for the index types
+  FauxtonAPI.getIndexTypePropNames = function () {
+    var indexTypes = FauxtonAPI.getExtensions('IndexTypes:propNames');
+    indexTypes.push('views');
+    return indexTypes;
+  };
+
   return FauxtonAPI;
 });
 

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/3ff6ff62/assets/less/fauxton.less
----------------------------------------------------------------------
diff --git a/assets/less/fauxton.less b/assets/less/fauxton.less
index 0699495..349c9ca 100644
--- a/assets/less/fauxton.less
+++ b/assets/less/fauxton.less
@@ -584,6 +584,14 @@ footer.pagination-footer {
   line-height: 30px;
 }
 
+.simple-header {
+  font-weight: 400;
+  font-size: 15pt;
+  border-bottom: 1px solid #cccccc;
+  margin-bottom: 30px;
+  margin-top: 0;
+}
+
 // left navigationbar is opened
 @media (max-width: 730px) {
   .closeMenu {


Mime
View raw message