You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ga...@apache.org on 2016/11/07 08:33:46 UTC

[3/3] fauxton commit: updated refs/heads/master to 369c326

break react components into individual files


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

Branch: refs/heads/master
Commit: 369c3265498f32a1f8fa793f3d5fd0bca456b0df
Parents: 0290732
Author: Garren Smith <ga...@gmail.com>
Authored: Wed Nov 2 15:04:42 2016 +0200
Committer: Garren Smith <ga...@gmail.com>
Committed: Mon Nov 7 10:33:01 2016 +0200

----------------------------------------------------------------------
 app/addons/components/base.js                   |    7 +-
 app/addons/components/components/apibar.js      |  152 ++
 app/addons/components/components/badge.js       |   83 +
 app/addons/components/components/beautify.js    |   59 +
 app/addons/components/components/bulkaction.js  |  109 ++
 app/addons/components/components/codeeditor.js  |  370 ++++
 .../components/components/codeeditorpanel.js    |  147 ++
 .../components/components/confirmbutton.js      |   66 +
 .../components/deletedatabasemodal.js           |  119 ++
 app/addons/components/components/document.js    |  126 ++
 app/addons/components/components/loadlines.js   |   21 +
 .../components/components/menudropdown.js       |   82 +
 .../components/components/paddedborderbox.js    |   25 +
 .../components/components/stringeditmodal.js    |   95 +
 .../components/components/styledselect.js       |   39 +
 app/addons/components/components/tabelement.js  |   55 +
 .../components/components/toggleheaderbutton.js |   46 +
 app/addons/components/components/tray.js        |  108 ++
 .../components/components/zenmodeoverlay.js     |  131 ++
 .../components/react-components.react.jsx       | 1657 +-----------------
 .../tests/paddedBorderedBoxSpec.react.jsx       |    1 +
 .../fauxton/tests/componentsSpec.react.jsx      |   22 +-
 22 files changed, 1884 insertions(+), 1636 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/369c3265/app/addons/components/base.js
----------------------------------------------------------------------
diff --git a/app/addons/components/base.js b/app/addons/components/base.js
index 1f6481d..47789ef 100644
--- a/app/addons/components/base.js
+++ b/app/addons/components/base.js
@@ -12,7 +12,6 @@
 
 import "./assets/less/components.less";
 
