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

fauxton commit: updated refs/heads/master to f5f375c

Repository: couchdb-fauxton
Updated Branches:
  refs/heads/master 9d7cd4421 -> f5f375c59


Refactoring <CodeEditor />

This PR refactors the <CodeEditor /> component to make it
more generic. Ace editor instances such as the mango, index
editor, and Zen mode now use this via wrapper components
(<CodeEditorPanel /> and <ZenModeOverlay />). The goal is
to make this the one any only place where the ace editor
is instantiated. It'll also be needed for the next step:
refactoring the full page doc editor.

Also: Removed extra code to clear notifications in
<Editor />. This was redundant since the "there have been
changed" popup doesn't (and shouldn't, I'd argue) appear
for small editors.


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

Branch: refs/heads/master
Commit: f5f375c59c2f43577a3457b2652bc0d066cc1466
Parents: 9d7cd44
Author: Ben Keen <be...@gmail.com>
Authored: Wed Jun 10 11:41:06 2015 -0700
Committer: Ben Keen <be...@gmail.com>
Committed: Tue Jun 16 12:36:36 2015 -0700

----------------------------------------------------------------------
 .../components/react-components.react.jsx       | 446 +++++++++++--------
 .../tests/codeEditorPanelSpec.react.jsx         |  66 +++
 app/addons/components/tests/codeEditorSpec.js   | 134 ------
 .../components/tests/codeEditorSpec.react.jsx   | 103 +----
 .../components/tests/zenModeSpec.react.jsx      |   5 +-
 .../documents/index-editor/components.react.jsx |  31 +-
 .../tests/viewIndex.componentsSpec.react.jsx    |   6 +-
 .../documents/mango/mango.components.react.jsx  |  11 +-
 8 files changed, 359 insertions(+), 443 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/f5f375c5/app/addons/components/react-components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/components/react-components.react.jsx b/app/addons/components/react-components.react.jsx
index d543a09..1186c83 100644
--- a/app/addons/components/react-components.react.jsx
+++ b/app/addons/components/react-components.react.jsx
@@ -64,6 +64,132 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
     }
   });
 
