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 2016/02/23 19:19:19 UTC

fauxton commit: updated refs/heads/master to 37cfb74

Repository: couchdb-fauxton
Updated Branches:
  refs/heads/master 40d956f07 -> 37cfb74bc


Make DesignDocSelector into a dumb component

The dumbifies the DesignDocSelector to remove all ties a to
store and passes everything via props so we can get more use out
of it. Specifically, I need this for my next PR which includes
a new, generic Clone Index modal that'll use this component
in different contexts (i.e. all index types).

- Includes validation in the component to make it more
self-contained and allows for better UX by focusing on error
fields.
- Save action significantly simplied to remove custom update
logic and always redirect to appropriate View page.


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

Branch: refs/heads/master
Commit: 37cfb74bc3acf62e08e08a30c09bcdda98249b56
Parents: 40d956f
Author: Ben Keen <be...@gmail.com>
Authored: Fri Feb 19 11:05:04 2016 -0800
Committer: Ben Keen <be...@gmail.com>
Committed: Tue Feb 23 07:24:34 2016 -0800

----------------------------------------------------------------------
 .../documents/assets/less/view-editor.less      |  23 ++
 app/addons/documents/index-editor/actions.js    | 112 ++++-----
 .../documents/index-editor/actiontypes.js       |   1 +
 .../documents/index-editor/components.react.jsx | 187 ++++++++-------
 app/addons/documents/index-editor/stores.js     |  66 ++++--
 .../documents/index-editor/tests/actionsSpec.js | 208 +----------------
 .../documents/index-editor/tests/storesSpec.js  |  61 +++--
 .../tests/viewIndex.componentsSpec.react.jsx    | 229 +++++++++++--------
 app/addons/documents/routes-index-editor.js     |   3 +-
 9 files changed, 388 insertions(+), 502 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/37cfb74b/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 05b60ab..51bca44 100644
--- a/app/addons/documents/assets/less/view-editor.less
+++ b/app/addons/documents/assets/less/view-editor.less
@@ -217,3 +217,26 @@ body .view-query-save .control-group {
     .spanX (@gridColumns);
   }
 }