-const Components = {};
-Components.initialize = function () {};
-
-export default Components;
+export default {
+  initialize () {}
+};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/369c3265/app/addons/components/components/apibar.js
----------------------------------------------------------------------
diff --git a/app/addons/components/components/apibar.js b/app/addons/components/components/apibar.js
new file mode 100644
index 0000000..c853cc8
--- /dev/null
+++ b/app/addons/components/components/apibar.js
@@ -0,0 +1,152 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import React from "react";
+import ReactDOM from "react-dom";
+import FauxtonAPI from "../../../core/api";
+import {TrayContents, TrayWrapper, connectToStores} from './tray';
+import FauxtonComponents from "../../fauxton/components.react";
+import Actions from "../actions";
+import Stores from "../stores";
+import {ToggleHeaderButton} from './toggleheaderbutton';
+const { componentStore } = Stores;
+
+export const APIBar = React.createClass({
+  propTypes: {
+    buttonVisible: React.PropTypes.bool.isRequired,
+    contentVisible: React.PropTypes.bool.isRequired,
+    docURL: React.PropTypes.string,
+    endpoint: React.PropTypes.string
+  },
+
+  showCopiedMessage () {
+    FauxtonAPI.addNotification({
+      msg: 'The API URL has been copied to the clipboard.',
+      type: 'success',
+      clear: true
+    });
+  },
+
+  getDocIcon () {
+    if (!this.props.docURL) {
+      return null;
+    }
+    return (
+      <a
+        className="help-link"
+        data-bypass="true"
+        href={this.props.docURL}
+        target="_blank"
+      >
+        <i className="icon icon-question-sign"></i>
+      </a>
+    );
+  },
+
+  getTray () {
+    if (!this.props.contentVisible) {
+      return null;
+    }
+
+    return (
+      <TrayContents className="tray show-tray api-bar-tray">
+        <div className="input-prepend input-append">
+          <span className="add-on">
+            API URL
+            {this.getDocIcon()}
+          </span>
+
+          <FauxtonComponents.ClipboardWithTextField
+            onClipBoardClick={this.showCopiedMessage}
+            text="Copy URL"
+            textToCopy={this.props.endpoint}
+            showCopyIcon={false}
+            uniqueKey="clipboard-apiurl" />
+
+          <div className="add-on">
+            <a
+              data-bypass="true"
+              href={this.props.endpoint}
+              target="_blank"
+              className="btn"
+            >
+              View JSON
+            </a>
+          </div>
+        </div>
+      </TrayContents>
+    );
+  },
+
+  toggleTrayVisibility () {
+    Actions.toggleApiBarVisibility(!this.props.contentVisible);
+  },
+
+  componentDidMount () {
+    $('body').on('click.APIBar', function (e) {
+
+      if (!$('.show-tray.api-bar-tray').length) {
+        return;
+      }
+
+      if ($(e.target).closest('.api-bar-tray,.control-toggle-api-url').length === 0) {
+        Actions.toggleApiBarVisibility(false);
+      }
+    }.bind(this));
+  },
+
+  componentWillUnmount () {
+    $('body').off('click.APIBar');
+  },
+
+  render () {
+    if (!this.props.buttonVisible || !this.props.endpoint) {
+      return null;
+    }
+
+    return (
+      <div>
+        <ToggleHeaderButton
+          containerClasses="header-control-box control-toggle-api-url"
+          title="API URL"
+          fonticon="fonticon-link"
+          text="API"
+          toggleCallback={this.toggleTrayVisibility} />
+
+        {this.getTray()}
+      </div>
+    );
+  }
+});
+
+export const ApiBarController = React.createClass({
+
+  getWrap () {
+    return connectToStores(TrayWrapper, [componentStore], function () {
+      return {
+        buttonVisible: componentStore.getIsAPIBarButtonVisible(),
+        contentVisible: componentStore.getIsAPIBarVisible(),
+        endpoint: componentStore.getEndpoint(),
+        docURL: componentStore.getDocURL()
+      };
+    });
+  },
+
+  render () {
+    var TrayWrapper = this.getWrap();
+    return (
+      <TrayWrapper>
+        <APIBar buttonVisible={true} contentVisible={false} />
+      </TrayWrapper>
+    );
+  }
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/369c3265/app/addons/components/components/badge.js
----------------------------------------------------------------------
diff --git a/app/addons/components/components/badge.js b/app/addons/components/components/badge.js
new file mode 100644
index 0000000..29b2599
--- /dev/null
+++ b/app/addons/components/components/badge.js
@@ -0,0 +1,83 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import React from "react";
+import ReactDOM from "react-dom";
+
+export const BadgeList = React.createClass({
+
+  propTypes: {
+    elements: React.PropTypes.array.isRequired,
+    removeBadge: React.PropTypes.func.isRequired
+  },
+
+  getDefaultProps () {
+    return {
+      getLabel (el) {
+        return el;
+      },
+
+      getId (el) {
+        return el;
+      }
+    };
+  },
+
+  getBadges () {
+    return this.props.elements.map(function (el, i) {
+      return <Badge
+        label={this.props.getLabel(el)}
+        key={i}
+        id={el}
+        remove={this.removeBadge} />;
+    }.bind(this));
+  },
+
+  removeBadge (label, el) {
+    this.props.removeBadge(label, el);
+  },
+
+  render () {
+    return (
+      <ul className="component-badgelist">
+        {this.getBadges()}
+      </ul>
+    );
+  }
+});
+
+export const Badge = React.createClass({
+  propTypes: {
+    label: React.PropTypes.string.isRequired,
+    remove: React.PropTypes.func.isRequired
+  },
+
+  remove (e) {
+    e.preventDefault();
+    this.props.remove(this.props.label, this.props.id);
+  },
+
+  render () {
+    return (
+      <li className="component-badge">
+        <span className="label label-info">{this.props.label}</span>
+        <a
+          href="#"
+          className="label label-info remove-filter"
+          onClick={this.remove} data-bypass="true"
+        >
+          &times;
+        </a>
+      </li>
+    );
+  }
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/369c3265/app/addons/components/components/beautify.js
----------------------------------------------------------------------
diff --git a/app/addons/components/components/beautify.js b/app/addons/components/components/beautify.js
new file mode 100644
index 0000000..3ff3519
--- /dev/null
+++ b/app/addons/components/components/beautify.js
@@ -0,0 +1,59 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+import React from "react";
+import ReactDOM from "react-dom";
+import beautifyHelper from "../../../../assets/js/plugins/beautify";
+
+export const Beautify = React.createClass({
+  noOfLines () {
+    return this.props.code.split(/\r\n|\r|\n/).length;
+  },
+
+  canBeautify () {
+    return this.noOfLines() === 1;
+  },
+
+  addTooltip () {
+    if (this.canBeautify) {
+      $('.beautify-tooltip').tooltip({ placement: 'right' });
+    }
+  },
+
+  componentDidMount () {
+    this.addTooltip();
+  },
+
+  beautify (event) {
+    event.preventDefault();
+    var beautifiedCode = beautifyHelper(this.props.code);
+    this.props.beautifiedCode(beautifiedCode);
+    $('.beautify-tooltip').tooltip('hide');
+  },
+
+  render () {
+    if (!this.canBeautify()) {
+      return null;
+    }
+
+    return (
+      <button
+        onClick={this.beautify}
+        className="beautify beautify_map btn btn-primary btn-small beautify-tooltip"
+        type="button"
+        data-toggle="tooltip"
+        title="Reformat your minified code to make edits to it."
+      >
+        beautify this code
+      </button>
+    );
+  }
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/369c3265/app/addons/components/components/bulkaction.js
----------------------------------------------------------------------
diff --git a/app/addons/components/components/bulkaction.js b/app/addons/components/components/bulkaction.js
new file mode 100644
index 0000000..d16e072
--- /dev/null
+++ b/app/addons/components/components/bulkaction.js
@@ -0,0 +1,109 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import React from "react";
+import ReactDOM from "react-dom";
+import {OverlayTrigger, Popover} from "react-bootstrap";
+
+export const BulkActionComponent = React.createClass({
+
+  propTypes: {
+    hasSelectedItem: React.PropTypes.bool.isRequired,
+    removeItem: React.PropTypes.func.isRequired,
+    selectAll: React.PropTypes.func,
+    toggleSelect: React.PropTypes.func.isRequired,
+    isChecked: React.PropTypes.bool.isRequired,
+    disabled: React.PropTypes.bool
+  },
+
+  getDefaultProps () {
+    return {
+      disabled: false,
+      title: 'Select rows that can be...',
+      bulkIcon: 'fonticon-trash',
+      buttonTitle: 'Delete all selected',
+      dropdownContentText: 'Deleted',
+      enableOverlay: false
+    };
+  },
+
+  render () {
+    return (
+      <div className="bulk-action-component">
+        <div className="bulk-action-component-selector-group">
+          {this.getMasterSelector()}
+          {this.getMultiSelectOptions()}
+        </div>
+      </div>
+    );
+  },
+
+  getMultiSelectOptions () {
+    if (!this.props.hasSelectedItem) {
+      return null;
+    }
+
+    return (
+      <button
+        onClick={this.props.removeItem}
+        className={'fonticon ' + this.props.bulkIcon}
+        title={this.props.buttonTitle} />
+    );
+  },
+
+  getPopupContent () {
+    return (
+      <ul className="bulk-action-component-popover-actions">
+        <li onClick={this.selectAll} >
+          <i className="icon fonticon-cancel"></i> {this.props.dropdownContentText}
+        </li>
+      </ul>
+    );
+  },
+
+  selectAll () {
+    this.refs.bulkActionPopover.hide();
+    this.props.selectAll();
+  },
+
+  getOverlay () {
+    return (
+      <OverlayTrigger
+        ref="bulkActionPopover"
+        trigger="click"
+        placement="bottom"
+        rootClose={true}
+        overlay={
+          <Popover id="bulk-action-component-popover" title={this.props.title}>
+            {this.getPopupContent()}
+          </Popover>
+        }>
+        <div className="arrow-button">
+          <i className="fonticon fonticon-play"></i>
+        </div>
+      </OverlayTrigger>
+    );
+  },
+
+  getMasterSelector () {
+    return (
+      <div className="bulk-action-component-panel">
+        <input type="checkbox"
+          checked={this.props.isChecked}
+          onChange={this.props.toggleSelect}
+          disabled={this.props.disabled} />
+        {this.props.enableOverlay ? <div className="separator"></div> : null}
+        {this.props.enableOverlay ? this.getOverlay() : null}
+      </div>
+    );
+  },
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/369c3265/app/addons/components/components/codeeditor.js
----------------------------------------------------------------------
diff --git a/app/addons/components/components/codeeditor.js b/app/addons/components/components/codeeditor.js
new file mode 100644
index 0000000..f8201dc
--- /dev/null
+++ b/app/addons/components/components/codeeditor.js
@@ -0,0 +1,370 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+import React from "react";
+import ReactDOM from "react-dom";
+import FauxtonAPI from "../../../core/api";
+import ace from "brace";
+import {StringEditModal} from './stringeditmodal';
+
+require('brace/mode/javascript');
+require('brace/mode/json');
+require('brace/theme/idle_fingers');
+
+export const CodeEditor = React.createClass({
+  getDefaultProps () {
+    return {
+      id: 'code-editor',
+      mode: 'javascript',
+      theme: 'idle_fingers',
+      fontSize: 13,
+
+      // this sets the default value for the editor. On the fly changes are stored in state in this component only. To
+      // change the editor content after initial construction use CodeEditor.setValue()
+      defaultCode: '',
+
+      showGutter: true,
+      highlightActiveLine: true,
+      showPrintMargin: false,
+      autoScrollEditorIntoView: true,
+      autoFocus: false,
+      stringEditModalEnabled: false,
+
+      // these two options create auto-resizeable code editors, with a maximum number of lines
+      setHeightToLineCount: false,
+      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 errors. Lets us filter out errors based on context
+      ignorableErrors: [],
+
+      // un-Reacty, but the code editor is a self-contained component and it's helpful to be able to tie into
+      // editor specific events like content changes and leaving the editor
+      change () {},
+      blur () {}
+    };
+  },
+
+  getInitialState () {
+    return {
+      originalCode: this.props.defaultCode,
+
+      // these are all related to the (optional) string edit modal
+      stringEditModalVisible: false,
+      stringEditIconVisible: false,
+      stringEditIconStyle: {},
+      stringEditModalValue: ''
+    };
+  },
+
+  hasChanged () {
+    return !_.isEqual(this.state.originalCode, this.getValue());
+  },
+
+  clearChanges () {
+    this.setState({
+      originalCode: this.getValue()
+    });
+  },
+
+  setupAce (props, shouldUpdateCode) {
+    this.editor = ace.edit(ReactDOM.findDOMNode(this.refs.ace));
+
+    // suppresses an Ace editor error
+    this.editor.$blockScrolling = Infinity;
+
+    if (shouldUpdateCode) {
+      this.setValue(props.defaultCode);
+    }
+
+    this.editor.setShowPrintMargin(props.showPrintMargin);
+    this.editor.autoScrollEditorIntoView = props.autoScrollEditorIntoView;
+
+    this.editor.setOption('highlightActiveLine', this.props.highlightActiveLine);
+
+    if (this.props.setHeightToLineCount) {
+      this.setHeightToLineCount();
+    }
+
+    if (this.props.ignorableErrors) {
+      this.removeIgnorableAnnotations();
+    }
+
+    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().setTabSize(2);
+    this.editor.getSession().setUseSoftTabs(true);
+
+    if (this.props.autoFocus) {
+      this.editor.focus();
+    }
+  },
+
+  addCommands () {
+    _.each(this.props.editorCommands, function (command) {
+      this.editor.commands.addCommand(command);
+    }, this);
+  },
+
+  setupEvents () {
+    this.editor.on('blur', _.bind(this.onBlur, this));
+    this.editor.on('change', _.bind(this.onContentChange, this));
+
+    if (this.props.stringEditModalEnabled) {
+      this.editor.on('changeSelection', _.bind(this.showHideEditStringGutterIcon, this));
+      this.editor.getSession().on('changeBackMarker', _.bind(this.showHideEditStringGutterIcon, this));
+      this.editor.getSession().on('changeScrollTop', _.bind(this.updateEditStringGutterIconPosition, this));
+    }
+
+    if (this.props.notifyUnsavedChanges) {
+      $(window).on('beforeunload.editor_' + this.props.id, _.bind(this.quitWarningMsg));
+      FauxtonAPI.beforeUnload('editor_' + this.props.id, _.bind(this.quitWarningMsg, this));
+    }
+  },
+
+  onBlur () {
+    this.props.blur(this.getValue());
+  },
+
+  onContentChange () {
+    if (this.props.setHeightToLineCount) {
+      this.setHeightToLineCount();
+    }
+    this.props.change(this.getValue());
+  },
+
+  quitWarningMsg () {
+    if (this.hasChanged()) {
+      return 'Your changes have not been saved. Click Cancel to return to the document, or OK to proceed.';
+    }
+  },
+
+  removeEvents () {
+    if (this.props.notifyUnsavedChanges) {
+      $(window).off('beforeunload.editor_' + this.props.id);
+      FauxtonAPI.removeBeforeUnload('editor_' + this.props.id);
+    }
+  },
+
+  setHeightToLineCount () {
+    var numLines = this.editor.getSession().getDocument().getLength();
+    var maxLines = (numLines > this.props.maxLines) ? this.props.maxLines : numLines;
+    this.editor.setOptions({
+      maxLines: maxLines
+    });
+  },
+
+  componentDidMount () {
+    this.setupAce(this.props, true);
+    this.setupEvents();
+
+    if (this.props.autoFocus) {
+      this.editor.focus();
+    }
+  },
+
+  componentWillUnmount () {
+    this.removeEvents();
+    this.editor.destroy();
+  },
+
+  componentWillReceiveProps (nextProps) {
+    this.setupAce(nextProps, false);
+  },
+
+  getAnnotations () {
+    return this.editor.getSession().getAnnotations();
+  },
+
+  isIgnorableError (msg) {
+    return _.contains(this.props.ignorableErrors, msg);
+  },
+
+  removeIgnorableAnnotations () {
+    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;
+      }, []);
+
+      if (annotations.length !== newAnnotations.length) {
+        this.editor.getSession().setAnnotations(newAnnotations);
+      }
+    }.bind(this));
+  },
+
+  showHideEditStringGutterIcon (e) {
+    if (this.hasErrors() || !this.parseLineForStringMatch()) {
+      this.setState({ stringEditIconVisible: false });
+      return false;
+    }
+
+    this.setState({
+      stringEditIconVisible: true,
+      stringEditIconStyle: {
+        top: this.getGutterIconPosition()
+      }
+    });
+
+    return true;
+  },
+
+  updateEditStringGutterIconPosition () {
+    if (!this.state.stringEditIconVisible) {
+      return;
+    }
+    this.setState({
+      stringEditIconStyle: {
+        top: this.getGutterIconPosition()
+      }
+    });
+  },
+
+  getGutterIconPosition () {
+    var rowHeight = this.getRowHeight();
+    var scrollTop = this.editor.session.getScrollTop();
+    var positionFromTop = (rowHeight * this.documentToScreenRow(this.getSelectionStart().row)) - scrollTop;
+    return positionFromTop + 'px';
+  },
+
+  parseLineForStringMatch () {
+    var selStart = this.getSelectionStart().row;
+    var selEnd   = this.getSelectionEnd().row;
+
+    // one JS(ON) string can't span more than one line - we edit one string, so ensure we don't select several lines
+    if (selStart >= 0 && selEnd >= 0 && selStart === selEnd && this.isRowExpanded(selStart)) {
+      var editLine = this.getLine(selStart),
+          editMatch = editLine.match(/^([ \t]*)("[a-zA-Z0-9_]*["|']: )?(["|'].*",?[ \t]*)$/);
+
+      if (editMatch) {
+        return editMatch;
+      }
+    }
+    return false;
+  },
+
+  openStringEditModal () {
+    var matches = this.parseLineForStringMatch();
+    var string = matches[3];
+    var lastChar = string.length - 1;
+    if (string.substring(string.length - 1) === ',') {
+      lastChar = string.length - 2;
+    }
+    string = string.substring(1, lastChar);
+
+    this.setState({
+      stringEditModalVisible: true,
+      stringEditModalValue: string
+    });
+  },
+
+  saveStringEditModal (newString) {
+    // replace the string on the selected line
+    var line = this.parseLineForStringMatch();
+    var indent = line[1] || '',
+        key = line[2] || '',
+        originalString = line[3],
+        comma = '';
+    if (originalString.substring(originalString.length - 1) === ',') {
+      comma = ',';
+    }
+    this.replaceCurrentLine(indent + key + JSON.stringify(newString) + comma + '\n');
+    this.closeStringEditModal();
+  },
+
+  closeStringEditModal () {
+    this.setState({
+      stringEditModalVisible: false
+    });
+  },
+
+  hasErrors () {
+    return !_.every(this.getAnnotations(), function (error) {
+      return this.isIgnorableError(error.raw);
+    }, this);
+  },
+
+  setReadOnly (readonly) {
+    this.editor.setReadOnly(readonly);
+  },
+
+  setValue (code, lineNumber) {
+    lineNumber = lineNumber ? lineNumber : -1;
+    this.editor.setValue(code, lineNumber);
+  },
+
+  getValue () {
+    return this.editor.getValue();
+  },
+
+  getEditor () {
+    return this;
+  },
+
+  getLine (lineNum) {
+    return this.editor.session.getLine(lineNum);
+  },
+
+  getSelectionStart () {
+    return this.editor.getSelectionRange().start;
+  },
+
+  getSelectionEnd () {
+    return this.editor.getSelectionRange().end;
+  },
+
+  getRowHeight () {
+    return this.editor.renderer.layerConfig.lineHeight;
+  },
+
+  isRowExpanded (row) {
+    return !this.editor.getSession().isRowFolded(row);
+  },
+
+  documentToScreenRow (row) {
+    return this.editor.getSession().documentToScreenRow(row, 0);
+  },
+
+  replaceCurrentLine (replacement) {
+    this.editor.getSelection().selectLine();
+    this.editor.insert(replacement);
+    this.editor.getSelection().moveCursorUp();
+  },
+
+  render () {
+    return (
+      <div>
+        <div ref="ace" className="js-editor" id={this.props.id}></div>
+        <button ref="stringEditIcon" className="btn string-edit" title="Edit string" disabled={!this.state.stringEditIconVisible}
+          style={this.state.stringEditIconStyle} onClick={this.openStringEditModal}>
+          <i className="icon icon-edit"></i>
+        </button>
+        <StringEditModal
+          ref="stringEditModal"
+          visible={this.state.stringEditModalVisible}
+          value={this.state.stringEditModalValue}
+          onSave={this.saveStringEditModal}
+          onClose={this.closeStringEditModal} />
+      </div>
+    );
+  }
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/369c3265/app/addons/components/components/codeeditorpanel.js
----------------------------------------------------------------------
diff --git a/app/addons/components/components/codeeditorpanel.js b/app/addons/components/components/codeeditorpanel.js
new file mode 100644
index 0000000..92e3693
--- /dev/null
+++ b/app/addons/components/components/codeeditorpanel.js
@@ -0,0 +1,147 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+import React from "react";
+import ReactDOM from "react-dom";
+import {CodeEditor} from './codeeditor';
+import {StringEditModal} from './stringeditmodal';
+import {Beautify} from './beautify';
+import {ZenModeOverlay} from './zenmodeoverlay';
+
+/**
+ * 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.
+ */
+export const CodeEditorPanel = React.createClass({
+  getDefaultProps () {
+    return {
+      id: 'code-editor',
+      className: '',
+      defaultCode: '',
+      title: '',
+      docLink: '',
+      allowZenMode: true,
+      blur () {}
+    };
+  },
+
+  getInitialState () {
+    return this.getStoreState();
+  },
+
+  getStoreState () {
+    return {
+      zenModeEnabled: false,
+      code: this.props.defaultCode
+    };
+  },
+
+  componentWillReceiveProps (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 () {
+    if (this.props.allowZenMode) {
+      return <span className="fonticon fonticon-resize-full zen-editor-icon" title="Enter Zen mode" onClick={this.enterZenMode}></span>;
+    }
+  },
+
+  getDocIcon () {
+    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 () {
+    if (this.state.zenModeEnabled) {
+      return (
+        <ZenModeOverlay
+          defaultCode={this.state.code}
+          mode={this.props.mode}
+          ignorableErrors={this.ignorableErrors}
+          onExit={this.exitZenMode} />
+      );
+    }
+  },
+
+  enterZenMode () {
+    this.setState({
+      zenModeEnabled: true,
+      code: this.refs.codeEditor.getValue()
+    });
+  },
+
+  exitZenMode (content) {
+    this.setState({ zenModeEnabled: false });
+    this.getEditor().setValue(content);
+  },
+
+  getEditor () {
+    return this.refs.codeEditor;
+  },
+
+  getValue () {
+    return this.getEditor().getValue();
+  },
+
+  beautify (code) {
+    this.setState({ code: code });
+    this.getEditor().setValue(code);
+  },
+
+  update () {
+    this.getEditor().setValue(this.state.code);
+  },
+
+  render () {
+    var classes = 'control-group';
+    if (this.props.className) {
+      classes += ' ' + this.props.className;
+    }
+    return (
+      <div className={classes}>
+        <label>
+          <span>{this.props.title}</span>
+          {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>
+    );
+  }
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/369c3265/app/addons/components/components/confirmbutton.js
----------------------------------------------------------------------
diff --git a/app/addons/components/components/confirmbutton.js b/app/addons/components/components/confirmbutton.js
new file mode 100644
index 0000000..72e157e
--- /dev/null
+++ b/app/addons/components/components/confirmbutton.js
@@ -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.
+
+import React from "react";
+import ReactDOM from "react-dom";
+
+export const ConfirmButton = React.createClass({
+  propTypes: {
+    showIcon: React.PropTypes.bool,
+    id: React.PropTypes.string,
+    customIcon: React.PropTypes.string,
+    style: React.PropTypes.object,
+    buttonType: React.PropTypes.string,
+    'data-id': React.PropTypes.string,
+    onClick: React.PropTypes.func,
+    disabled: React.PropTypes.bool,
+  },
+
+  getDefaultProps () {
+    return {
+      disabled: false,
+      showIcon: true,
+      customIcon: 'fonticon-ok-circled',
+      buttonType: 'btn-success',
+      style: {},
+      'data-id': null,
+      onClick () { }
+    };
+  },
+
+  getIcon () {
+    if (!this.props.showIcon) {
+      return null;
+    }
+    return (
+      <i className={"icon " + this.props.customIcon} />
+    );
+  },
+
+  render () {
+    const { onClick, buttonType, id, style, text, disabled } = this.props;
+    return (
+      <button
+        onClick={onClick}
+        type="submit"
+        disabled={disabled}
+        data-id={this.props['data-id']}
+        className={'btn save ' + buttonType}
+        id={id}
+        style={style}
+      >
+        {this.getIcon()}
+        {text}
+      </button>
+    );
+  }
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/369c3265/app/addons/components/components/deletedatabasemodal.js
----------------------------------------------------------------------
diff --git a/app/addons/components/components/deletedatabasemodal.js b/app/addons/components/components/deletedatabasemodal.js
new file mode 100644
index 0000000..f24e553
--- /dev/null
+++ b/app/addons/components/components/deletedatabasemodal.js
@@ -0,0 +1,119 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import React from "react";
+import ReactDOM from "react-dom";
+import {Modal} from "react-bootstrap";
+import Actions from "../actions";
+
+export const DeleteDatabaseModal = React.createClass({
+
+  getInitialState () {
+    return {
+      inputValue: '',
+      disableSubmit: true
+    };
+  },
+
+  propTypes: {
+    showHide: React.PropTypes.func.isRequired,
+    modalProps: React.PropTypes.object
+  },
+
+  close (e) {
+    if (e) {
+      e.preventDefault();
+    }
+
+    this.setState({
+      inputValue: '',
+      disableSubmit: true
+    });
+
+    this.props.showHide({showModal: false});
+  },
+
+  open () {
+    this.props.showHide({showModal: true});
+  },
+
+  getDatabaseName () {
+    return this.props.modalProps.dbId.trim();
+  },
+
+  onInputChange (e) {
+    const val = e.target.value.trim();
+
+    this.setState({
+      inputValue: val
+    });
+
+    this.setState({
+      disableSubmit: val !== this.getDatabaseName()
+    });
+  },
+
+  onDeleteClick (e) {
+    e.preventDefault();
+
+    Actions.deleteDatabase(this.getDatabaseName());
+  },
+
+  onInputKeypress (e) {
+    if (e.keyCode === 13 && this.state.disableSubmit !== true) {
+      Actions.deleteDatabase(this.getDatabaseName());
+    }
+  },
+
+  render () {
+    var isSystemDatabase = this.props.modalProps.isSystemDatabase;
+    var showDeleteModal = this.props.modalProps.showDeleteModal;
+    var dbId = this.props.modalProps.dbId;
+
+    var warning = isSystemDatabase ? (
+      <p style={{color: '#d14'}} className="warning">
+        <b>You are about to delete a system database, be careful!</b>
+      </p>
+    ) : null;
+
+    return (
+      <Modal dialogClassName="delete-db-modal" show={showDeleteModal} onHide={this.close}>
+        <Modal.Header closeButton={true}>
+          <Modal.Title>Delete Database</Modal.Title>
+        </Modal.Header>
+        <Modal.Body>
+          {warning}
+          <p>
+            Warning: This action will permanently delete <code>{dbId}</code>.
+            To confirm the deletion of the database and all of the
+            database's documents, you must enter the database's name.
+          </p>
+          <input
+            type="text"
+            className="input-block-level"
+            onKeyUp={this.onInputKeypress}
+            onChange={this.onInputChange}
+            autoFocus={true} />
+        </Modal.Body>
+        <Modal.Footer>
+          <a href="#" onClick={this.close} data-bypass="true" className="cancel-link">Cancel</a>
+          <button
+            disabled={this.state.disableSubmit}
+            onClick={this.onDeleteClick}
+            className="btn btn-danger delete">
+            <i className="icon fonticon-cancel-circled" /> Delete
+          </button>
+        </Modal.Footer>
+      </Modal>
+    );
+  }
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/369c3265/app/addons/components/components/document.js
----------------------------------------------------------------------
diff --git a/app/addons/components/components/document.js b/app/addons/components/components/document.js
new file mode 100644
index 0000000..1bf6ced
--- /dev/null
+++ b/app/addons/components/components/document.js
@@ -0,0 +1,126 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+import React from "react";
+import ReactDOM from "react-dom";
+import FauxtonAPI from "../../../core/api";
+import Helpers from "../../documents/helpers";
+
+export const Document = React.createClass({
+  propTypes: {
+    docIdentifier: React.PropTypes.string.isRequired,
+    docChecked: React.PropTypes.func.isRequired,
+    truncate: React.PropTypes.bool,
+    maxRows: React.PropTypes.number
+  },
+
+  getDefaultProps () {
+    return {
+      truncate: true,
+      maxRows: 500
+    };
+  },
+
+  onChange (e) {
+    e.preventDefault();
+    this.props.docChecked(this.props.doc.id, this.props.doc._rev);
+  },
+
+  getUrlFragment () {
+    if (!this.props.children) {
+      return '';
+    }
+
+    return (
+      <div className="doc-edit-symbol pull-right" title="Edit document">
+        {this.props.children}
+      </div>
+    );
+  },
+
+  getExtensionIcons () {
+    var extensions = FauxtonAPI.getExtensions('DocList:icons');
+    return _.map(extensions, function (Extension, i) {
+      return (<Extension doc={this.props.doc} key={i} />);
+    }, this);
+  },
+
+  getCheckbox () {
+    if (!this.props.isDeletable) {
+      return <div className="checkbox-dummy"></div>;
+    }
+
+    return (
+      <div className="checkbox inline">
+        <input
+          id={'checkbox-' + this.props.docIdentifier}
+          checked={this.props.checked}
+          data-checked={this.props.checked}
+          type="checkbox"
+          onChange={this.onChange}
+          className="js-row-select" />
+        <label onClick={this.onChange}
+          className="label-checkbox-doclist"
+          htmlFor={'checkbox-' + this.props.docIdentifier} />
+      </div>
+    );
+  },
+
+  onDoubleClick (e) {
+    this.props.onDoubleClick(this.props.docIdentifier, this.props.doc, e);
+  },
+
+  getDocContent () {
+    if (_.isEmpty(this.props.docContent)) {
+      return null;
+    }
+
+    // if need be, truncate the document
+    var content = this.props.docContent;
+    var isTruncated = false;
+    if (this.props.truncate) {
+      var result = Helpers.truncateDoc(this.props.docContent, this.props.maxRows);
+      isTruncated = result.isTruncated;
+      content = result.content;
+    }
+
+    return (
+      <div className="doc-data">
+        <pre className="prettyprint">{content}</pre>
+        {isTruncated ? <div className="doc-content-truncated">(truncated)</div> : null}
+      </div>
+    );
+  },
+
+  render () {
+    return (
+      <div data-id={this.props.docIdentifier} onDoubleClick={this.onDoubleClick} className="doc-row">
+        <div className="custom-inputs">
+          {this.getCheckbox()}
+        </div>
+        <div className="doc-item">
+          <header>
+            <span className="header-keylabel">
+              {this.props.keylabel}
+            </span>
+            <span className="header-doc-id">
+              {this.props.header ? '"' + this.props.header + '"' : null}
+            </span>
+            {this.getUrlFragment()}
+            <div className="doc-item-extension-icons pull-right">{this.getExtensionIcons()}</div>
+          </header>
+          {this.getDocContent()}
+        </div>
+        <div className="clearfix"></div>
+      </div>
+    );
+  }
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/369c3265/app/addons/components/components/loadlines.js
----------------------------------------------------------------------
diff --git a/app/addons/components/components/loadlines.js b/app/addons/components/components/loadlines.js
new file mode 100644
index 0000000..0fa2b74
--- /dev/null
+++ b/app/addons/components/components/loadlines.js
@@ -0,0 +1,21 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+import React from "react";
+import ReactDOM from "react-dom";
+
+export const LoadLines = () =>
+  <div className="loading-lines">
+    <div id="line1"> </div>
+    <div id="line2"> </div>
+    <div id="line3"> </div>
+    <div id="line4"> </div>
+  </div>;

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/369c3265/app/addons/components/components/menudropdown.js
----------------------------------------------------------------------
diff --git a/app/addons/components/components/menudropdown.js b/app/addons/components/components/menudropdown.js
new file mode 100644
index 0000000..18127a1
--- /dev/null
+++ b/app/addons/components/components/menudropdown.js
@@ -0,0 +1,82 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+import React from "react";
+import ReactDOM from "react-dom";
+
+export const MenuDropDown = React.createClass({
+
+  getDefaultProps () {
+    return {
+      icon: 'fonticon-plus-circled'
+    };
+  },
+
+  createSectionLinks (links) {
+    if (!links) { return null; }
+
+    return links.map((link, key) => {
+      return this.createEntry(link, key);
+    });
+  },
+
+  createEntry (link, key) {
+    return (
+      <li key={key}>
+        <a className={link.icon ? 'icon ' + link.icon : ''}
+          data-bypass={link.external ? 'true' : ''}
+          href={link.url}
+          onClick={link.onClick}
+          target={link.external ? '_blank' : ''}>
+          {link.title}
+        </a>
+      </li>
+    );
+  },
+
+  createSectionTitle (title) {
+    if (!title) {
+      return null;
+    }
+
+    return (
+      <li className="header-label">{title}</li>
+    );
+  },
+
+  createSection () {
+    return this.props.links.map((linkSection, key) => {
+      if (linkSection.title && linkSection.links) {
+        return ([
+          this.createSectionTitle(linkSection.title),
+          this.createSectionLinks(linkSection.links)
+        ]);
+      }
+
+      return this.createEntry(linkSection, 'el' + key);
+
+    });
+  },
+
+  render () {
+    return (
+      <div className="dropdown">
+        <a className={"dropdown-toggle icon " + this.props.icon}
+          data-toggle="dropdown"
+          href="#"
+          data-bypass="true"></a>
+        <ul className="dropdown-menu arrow" role="menu" aria-labelledby="dLabel">
+          {this.createSection()}
+        </ul>
+      </div>
+    );
+  }
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/369c3265/app/addons/components/components/paddedborderbox.js
----------------------------------------------------------------------
diff --git a/app/addons/components/components/paddedborderbox.js b/app/addons/components/components/paddedborderbox.js
new file mode 100644
index 0000000..22541fd
--- /dev/null
+++ b/app/addons/components/components/paddedborderbox.js
@@ -0,0 +1,25 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+import React from "react";
+import ReactDOM from "react-dom";
+
+export const PaddedBorderedBox = React.createClass({
+  render: function () {
+    return (
+      <div className="bordered-box">
+        <div className="padded-box">
+          {this.props.children}
+        </div>
+      </div>
+    );
+   }
+ });

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/369c3265/app/addons/components/components/stringeditmodal.js
----------------------------------------------------------------------
diff --git a/app/addons/components/components/stringeditmodal.js b/app/addons/components/components/stringeditmodal.js
new file mode 100644
index 0000000..782a3db
--- /dev/null
+++ b/app/addons/components/components/stringeditmodal.js
@@ -0,0 +1,95 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import React from "react";
+import ReactDOM from "react-dom";
+import {Modal} from "react-bootstrap";
+import ace from "brace";
+import Helpers from "../../documents/helpers";
+require('brace/mode/javascript');
+require('brace/mode/json');
+require('brace/theme/idle_fingers');
+
+// this appears when the cursor is over a string. It shows an icon in the gutter that opens the modal.
+export const StringEditModal = React.createClass({
+
+  propTypes: {
+    value: React.PropTypes.string.isRequired,
+    visible: React.PropTypes.bool.isRequired,
+    onClose: React.PropTypes.func.isRequired,
+    onSave: React.PropTypes.func.isRequired
+  },
+
+  getDefaultProps () {
+    return {
+      visible: false,
+      onClose () { },
+      onSave () { }
+    };
+  },
+
+  componentDidMount () {
+    if (!this.props.visible) {
+      return;
+    }
+    this.initEditor(this.props.value);
+  },
+
+  componentDidUpdate (prevProps) {
+    if (!this.props.visible) {
+      return;
+    }
+    var val = '';
+    if (!prevProps.visible && this.props.visible) {
+      val = Helpers.parseJSON(this.props.value);
+    }
+
+    this.initEditor(val);
+  },
+
+  initEditor (val) {
+    this.editor = ace.edit(ReactDOM.findDOMNode(this.refs.stringEditor));
+    this.editor.$blockScrolling = Infinity; // suppresses an Ace editor error
+    this.editor.setShowPrintMargin(false);
+    this.editor.setOption('highlightActiveLine', true);
+    this.editor.setTheme('ace/theme/idle_fingers');
+    this.editor.setValue(val, -1);
+  },
+
+  closeModal () {
+    this.props.onClose();
+  },
+
+  save () {
+    this.props.onSave(this.editor.getValue());
+  },
+
+  render () {
+    return (
+      <Modal dialogClassName="string-editor-modal" show={this.props.visible} onHide={this.closeModal}>
+        <Modal.Header closeButton={true}>
+          <Modal.Title>Edit Value <span id="string-edit-header"></span></Modal.Title>
+        </Modal.Header>
+        <Modal.Body>
+          <div id="modal-error" className="hide alert alert-error"/>
+          <div id="string-editor-wrapper"><div ref="stringEditor" className="doc-code"></div></div>
+        </Modal.Body>
+        <Modal.Footer>
+          <a className="cancel-link" onClick={this.closeModal}>Cancel</a>
+          <button id="string-edit-save-btn" onClick={this.save} className="btn btn-success save">
+            <i className="fonticon-circle-check"></i> Modify Text
+          </button>
+        </Modal.Footer>
+      </Modal>
+    );
+  }
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/369c3265/app/addons/components/components/styledselect.js
----------------------------------------------------------------------
diff --git a/app/addons/components/components/styledselect.js b/app/addons/components/components/styledselect.js
new file mode 100644
index 0000000..85a6ff4
--- /dev/null
+++ b/app/addons/components/components/styledselect.js
@@ -0,0 +1,39 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+import React from "react";
+import ReactDOM from "react-dom";
+
+export const StyledSelect = React.createClass({
+  propTypes: {
+    selectValue: React.PropTypes.string.isRequired,
+    selectId: React.PropTypes.string.isRequired,
+    selectChange: React.PropTypes.func.isRequired
+  },
+
+  render: function () {
+    return (
+      <div className="styled-select">
+        <label htmlFor={this.props.selectId}>
+          <i className="fonticon-down-dir"></i>
+          <select
+            value={this.props.selectValue}
+            id={this.props.selectId}
+            className={this.props.selectValue}
+            onChange={this.props.selectChange}
+          >
+            {this.props.selectContent}
+          </select>
+        </label>
+      </div>
+    );
+  }
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/369c3265/app/addons/components/components/tabelement.js
----------------------------------------------------------------------
diff --git a/app/addons/components/components/tabelement.js b/app/addons/components/components/tabelement.js
new file mode 100644
index 0000000..0946597
--- /dev/null
+++ b/app/addons/components/components/tabelement.js
@@ -0,0 +1,55 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import React from "react";
+import ReactDOM from "react-dom";
+
+export const TabElement = ({selected, text, onChange, iconClass}) => {
+
+  const additionalClass = selected ? 'tab-element-checked' : '';
+
+  return (
+    <li className={`component-tab-element ${additionalClass}`}>
+
+      <label>
+        <div className="tab-element-indicator-wrapper">
+          <div className="tab-element-indicator"></div>
+        </div>
+        <div className="tab-element-content">
+          <i className={iconClass}></i>
+          <input
+            type="radio"
+            value={text}
+            checked={selected}
+            onChange={onChange} />
+
+          {text}
+        </div>
+      </label>
+    </li>
+
+  );
+};
+TabElement.propTypes = {
+  selected: React.PropTypes.bool.isRequired,
+  text: React.PropTypes.string.isRequired,
+  onChange: React.PropTypes.func.isRequired,
+  iconClass: React.PropTypes.string,
+};
+
+export const TabElementWrapper = ({children}) => {
+  return (
+    <ul className="component-tab-element-wrapper">
+      {children}
+    </ul>
+  );
+};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/369c3265/app/addons/components/components/toggleheaderbutton.js
----------------------------------------------------------------------
diff --git a/app/addons/components/components/toggleheaderbutton.js b/app/addons/components/components/toggleheaderbutton.js
new file mode 100644
index 0000000..782c369
--- /dev/null
+++ b/app/addons/components/components/toggleheaderbutton.js
@@ -0,0 +1,46 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import React from "react";
+import ReactDOM from "react-dom";
+
+export const ToggleHeaderButton = React.createClass({
+  getDefaultProps () {
+    return {
+      innerClasses: '',
+      fonticon: '',
+      containerClasses: '',
+      selected: false,
+      title: '',
+      disabled: false,
+      toggleCallback: null,
+      text: '',
+      iconDefaultClass: 'icon'
+    };
+  },
+
+  render () {
+    const { iconDefaultClass, fonticon, innerClasses, selected, containerClasses, title, disabled, text, toggleCallback } = this.props;
+    const selectedBtnClass = (selected) ? 'js-headerbar-togglebutton-selected' : '';
+
+    return (
+      <button
+        title={title}
+        disabled={disabled}
+        onClick={toggleCallback}
+        className={`button ${containerClasses} ${selectedBtnClass}`}
+        >
+        <i className={`${iconDefaultClass} ${fonticon} ${innerClasses}`}></i><span>{text}</span>
+      </button>
+    );
+  }
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/369c3265/app/addons/components/components/tray.js
----------------------------------------------------------------------
diff --git a/app/addons/components/components/tray.js b/app/addons/components/components/tray.js
new file mode 100644
index 0000000..6fc8f86
--- /dev/null
+++ b/app/addons/components/components/tray.js
@@ -0,0 +1,108 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import React from "react";
+import ReactDOM from "react-dom";
+import ReactCSSTransitionGroup from "react-addons-css-transition-group";
+
+export const TrayContents = React.createClass({
+  getChildren () {
+    var className = "tray show-tray " + this.props.className;
+    return (
+      <div key={1} id={this.props.id} className={className}>
+        {this.props.children}
+      </div>);
+  },
+
+  render () {
+    return (
+      <ReactCSSTransitionGroup transitionName="tray" transitionAppear={true} component="div" transitionAppearTimeout={500}
+        transitionEnterTimeout={500} transitionLeaveTimeout={300}>
+        {this.getChildren()}
+      </ReactCSSTransitionGroup>
+    );
+  }
+});
+
+
+export const connectToStores = (Component, stores, getStateFromStores) => {
+
+  var WrappingElement = React.createClass({
+
+    componentDidMount () {
+      stores.forEach(function (store) {
+        store.on('change', this.onChange, this);
+      }.bind(this));
+    },
+
+    componentWillUnmount () {
+      stores.forEach(function (store) {
+        store.off('change', this.onChange);
+      }.bind(this));
+    },
+
+    getInitialState () {
+      return getStateFromStores(this.props);
+    },
+
+    onChange () {
+      if (!this.isMounted()) {
+        return;
+      }
+
+      this.setState(getStateFromStores(this.props));
+    },
+
+    handleStoresChanged () {
+      if (this.isMounted()) {
+        this.setState(getStateFromStores(this.props));
+      }
+    },
+
+    render () {
+      return <Component {...this.state} {...this.props} />;
+    }
+
+  });
+
+  return WrappingElement;
+};
+
+export const TrayWrapper = React.createClass({
+  getDefaultProps () {
+    return {
+      className: ''
+    };
+  },
+
+  renderChildren () {
+    return React.Children.map(this.props.children, function (child, key) {
+
+      const props = {};
+      Object.keys(this.props).filter((k) => {
+        return this.props.hasOwnProperty(k);
+      }).map((k) => {
+        return props[k] = this.props[k];
+      });
+
+      return React.cloneElement(child, props);
+    }.bind(this));
+  },
+
+  render () {
+    return (
+      <div>
+        {this.renderChildren()}
+      </div>
+    );
+  }
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/369c3265/app/addons/components/components/zenmodeoverlay.js
----------------------------------------------------------------------
diff --git a/app/addons/components/components/zenmodeoverlay.js b/app/addons/components/components/zenmodeoverlay.js
new file mode 100644
index 0000000..8490d92
--- /dev/null
+++ b/app/addons/components/components/zenmodeoverlay.js
@@ -0,0 +1,131 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+import React from "react";
+import ReactDOM from "react-dom";
+import app from "../../../app";
+import {CodeEditor} from './codeeditor';
+
+require('brace/theme/dawn');
+
+// Zen mode editing has very few options:
+// - It covers the full screen, hiding everything else
+// - Two themes: light & dark (choice stored in local storage)
+// - No save option, but has a 1-1 map with a <CodeEditor /> element which gets updated when the user leaves
+// - [Escape] closes the mode, as does clicking the shrink icon at the top right
+export const ZenModeOverlay = React.createClass({
+  getDefaultProps () {
+    return {
+      mode: 'javascript',
+      defaultCode: '',
+      ignorableErrors: [],
+      onExit: null,
+      highlightActiveLine: false
+    };
+  },
+
+  themes: {
+    dark: 'idle_fingers',
+    light: 'dawn'
+  },
+
+  getInitialState () {
+    return this.getStoreState();
+  },
+
+  getStoreState () {
+    return {
+      theme: this.getZenTheme(),
+      code: this.props.defaultCode
+    };
+  },
+
+  getZenTheme () {
+    var selectedTheme = app.utils.localStorageGet('zenTheme');
+    return _.isUndefined(selectedTheme) ? 'dark' : selectedTheme;
+  },
+
+  onChange () {
+    this.setState(this.getStoreState());
+  },
+
+  componentDidMount () {
+    $(ReactDOM.findDOMNode(this.refs.exit)).tooltip({ placement: 'left' });
+    $(ReactDOM.findDOMNode(this.refs.theme)).tooltip({ placement: 'left' });
+  },
+
+  exitZenMode () {
+    this.props.onExit(this.getValue());
+  },
+
+  getValue () {
+    return this.refs.ace.getValue();
+  },
+
+  toggleTheme () {
+    var newTheme = (this.state.theme === 'dark') ? 'light' : 'dark';
+    this.setState({
+      theme: newTheme,
+      code: this.getValue()
+    });
+    app.utils.localStorageSet('zenTheme', newTheme);
+  },
+
+  setValue (code, lineNumber) {
+    lineNumber = lineNumber ? lineNumber : -1;
+    this.editor.setValue(code, lineNumber);
+  },
+
+  render () {
+    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">
+          <ul>
+            <li>
+              <span ref="exit"
+                className="fonticon fonticon-resize-small js-exit-zen-mode"
+                data-toggle="tooltip"
+                data-container=".zen-mode-controls .tooltips"
+                title="Exit zen mode (`esc`)"
+                onClick={this.exitZenMode}></span>
+            </li>
+            <li>
+              <span ref="theme"
+                className="fonticon fonticon-picture js-toggle-theme"
+                data-toggle="tooltip"
+                data-container=".zen-mode-controls .tooltips"
+                title="Switch zen theme"
+                onClick={this.toggleTheme}></span>
+            </li>
+          </ul>
+          <div className="tooltips"></div>
+        </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>
+    );
+  }
+});