+
+  /**
+   * A pre-packaged JS editor panel for use on the Edit Index / Mango pages. Includes options for a title, zen mode
+   * icon and beautify button.
+   */
+  var CodeEditorPanel = React.createClass({
+    getDefaultProps: function () {
+      return {
+        id: 'code-editor',
+        defaultCode: '',
+        title: '',
+        docLink: '',
+        allowZenMode: true,
+        blur: function () {}
+      };
+    },
+
+    getInitialState: function () {
+      return this.getStoreState();
+    },
+
+    getStoreState: function () {
+      return {
+        zenModeEnabled: false,
+        code: this.props.defaultCode
+      };
+    },
+
+    componentWillReceiveProps: function (nextProps) {
+      if (nextProps.defaultCode !== this.props.defaultCode) {
+        this.setState({ code: nextProps.defaultCode });
+      }
+    },
+
+    // list of JSHINT errors to ignore: gets around problem of anonymous functions not being valid
+    ignorableErrors: [
+      'Missing name in function declaration.',
+      "['{a}'] is better written in dot notation."
+    ],
+
+    getZenModeIcon: function () {
+      if (this.props.allowZenMode) {
+        return <span className="fonticon fonticon-resize-full zen-editor-icon" title="Enter Zen mode" onClick={this.enterZenMode}></span>;
+      }
+    },
+
+    getDocIcon: function () {
+      if (this.props.docLink) {
+        return (
+          <a className="help-link"
+            data-bypass="true"
+            href={this.props.docLink}
+            target="_blank"
+          >
+            <i className="icon-question-sign"></i>
+          </a>
+        );
+      }
+    },
+
+    getZenModeOverlay: function () {
+      if (this.state.zenModeEnabled) {
+        return (
+          <ZenModeOverlay
+            defaultCode={this.state.code}
+            mode={this.props.mode}
+            ignorableErrors={this.ignorableErrors}
+            onExit={this.exitZenMode}
+          />
+        );
+      }
+    },
+
+    enterZenMode: function () {
+      this.setState({
+        zenModeEnabled: true,
+        code: this.refs.codeEditor.getValue()
+      });
+    },
+
+    exitZenMode: function (content) {
+      this.setState({
+        zenModeEnabled: false,
+        code: content
+      });
+    },
+
+    getEditor: function () {
+      return this.refs.codeEditor;
+    },
+
+    getValue: function () {
+      return this.getEditor().getValue();
+    },
+
+    beautify: function (code) {
+      this.setState({ code: code });
+    },
+
+    render: function () {
+      return (
+        <div className="control-group">
+          <label>
+            <strong>{this.props.title + ' '}</strong>
+            {this.getDocIcon()}
+            {this.getZenModeIcon()}
+          </label>
+          <CodeEditor
+            id={this.props.id}
+            ref="codeEditor"
+            mode="javascript"
+            defaultCode={this.state.code}
+            showGutter={true}
+            ignorableErrors={this.ignorableErrors}
+            setHeightToLineCount={true}
+            blur={this.props.blur}
+          />
+          <Beautify code={this.state.code} beautifiedCode={this.beautify} />
+          {this.getZenModeOverlay()}
+        </div>
+      );
+    }
+  });
+
+
+  // a generic Ace Editor component. This should be the only place in the app that instantiates an editor
   var CodeEditor = React.createClass({
     getDefaultProps: function () {
       return {
@@ -71,82 +197,106 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
         mode: 'javascript',
         theme: 'idle_fingers',
         fontSize: 13,
-        code: '',
-        showEditorOnly: false,
+        defaultCode: '',
         showGutter: true,
         highlightActiveLine: true,
         showPrintMargin: false,
         autoScrollEditorIntoView: true,
-        setHeightWithJS: true,
-        isFullPageEditor: false,
-        disableUnload: false,
-        allowZenMode: true,
-        allowAnonFunction: true, // allows JS fragments, like `function (doc) { emit(doc._id, 1); `}. Suppresses JS errors
-        change: function () {}
+        autoFocus: false,
+
+        // these two options create auto-resizeable code editors, with a maximum number of lines
+        setHeightToLineCount: false,
+        maxLines: 10,
+
+        // optional editor key commands (e.g. specific save action)
+        editorCommands: [],
+
+        // notifies users that there is unsaved changes in the editor when navigating away from the page
+        notifyUnsavedChanges: false,
+
+        // an optional array of ignorable Ace editors. Lets us filter out errors based on context
+        ignorableErrors: [],
+
+        // un-Reacty, but the code editor is a self-contained component and it's helpful to be able to tie into
+        // editor specific events like content changes and leaving the editor
+        change: function () {},
+        blur: function () {}
       };
     },
 
+    // used purely to keep track of content changes. This is only reset via an explicit clearChanges() call
     getInitialState: function () {
-      return this.getStoreState();
-    },
-
-    getStoreState: function () {
       return {
-        zenModeEnabled: false
+        originalCode: this.props.defaultCode
       };
     },
 
     hasChanged: function () {
-      return !_.isEqual(this.props.code, this.getValue());
+      return !_.isEqual(this.state.originalCode, this.getValue());
     },
 
-    setupAce: function (props, shouldUpdateCode) {
-      var el = this.refs.ace.getDOMNode();
+    clearChanges: function () {
+      this.setState({
+        originalCode: this.getValue()
+      });
+    },
 
-      //set the id so our nightwatch tests can find it
-      el.id = props.id;
+    setupAce: function (props, shouldUpdateCode) {
+      this.editor = ace.edit(this.refs.ace.getDOMNode());
 
-      this.editor = ace.edit(el);
-      // Automatically scrolling cursor into view after selection
-      // change this will be disabled in the next version
-      // set editor.$blockScrolling = Infinity to disable this message
+      // suppresses an Ace editor error
       this.editor.$blockScrolling = Infinity;
 
       if (shouldUpdateCode) {
-        this.setEditorValue(props.code);
+        this.setEditorValue(props.defaultCode);
       }
 
       this.editor.setShowPrintMargin(props.showPrintMargin);
       this.editor.autoScrollEditorIntoView = props.autoScrollEditorIntoView;
       this.editor.setOption('highlightActiveLine', this.props.highlightActiveLine);
-      this.setHeightToLineCount();
 
-      if (this.props.allowAnonFunction) {
-        this.removeIncorrectAnnotations(this.editor);
+      if (this.props.setHeightToLineCount) {
+        this.setHeightToLineCount();
+      }
+
+      if (this.props.ignorableErrors) {
+        this.removeIgnorableAnnotations();
       }
 
-      this.editor.getSession().setMode("ace/mode/" + props.mode);
-      this.editor.setTheme("ace/theme/" + props.theme);
+      this.addCommands();
+      this.editor.getSession().setMode('ace/mode/' + props.mode);
+      this.editor.setTheme('ace/theme/' + props.theme);
       this.editor.setFontSize(props.fontSize);
       this.editor.getSession().setUseSoftTabs(true);
+
+      if (this.props.autoFocus) {
+        this.editor.focus();
+      }
     },
 
-    onChange: function () {
-      this.setState(this.getStoreState());
+    addCommands: function () {
+      _.each(this.props.editorCommands, function (command) {
+        this.editor.commands.addCommand(command);
+      }, this);
     },
 
     setupEvents: function () {
-      this.editor.on('blur', _.bind(this.saveCodeChange, this));
-
-      if (this.props.disableUnload) {
-        return;
+      this.editor.on('blur', _.bind(this.onBlur, this));
+      this.editor.on('change', _.bind(this.onContentChange, this));
+      if (this.props.notifyUnsavedChanges) {
+        $(window).on('beforeunload.editor_' + this.props.id, _.bind(this.quitWarningMsg));
+        FauxtonAPI.beforeUnload('editor_' + this.props.id, _.bind(this.quitWarningMsg, this));
       }
+    },
 
-      $(window).on('beforeunload.editor_' + this.props.id, _.bind(this.quitWarningMsg));
-      FauxtonAPI.beforeUnload('editor_' + this.props.id, _.bind(this.quitWarningMsg, this));
+    onBlur: function () {
+      this.props.blur(this.getValue());
     },
 
-    saveCodeChange: function () {
+    onContentChange: function () {
+      if (this.props.setHeightToLineCount) {
+        this.setHeightToLineCount();
+      }
       this.props.change(this.getValue());
     },
 
@@ -157,55 +307,17 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
     },
 
     removeEvents: function () {
-      if (this.props.disableUnload) {
-        return;
+      if (this.props.notifyUnsavedChanges) {
+        $(window).off('beforeunload.editor_' + this.props.id);
+        FauxtonAPI.removeBeforeUnload('editor_' + this.props.id);
       }
-
-      $(window).off('beforeunload.editor_' + this.props.id);
-      FauxtonAPI.removeBeforeUnload('editor_' + this.props.id);
     },
 
     setHeightToLineCount: function () {
-      if (!this.props.setHeightWithJS) {
-        return;
-      }
-
-      var lines = this.editor.getSession().getDocument().getLength();
-
-      if (this.props.isFullPageEditor) {
-        var maxLines = this.getMaxAvailableLinesOnPage();
-        lines = lines < maxLines ? lines : maxLines;
-      }
+      var numLines = this.editor.getSession().getDocument().getLength();
+      var maxLines = (numLines > this.props.maxLines) ? this.props.maxLines : numLines;
       this.editor.setOptions({
-        maxLines: lines
-      });
-    },
-
-    // List of JSHINT errors to ignore
-    // Gets around problem of anonymous functions not being a valid statement
-    excludedViewErrors: [
-      "Missing name in function declaration.",
-      "['{a}'] is better written in dot notation."
-    ],
-
-    isIgnorableError: function (msg) {
-      return _.contains(this.excludedViewErrors, msg);
-    },
-
-    removeIncorrectAnnotations: function (editor) {
-      var isIgnorableError = this.isIgnorableError;
-      editor.getSession().on("changeAnnotation", function () {
-        var annotations = editor.getSession().getAnnotations();
-        var newAnnotations = _.reduce(annotations, function (annotations, error) {
-          if (!isIgnorableError(error.raw)) {
-            annotations.push(error);
-          }
-          return annotations;
-        }, []);
-
-        if (annotations.length !== newAnnotations.length) {
-          editor.getSession().setAnnotations(newAnnotations);
-        }
+        maxLines: maxLines
       });
     },
 
@@ -220,56 +332,45 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
     },
 
     componentWillReceiveProps: function (nextProps) {
-      var codeChanged = !_.isEqual(nextProps.code, this.getValue());
+      var codeChanged = !_.isEqual(nextProps.defaultCode, this.getValue());
       this.setupAce(nextProps, codeChanged);
     },
 
-    editSaved: function () {
-      return this.hasChanged();
-    },
-
-    zenModeIcon: function () {
-      if (this.props.allowZenMode) {
-        return <span className="fonticon fonticon-resize-full zen-editor-icon" title="Enter Zen mode" onClick={this.enterZenMode}></span>;
-      }
+    getAnnotations: function () {
+      return this.editor.getSession().getAnnotations();
     },
 
-    enterZenMode: function () {
-      this.setState({ zenModeEnabled: true });
+    isIgnorableError: function (msg) {
+      return _.contains(this.props.ignorableErrors, msg);
     },
 
-    getTitleFragment: function () {
-      if (!this.props.docs) {
-        return (<strong>{this.props.title}</strong>);
-      }
-
-      return (
-        <label>
-          <strong>{this.props.title + ' '}</strong>
-          <a
-            className="help-link"
-            data-bypass="true"
-            href={this.props.docs}
-            target="_blank"
-          >
-          <i className="icon-question-sign"></i>
-          </a>
-          {this.zenModeIcon()}
-        </label>
-      );
-    },
+    removeIgnorableAnnotations: function () {
+      var isIgnorableError = this.isIgnorableError;
+      this.editor.getSession().on('changeAnnotation', function () {
+        var annotations = this.editor.getSession().getAnnotations();
+        var newAnnotations = _.reduce(annotations, function (annotations, error) {
+          if (!isIgnorableError(error.raw)) {
+            annotations.push(error);
+          }
+          return annotations;
+        }, []);
 
-    getAnnotations: function () {
-      return this.editor.getSession().getAnnotations();
+        if (annotations.length !== newAnnotations.length) {
+          this.editor.getSession().setAnnotations(newAnnotations);
+        }
+      }.bind(this));
     },
 
+    // ------------------
+    // TODO two things to do after full page doc editor refactor:
+    // 1. rename to hasErrors()
     hadValidCode: function () {
       var errors = this.getAnnotations();
-      // By default CouchDB view functions don't pass lint
       return _.every(errors, function (error) {
         return this.isIgnorableError(error.raw);
       }, this);
     },
+    // ------------------
 
     setEditorValue: function (code, lineNumber) {
       lineNumber = lineNumber ? lineNumber : -1;
@@ -280,41 +381,9 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
       return this.editor.getValue();
     },
 
-    getEditor: function () {
-      return this;
-    },
-
-    getZenModeOverlay: function () {
-      if (this.state.zenModeEnabled) {
-        return (
-          <ZenModeOverlay
-            visible={this.state.zenModeEnabled}
-            defaultCode={this.getValue()}
-            mode={this.props.mode}
-            removeIncorrectAnnotations={this.removeIncorrectAnnotations}
-            onExit={this.onExitZenMode}
-          />
-        );
-      }
-    },
-
-    onExitZenMode: function (content) {
-      this.setEditorValue(content);
-      this.setState({ zenModeEnabled: false });
-    },
-
     render: function () {
-      if (this.props.showEditorOnly) {
-        return (<div ref="ace" className="js-editor" id={this.props.id}></div>);
-      }
-
       return (
-        <div className="control-group">
-          {this.getTitleFragment()}
-          <div ref="ace" className="js-editor" id={this.props.id}></div>
-          <Beautify code={this.props.code} beautifiedCode={this.setEditorValue} />
-          {this.getZenModeOverlay()}
-        </div>
+        <div ref="ace" className="js-editor" id={this.props.id}></div>
       );
     }
   });
@@ -330,12 +399,17 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
       return {
         mode: 'javascript',
         defaultCode: '',
-        removeIncorrectAnnotations: null,
+        ignorableErrors: [],
         onExit: null,
         highlightActiveLine: false
       };
     },
 
+    themes: {
+      dark: 'idle_fingers',
+      light: 'dawn'
+    },
+
     getInitialState: function () {
       return this.getStoreState();
     },
@@ -357,64 +431,41 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
     },
 
     componentDidMount: function () {
-      this.update();
-      this.editor.focus();
       $(this.refs.exit.getDOMNode()).tooltip({ placement: 'left' });
       $(this.refs.theme.getDOMNode()).tooltip({ placement: 'left' });
     },
 
-    componentDidUpdate: function () {
-      this.update();
+    exitZenMode: function () {
+      this.props.onExit(this.getValue());
     },
 
-    exitZenMode: function () {
-      this.props.onExit(this.editor.getValue());
+    getValue: function () {
+      return this.refs.ace.getValue();
     },
 
     toggleTheme: function () {
       var newTheme = (this.state.theme === 'dark') ? 'light' : 'dark';
       this.setState({
         theme: newTheme,
-        code: this.editor.getValue()
+        code: this.getValue()
       });
       app.utils.localStorageSet('zenTheme', newTheme);
     },
 
-    update: function () {
-      var el = this.refs.ace.getDOMNode();
-      this.editor = ace.edit(el);
-      this.editor.$blockScrolling = Infinity;
-      this.setEditorValue(this.state.code);
-
-      var theme = this.state.theme === 'dark' ? 'idle_fingers' : 'dawn';
-      this.editor.setTheme('ace/theme/' + theme);
-      this.editor.getSession().setMode('ace/mode/' + this.props.mode);
-      this.editor.getSession().setUseSoftTabs(true);
-      this.editor.setOption('highlightActiveLine', this.props.highlightActiveLine);
-      this.editor.setShowPrintMargin(false);
-
-      // escape exits zen mode. Add the key binding
-      this.editor.commands.addCommand({
-        name: "close",
-        bindKey: { win: "ESC", mac: "ESC" },
-        exec: function () {
-          this.exitZenMode();
-        }.bind(this)
-      });
-
-      // if an annotation removal method has been passed, ensure it's called so that error messages are cleaned
-      if (this.props.removeIncorrectAnnotations) {
-        this.props.removeIncorrectAnnotations(this.editor);
-      }
-    },
-
     setEditorValue: function (code, lineNumber) {
       lineNumber = lineNumber ? lineNumber : -1;
       this.editor.setValue(code, lineNumber);
     },
 
     render: function () {
-      var classes = "full-page-editor-modal-wrapper zen-theme-" + this.state.theme;
+      var classes = 'full-page-editor-modal-wrapper zen-theme-' + this.state.theme;
+
+      var editorCommands = [{
+        name: 'close',
+        bindKey: { win: 'ESC', mac: 'ESC' },
+        exec: this.exitZenMode
+      }];
+
       return (
         <div className={classes}>
           <div className="zen-mode-controls">
@@ -438,7 +489,15 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
             </ul>
             <div className="tooltips"></div>
           </div>
-          <div ref="ace" className="js-editor"></div>
+          <CodeEditor
+            ref="ace"
+            autoFocus={true}
+            theme={this.themes[this.state.theme]}
+            defaultCode={this.props.defaultCode}
+            editorCommands={editorCommands}
+            ignorableErrors={this.props.ignorableErrors}
+            highlightActiveLine={this.props.highlightActiveLine}
+          />
         </div>
       );
     }
@@ -503,7 +562,6 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
   });
 
   var Document = React.createClass({
-
     propTypes: {
       docIdentifier: React.PropTypes.string.isRequired,
       docChecked: React.PropTypes.func.isRequired
@@ -534,7 +592,6 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
     },
 
     getCheckbox: function () {
-
       if (!this.props.isDeletable) {
         return <div className="checkbox-dummy"></div>;
       }
@@ -594,9 +651,7 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
   });
 
   var LoadLines = React.createClass({
-
     render: function () {
-
       return (
         <div className="loading-lines">
           <div id="line1"> </div>
@@ -606,7 +661,6 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
         </div>
       );
     }
-
   });
 
   var ConfirmButton = React.createClass({
@@ -660,8 +714,8 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
           <ul className="dropdown-menu arrow" key={key} role="menu" aria-labelledby="dLabel">
             {this.createSectionTitle(linkSection.title)}
             {this.createSectionLinks(linkSection.links)}
-        </ul>
-      );
+          </ul>
+        );
       }.bind(this));
     },
 
@@ -676,10 +730,12 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
 
   });
 
-  var ReactComponents = {
+
+  return {
     ConfirmButton: ConfirmButton,
     ToggleHeaderButton: ToggleHeaderButton,
     StyledSelect: StyledSelect,
+    CodeEditorPanel: CodeEditorPanel,
     CodeEditor: CodeEditor,
     ZenModeOverlay: ZenModeOverlay,
     Beautify: Beautify,
@@ -689,6 +745,4 @@ function (app, FauxtonAPI, React, Components, ace, beautifyHelper) {
     MenuDropDown: MenuDropDown
   };
 
-  return ReactComponents;
-
 });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/f5f375c5/app/addons/components/tests/codeEditorPanelSpec.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/components/tests/codeEditorPanelSpec.react.jsx b/app/addons/components/tests/codeEditorPanelSpec.react.jsx
new file mode 100644
index 0000000..b0bbcce
--- /dev/null
+++ b/app/addons/components/tests/codeEditorPanelSpec.react.jsx
@@ -0,0 +1,66 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+define([
+  'api',
+  'addons/components/react-components.react',
+  'testUtils',
+  'react'
+], function (FauxtonAPI, ReactComponents, utils, React) {
+
+  var assert = utils.assert;
+  var TestUtils = React.addons.TestUtils;
+  var code = 'function (doc) {\n  emit(doc._id, 1);\n}';
+
+  describe('CodeEditorPanel', function () {
+
+    describe('Doc icon', function () {
+      it('hidden by default', function () {
+        var container = document.createElement('div');
+        var codeEditorEl = TestUtils.renderIntoDocument(
+          <ReactComponents.CodeEditorPanel defaultCode={code} />,
+          container
+        );
+        assert.equal($(codeEditorEl.getDOMNode()).find('.icon-question-sign').length, 0);
+      });
+      it('hidden by default', function () {
+        var container = document.createElement('div');
+        var codeEditorEl = TestUtils.renderIntoDocument(
+          <ReactComponents.CodeEditorPanel defaultCode={code} docLink="http://link.com" />,
+          container
+        );
+        assert.equal($(codeEditorEl.getDOMNode()).find('.icon-question-sign').length, 1);
+      });
+    });
+
+    describe('Zen Mode', function () {
+      it('shows zen mode by default', function () {
+        var container = document.createElement('div');
+        var codeEditorEl = TestUtils.renderIntoDocument(
+          <ReactComponents.CodeEditorPanel defaultCode={code} />,
+          container
+        );
+        assert.equal($(codeEditorEl.getDOMNode()).find('.zen-editor-icon').length, 1);
+      });
+
+      it('omits zen mode if explicitly turned off', function () {
+        var container = document.createElement('div');
+        var codeEditorEl = TestUtils.renderIntoDocument(
+          <ReactComponents.CodeEditor defaultCode={code} allowZenMode={false} />,
+          container
+        );
+        assert.equal($(codeEditorEl.getDOMNode()).find('.zen-editor-icon').length, 0);
+      });
+    });
+
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/f5f375c5/app/addons/components/tests/codeEditorSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/components/tests/codeEditorSpec.js b/app/addons/components/tests/codeEditorSpec.js
deleted file mode 100644
index 3832cd4..0000000
--- a/app/addons/components/tests/codeEditorSpec.js
+++ /dev/null
@@ -1,134 +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([
-  'api',
-  'addons/components/react-components.react',
-
-  'testUtils',
-  'react'
-], function (FauxtonAPI, ReactComponents, utils, React) {
-
-  var assert = utils.assert;
-  var TestUtils = React.addons.TestUtils;
-  var code = 'function (doc) {\n  emit(doc._id, 1);\n}';
-  var code2 = 'function (doc) {\n if(doc._id) { \n emit(doc._id, 2); \n } \n}';
-
-  describe('Code Editor', function () {
-    var container, codeEditorEl, spy;
-
-    beforeEach(function () {
-      spy = sinon.spy();
-      container = document.createElement('div');
-      codeEditorEl = TestUtils.renderIntoDocument(
-        React.createElement(ReactComponents.CodeEditor, {code: code, change: spy}),
-        container
-      );
-    });
-
-    afterEach(function () {
-      React.unmountComponentAtNode(container);
-    });
-
-    describe('Tracking edits', function () {
-
-      it('no change on mount', function () {
-        assert.notOk(codeEditorEl.hasChanged());
-      });
-
-      it('detects change on user input', function () {
-        codeEditorEl.editor.setValue(code2, -1);
-
-        assert.ok(codeEditorEl.hasChanged());
-      });
-
-    });
-
-    describe('onBlur', function () {
-
-      it('calls changed function', function () {
-        codeEditorEl.editor._emit('blur');
-        assert.ok(spy.calledOnce);
-      });
-
-    });
-
-    describe('setHeightToLineCount', function () {
-
-      beforeEach(function () {
-        codeEditorEl = TestUtils.renderIntoDocument(
-          React.createElement(ReactComponents.CodeEditor, {code: code, isFullPageEditor: false, setHeightWithJS: true}),
-          container
-        );
-
-      });
-
-      it('sets line height correctly for non full page', function () {
-        var spy = sinon.spy(codeEditorEl.editor, 'setOptions');
-
-        codeEditorEl.setHeightToLineCount();
-        assert.ok(spy.calledOnce);
-        assert.equal(spy.getCall(0).args[0].maxLines, 3);
-      });
-
-    });
-
-    describe('removeIncorrectAnnotations', function () {
-
-      beforeEach(function () {
-        codeEditorEl = TestUtils.renderIntoDocument(
-          React.createElement(ReactComponents.CodeEditor, {code: code}),
-          container
-        );
-
-      });
-
-      it('removes default errors that do not apply to CouchDB Views', function () {
-        assert.equal(codeEditorEl.getAnnotations(), 0);
-      });
-
-    });
-
-    describe('setEditorValue', function () {
-
-      it('sets new code', function () {
-        codeEditorEl = TestUtils.renderIntoDocument(
-          React.createElement(ReactComponents.CodeEditor, {code: code}),
-          container
-        );
-
-        codeEditorEl.setEditorValue(code2);
-        assert.deepEqual(codeEditorEl.getValue(), code2);
-      });
-
-    });
-
-    describe('showEditorOnly', function () {
-
-      it('only shows editor when showEditorOnly=true', function () {
-        codeEditorEl = TestUtils.renderIntoDocument(
-          React.createElement(ReactComponents.CodeEditor, {code: code, showEditorOnly: true}),
-          container
-        );
-        assert.notOk($(codeEditorEl.getDOMNode()).hasClass('control-group'));
-      });
-
-      it('shows everything by default', function () {
-        var codeEditorEl = TestUtils.renderIntoDocument(
-          React.createElement(ReactComponents.CodeEditor, {code: code}),
-          container
-        );
-        assert.ok($(codeEditorEl.getDOMNode()).hasClass('control-group'));
-      });
-
-    });
-  });
-});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/f5f375c5/app/addons/components/tests/codeEditorSpec.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/components/tests/codeEditorSpec.react.jsx b/app/addons/components/tests/codeEditorSpec.react.jsx
index 3d77c89..4f1fcaf 100644
--- a/app/addons/components/tests/codeEditorSpec.react.jsx
+++ b/app/addons/components/tests/codeEditorSpec.react.jsx
@@ -21,6 +21,11 @@ define([
   var code = 'function (doc) {\n  emit(doc._id, 1);\n}';
   var code2 = 'function (doc) {\n if(doc._id) { \n emit(doc._id, 2); \n } \n}';
 
+  var ignorableErrors = [
+    'Missing name in function declaration.',
+    "['{a}'] is better written in dot notation."
+  ];
+
   describe('Code Editor', function () {
     var container, codeEditorEl, spy;
 
@@ -28,7 +33,7 @@ define([
       spy = sinon.spy();
       container = document.createElement('div');
       codeEditorEl = TestUtils.renderIntoDocument(
-        <ReactComponents.CodeEditor code={code} change={spy} />,
+        <ReactComponents.CodeEditor defaultCode={code} blur={spy} />,
         container
       );
     });
@@ -38,124 +43,58 @@ define([
     });
 
     describe('Tracking edits', function () {
-
       it('no change on mount', function () {
         assert.notOk(codeEditorEl.hasChanged());
       });
 
       it('detects change on user input', function () {
         codeEditorEl.editor.setValue(code2, -1);
-
         assert.ok(codeEditorEl.hasChanged());
       });
-
     });
 
     describe('onBlur', function () {
-
-      it('calls changed function', function () {
+      it('calls blur function', function () {
         codeEditorEl.editor._emit('blur');
         assert.ok(spy.calledOnce);
       });
-
     });
 
     describe('setHeightToLineCount', function () {
-
-      beforeEach(function () {
+      it('check default num lines #1', function () {
         codeEditorEl = TestUtils.renderIntoDocument(
-          <ReactComponents.CodeEditor code={code} isFullPageEditor={false}  setHeightWithJS={true}/>,
+          <ReactComponents.CodeEditor code={code} setHeightToLineCount={true} />,
           container
         );
-
-      });
-
-      it('sets line height correctly for non full page', function () {
-        var spy = sinon.spy(codeEditorEl.editor, 'setOptions');
-
-        codeEditorEl.setHeightToLineCount();
-        assert.ok(spy.calledOnce);
-        assert.equal(spy.getCall(0).args[0].maxLines, 3);
+        assert.ok(codeEditorEl.editor.getSession().getDocument().getLength(), 3);
       });
-
-    });
-
-    describe('removeIncorrectAnnotations', function () {
-
-      beforeEach(function () {
+      it('check default num lines #2', function () {
         codeEditorEl = TestUtils.renderIntoDocument(
-          <ReactComponents.CodeEditor code={code}/>,
+          <ReactComponents.CodeEditor code={code2} setHeightToLineCount={true} />,
           container
         );
-
+        assert.ok(codeEditorEl.editor.getSession().getDocument().getLength(), 5);
       });
-
-      it('removes default errors that do not apply to CouchDB Views', function () {
-        assert.equal(codeEditorEl.getAnnotations(), 0);
-      });
-
-    });
-
-    describe('setEditorValue', function () {
-
-      it('sets new code', function () {
+      it('check maxLines', function () {
         codeEditorEl = TestUtils.renderIntoDocument(
-          <ReactComponents.CodeEditor code={code}/>,
+          <ReactComponents.CodeEditor code={code2} setHeightToLineCount={true} maxLines={2} />,
           container
         );
-
-        codeEditorEl.setEditorValue(code2);
-        assert.deepEqual(codeEditorEl.getValue(), code2);
+        assert.ok(codeEditorEl.editor.getSession().getDocument().getLength(), 2);
       });
-
     });
 
-    describe('showEditorOnly', function () {
-
-      it('only shows editor when showEditorOnly=true', function () {
+    describe('removeIncorrectAnnotations', function () {
+      beforeEach(function () {
         codeEditorEl = TestUtils.renderIntoDocument(
-          <ReactComponents.CodeEditor code={code} showEditorOnly={true} />,
+          <ReactComponents.CodeEditor defaultCode={code} ignorableErrors={ignorableErrors} />,
           container
         );
-        assert.notOk($(codeEditorEl.getDOMNode()).hasClass('control-group'));
       });
-
-      it('shows everything by default', function () {
-        var codeEditorEl = TestUtils.renderIntoDocument(
-          <ReactComponents.CodeEditor code={code} />,
-          container
-        );
-        assert.ok($(codeEditorEl.getDOMNode()).hasClass('control-group'));
+      it('removes default errors that do not apply to CouchDB Views', function () {
+        assert.equal(codeEditorEl.getAnnotations(), 0);
       });
-
     });
 
-    describe('Zen Mode', function () {
-      it('shows zen mode by default', function () {
-        var container = document.createElement('div');
-        var codeEditorEl = TestUtils.renderIntoDocument(
-          <ReactComponents.CodeEditor code={code} change={spy} docs="http://link.com" />,
-          container
-        );
-        assert.equal($(codeEditorEl.getDOMNode()).find('.zen-editor-icon').length, 1);
-      });
-
-      it('omits zen mode if explicitly turned off', function () {
-        var container = document.createElement('div');
-        var codeEditorEl = TestUtils.renderIntoDocument(
-          <ReactComponents.CodeEditor code={code} change={spy} docs="http://link.com" allowZenMode={false} />,
-          container
-        );
-        assert.equal($(codeEditorEl.getDOMNode()).find('.zen-editor-icon').length, 0);
-      });
-
-      it('updates parent editor after changing content in zen mode', function () {
-        var container = document.createElement('div');
-        var codeEditorEl = TestUtils.renderIntoDocument(
-          <ReactComponents.CodeEditor code={code} change={spy} docs="http://link.com" />,
-          container
-        );
-      });
-    });
   });
 });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/f5f375c5/app/addons/components/tests/zenModeSpec.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/components/tests/zenModeSpec.react.jsx b/app/addons/components/tests/zenModeSpec.react.jsx
index 5e51e1d..ec742d4 100644
--- a/app/addons/components/tests/zenModeSpec.react.jsx
+++ b/app/addons/components/tests/zenModeSpec.react.jsx
@@ -34,6 +34,7 @@ define([
 
     afterEach(function () {
       React.unmountComponentAtNode(container);
+      window.localStorage.removeItem('zenTheme');
     });
 
     describe('Toggle theme', function () {
@@ -44,11 +45,13 @@ define([
       it('switch to light theme on click', function () {
         TestUtils.Simulate.click($(el.getDOMNode()).find('.js-toggle-theme')[0]);
         assert.ok($(el.getDOMNode()).hasClass('zen-theme-light'));
+        // reset
+        TestUtils.Simulate.click($(el.getDOMNode()).find('.js-toggle-theme')[0]);
       });
     });
 
     describe('Closing zen mode', function () {
-      it('defaults to dark theme', function () {
+      it('method called', function () {
         TestUtils.Simulate.click($(el.getDOMNode()).find('.js-exit-zen-mode')[0]);
         assert.ok(spy.calledOnce);
       });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/f5f375c5/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 2450f62..719bf56 100644
--- a/app/addons/documents/index-editor/components.react.jsx
+++ b/app/addons/documents/index-editor/components.react.jsx
@@ -24,7 +24,7 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents)
   var indexEditorStore = Stores.indexEditorStore;
   var getDocUrl = app.helpers.getDocUrl;
   var StyledSelect = ReactComponents.StyledSelect;
-  var CodeEditor = ReactComponents.CodeEditor;
+  var CodeEditorPanel = ReactComponents.CodeEditorPanel;
   var PaddedBorderedBox = ReactComponents.PaddedBorderedBox;
   var ConfirmButton = ReactComponents.ConfirmButton;
   var LoadLines = ReactComponents.LoadLines;
@@ -178,12 +178,13 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents)
           customReduceSection;
 
       if (this.state.hasCustomReduce) {
-        customReduceSection = <CodeEditor
+        customReduceSection = <CodeEditorPanel
           ref='reduceEditor'
           id='reduce-function'
-          code={this.state.reduce}
-          change={this.updateReduceCode}
-          docs={false} title={'Custom Reduce function'} />;
+          title={'Custom Reduce function'}
+          defaultCode={this.state.reduce}
+          blur={this.updateReduceCode}
+        />;
       }
 
       return (
@@ -319,16 +320,6 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents)
       }, this);
     },
 
-    clearNotifications: function () {
-      ['mapEditor', 'reduceEditor'].forEach(function (editorName) {
-        if (editorName === 'reduceEditor' && !indexEditorStore.hasCustomReduce()) {
-          return;
-        }
-        var editor = this.refs[editorName].getEditor();
-        editor.editSaved();
-      }.bind(this));
-    },
-
     saveView: function (event) {
       event.preventDefault();
 
@@ -341,8 +332,6 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents)
         return;
       }
 
-      this.clearNotifications();
-
       Actions.saveView({
         database: this.state.database,
         newView: this.state.isNewView,
@@ -423,13 +412,13 @@ function (app, FauxtonAPI, React, Stores, Actions, Components, ReactComponents)
             </div>
             <div className="control-group">
               <PaddedBorderedBox>
-                <CodeEditor
+                <CodeEditorPanel
                   id={'map-function'}
                   ref="mapEditor"
                   title={"Map function"}
-                  docs={getDocUrl('MAP_FUNCS')}
-                  change={this.updateMapCode}
-                  code={this.state.map} />
+                  docLink={getDocUrl('MAP_FUNCS')}
+                  blur={this.updateMapCode}
+                  defaultCode={this.state.map} />
               </PaddedBorderedBox>
             </div>
             <PaddedBorderedBox>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/f5f375c5/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 5fea728..4423572 100644
--- a/app/addons/documents/index-editor/tests/viewIndex.componentsSpec.react.jsx
+++ b/app/addons/documents/index-editor/tests/viewIndex.componentsSpec.react.jsx
@@ -221,7 +221,7 @@ define([
   });
 
   describe('Editor', function () {
-    var container, editorEl, reduceStub;
+    var container, editorEl;
 
     beforeEach(function () {
       container = document.createElement('div');
@@ -235,13 +235,13 @@ define([
     });
 
     it('returns false on invalid map editor code', function () {
-      var stub = sinon.stub(editorEl.refs.mapEditor, 'hadValidCode');
+      var stub = sinon.stub(editorEl.refs.mapEditor.getEditor(), 'hadValidCode');
       stub.returns(false);
       assert.notOk(editorEl.hasValidCode());
     });
 
     it('returns true on valid map editor code', function () {
-      var stub = sinon.stub(editorEl.refs.mapEditor, 'hadValidCode');
+      var stub = sinon.stub(editorEl.refs.mapEditor.getEditor(), 'hadValidCode');
       stub.returns(true);
       assert.ok(editorEl.hasValidCode());
     });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/f5f375c5/app/addons/documents/mango/mango.components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/documents/mango/mango.components.react.jsx b/app/addons/documents/mango/mango.components.react.jsx
index 6dba412..303fbaa 100644
--- a/app/addons/documents/mango/mango.components.react.jsx
+++ b/app/addons/documents/mango/mango.components.react.jsx
@@ -30,7 +30,7 @@ function (app, FauxtonAPI, React, Stores, Actions,
   var getDocUrl = app.helpers.getDocUrl;
 
   var PaddedBorderedBox = ReactComponents.PaddedBorderedBox;
-  var CodeEditor = ReactComponents.CodeEditor;
+  var CodeEditorPanel = ReactComponents.CodeEditorPanel;
   var ConfirmButton = ReactComponents.ConfirmButton;
 
   var MangoQueryEditorController = React.createClass({
@@ -45,7 +45,7 @@ function (app, FauxtonAPI, React, Stores, Actions,
         changedQuery: mangoStore.getQueryFindCodeChanged(),
         availableIndexes: mangoStore.getAvailableQueryIndexes(),
         additionalIndexes: mangoStore.getAvailableAdditionalIndexes(),
-        isLoading: mangoStore.getLoadingIndexes(),
+        isLoading: mangoStore.getLoadingIndexes()
       };
     },
 
@@ -144,13 +144,12 @@ function (app, FauxtonAPI, React, Stores, Actions,
           </PaddedBorderedBox>
           <form className="form-horizontal" onSubmit={this.props.onSubmit}>
             <PaddedBorderedBox>
-              <CodeEditor
+              <CodeEditorPanel
                 id="query-field"
                 ref="field"
                 title={this.props.title}
-                docs={this.props.docs}
-                code={this.props.exampleCode}
-                disableUnload={true} />
+                docLink={this.props.docs}
+                defaultCode={this.props.exampleCode} />
               {this.getChangedQueryText()}
             </PaddedBorderedBox>
             {this.getIndexBox()}