+
+
+/* temporary CSS overrides. This will be removed once the Views is moved to the standard 2-panel layout */
+.define-view .new-ddoc-section {
+  .span5 {
+    .label {
+      display: none;
+    }
+    .control-label {
+      display: none;
+    }
+  }
+  .span3 span {
+    font-weight: bold;
+  }
+  #new-ddoc-section {
+    margin: 16px 0 0;
+    .controls {
+      margin-left: 0;
+    }
+  }
+}
+/* end temporary override */

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/37cfb74b/app/addons/documents/index-editor/actions.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/index-editor/actions.js b/app/addons/documents/index-editor/actions.js
index 294b02d..552ca6f 100644
--- a/app/addons/documents/index-editor/actions.js
+++ b/app/addons/documents/index-editor/actions.js
@@ -18,25 +18,16 @@ define([
   'addons/documents/index-results/actions'
 ],
 function (app, FauxtonAPI, Documents, ActionTypes, IndexResultsActions) {
-  var ActionHelpers = {
-    createNewDesignDoc: function (id, database) {
-      var designDoc = {
-        _id: id,
-        views: {
-        }
-      };
-
-      return new Documents.Doc(designDoc, {database: database});
-    },
 
+  var ActionHelpers = {
     findDesignDoc: function (designDocs, designDocId) {
       return _.find(designDocs, function (doc) {
         return doc.id === designDocId;
       }).dDocModel();
-
     }
   };
 
+
   return {
     //helpers are added here for use in testing actions
     helpers: ActionHelpers,
@@ -48,20 +39,6 @@ function (app, FauxtonAPI, Documents, ActionTypes, IndexResultsActions) {
       });
     },
 
-    newDesignDoc: function () {
-      FauxtonAPI.dispatch({
-        type: ActionTypes.NEW_DESIGN_DOC
-      });
-    },
-
-    designDocChange: function (id, newDesignDoc) {
-      FauxtonAPI.dispatch({
-        type: ActionTypes.DESIGN_DOC_CHANGE,
-        newDesignDoc: newDesignDoc,
-        designDocId: id
-      });
-    },
-
     changeViewName: function (name) {
       FauxtonAPI.dispatch({
         type: ActionTypes.VIEW_NAME_CHANGE,
@@ -87,58 +64,33 @@ function (app, FauxtonAPI, Documents, ActionTypes, IndexResultsActions) {
     },
 
     saveView: function (viewInfo) {
-      var designDoc;
-      var designDocs = viewInfo.designDocs;
-
-      if (_.isUndefined(viewInfo.designDocId)) {
-        FauxtonAPI.addNotification({
-          msg:  "Please enter a design doc name.",
-          type: "error",
-          clear: true
-        });
-
-        return;
-      }
-
-      if (viewInfo.newDesignDoc) {
-        designDoc = ActionHelpers.createNewDesignDoc(viewInfo.designDocId, viewInfo.database);
-      } else {
-        designDoc = ActionHelpers.findDesignDoc(designDocs, viewInfo.designDocId);
-      }
-
-      var result = designDoc.setDdocView(viewInfo.viewName,
-                            viewInfo.map,
-                            viewInfo.reduce);
+      var designDoc = viewInfo.designDoc;
+      designDoc.setDdocView(viewInfo.viewName, viewInfo.map, viewInfo.reduce);
 
-      if (result) {
+      FauxtonAPI.addNotification({
+        msg:  "Saving View...",
+        type: "info",
+        clear: true
+      });
 
+      designDoc.save().then(function () {
         FauxtonAPI.addNotification({
-          msg:  "Saving View...",
-          type: "info",
+          msg:  "View Saved.",
+          type: "success",
           clear: true
         });
 
-        designDoc.save().then(function () {
-          FauxtonAPI.addNotification({
-            msg:  "View Saved.",
-            type: "success",
-            clear: true
-          });
-
-
-          if (_.any([viewInfo.designDocChanged, viewInfo.hasViewNameChanged, viewInfo.newDesignDoc, viewInfo.newView])) {
-            FauxtonAPI.dispatch({
-              type: ActionTypes.VIEW_SAVED
-            });
-            var fragment = FauxtonAPI.urls('view', 'showNewlySavedView', viewInfo.database.safeID(), designDoc.safeID(), app.utils.safeURLName(viewInfo.viewName));
-            FauxtonAPI.navigate(fragment, {trigger: true});
-          } else {
-            this.updateDesignDoc(designDoc);
-          }
-
-          IndexResultsActions.reloadResultsList();
-        }.bind(this));
-      }
+        if (_.any([viewInfo.designDocChanged, viewInfo.hasViewNameChanged, viewInfo.newDesignDoc, viewInfo.newView])) {
+          FauxtonAPI.dispatch({ type: ActionTypes.VIEW_SAVED });
+          var fragment = FauxtonAPI.urls('view', 'showNewlySavedView', viewInfo.database.safeID(), designDoc.safeID(), app.utils.safeURLName(viewInfo.viewName));
+          FauxtonAPI.navigate(fragment, { trigger: true });
+        } else {
+          this.updateDesignDoc(designDoc);
+        }
+
+        // this can be removed after the Views are on their own page
+        IndexResultsActions.reloadResultsList();
+      }.bind(this));
     },
 
     updateDesignDoc: function (designDoc) {
@@ -181,6 +133,24 @@ function (app, FauxtonAPI, Documents, ActionTypes, IndexResultsActions) {
         type: ActionTypes.VIEW_UPDATE_REDUCE_CODE,
         code: code
       });
+    },
+
+    selectDesignDoc: function (designDoc) {
+      FauxtonAPI.dispatch({
+        type: ActionTypes.DESIGN_DOC_CHANGE,
+        options: {
+          value: designDoc
+        }
+      });
+    },
+
+    updateNewDesignDocName: function (designDocName) {
+      FauxtonAPI.dispatch({
+        type: ActionTypes.DESIGN_DOC_NEW_NAME_UPDATED,
+        options: {
+          value: designDocName
+        }
+      });
     }
   };
 });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/37cfb74b/app/addons/documents/index-editor/actiontypes.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/index-editor/actiontypes.js b/app/addons/documents/index-editor/actiontypes.js
index 43fe021..7104d31 100644
--- a/app/addons/documents/index-editor/actiontypes.js
+++ b/app/addons/documents/index-editor/actiontypes.js
@@ -19,6 +19,7 @@ define([], function () {
     VIEW_SAVED: 'VIEW_SAVED',
     VIEW_CREATED: 'VIEW_CREATED',
     DESIGN_DOC_CHANGE: 'DESIGN_DOC_CHANGE',
+    DESIGN_DOC_NEW_NAME_UPDATED: 'DESIGN_DOC_NEW_NAME_UPDATED',
     NEW_DESIGN_DOC: 'NEW_DESIGN_DOC',
     VIEW_NAME_CHANGE: 'VIEW_NAME_CHANGE',
     VIEW_UPDATE_DESIGN_DOC: 'VIEW_UPDATE_DESIGN_DOC',

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/37cfb74b/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 2d9e710..7c40570 100644
--- a/app/addons/documents/index-editor/components.react.jsx
+++ b/app/addons/documents/index-editor/components.react.jsx
@@ -14,13 +14,14 @@ define([
   'app',
   'api',
   'react',
+  'react-dom',
   'addons/documents/index-editor/stores',
   'addons/documents/index-editor/actions',
   'addons/fauxton/components',
   'addons/components/react-components.react'
 ],
 
-function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents) {
+function (app, FauxtonAPI, React, ReactDOM, Stores, Actions, Components, ReactComponents) {
   var indexEditorStore = Stores.indexEditorStore;
   var getDocUrl = app.helpers.getDocUrl;
   var StyledSelect = ReactComponents.StyledSelect;
@@ -29,111 +30,102 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents)
   var ConfirmButton = ReactComponents.ConfirmButton;
   var LoadLines = ReactComponents.LoadLines;
 
+
   var DesignDocSelector = React.createClass({
+    propTypes: {
+      designDocList: React.PropTypes.array.isRequired,
+      onSelectDesignDoc: React.PropTypes.func.isRequired,
+      onChangeNewDesignDocName: React.PropTypes.func.isRequired,
+      selectedDesignDocName: React.PropTypes.string.isRequired,
+      newDesignDocName: React.PropTypes.string.isRequired,
+      designDocLabel: React.PropTypes.string,
+      docURL: React.PropTypes.string
+    },
 
-    getStoreState: function () {
+    getDefaultProps: function () {
       return {
-        designDocId: indexEditorStore.getDesignDocId(),
-        designDocs: indexEditorStore.getDesignDocs(),
-        newDesignDoc: indexEditorStore.isNewDesignDoc()
+        designDocLabel: 'Design Document'
       };
     },
 
-    getInitialState: function () {
-      return this.getStoreState();
+    validate: function () {
+      if (this.props.selectedDesignDocName === 'new-doc' && this.props.newDesignDocName === '') {
+        FauxtonAPI.addNotification({
+          msg: 'Please name your design doc.',
+          type: 'error'
+        });
+        ReactDOM.findDOMNode(this.refs.newDesignDoc).focus();
+        return false;
+      }
+      return true;
     },
 
-    getNewDesignDocInput: function () {
-      return (
-        <div className="new-ddoc-section">
-          <div className="new-ddoc-input">
-            <input value={this.state.designDoc} type="text" id="new-ddoc" onChange={this.onDesignDocChange} placeholder="Name" />
-          </div>
-        </div>
-      );
+    getDocList: function () {
+      return _.map(this.props.designDocList, function (designDoc) {
+        return (<option key={designDoc} value={designDoc}>{designDoc}</option>);
+      });
     },
 
-    onDesignDocChange: function (event) {
-      Actions.designDocChange('_design/' + event.target.value, true);
+    selectDesignDoc: function (e) {
+      this.props.onSelectDesignDoc(e.target.value);
     },
 
-    getDesignDocOptions: function () {
-      return this.state.designDocs.map(function (doc, i) {
-        return <option key={i} value={doc.id}>{doc.id}</option>;
-      });
+    updateDesignDocName: function (e) {
+      this.props.onChangeNewDesignDocName(e.target.value);
     },
 
-    getSelectContent: function () {
-      var designDocOptions = this.getDesignDocOptions();
-
+    getNewDDocField: function () {
+      if (this.props.selectedDesignDocName !== 'new-doc') {
+        return;
+      }
       return (
-        <optgroup label="Select a document">
-          <option value="new">New Design Document</option>
-          {designDocOptions}
-        </optgroup>
+        <div id="new-ddoc-section" className="span5">
+          <label className="control-label" htmlFor="new-ddoc">_design/</label>
+          <div className="controls">
+            <input type="text" ref="newDesignDoc" id="new-ddoc" placeholder="newDesignDoc"
+               onChange={this.updateDesignDocName}/>
+          </div>
+        </div>
       );
     },
 
-    render: function () {
-      var designDocInput;
-      var designDocId = this.state.designDocId;
-
-      if (this.state.newDesignDoc) {
-        designDocInput = this.getNewDesignDocInput();
-        designDocId = 'new';
+    getDocLink: function () {
+      if (!this.props.docLink) {
+        return null;
       }
+      return (
+        <a className="help-link" data-bypass="true" href={this.props.docLink} target="_blank">
+          <i className="icon-question-sign" />
+        </a>
+      );
+    },
 
+    render: function () {
       return (
-        <div className="new-ddoc-section">
-          <PaddedBorderedBox>
-            <div className="control-group design-doc-group">
-              <div className="pull-left">
-                <label htmlFor="ddoc"><strong>Design Document</strong>
-                  <a className="help-link" data-bypass="true" href={getDocUrl('DESIGN_DOCS')} target="_blank">
-                    <i className="icon-question-sign">
-                    </i>
-                  </a>
-                </label>
-                <StyledSelect
-                  selectContent={this.getSelectContent()}
-                  selectChange={this.selectChange}
-                  selectId="ddoc"
-                  selectValue={designDocId}
-                />
-              </div>
-              <div className="pull-left">
-                {designDocInput}
-              </div>
+        <div className="design-doc-group control-group">
+          <div className="span3">
+            <label htmlFor="ddoc">{this.props.designDocLabel}
+              {this.getDocLink()}
+            </label>
+            <div className="styled-select">
+              <label htmlFor="js-backup-list-select">
+                <i className="fonticon-down-dir" />
+                <select id="ddoc" onChange={this.selectDesignDoc} value={this.props.selectedDesignDocName}>
+                  <optgroup label="Select a document">
+                    <option value="new-doc">New document</option>
+                    {this.getDocList()}
+                  </optgroup>
+                </select>
+              </label>
             </div>
-          </PaddedBorderedBox>
+          </div>
+          {this.getNewDDocField()}
         </div>
       );
-    },
-
-    selectChange: function (event) {
-      var designDocId = event.target.value;
-
-      if (designDocId === 'new') {
-        Actions.newDesignDoc();
-      } else {
-        Actions.designDocChange(designDocId, false);
-      }
-    },
-
-    onChange: function () {
-      this.setState(this.getStoreState());
-    },
-
-    componentDidMount: function () {
-      indexEditorStore.on('change', this.onChange, this);
-    },
-
-    componentWillUnmount: function () {
-      indexEditorStore.off('change', this.onChange);
     }
-
   });
 
+
   var ReduceEditor = React.createClass({
 
     getStoreState: function () {
@@ -277,6 +269,7 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents)
   });
 
   var Editor = React.createClass({
+
     getStoreState: function () {
       return {
         hasViewNameChanged: indexEditorStore.hasViewNameChanged(),
@@ -284,9 +277,12 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents)
         isNewView: indexEditorStore.isNewView(),
         viewName: indexEditorStore.getViewName(),
         designDocs: indexEditorStore.getDesignDocs(),
+        designDocList: indexEditorStore.getAvailableDesignDocs(),
         hasDesignDocChanged: indexEditorStore.hasDesignDocChanged(),
         newDesignDoc: indexEditorStore.isNewDesignDoc(),
         designDocId: indexEditorStore.getDesignDocId(),
+        newDesignDocName: indexEditorStore.getNewDesignDocName(),
+        saveDesignDoc: indexEditorStore.getSaveDesignDoc(),
         map: indexEditorStore.getMap(),
         isLoading: indexEditorStore.isLoading()
       };
@@ -297,7 +293,9 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents)
     },
 
     onChange: function () {
-      this.setState(this.getStoreState());
+      if (this.isMounted()) {
+        this.setState(this.getStoreState());
+      }
     },
 
     componentDidMount: function () {
@@ -314,8 +312,12 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents)
       return mapEditorErrors || customReduceErrors;
     },
 
-    saveView: function (event) {
-      event.preventDefault();
+    saveView: function (e) {
+      e.preventDefault();
+
+      if (!this.refs.designDocSelector.validate()) {
+        return;
+      }
 
       if (this.hasErrors()) {
         FauxtonAPI.addNotification({
@@ -330,6 +332,7 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents)
         database: this.state.database,
         newView: this.state.isNewView,
         viewName: this.state.viewName,
+        designDoc: this.state.saveDesignDoc,
         designDocId: this.state.designDocId,
         newDesignDoc: this.state.newDesignDoc,
         designDocChanged: this.state.hasDesignDocChanged,
@@ -340,8 +343,8 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents)
       });
     },
 
-    viewChange: function (event) {
-      Actions.changeViewName(event.target.value);
+    viewChange: function (e) {
+      Actions.changeViewName(e.target.value);
     },
 
     updateMapCode: function (code) {
@@ -358,7 +361,6 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents)
       }
 
       var url = '#/' + FauxtonAPI.urls('allDocs', 'app', this.state.database.id, '');
-
       return (
         <div className="define-view">
           <PaddedBorderedBox>
@@ -371,7 +373,20 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents)
             </div>
           </PaddedBorderedBox>
           <form className="form-horizontal view-query-save" onSubmit={this.saveView}>
-            <DesignDocSelector />
+
+            <div className="new-ddoc-section">
+              <PaddedBorderedBox>
+                <DesignDocSelector
+                  ref="designDocSelector"
+                  designDocList={this.state.designDocList}
+                  selectedDesignDocName={this.state.designDocId}
+                  newDesignDocName={this.state.newDesignDocName}
+                  onSelectDesignDoc={Actions.selectDesignDoc}
+                  onChangeNewDesignDocName={Actions.updateNewDesignDocName}
+                  docLink={getDocUrl('DESIGN_DOCS')} />
+              </PaddedBorderedBox>
+            </div>
+
             <div className="control-group">
               <PaddedBorderedBox>
                 <label htmlFor="index-name">

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/37cfb74b/app/addons/documents/index-editor/stores.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/index-editor/stores.js b/app/addons/documents/index-editor/stores.js
index 9615451..ebc37ae 100644
--- a/app/addons/documents/index-editor/stores.js
+++ b/app/addons/documents/index-editor/stores.js
@@ -12,10 +12,11 @@
 
 define([
   'api',
-  'addons/documents/index-editor/actiontypes'
+  'addons/documents/index-editor/actiontypes',
+  'addons/documents/resources'
 ],
 
-function (FauxtonAPI, ActionTypes) {
+function (FauxtonAPI, ActionTypes, Resources) {
   var Stores = {};
 
   Stores.IndexEditorStore = FauxtonAPI.Store.extend({
@@ -37,8 +38,9 @@ function (FauxtonAPI, ActionTypes) {
     editIndex: function (options) {
       this._database = options.database;
       this._newView = options.newView;
-      this._newDesignDoc = options.newDesignDoc || false;
       this._viewName = options.viewName || 'viewName';
+      this._newDesignDoc = options.newDesignDoc || false;
+      this._newDesignDocName = '';
       this._designDocs = options.designDocs;
       this._designDocId = options.designDocId;
       this._designDocChanged = false;
@@ -83,7 +85,6 @@ function (FauxtonAPI, ActionTypes) {
       return this._designDocs.find(function (ddoc) {
         return this._designDocId == ddoc.id;
       }, this).dDocModel();
-
     },
 
     getDesignDocs: function () {
@@ -92,14 +93,19 @@ function (FauxtonAPI, ActionTypes) {
       });
     },
 
+    // returns a simple array of design doc IDs
+    getAvailableDesignDocs: function () {
+      return _.map(this.getDesignDocs(), function (doc) {
+        return doc.id;
+      });
+    },
+
     getDesignDocId: function () {
       return this._designDocId;
     },
 
-    setDesignDocId: function (designDocId, newDesignDoc) {
+    setDesignDocId: function (designDocId) {
       this._designDocId = designDocId;
-      this._newDesignDoc = newDesignDoc;
-      this._designDocChanged = true;
     },
 
     hasDesignDocChanged: function () {
@@ -177,6 +183,27 @@ function (FauxtonAPI, ActionTypes) {
       this._designDocs.add(designDoc, {merge: true});
     },
 
+    getNewDesignDocName: function () {
+      return this._newDesignDocName;
+    },
+
+    getSaveDesignDoc: function () {
+      if (this._designDocId === 'new-doc') {
+        var doc = {
+          _id: '_design/' + this._newDesignDocName,
+          views: {},
+          language: 'javascript'
+        };
+        return new Resources.Doc(doc, { database: this._database });
+      }
+
+      var foundDoc = this._designDocs.find(function (ddoc) {
+        return ddoc.id === this._designDocId;
+      }.bind(this));
+
+      return (!foundDoc) ? null : foundDoc.dDocModel();
+    },
+
     dispatch: function (action) {
       switch (action.type) {
         case ActionTypes.CLEAR_INDEX:
@@ -185,68 +212,57 @@ function (FauxtonAPI, ActionTypes) {
 
         case ActionTypes.EDIT_INDEX:
           this.editIndex(action.options);
-          this.triggerChange();
         break;
 
         case ActionTypes.VIEW_NAME_CHANGE:
           this.setViewName(action.name);
-          this.triggerChange();
         break;
 
         case ActionTypes.EDIT_NEW_INDEX:
           this.editIndex(action.options);
-          this.triggerChange();
         break;
 
         case ActionTypes.SELECT_REDUCE_CHANGE:
           this.updateReduceFromSelect(action.reduceSelectedOption);
-          this.triggerChange();
         break;
 
         case ActionTypes.DESIGN_DOC_CHANGE:
-          this.setDesignDocId(action.designDocId, action.newDesignDoc);
-          this.triggerChange();
-        break;
-
-        case ActionTypes.NEW_DESIGN_DOC:
-          this.setDesignDocId('', true);
-          this.triggerChange();
+          this.setDesignDocId(action.options.value);
         break;
 
         case ActionTypes.VIEW_SAVED:
-          this.triggerChange();
         break;
 
         case ActionTypes.VIEW_CREATED:
-          this.triggerChange();
         break;
 
         case ActionTypes.VIEW_UPDATE_DESIGN_DOC:
           this.updateDesignDoc(action.designDoc);
           this.setView();
-          this.triggerChange();
         break;
 
         case ActionTypes.VIEW_UPDATE_MAP_CODE:
           this.setMap(action.code);
-          this.triggerChange();
         break;
 
         case ActionTypes.VIEW_UPDATE_REDUCE_CODE:
           this.setReduce(action.code);
-          this.triggerChange();
+        break;
+
+        case ActionTypes.DESIGN_DOC_NEW_NAME_UPDATED:
+          this._newDesignDocName = action.options.value;
         break;
 
         default:
         return;
-        // do nothing
       }
+
+      this.triggerChange();
     }
 
   });
 
   Stores.indexEditorStore = new Stores.IndexEditorStore();
-
   Stores.indexEditorStore.dispatchToken = FauxtonAPI.dispatcher.register(Stores.indexEditorStore.dispatch);
 
   return Stores;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/37cfb74b/app/addons/documents/index-editor/tests/actionsSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/index-editor/tests/actionsSpec.js b/app/addons/documents/index-editor/tests/actionsSpec.js
index 9461fd6..8064962 100644
--- a/app/addons/documents/index-editor/tests/actionsSpec.js
+++ b/app/addons/documents/index-editor/tests/actionsSpec.js
@@ -25,208 +25,12 @@ define([
 
   FauxtonAPI.router = new FauxtonAPI.Router([]);
 
+
   describe('Index Editor Actions', function () {
     var database = {
-      safeID: function () { return 'id';}
+      safeID: function () { return 'id'; }
     };
 
-    describe('save view', function () {
-      var designDoc, designDocs;
-      beforeEach(function () {
-        designDoc = {
-          _id: '_design/test-doc',
-          _rev: '1-231313',
-          views: {
-            'test-view': {
-              map: 'function () {};',
-            }
-          }
-        };
-        var doc = new Documents.Doc(designDoc, {database: database});
-        designDocs = new Documents.AllDocs([doc], {
-          params: { limit: 10 },
-          database: database
-        });
-
-        designDocs = designDocs.models;
-      });
-
-      afterEach(function () {
-        restore(FauxtonAPI.navigate);
-        restore(FauxtonAPI.triggerRouteEvent);
-        restore(IndexResultsActions.reloadResultsList);
-        restore(Actions.updateDesignDoc);
-      });
-
-      it('shows a notification if no design doc id given', function () {
-        var spy = sinon.spy(FauxtonAPI, 'addNotification');
-
-        var viewInfo = {
-          database: database,
-          viewName: 'new-doc',
-          designDocId: undefined,
-          map: 'map',
-          reduce: '_sum',
-          newDesignDoc: true,
-          newView: true,
-          designDocs: designDocs
-        };
-
-        Actions.saveView(viewInfo);
-        assert.ok(spy.calledOnce);
-        FauxtonAPI.addNotification.restore();
-      });
-
-      it('creates new design Doc for new design doc', function () {
-        var spy = sinon.spy(Actions.helpers, 'createNewDesignDoc');
-
-        var viewInfo = {
-          database: database,
-          viewName: 'new-doc',
-          designDocId: '_design/test-doc',
-          map: 'map',
-          reduce: '_sum',
-          newDesignDoc: true,
-          newView: true,
-          designDocs: designDocs
-        };
-
-        Actions.saveView(viewInfo);
-        assert.ok(spy.calledOnce);
-      });
-
-      it('sets the design doc with updated view', function () {
-        var viewInfo = {
-          viewName: 'test-view',
-          designDocId: '_design/test-doc',
-          map: 'map',
-          reduce: '_sum',
-          newDesignDoc: false,
-          newView: true,
-          designDocs: designDocs
-        };
-
-        Actions.saveView(viewInfo);
-
-        var updatedDesignDoc = _.first(designDocs).dDocModel();
-        assert.equal(updatedDesignDoc.get('views')['test-view'].reduce, '_sum');
-      });
-
-      it('saves doc', function () {
-        var viewInfo = {
-          viewName: 'test-view',
-          designDocId: '_design/test-doc',
-          map: 'map',
-          reduce: '_sum',
-          newDesignDoc: false,
-          newView: true,
-          designDocs: designDocs
-        };
-
-        var updatedDesignDoc = _.first(designDocs).dDocModel();
-        var spy = sinon.spy(updatedDesignDoc, 'save');
-        Actions.saveView(viewInfo);
-
-        assert.ok(spy.calledOnce);
-      });
-
-      it('updates design doc', function () {
-        var viewInfo = {
-          viewName: 'test-view',
-          designDocId: '_design/test-doc',
-          map: 'map',
-          reduce: '_sum',
-          newDesignDoc: false,
-          newView: false,
-          designDocs: designDocs,
-          database: {
-            safeID: function () { return '1';}
-          }
-        };
-
-        designDocs.find = function () {};
-        designDocs.add = function () {};
-        designDocs.dDocModel = function () {};
-
-        Actions.editIndex({
-          database: {id: 'rockos-db'},
-          newView: true,
-          viewName: 'test-view',
-          designDocs: designDocs,
-          designDocId: designDocs[0]._id
-        });
-
-        var promise = FauxtonAPI.Deferred();
-        promise.resolve();
-
-        var updatedDesignDoc = _.first(designDocs).dDocModel();
-        var stub = sinon.stub(updatedDesignDoc, 'save');
-        stub.returns(promise);
-
-        var spy = sinon.spy(Actions, 'updateDesignDoc');
-        Actions.saveView(viewInfo);
-
-        assert.ok(spy.calledOnce);
-      });
-
-      it('navigates to new url for new view', function () {
-        var spy = sinon.spy(FauxtonAPI, 'navigate');
-
-        var viewInfo = {
-          database: database,
-          viewName: 'test-view',
-          designDocId: '_design/test-doc',
-          map: 'map',
-          reduce: '_sum',
-          newDesignDoc: false,
-          newView: true,
-          designDocs: designDocs
-        };
-        var designDoc = _.first(designDocs);
-
-        designDoc.save = function () {
-          var promise = $.Deferred();
-          promise.resolve();
-          return promise;
-        };
-
-        Actions.saveView(viewInfo);
-        assert.ok(spy.calledOnce);
-        assert.ok(spy.getCall(0).args[0].match(/_view\/test-view/));
-      });
-
-      it('triggers reload results list', function () {
-        var spy = sinon.spy(IndexResultsActions, 'reloadResultsList');
-
-        var viewInfo = {
-          viewName: 'test-view',
-          designDocId: '_design/test-doc',
-          map: 'map',
-          reduce: '_sum',
-          newDesignDoc: false,
-          newView: false,
-          designDocs: designDocs,
-          database: {
-            safeID: function () {
-              return 'foo';
-            }
-          }
-        };
-        var designDoc = _.first(designDocs);
-
-        designDoc.save = function () {
-          var promise = $.Deferred();
-          promise.resolve();
-          return promise;
-        };
-
-        var stub = sinon.stub(Actions, 'updateDesignDoc');
-        stub.returns(true);
-
-        Actions.saveView(viewInfo);
-        assert.ok(spy.calledOnce);
-      });
-    });
 
     describe('delete view', function () {
       var designDocs, database, designDoc, designDocId, viewName;
@@ -242,10 +46,10 @@ define([
           _rev: '1-231',
           views: {
               'test-view': {
-                map: 'function () {};',
+                map: 'function () {};'
               },
               'test-view2': {
-                map: 'function () {};',
+                map: 'function () {};'
               }
             }
           }], {
@@ -254,7 +58,6 @@ define([
         });
         designDocs = designDocs.models;
         designDoc = _.first(designDocs);
-
       });
 
       afterEach(function () {
@@ -299,7 +102,6 @@ define([
         });
 
         assert.ok(spy.calledOnce);
-
       });
 
       it('navigates to all docs', function () {
@@ -318,7 +120,6 @@ define([
           designDocs: designDocs
         });
 
-
         assert.ok(spy.getCall(0).args[0].match(/_all_docs/));
         assert.ok(spy.calledOnce);
       });
@@ -345,4 +146,5 @@ define([
 
     });
   });
+
 });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/37cfb74b/app/addons/documents/index-editor/tests/storesSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/index-editor/tests/storesSpec.js b/app/addons/documents/index-editor/tests/storesSpec.js
index c60295c..db098a6 100644
--- a/app/addons/documents/index-editor/tests/storesSpec.js
+++ b/app/addons/documents/index-editor/tests/storesSpec.js
@@ -222,10 +222,43 @@ define([
           }
         };
 
-        var designDocs = new Documents.AllDocs([designDoc], {
+        var mangoDoc = {
+          "_id": "_design/123mango",
+          "id": "_design/123mango",
+          "key": "_design/123mango",
+          "value": {
+            "rev": "20-9e4bc8b76fd7d752d620bbe6e0ea9a80"
+          },
+          "doc": {
+            "_id": "_design/123mango",
+            "_rev": "20-9e4bc8b76fd7d752d620bbe6e0ea9a80",
+            "views": {
+              "test-view": {
+                "map": "function(doc) {\n  emit(doc._id, 2);\n}"
+              },
+              "new-view": {
+                "map": "function(doc) {\n  if (doc.class === \"mammal\" && doc.diet === \"herbivore\")\n    emit(doc._id, 1);\n}",
+                "reduce": "_sum"
+              }
+            },
+            "language": "query",
+            "indexes": {
+              "newSearch": {
+                "analyzer": "standard",
+                "index": "function(doc){\n index(\"default\", doc._id);\n}"
+              }
+            }
+          }
+        };
+
+        var designDocArray = _.map([designDoc, mangoDoc], function (doc) {
+          return Documents.Doc.prototype.parse(doc);
+        });
+
+        var designDocs = new Documents.AllDocs(designDocArray, {
           params: { limit: 10 },
           database: {
-            safeID: function () { return 'id';}
+            safeID: function () { return 'id'; }
           }
         });
 
@@ -240,25 +273,25 @@ define([
         });
       });
 
+      afterEach(function () {
+        store.reset();
+      });
+
       it('DESIGN_DOC_CHANGE changes design doc id', function () {
-        var designDocId =  'another-one';
+        var designDocId = 'another-one';
         FauxtonAPI.dispatch({
           type: ActionTypes.DESIGN_DOC_CHANGE,
-          designDocId: designDocId,
-          newDesignDoc: false
+          options: {
+            value: designDocId
+          }
         });
-
         assert.equal(store.getDesignDocId(), designDocId);
-        assert.notOk(store.isNewDesignDoc());
       });
 
-      it('sets new design doc on NEW_DESIGN_DOC', function () {
-        FauxtonAPI.dispatch({
-          type: ActionTypes.NEW_DESIGN_DOC
-        });
-
-        assert.ok(store.isNewDesignDoc());
-        assert.equal(store.getDesignDocId(), '');
+      it('only filters mango docs', function () {
+        var designDocs = store.getDesignDocs();
+        assert.equal(designDocs.length, 1);
+        assert.equal(designDocs[0].id, '_design/test-doc');
       });
     });
 

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/37cfb74b/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 e217d3c..51ed2a2 100644
--- a/app/addons/documents/index-editor/tests/viewIndex.componentsSpec.react.jsx
+++ b/app/addons/documents/index-editor/tests/viewIndex.componentsSpec.react.jsx
@@ -11,41 +11,44 @@
 // the License.
 define([
   'api',
+  'addons/documents/resources',
   'addons/documents/index-editor/components.react',
   'addons/documents/index-editor/stores',
   'addons/documents/index-editor/actions',
-  'addons/documents/resources',
   'testUtils',
   "react",
   'react-dom'
-], function (FauxtonAPI, Views, Stores, Actions, Documents, utils, React, ReactDOM) {
+], function (FauxtonAPI, Resources, Views, Stores, Actions, utils, React, ReactDOM) {
   FauxtonAPI.router = new FauxtonAPI.Router([]);
 
   var assert = utils.assert;
   var TestUtils = React.addons.TestUtils;
-  var restore = utils.restore;
+
 
   var resetStore = function (designDocs) {
+    Actions.editIndex({
+      database: { id: 'rockos-db' },
+      newView: false,
+      viewName: 'test-view',
+      designDocs: getDesignDocsCollection(designDocs),
+      designDocId: designDocs[0]._id
+    });
+  };
+
+  var getDesignDocsCollection = function (designDocs) {
     designDocs = designDocs.map(function (doc) {
-      return Documents.Doc.prototype.parse(doc);
+      return Resources.Doc.prototype.parse(doc);
     });
 
-    var ddocs = new Documents.AllDocs(designDocs, {
+    return new Resources.AllDocs(designDocs, {
       params: { limit: 10 },
       database: {
-        safeID: function () { return 'id';}
+        safeID: function () { return 'id'; }
       }
     });
-
-    Actions.editIndex({
-      database: {id: 'rockos-db'},
-      newView: false,
-      viewName: 'test-view',
-      designDocs: ddocs,
-      designDocId: designDocs[0]._id
-    });
   };
 
+
   describe('reduce editor', function () {
     var container, reduceEl;
 
@@ -70,8 +73,7 @@ define([
           _id: '_design/test-doc',
           views: {
             'test-view': {
-              map: 'function () {};',
-              //reduce: 'function (reduce) { reduce(); }'
+              map: 'function () {};'
             }
           }
         };
@@ -102,121 +104,146 @@ define([
     });
   });
 
-  describe('design Doc Selector', function () {
+  describe('DesignDocSelector component', function () {
     var container, selectorEl;
-
-    beforeEach(function () {
-      container = document.createElement('div');
-
-      var designDoc = {
-        "id": "_design/test-doc",
-        "key": "_design/test-doc",
-        "value": {
-          "rev": "20-9e4bc8b76fd7d752d620bbe6e0ea9a80"
-        },
-        "doc": {
-          "_id": "_design/test-doc",
-          "_rev": "20-9e4bc8b76fd7d752d620bbe6e0ea9a80",
-          "views": {
-            "test-view": {
-              "map": "function(doc) {\n  emit(doc._id, 2);\n}"
-            },
-            "new-view": {
-              "map": "function(doc) {\n  if (doc.class === \"mammal\" && doc.diet === \"herbivore\")\n    emit(doc._id, 1);\n}",
-              "reduce": "_sum"
-            }
+    var database = { id: 'db' };
+    var designDoc = {
+      "id": "_design/test-doc",
+      "key": "_design/test-doc",
+      "value": {
+        "rev": "20-9e4bc8b76fd7d752d620bbe6e0ea9a80"
+      },
+      "doc": {
+        "_id": "_design/test-doc",
+        "_rev": "20-9e4bc8b76fd7d752d620bbe6e0ea9a80",
+        "views": {
+          "test-view": {
+            "map": "function(doc) {\n  emit(doc._id, 2);\n}"
           },
-          "language": "javascript",
-          "indexes": {
-            "newSearch": {
-              "analyzer": "standard",
-              "index": "function(doc){\n index(\"default\", doc._id);\n}"
-            }
+          "new-view": {
+            "map": "function(doc) {\n  if (doc.class === \"mammal\" && doc.diet === \"herbivore\")\n    emit(doc._id, 1);\n}",
+            "reduce": "_sum"
           }
-        }
-      };
-      var mangodoc = {
-        "id": "_design/123mango",
-        "key": "_design/123mango",
-        "value": {
-          "rev": "20-9e4bc8b76fd7d752d620bbe6e0ea9a80"
         },
-        "doc": {
-          "_id": "_design/123mango",
-          "_rev": "20-9e4bc8b76fd7d752d620bbe6e0ea9a80",
-          "views": {
-            "test-view": {
-              "map": "function(doc) {\n  emit(doc._id, 2);\n}"
-            },
-            "new-view": {
-              "map": "function(doc) {\n  if (doc.class === \"mammal\" && doc.diet === \"herbivore\")\n    emit(doc._id, 1);\n}",
-              "reduce": "_sum"
-            }
-          },
-          "language": "query",
-          "indexes": {
-            "newSearch": {
-              "analyzer": "standard",
-              "index": "function(doc){\n index(\"default\", doc._id);\n}"
-            }
+        "language": "javascript",
+        "indexes": {
+          "newSearch": {
+            "analyzer": "standard",
+            "index": "function(doc){\n index(\"default\", doc._id);\n}"
           }
         }
-      };
-      resetStore([designDoc, mangodoc]);
-      selectorEl = TestUtils.renderIntoDocument(<Views.DesignDocSelector/>, container);
-    });
+      }
+    };
 
+    beforeEach(function () {
+      container = document.createElement('div');
+    });
 
     afterEach(function () {
-      restore(Actions.newDesignDoc);
-      restore(Actions.designDocChange);
       ReactDOM.unmountComponentAtNode(container);
     });
 
-    it('calls new design doc on new selected', function () {
-      var spy = sinon.spy(Actions, 'newDesignDoc');
+
+    it('calls onSelectDesignDoc on change', function () {
+      var spy = sinon.spy();
+      selectorEl = TestUtils.renderIntoDocument(
+        <Views.DesignDocSelector
+          designDocList={getDesignDocsCollection([designDoc])}
+          selectedDDocName={'new-doc'}
+          onSelectDesignDoc={spy}
+        />, container);
+
       TestUtils.Simulate.change($(ReactDOM.findDOMNode(selectorEl)).find('#ddoc')[0], {
         target: {
-          value: 'new'
+          value: '_design/test-doc'
         }
       });
-
       assert.ok(spy.calledOnce);
     });
 
-    it('calls design doc changed on a different design doc selected', function () {
-      var spy = sinon.spy(Actions, 'designDocChange');
-      TestUtils.Simulate.change($(ReactDOM.findDOMNode(selectorEl)).find('#ddoc')[0], {
-        target: {
-          value: 'another-doc'
-        }
-      });
+    it('shows new design doc field when set to new-doc', function () {
+      selectorEl = TestUtils.renderIntoDocument(
+        <Views.DesignDocSelector
+          designDocList={['_design/test-doc']}
+          selectedDesignDocName={'new-doc'}
+          onSelectDesignDoc={function () { }}
+        />, container);
 
-      assert.ok(spy.calledWith('another-doc', false));
+      assert.equal($(ReactDOM.findDOMNode(selectorEl)).find('#new-ddoc-section').length, 1);
     });
 
-    it('calls design doc changed on new design doc entered', function () {
-      var spy = sinon.spy(Actions, 'designDocChange');
-      Actions.newDesignDoc();
-      TestUtils.Simulate.change($(ReactDOM.findDOMNode(selectorEl)).find('#new-ddoc')[0], {
-        target: {
-          value: 'new-doc-entered'
-        }
-      });
+    it('hides new design doc field when design doc selected', function () {
+      selectorEl = TestUtils.renderIntoDocument(
+        <Views.DesignDocSelector
+          designDocList={['_design/test-doc']}
+          selectedDesignDocName={'_design/test-doc'}
+          onSelectDesignDoc={function () { }}
+        />, container);
+
+      assert.equal($(ReactDOM.findDOMNode(selectorEl)).find('#new-ddoc-section').length, 0);
+    });
+
+    it('always passes validation when design doc selected', function () {
+      selectorEl = TestUtils.renderIntoDocument(
+        <Views.DesignDocSelector
+          designDocList={['_design/test-doc']}
+          selectedDesignDocName={'_design/test-doc'}
+          onSelectDesignDoc={function () { }}
+        />, container);
 
-      assert.ok(spy.calledWith('_design/new-doc-entered', true));
+      assert.equal(selectorEl.validate(), true);
     });
 
-    it('does not filter usual design docs', function () {
-      assert.ok(/_design\/test-doc/.test($(ReactDOM.findDOMNode(selectorEl)).text()));
+    it('fails validation if new doc name entered/not entered', function () {
+      selectorEl = TestUtils.renderIntoDocument(
+        <Views.DesignDocSelector
+          designDocList={['_design/test-doc']}
+          selectedDesignDocName={'new-doc'}
+          newDesignDocName=''
+          onSelectDesignDoc={function () { }}
+        />, container);
+
+      // it shouldn't validate at this point: no new design doc name has been entered
+      assert.equal(selectorEl.validate(), false);
     });
 
-    it('filters mango docs', function () {
-      selectorEl = TestUtils.renderIntoDocument(<Views.DesignDocSelector/>, container);
-      assert.notOk(/_design\/123mango/.test($(ReactDOM.findDOMNode(selectorEl)).text()));
+    it('passes validation if new doc name entered/not entered', function () {
+      selectorEl = TestUtils.renderIntoDocument(
+        <Views.DesignDocSelector
+          designDocList={['_design/test-doc']}
+          selectedDesignDocName={'new-doc'}
+          newDesignDocName='new-doc-name'
+          onSelectDesignDoc={function () { }}
+        />, container);
+      assert.equal(selectorEl.validate(), true);
+    });
+
+
+    it('omits doc URL when not supplied', function () {
+      selectorEl = TestUtils.renderIntoDocument(
+        <Views.DesignDocSelector
+          designDocList={['_design/test-doc']}
+          selectedDesignDocName={'new-doc'}
+          onSelectDesignDoc={function () { }}
+        />, container);
+      assert.equal($(ReactDOM.findDOMNode(selectorEl)).find('.help-link').length, 0);
+    });
+
+    it('includes help doc link when supplied', function () {
+      var docLink = 'http://docs.com';
+      selectorEl = TestUtils.renderIntoDocument(
+        <Views.DesignDocSelector
+          designDocList={['_design/test-doc']}
+          selectedDesignDocName={'new-doc'}
+          onSelectDesignDoc={function () { }}
+          docLink={docLink}
+        />, container);
+      assert.equal($(ReactDOM.findDOMNode(selectorEl)).find('.help-link').length, 1);
+      assert.equal($(ReactDOM.findDOMNode(selectorEl)).find('.help-link').attr('href'), docLink);
     });
   });
 
+
   describe('Editor', function () {
     var container, editorEl, sandbox;
 

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/37cfb74b/app/addons/documents/routes-index-editor.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/routes-index-editor.js b/app/addons/documents/routes-index-editor.js
index 8d94a39..3a66cbd 100644
--- a/app/addons/documents/routes-index-editor.js
+++ b/app/addons/documents/routes-index-editor.js
@@ -122,9 +122,8 @@ function (app, FauxtonAPI, Helpers, BaseRoute, Documents, IndexEditorComponents,
     },
 
     newViewEditor: function (database, _designDoc) {
-      var params = app.getParams();
       var newDesignDoc = true;
-      var designDoc;
+      var designDoc = 'new-doc';
 
       if (_designDoc) {
         designDoc = '_design/' + _designDoc;