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 2017/06/21 07:34:12 UTC

[couchdb-fauxton] branch master updated: (#906) - Toolbar redesign for all docs

This is an automated email from the ASF dual-hosted git repository.

garren pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/couchdb-fauxton.git


The following commit(s) were added to refs/heads/master by this push:
     new d9e361a  (#906) - Toolbar redesign for all docs
d9e361a is described below

commit d9e361a8aee322e4b028b1f77396cfea409e5ca9
Author: Ryan Millay <ry...@gmail.com>
AuthorDate: Wed Jun 21 03:34:10 2017 -0400

    (#906) - Toolbar redesign for all docs
    
    This is a full redesign of the `_all_docs` page. Add support for table, json and metadata view. It also fixes up pagination for the `_all_docs` page.
---
 app/addons/components/assets/less/docs.less        |   1 +
 app/addons/components/components/document.js       |   8 +-
 app/addons/components/tests/docSpec.js             |   6 +-
 .../documents/__tests__/additional-params.test.js  |  58 +++
 app/addons/documents/__tests__/base-api.test.js    | 175 ++++++++
 app/addons/documents/__tests__/fetch-api.test.js   | 346 +++++++++++++++
 .../documents/__tests__/index-results.test.js      | 123 ++++++
 app/addons/documents/__tests__/json-view.test.js   | 128 ++++++
 .../documents/__tests__/key-search-fields.test.js  | 123 ++++++
 .../documents/__tests__/main-fields-view.test.js   |  91 ++++
 .../documents/__tests__/pagination-api.test.js     | 110 +++++
 .../documents/__tests__/pagination-footer.test.js  | 199 +++++++++
 .../documents/__tests__/paging-controls.test.js    |  62 +++
 .../perpage-selector.test.js}                      |  22 +-
 .../query-buttons.test.js}                         |  22 +-
 .../documents/__tests__/query-options.test.js      | 119 ++++++
 .../documents/__tests__/queryoptions-api.test.js   | 159 +++++++
 app/addons/documents/__tests__/reducers.test.js    | 469 +++++++++++++++++++++
 .../documents/__tests__/results-toolbar.test.js    |  46 ++
 .../documents/__tests__/shared-helpers.test.js     | 170 ++++++++
 .../documents/__tests__/table-controls.test.js     |  51 +++
 app/addons/documents/__tests__/table-view.test.js  | 179 ++++++++
 app/addons/documents/assets/less/header.less       |   4 +
 .../documents/assets/less/index-results.less       |  52 ++-
 app/addons/documents/base.js                       |   5 +
 .../documents/components/header-docs-right.js      |  19 +-
 app/addons/documents/components/results-toolbar.js |  77 ++++
 .../{header/header.actiontypes.js => constants.js} |   6 +-
 app/addons/documents/header/header.actions.js      |   6 +-
 app/addons/documents/header/header.actiontypes.js  |   2 +-
 app/addons/documents/header/header.js              | 133 ++++--
 app/addons/documents/helpers.js                    |  47 +--
 app/addons/documents/index-results/actions.js      |   2 +-
 app/addons/documents/index-results/actiontypes.js  |  12 +-
 app/addons/documents/index-results/apis/base.js    | 100 +++++
 app/addons/documents/index-results/apis/fetch.js   | 200 +++++++++
 .../documents/index-results/apis/pagination.js     |  88 ++++
 .../documents/index-results/apis/queryoptions.js   | 113 +++++
 .../components/pagination/PaginationFooter.js      |  92 ++++
 .../components/pagination/PagingControls.js        |  46 ++
 .../components/pagination/PerPageSelector.js       |  56 +++
 .../components/pagination/TableControls.js         |  64 +++
 .../components/queryoptions/AdditionalParams.js    |  78 ++++
 .../components/queryoptions/KeySearchFields.js     | 111 +++++
 .../components/queryoptions/MainFieldsView.js      | 102 +++++
 .../components/queryoptions/QueryButtons.js        |  38 ++
 .../components/queryoptions/QueryOptions.js        | 147 +++++++
 .../components/results/IndexResults.js             | 100 +++++
 .../components/results/NoResultsScreen.js}         |  19 +-
 .../components/results/ResultsScreen.js            | 132 ++++++
 .../index-results/components/results/TableRow.js   | 148 +++++++
 .../index-results/components/results/TableView.js  | 105 +++++
 .../components/results/WrappedAutocomplete.js      |  41 ++
 .../index-results/containers/ApiBarContainer.js    |  36 ++
 .../containers/IndexResultsContainer.js            |  98 +++++
 .../containers/PaginationContainer.js              |  76 ++++
 .../containers/QueryOptionsContainer.js            | 119 ++++++
 .../documents/index-results/helpers/json-view.js   |  35 ++
 .../index-results/helpers/shared-helpers.js        |  85 ++++
 .../documents/index-results/helpers/table-view.js  | 179 ++++++++
 .../index-results/index-results.components.js      | 176 +++-----
 app/addons/documents/index-results/reducers.js     | 306 ++++++++++++++
 app/addons/documents/index-results/stores.js       | 167 ++++----
 .../tests/index-results.componentsSpec.js          |  33 +-
 .../tests/index-results.storesSpec.js              |  39 +-
 app/addons/documents/layouts.js                    |  72 +++-
 app/addons/documents/mango/mango.components.js     |   3 +-
 app/addons/documents/mangolayout.js                |  12 +-
 app/addons/documents/pagination/actions.js         | 138 +++---
 app/addons/documents/pagination/actiontypes.js     |   6 +-
 app/addons/documents/pagination/pagination.js      |  14 +-
 app/addons/documents/queryoptions/queryoptions.js  |  12 +-
 app/addons/documents/resources.js                  |   7 +-
 app/addons/documents/routes-documents.js           |  56 +--
 app/addons/documents/routes-index-editor.js        |  22 +-
 app/addons/documents/routes-mango.js               |  13 +-
 app/addons/documents/shared-routes.js              |  17 +-
 .../documents/tests/nightwatch/bulkDelete.js       |  38 +-
 app/addons/documents/tests/nightwatch/changes.js   |  15 -
 .../documents/tests/nightwatch/createsDocument.js  |  54 ++-
 .../documents/tests/nightwatch/deletesDocuments.js |  46 +-
 .../tests/nightwatch/doubleEmitResults.js          |   1 +
 .../tests/nightwatch/editDocumentsFromView.js      |   1 +
 .../tests/nightwatch/fixRegressionTableView.js     |   4 +-
 app/addons/documents/tests/nightwatch/jsonView.js  |  13 +-
 .../documents/tests/nightwatch/mangoIndex.js       |   1 +
 .../documents/tests/nightwatch/mangoQuery.js       |   1 +
 .../documents/tests/nightwatch/paginateAllDocs.js  |  34 +-
 .../documents/tests/nightwatch/paginateView.js     |  44 +-
 .../documents/tests/nightwatch/queryOptions.js     |   1 +
 .../documents/tests/nightwatch/resultsToolbar.js   |  72 ++++
 .../documents/tests/nightwatch/revBrowser.js       |   1 +
 .../tests/nightwatch/selectDocViaTypeahead.js      |   2 +
 app/addons/documents/tests/nightwatch/tableView.js |  17 +-
 .../tests/nightwatch/tableViewConflicts.js         |   5 +-
 app/addons/documents/tests/nightwatch/viewClone.js |   1 +
 .../documents/tests/nightwatch/viewCreate.js       |   5 +
 .../tests/nightwatch/viewCreateBadView.js          |   4 +-
 .../documents/tests/nightwatch/viewDelete.js       |   1 +
 app/addons/documents/tests/nightwatch/viewEdit.js  |   2 +
 .../documents/tests/nightwatch/viewQueryOptions.js |   2 +
 .../fauxton/tests/nightwatch/notificationCenter.js |   1 +
 .../tests/nightwatch/updatesUrlsSameRouteobject.js |   1 +
 assets/js/plugins/cloudant.pagingcollection.js     |  13 +-
 assets/less/formstyles.less                        |   8 +-
 assets/less/templates.less                         |   2 +-
 106 files changed, 6384 insertions(+), 539 deletions(-)

diff --git a/app/addons/components/assets/less/docs.less b/app/addons/components/assets/less/docs.less
index 988b7f0..473a305 100644
--- a/app/addons/components/assets/less/docs.less
+++ b/app/addons/components/assets/less/docs.less
@@ -21,6 +21,7 @@
   div.doc-row {
     margin-bottom: 20px;
     .doc-item {
+      cursor: pointer;
       vertical-align: top;
       position: relative;
       .box-shadow(3px 4px 0 rgba(0, 0, 0, 0.3));
diff --git a/app/addons/components/components/document.js b/app/addons/components/components/document.js
index 1bf6ced..a0cdde8 100644
--- a/app/addons/components/components/document.js
+++ b/app/addons/components/components/document.js
@@ -74,8 +74,8 @@ export const Document = React.createClass({
     );
   },
 
-  onDoubleClick (e) {
-    this.props.onDoubleClick(this.props.docIdentifier, this.props.doc, e);
+  onClick (e) {
+    this.props.onClick(this.props.docIdentifier, this.props.doc, e);
   },
 
   getDocContent () {
@@ -102,11 +102,11 @@ export const Document = React.createClass({
 
   render () {
     return (
-      <div data-id={this.props.docIdentifier} onDoubleClick={this.onDoubleClick} className="doc-row">
+      <div data-id={this.props.docIdentifier} className="doc-row">
         <div className="custom-inputs">
           {this.getCheckbox()}
         </div>
-        <div className="doc-item">
+        <div className="doc-item" onClick={this.onClick}>
           <header>
             <span className="header-keylabel">
               {this.props.keylabel}
diff --git a/app/addons/components/tests/docSpec.js b/app/addons/components/tests/docSpec.js
index 5791935..442e7e5 100644
--- a/app/addons/components/tests/docSpec.js
+++ b/app/addons/components/tests/docSpec.js
@@ -81,14 +81,14 @@ describe('Document', function () {
     assert.ok(spy.calledOnce);
   });
 
-  it('it calls an dblclick callback', function () {
+  it('it calls an onclick callback', function () {
     var spy = sinon.spy();
 
     el = TestUtils.renderIntoDocument(
-      <ReactComponents.Document isDeletable={true} onDoubleClick={spy} docIdentifier="foo" />,
+      <ReactComponents.Document isDeletable={true} onClick={spy} docIdentifier="foo" />,
       container
     );
-    TestUtils.Simulate.doubleClick(ReactDOM.findDOMNode(el));
+    TestUtils.Simulate.click($(ReactDOM.findDOMNode(el)).find('.doc-item')[0]);
     assert.ok(spy.calledOnce);
   });
 
diff --git a/app/addons/documents/__tests__/additional-params.test.js b/app/addons/documents/__tests__/additional-params.test.js
new file mode 100644
index 0000000..1231e3a
--- /dev/null
+++ b/app/addons/documents/__tests__/additional-params.test.js
@@ -0,0 +1,58 @@
+// 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 { mount } from 'enzyme';
+import AdditionalParams from '../index-results/components/queryoptions/AdditionalParams';
+import sinon from 'sinon';
+
+describe('AdditionalParams', () => {
+  it('updateSkip is called after change', () => {
+    const spy = sinon.spy();
+    const wrapper = mount(<AdditionalParams
+      limit={10}
+      descending={false}
+      skip={0}
+      updateSkip={spy}
+    />);
+
+    wrapper.find('#qoSkip').simulate('change');
+    expect(spy.calledOnce).toBe(true);
+  });
+
+  it('updateLimit is called after change', () => {
+    const spy = sinon.spy();
+    const wrapper = mount(<AdditionalParams
+      limit={10}
+      descending={false}
+      skip={0}
+      updateLimit={spy}
+    />);
+
+    wrapper.find('#qoLimit').simulate('change');
+    expect(spy.calledOnce).toBe(true);
+  });
+
+  it('toggleDescending is called after change', () => {
+    const spy = sinon.spy();
+    const wrapper = mount(<AdditionalParams
+      limit={10}
+      descending={false}
+      skip={0}
+      toggleDescending={spy}
+    />);
+
+    wrapper.find('#qoDescending').simulate('change');
+    expect(spy.calledOnce).toBe(true);
+  });
+});
diff --git a/app/addons/documents/__tests__/base-api.test.js b/app/addons/documents/__tests__/base-api.test.js
new file mode 100644
index 0000000..c966316
--- /dev/null
+++ b/app/addons/documents/__tests__/base-api.test.js
@@ -0,0 +1,175 @@
+// 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 {
+  nowLoading,
+  resetState,
+  newResultsAvailable,
+  newSelectedDocs,
+  selectDoc,
+  bulkCheckOrUncheck,
+  changeLayout,
+  changeTableHeaderAttribute
+} from '../index-results/apis/base';
+import ActionTypes from '../index-results/actiontypes';
+import Constants from '../constants';
+
+describe('Docs Base API', () => {
+  let docs;
+  beforeEach(() => {
+    docs = [
+      {
+        _id: 'test1',
+        _rev: 'foo'
+      },
+      {
+        _id: 'test2',
+        _rev: 'bar'
+      }
+    ];
+  });
+
+  it('nowLoading returns the proper event to dispatch', () => {
+    expect(nowLoading()).toEqual({
+      type: ActionTypes.INDEX_RESULTS_REDUX_IS_LOADING
+    });
+  });
+
+  it('resetState returns the proper event to dispatch', () => {
+    expect(resetState()).toEqual({
+      type: ActionTypes.INDEX_RESULTS_REDUX_RESET_STATE
+    });
+  });
+
+  it('newResultsAvailable returns the proper event to dispatch', () => {
+    const params = {
+      skip: 0,
+      limit: 21
+    };
+    const canShowNext = true;
+
+    expect(newResultsAvailable(docs, params, canShowNext)).toEqual({
+      type: ActionTypes.INDEX_RESULTS_REDUX_NEW_RESULTS,
+      docs: docs,
+      params: params,
+      canShowNext: canShowNext
+    });
+  });
+
+  it('newSelectedDocs returns the proper event to dispatch', () => {
+    const selectedDocs = [
+      {
+        _id: 'test1',
+        _rev: 'foo',
+        _deleted: true
+      }
+    ];
+
+    expect(newSelectedDocs(selectedDocs)).toEqual({
+      type: ActionTypes.INDEX_RESULTS_REDUX_NEW_SELECTED_DOCS,
+      selectedDocs: selectedDocs
+    });
+  });
+
+  it('selectDoc returns the proper event to dispatch', () => {
+    const doc = {
+      _id: 'apple',
+      _rev: 'pie',
+      _deleted: true
+    };
+
+    const selectedDocs = [
+      {
+        _id: 'test1',
+        _rev: 'foo',
+        _deleted: true
+      }
+    ];
+
+    expect(selectDoc(doc, selectedDocs)).toEqual({
+      type: ActionTypes.INDEX_RESULTS_REDUX_NEW_SELECTED_DOCS,
+      selectedDocs: [
+        {
+          _id: 'test1',
+          _rev: 'foo',
+          _deleted: true
+        },
+        {
+          _id: 'apple',
+          _rev: 'pie',
+          _deleted: true
+        }
+      ]
+    });
+  });
+
+  describe('bulkCheckOrUncheck', () => {
+    it('returns the proper event to dispatch when allDocumentsSelected false', () => {
+      const selectedDocs = [];
+      const allDocumentsSelected = false;
+      expect(bulkCheckOrUncheck(docs, selectedDocs, allDocumentsSelected)).toEqual({
+        type: ActionTypes.INDEX_RESULTS_REDUX_NEW_SELECTED_DOCS,
+        selectedDocs: [
+          {
+            _id: 'test1',
+            _rev: 'foo',
+            _deleted: true
+          },
+          {
+            _id: 'test2',
+            _rev: 'bar',
+            _deleted: true
+          }
+        ]
+      });
+    });
+
+    it('returns the proper event to dispatch when allDocumentsSelected true', () => {
+      const selectedDocs = [
+        {
+          _id: 'test1',
+          _rev: 'foo',
+          _deleted: true
+        },
+        {
+          _id: 'test2',
+          _rev: 'bar',
+          _deleted: true
+        }
+      ];
+      const allDocumentsSelected = true;
+      expect(bulkCheckOrUncheck(docs, selectedDocs, allDocumentsSelected)).toEqual({
+        type: ActionTypes.INDEX_RESULTS_REDUX_NEW_SELECTED_DOCS,
+        selectedDocs: []
+      });
+    });
+  });
+
+  it('changeLayout returns the proper event to dispatch', () => {
+    expect(changeLayout(Constants.LAYOUT_ORIENTATION.JSON)).toEqual({
+      type: ActionTypes.INDEX_RESULTS_REDUX_CHANGE_LAYOUT,
+      layout: Constants.LAYOUT_ORIENTATION.JSON
+    });
+  });
+
+  it('changeTableHeaderAttribute returns the proper event to dispatch', () => {
+    const selectedFields = ['_id', '_rev', 'foo'];
+    const newField = {
+      index: 1,
+      newSelectedRow: 'bar'
+    };
+    expect(changeTableHeaderAttribute(newField, selectedFields)).toEqual({
+      type: ActionTypes.INDEX_RESULTS_REDUX_CHANGE_TABLE_HEADER_ATTRIBUTE,
+      selectedFieldsTableView: ['_id', 'bar', 'foo']
+    });
+  });
+});
diff --git a/app/addons/documents/__tests__/fetch-api.test.js b/app/addons/documents/__tests__/fetch-api.test.js
new file mode 100644
index 0000000..34d936f
--- /dev/null
+++ b/app/addons/documents/__tests__/fetch-api.test.js
@@ -0,0 +1,346 @@
+// 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 {
+  mergeParams,
+  removeOverflowDocsAndCalculateHasNext,
+  queryEndpoint,
+  validateBulkDelete,
+  postToBulkDocs,
+  processBulkDeleteResponse
+} from '../index-results/apis/fetch';
+import fetchMock from 'fetch-mock';
+import queryString from 'query-string';
+import sinon from 'sinon';
+import SidebarActions from '../sidebar/actions';
+import FauxtonAPI from '../../../core/api';
+
+describe('Docs Fetch API', () => {
+  describe('mergeParams', () => {
+    let fetchParams, queryOptionsParams;
+    beforeEach(() => {
+      fetchParams = {
+        skip: 0,
+        limit: 21
+      };
+      queryOptionsParams = {};
+    });
+
+    it('supports default fetch and queryOptions params', () => {
+      expect(mergeParams(fetchParams, queryOptionsParams)).toEqual({
+        params: {
+          skip: 0,
+          limit: 21
+        },
+        totalDocsRemaining: NaN
+      });
+    });
+
+    it('supports a manual skip in queryOptionsParams', () => {
+      queryOptionsParams.skip = 5;
+      expect(mergeParams(fetchParams, queryOptionsParams)).toEqual({
+        params: {
+          skip: 5,
+          limit: 21
+        },
+        totalDocsRemaining: NaN
+      });
+    });
+
+    it('manual limit in queryOptionsParams does not affect merge limit', () => {
+      queryOptionsParams.limit = 50;
+      expect(mergeParams(fetchParams, queryOptionsParams)).toEqual({
+        params: {
+          skip: 0,
+          limit: 21
+        },
+        totalDocsRemaining: 50
+      });
+    });
+
+    it('totalDocsRemaining is determined by queryOptions limit and skip on first page', () => {
+      queryOptionsParams.skip = 10;
+      queryOptionsParams.limit = 200;
+      expect(mergeParams(fetchParams, queryOptionsParams)).toEqual({
+        params: {
+          skip: 10,
+          limit: 21
+        },
+        totalDocsRemaining: 200
+      });
+    });
+
+    it('totalDocsRemaining is determined by queryOptions limit and fetch skip on later pages', () => {
+      queryOptionsParams.skip = 10;
+      queryOptionsParams.limit = 200;
+      fetchParams.skip = 30;
+      expect(mergeParams(fetchParams, queryOptionsParams)).toEqual({
+        params: {
+          skip: 30,
+          limit: 21
+        },
+        totalDocsRemaining: 180
+      });
+    });
+
+    it('include conflicts if requested in fetchParams', () => {
+      fetchParams.conflicts = true;
+      expect(mergeParams(fetchParams, queryOptionsParams)).toEqual({
+        params: {
+          skip: 0,
+          limit: 21,
+          conflicts: true
+        },
+        totalDocsRemaining: NaN
+      });
+    });
+  });
+
+  describe('removeOverflowDocsAndCalculateHasNext', () => {
+    let docs;
+    beforeEach(() => {
+      docs = [
+        {
+          _id: 'foo',
+          _rev: 'bar'
+        },
+        {
+          _id: 'xyz',
+          _rev: 'abc'
+        },
+        {
+          _id: 'test',
+          _rev: 'value'
+        }
+      ];
+    });
+
+    it('truncates last doc and has next if length equal to fetch limit', () => {
+      const totalDocsRemaining = NaN;
+      const fetchLimit = 3;
+      expect(removeOverflowDocsAndCalculateHasNext(docs, totalDocsRemaining, fetchLimit)).toEqual({
+        finalDocList: [
+          {
+            _id: 'foo',
+            _rev: 'bar'
+          },
+          {
+            _id: 'xyz',
+            _rev: 'abc'
+          }
+        ],
+        canShowNext: true
+      });
+    });
+
+    it('does not truncate and does not have next if length less than fetch limit', () => {
+      const totalDocsRemaining = NaN;
+      const fetchLimit = 4;
+      expect(removeOverflowDocsAndCalculateHasNext(docs, totalDocsRemaining, fetchLimit)).toEqual({
+        finalDocList: [
+          {
+            _id: 'foo',
+            _rev: 'bar'
+          },
+          {
+            _id: 'xyz',
+            _rev: 'abc'
+          },
+          {
+            _id: 'test',
+            _rev: 'value'
+          }
+        ],
+        canShowNext: false
+      });
+    });
+
+    it('truncates all extra docs if length is greater than totalDocsRemaining', () => {
+      const totalDocsRemaining = 1;
+      const fetchLimit = 3;
+      expect(removeOverflowDocsAndCalculateHasNext(docs, totalDocsRemaining, fetchLimit)).toEqual({
+        finalDocList: [
+          {
+            _id: 'foo',
+            _rev: 'bar'
+          }
+        ],
+        canShowNext: false
+      });
+    });
+  });
+
+  describe('queryEndpoint', () => {
+    const params = {
+      limit: 21,
+      skip: 0
+    };
+    const docs = {
+      "total_rows": 2,
+      "offset": 0,
+      "rows": [
+        {
+          "id": "foo",
+          "key": "foo",
+          "value": {
+            "rev": "1-1390740c4877979dbe8998382876556c"
+          }
+        },
+        {
+          "id": "foo2",
+          "key": "foo2",
+          "value": {
+            "rev": "2-1390740c4877979dbe8998382876556c"
+          }
+        }
+      ]
+    };
+
+    it('queries _all_docs with default params', () => {
+      const fetchUrl = '/testdb/_all_docs';
+      const query = queryString.stringify(params);
+      const url = `${fetchUrl}?${query}`;
+      fetchMock.getOnce(url, docs);
+
+      return queryEndpoint(fetchUrl, params).then((docs) => {
+        expect(docs).toEqual([
+          {
+            id: "foo",
+            key: "foo",
+            value: {
+              rev: "1-1390740c4877979dbe8998382876556c"
+            }
+          },
+          {
+            id: "foo2",
+            key: "foo2",
+            value: {
+              rev: "2-1390740c4877979dbe8998382876556c"
+            }
+          }
+        ]);
+      });
+    });
+  });
+
+  describe('Bulk Delete', () => {
+    describe('validation', () => {
+      let selectedDocs;
+      beforeEach(() => {
+        selectedDocs = [
+          {
+            _id: 'foo',
+            _rev: 'bar',
+            _deleted: true
+          }
+        ];
+      });
+
+      it('validation fails if no docs selected', () => {
+        selectedDocs = [];
+        expect(validateBulkDelete(selectedDocs)).toBe(false);
+      });
+
+      it('validation fails if user does not wish to continue', () => {
+        global.confirm = () => false;
+        expect(validateBulkDelete(selectedDocs)).toBe(false);
+      });
+
+      it('validation succeeds otherwise', () => {
+        global.confirm = () => true;
+        expect(validateBulkDelete(selectedDocs)).toBe(true);
+      });
+    });
+
+    describe('postToBulkDocs', () => {
+      it('deletes list of docs', () => {
+        const payload = {
+          docs: [
+            {
+              _id: 'foo',
+              _rev: 'bar',
+              _deleted: true
+            }
+          ]
+        };
+        const res = [
+          {
+            "ok": true,
+            "id":"foo",
+            "rev":"2-fe3a51be430401d97872d14a40f590dd"
+          }
+        ];
+        const databaseName = 'testdb';
+        fetchMock.postOnce(`/${databaseName}/_bulk_docs`, res);
+        return postToBulkDocs(databaseName, payload).then((json) => {
+          expect(json).toEqual(res);
+        });
+      });
+    });
+
+    describe('processBulkDeleteResponse', () => {
+      let notificationSpy, sidebarSpy;
+
+      beforeEach(() => {
+        notificationSpy = sinon.spy(FauxtonAPI, 'addNotification');
+        sidebarSpy = sinon.stub(SidebarActions, 'updateDesignDocs');
+      });
+
+      afterEach(() => {
+        notificationSpy.restore();
+        sidebarSpy.restore();
+      });
+
+      it('creates two notifications when number of failed docs is positive', () => {
+        const res = [
+          {
+            id: 'foo',
+            error: 'conflict',
+            reason: 'Document update conflict'
+          }
+        ];
+        const originalDocs = [
+          {
+            _id: 'foo',
+            _rev: 'bar',
+            _deleted: true
+          }
+        ];
+        const designDocs = [];
+        processBulkDeleteResponse(res, originalDocs, designDocs);
+        expect(notificationSpy.calledTwice).toBe(true);
+        expect(sidebarSpy.calledOnce).toBe(false);
+      });
+
+      it('calls updateDesignDocs when one of the deleted docs is a ddoc', () => {
+        const res = [
+          {
+            id: '_design/foo',
+            rev: 'bar',
+            ok: true
+          }
+        ];
+        const originalDocs = [
+          {
+            _id: '_design/foo',
+            _rev: 'bar',
+            _deleted: true
+          }
+        ];
+        const designDocs = ['_design/foo'];
+        processBulkDeleteResponse(res, originalDocs, designDocs);
+        expect(notificationSpy.calledOnce).toBe(true);
+        expect(sidebarSpy.calledOnce).toBe(true);
+      });
+    });
+  });
+});
diff --git a/app/addons/documents/__tests__/index-results.test.js b/app/addons/documents/__tests__/index-results.test.js
new file mode 100644
index 0000000..574db68
--- /dev/null
+++ b/app/addons/documents/__tests__/index-results.test.js
@@ -0,0 +1,123 @@
+// 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 { shallow } from 'enzyme';
+import IndexResults from '../index-results/components/results/IndexResults';
+import sinon from 'sinon';
+
+describe('IndexResults', () => {
+  it('calls fetchAllDocs on mount', () => {
+    const spy = sinon.spy();
+    const wrapper = shallow(<IndexResults
+      fetchParams={{}}
+      selectedDocs={[]}
+      queryOptionsParams={{}}
+      fetchAllDocs={spy}
+      results={[]}
+    />);
+
+    wrapper.instance().componentDidMount();
+    expect(spy.calledOnce).toBe(true);
+  });
+
+  it('calls fetchAllDocs on update if ddocsOnly switches', () => {
+    const spy = sinon.spy();
+    const wrapper = shallow(<IndexResults
+      fetchParams={{}}
+      selectedDocs={[]}
+      queryOptionsParams={{}}
+      fetchAllDocs={() => {}}
+      results={[]}
+      ddocsOnly={false}
+    />);
+
+    wrapper.instance().componentWillUpdate({
+      ddocsOnly: true,
+      fetchParams: {},
+      queryOptionsParams: {},
+      fetchAllDocs: spy
+    });
+
+    expect(spy.calledOnce).toBe(true);
+  });
+
+  it('deleteSelectedDocs calls bulkDeleteDocs', () => {
+    const spy = sinon.spy();
+    const wrapper = shallow(<IndexResults
+      bulkDeleteDocs={spy}
+      fetchParams={{}}
+      selectedDocs={[]}
+      queryOptionsParams={{}}
+      fetchAllDocs={() => {}}
+      results={[]}
+    />);
+
+    wrapper.instance().deleteSelectedDocs();
+    expect(spy.calledOnce).toBe(true);
+  });
+
+  it('isSelected returns true when id is in selectedDocs', () => {
+    const selectedDocs = [{
+      _id: 'foo'
+    }];
+    const wrapper = shallow(<IndexResults
+      selectedDocs={selectedDocs}
+      fetchAllDocs={() => {}}
+      results={[]}
+    />);
+
+    expect(wrapper.instance().isSelected('foo')).toBe(true);
+  });
+
+  it('isSelected returns false when id is not in selectedDocs', () => {
+    const selectedDocs = [{
+      _id: 'bar'
+    }];
+    const wrapper = shallow(<IndexResults
+      selectedDocs={selectedDocs}
+      fetchAllDocs={() => {}}
+      results={[]}
+    />);
+
+    expect(wrapper.instance().isSelected('foo')).toBe(false);
+  });
+
+  it('docChecked calls selectDoc', () => {
+    const spy = sinon.spy();
+    const wrapper = shallow(<IndexResults
+      selectedDocs={[]}
+      fetchAllDocs={() => {}}
+      results={[]}
+      selectDoc={spy}
+    />);
+
+    wrapper.instance().docChecked('foo', '1-123324345');
+    expect(spy.calledOnce).toBe(true);
+  });
+
+  it('toggleSelectAll calls bulkCheckOrUncheck', () => {
+    const spy = sinon.spy();
+    const wrapper = shallow(<IndexResults
+      selectedDocs={[]}
+      fetchAllDocs={() => {}}
+      results={[]}
+      docs={[]}
+      allDocumentsSelected={false}
+      bulkCheckOrUncheck={spy}
+    />);
+
+    wrapper.instance().toggleSelectAll();
+    expect(spy.calledOnce).toBe(true);
+  });
+});
diff --git a/app/addons/documents/__tests__/json-view.test.js b/app/addons/documents/__tests__/json-view.test.js
new file mode 100644
index 0000000..0e3c2a2
--- /dev/null
+++ b/app/addons/documents/__tests__/json-view.test.js
@@ -0,0 +1,128 @@
+// 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 { getJsonViewData } from '../index-results/helpers/json-view';
+import { getDocUrl } from '../index-results/helpers/shared-helpers';
+import '../base';
+
+describe('Docs JSON View', () => {
+  const databaseName = 'testdb';
+  let typeOfIndex = 'view';
+  const docs = [
+    {
+      id: "aardvark",
+      key: "aardvark",
+      value: {
+        rev: "5-717f5e88689af3ad191b47321de10c95"
+      },
+      doc: {
+        _id: "aardvark",
+        _rev: "5-717f5e88689af3ad191b47321de10c95",
+        min_weight: 40,
+        max_weight: 65,
+        min_length: 1,
+        max_length: 2.2,
+        latin_name: "Orycteropus afer",
+        wiki_page: "http://en.wikipedia.org/wiki/Aardvark",
+        class: "mammal",
+        diet: "omnivore"
+      }
+    },
+    {
+      id: "badger",
+      key: "badger",
+      value: {
+        rev: "8-db03387de9cbd5c2814523b043566dfe"
+      },
+      doc: {
+        _id: "badger",
+        _rev: "8-db03387de9cbd5c2814523b043566dfe",
+        wiki_page: "http://en.wikipedia.org/wiki/Badger",
+        min_weight: 7,
+        max_weight: 30,
+        min_length: 0.6,
+        max_length: 0.9,
+        latin_name: "Meles meles",
+        class: "mammal",
+        diet: "omnivore"
+      }
+    }
+  ];
+  let testDocs;
+
+  beforeEach(() => {
+    testDocs = docs;
+    typeOfIndex = 'view';
+  });
+
+  it('getJsonViewData returns proper meta object with vanilla inputs', () => {
+    expect(getJsonViewData(testDocs, {databaseName, typeOfIndex})).toEqual({
+      displayedFields: null,
+      hasBulkDeletableDoc: true,
+      results: [
+        {
+          content: JSON.stringify(testDocs[0], null, ' '),
+          id: testDocs[0].id,
+          _rev: testDocs[0].value.rev,
+          header: testDocs[0].id,
+          keylabel: 'id',
+          url: getDocUrl('app', testDocs[0].id, databaseName),
+          isDeletable: true,
+          isEditable: true
+        },
+        {
+          content: JSON.stringify(testDocs[1], null, ' '),
+          id: testDocs[1].id,
+          _rev: testDocs[1].value.rev,
+          header: testDocs[1].id,
+          keylabel: 'id',
+          url: getDocUrl('app', testDocs[1].id, databaseName),
+          isDeletable: true,
+          isEditable: true
+        }
+      ]
+    });
+  });
+
+  it('getJsonViewData false hasBulkDeletableDoc when all special mango docs', () => {
+    typeOfIndex = 'MangoIndex';
+    testDocs[0].type = 'special';
+    testDocs[1].type = 'special';
+
+    expect(getJsonViewData(testDocs, {databaseName, typeOfIndex})).toEqual({
+      displayedFields: null,
+      hasBulkDeletableDoc: false,
+      results: [
+        {
+          content: JSON.stringify(testDocs[0], null, ' '),
+          id: testDocs[0].id,
+          _rev: testDocs[0].value.rev,
+          header: testDocs[0].id,
+          keylabel: 'id',
+          url: getDocUrl('app', testDocs[0].id, databaseName),
+          isDeletable: true,
+          isEditable: true
+        },
+        {
+          content: JSON.stringify(testDocs[1], null, ' '),
+          id: testDocs[1].id,
+          _rev: testDocs[1].value.rev,
+          header: testDocs[1].id,
+          keylabel: 'id',
+          url: getDocUrl('app', testDocs[1].id, databaseName),
+          isDeletable: true,
+          isEditable: true
+        }
+      ]
+    });
+  });
+});
diff --git a/app/addons/documents/__tests__/key-search-fields.test.js b/app/addons/documents/__tests__/key-search-fields.test.js
new file mode 100644
index 0000000..d337ab1
--- /dev/null
+++ b/app/addons/documents/__tests__/key-search-fields.test.js
@@ -0,0 +1,123 @@
+// 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 { mount } from 'enzyme';
+import KeySearchFields from '../index-results/components/queryoptions/KeySearchFields';
+import sinon from 'sinon';
+
+describe('KeySearchFields', () => {
+  const betweenKeys = {
+    startkey: 'foo',
+    endKey: 'bar',
+    include: true
+  };
+
+  it('keysGroupClass contains \'hide\' when showByKeys and showBetweenKeys are false', () => {
+    const wrapper = mount(<KeySearchFields
+      showByKeys={false}
+      showBetweenKeys={false}
+      betweenKeys={betweenKeys}
+    />);
+
+    expect(wrapper.find('.js-query-keys-wrapper').hasClass('hide')).toBe(true);
+  });
+
+  it('byKeysClass contains \'hide\' and byKeysButtonClass contains \'active\' when showByKeys is false', () => {
+    const wrapper = mount(<KeySearchFields
+      showByKeys={false}
+      showBetweenKeys={true}
+      betweenKeys={betweenKeys}
+    />);
+
+    expect(wrapper.find('#js-showKeys').hasClass('hide')).toBe(true);
+    expect(wrapper.find('#betweenKeys').hasClass('active')).toBe(true);
+  });
+
+  it('betweenKeysClass contains \'hide\' and betweenKeysButtonClass contains \'active\' when showBetweenKeys is false', () => {
+    const wrapper = mount(<KeySearchFields
+      showByKeys={true}
+      showBetweenKeys={false}
+      betweenKeys={betweenKeys}
+    />);
+
+    expect(wrapper.find('#js-showStartEnd').hasClass('hide')).toBe(true);
+    expect(wrapper.find('#byKeys').hasClass('active')).toBe(true);
+  });
+
+  it('calls toggleByKeys onClick', () => {
+    const spy = sinon.spy();
+    const wrapper = mount(<KeySearchFields
+      showByKeys={true}
+      showBetweenKeys={false}
+      betweenKeys={betweenKeys}
+      toggleByKeys={spy}
+    />);
+
+    wrapper.find('#byKeys').simulate('click');
+    expect(spy.calledOnce).toBe(true);
+  });
+
+  it('calls toggleBetweenKeys onClick', () => {
+    const spy = sinon.spy();
+    const wrapper = mount(<KeySearchFields
+      showByKeys={false}
+      showBetweenKeys={true}
+      betweenKeys={betweenKeys}
+      toggleBetweenKeys={spy}
+    />);
+
+    wrapper.find('#betweenKeys').simulate('click');
+    expect(spy.calledOnce).toBe(true);
+  });
+
+  it('calls updateBetweenKeys onChange', () => {
+    const spy = sinon.spy();
+    const wrapper = mount(<KeySearchFields
+      showByKeys={false}
+      showBetweenKeys={true}
+      betweenKeys={betweenKeys}
+      updateBetweenKeys={spy}
+    />);
+
+    wrapper.find('#startkey').simulate('change');
+    wrapper.find('#endkey').simulate('change');
+    expect(spy.calledTwice).toBe(true);
+  });
+
+  it('calls updateInclusiveEnd onChange', () => {
+    const spy = sinon.spy();
+    const wrapper = mount(<KeySearchFields
+      showByKeys={false}
+      showBetweenKeys={true}
+      betweenKeys={betweenKeys}
+      updateBetweenKeys={spy}
+    />);
+
+    wrapper.find('#qoIncludeEndKeyInResults').simulate('change');
+    expect(spy.calledOnce).toBe(true);
+  });
+
+  it('calls updateByKeys onChange', () => {
+    const spy = sinon.spy();
+    const wrapper = mount(<KeySearchFields
+      showByKeys={false}
+      showBetweenKeys={true}
+      betweenKeys={betweenKeys}
+      updateByKeys={spy}
+    />);
+
+    wrapper.find('#keys-input').simulate('change');
+    expect(spy.calledOnce).toBe(true);
+  });
+});
diff --git a/app/addons/documents/__tests__/main-fields-view.test.js b/app/addons/documents/__tests__/main-fields-view.test.js
new file mode 100644
index 0000000..80b0c45
--- /dev/null
+++ b/app/addons/documents/__tests__/main-fields-view.test.js
@@ -0,0 +1,91 @@
+// 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 { mount } from 'enzyme';
+import MainFieldsView from '../index-results/components/queryoptions/MainFieldsView';
+import sinon from 'sinon';
+
+describe('MainFieldsView', () => {
+  const docURL = 'http://foo.com';
+  it('does not render reduce when showReduce is false', () => {
+    const wrapper = mount(<MainFieldsView
+      includeDocs={false}
+      showReduce={false}
+      reduce={false}
+      toggleIncludeDocs={() => {}}
+      docURL={docURL}
+    />);
+
+    expect(wrapper.find('#qoReduce').length).toBe(0);
+  });
+
+  it('render reduce when showReduce is true but does not render grouplevel when reduce is false', () => {
+    const wrapper = mount(<MainFieldsView
+      includeDocs={false}
+      showReduce={true}
+      reduce={false}
+      toggleIncludeDocs={() => {}}
+      docURL={docURL}
+    />);
+
+    expect(wrapper.find('#qoReduce').length).toBe(1);
+    expect(wrapper.find('#qoGroupLevelGroup').length).toBe(0);
+  });
+
+  it('calls toggleIncludeDocs onChange', () => {
+    const spy = sinon.spy();
+    const wrapper = mount(<MainFieldsView
+      includeDocs={false}
+      showReduce={true}
+      reduce={false}
+      toggleIncludeDocs={spy}
+      docURL={docURL}
+    />);
+
+    wrapper.find('#qoIncludeDocs').simulate('change');
+    expect(spy.calledOnce).toBe(true);
+  });
+
+  it('calls groupLevelChange onChange', () => {
+    const spy = sinon.spy();
+    const wrapper = mount(<MainFieldsView
+      includeDocs={false}
+      showReduce={true}
+      reduce={true}
+      toggleIncludeDocs={() => {}}
+      updateGroupLevel={spy}
+      toggleReduce={() => {}}
+      docURL={docURL}
+    />);
+
+    wrapper.find('#qoGroupLevel').simulate('change');
+    expect(spy.calledOnce).toBe(true);
+  });
+
+  it('calls toggleReduce onChange', () => {
+    const spy = sinon.spy();
+    const wrapper = mount(<MainFieldsView
+      includeDocs={false}
+      showReduce={true}
+      reduce={true}
+      toggleIncludeDocs={() => {}}
+      updateGroupLevel={() => {}}
+      toggleReduce={spy}
+      docURL={docURL}
+    />);
+
+    wrapper.find('#qoReduce').simulate('change');
+    expect(spy.calledOnce).toBe(true);
+  });
+});
diff --git a/app/addons/documents/__tests__/pagination-api.test.js b/app/addons/documents/__tests__/pagination-api.test.js
new file mode 100644
index 0000000..75de7e6
--- /dev/null
+++ b/app/addons/documents/__tests__/pagination-api.test.js
@@ -0,0 +1,110 @@
+// 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 {
+  toggleShowAllColumns,
+  setPerPage,
+  resetFetchParamsBeforePerPageChange,
+  incrementSkipForPageNext,
+  decrementSkipForPagePrevious,
+  resetPagination
+} from '../index-results/apis/pagination';
+import ActionTypes from '../index-results/actiontypes';
+import FauxtonAPI from '../../../core/api';
+
+describe('Docs Pagination API', () => {
+  it('toggleShowAllColumns returns the proper event to dispatch', () => {
+    expect(toggleShowAllColumns()).toEqual({
+      type: ActionTypes.INDEX_RESULTS_REDUX_TOGGLE_SHOW_ALL_COLUMNS
+    });
+  });
+
+  it('setPerPage returns the proper event to dispatch', () => {
+    const pageSize = 10;
+    expect(setPerPage(pageSize)).toEqual({
+      type: ActionTypes.INDEX_RESULTS_REDUX_SET_PER_PAGE,
+      perPage: pageSize
+    });
+  });
+
+  describe('resetFetchParamsBeforePerPageChange', () => {
+    let fetchParams, queryOptionsParams;
+    let amount = 10;
+    beforeEach(() => {
+      fetchParams = {
+        skip: 20,
+        limit: 21
+      };
+      queryOptionsParams = {};
+    });
+
+    it('fetchs with proper params when queryOptions doesnt have skip', () => {
+      expect(resetFetchParamsBeforePerPageChange(fetchParams, queryOptionsParams, amount)).toEqual({
+        limit: 11,
+        skip: 0
+      });
+    });
+
+    it('fetches with the proper params when queryOptions does have skip', () => {
+      queryOptionsParams.skip = 5;
+      expect(resetFetchParamsBeforePerPageChange(fetchParams, queryOptionsParams, amount)).toEqual({
+        limit: 11,
+        skip: 5
+      });
+    });
+  });
+
+  it('incrementSkipForPageNext returns the proper fetch params', () => {
+    const fetchParams = {
+      skip: 0,
+      limit: 21
+    };
+    const perPage = 20;
+    expect(incrementSkipForPageNext(fetchParams, perPage)).toEqual({
+      skip: 20,
+      limit: 21
+    });
+  });
+
+  describe('decrementSkipForPagePrevious', () => {
+    it('returns the proper fetch params when greater than zero', () => {
+      const fetchParams = {
+        skip: 40,
+        limit: 21
+      };
+      const perPage = 20;
+      expect(decrementSkipForPagePrevious(fetchParams, perPage)).toEqual({
+        skip: 20,
+        limit: 21
+      });
+    });
+
+    it('returns the proper fetch params when skip less than zero', () => {
+      const fetchParams = {
+        skip: 5,
+        limit: 21
+      };
+      const perPage = 20;
+      expect(decrementSkipForPagePrevious(fetchParams, perPage)).toEqual({
+        skip: 0,
+        limit: 21
+      });
+    });
+  });
+
+  it('resetPagination defaults to FauxtonAPI page size if arg empty', () => {
+    expect(resetPagination()).toEqual({
+      type: ActionTypes.INDEX_RESULTS_REDUX_SET_PER_PAGE,
+      perPage: FauxtonAPI.constants.MISC.DEFAULT_PAGE_SIZE
+    });
+  });
+});
diff --git a/app/addons/documents/__tests__/pagination-footer.test.js b/app/addons/documents/__tests__/pagination-footer.test.js
new file mode 100644
index 0000000..97c0d95
--- /dev/null
+++ b/app/addons/documents/__tests__/pagination-footer.test.js
@@ -0,0 +1,199 @@
+// 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 { mount } from 'enzyme';
+import PaginationFooter from '../index-results/components/pagination/PaginationFooter';
+import sinon from 'sinon';
+
+describe('PaginationFooter', () => {
+  const displayedFields = {};
+  beforeEach(() => {
+    displayedFields.shown = 5;
+    displayedFields.allFieldCount = 10;
+  });
+
+  it('does not show table controls if showPrioritizedEnabled is false', () => {
+    const wrapper = mount(<PaginationFooter
+        showPrioritizedEnabled={false}
+        hasResults={true}
+        prioritizedEnabled={false}
+        displayedFields={displayedFields}
+        perPage={20}
+        canShowNext={false}
+        canShowPrevious={false}
+        toggleShowAllColumns={() => {}}
+        docs={[]}
+        pageStart={1}
+        pageEnd={20}
+      />);
+
+    expect(wrapper.find('#footer-doc-control-prioritized').length).toBe(0);
+  });
+
+  it('does not show table controls if hasResults is false', () => {
+    const wrapper = mount(<PaginationFooter
+        showPrioritizedEnabled={true}
+        hasResults={false}
+        prioritizedEnabled={false}
+        displayedFields={displayedFields}
+        perPage={20}
+        canShowNext={false}
+        canShowPrevious={false}
+        toggleShowAllColumns={() => {}}
+        docs={[]}
+        pageStart={1}
+        pageEnd={20}
+      />);
+
+    expect(wrapper.find('#footer-doc-control-prioritized').length).toBe(0);
+  });
+
+  it('does show table controls if showPrioritizedEnabled and hasResults are true', () => {
+    const wrapper = mount(<PaginationFooter
+        showPrioritizedEnabled={true}
+        hasResults={true}
+        prioritizedEnabled={false}
+        displayedFields={displayedFields}
+        perPage={20}
+        canShowNext={false}
+        canShowPrevious={false}
+        toggleShowAllColumns={() => {}}
+        docs={[]}
+        pageStart={1}
+        pageEnd={20}
+      />);
+
+    expect(wrapper.find('#footer-doc-control-prioritized').length).toBe(1);
+  });
+
+  it('calls paginateNext when clicked and available', () => {
+    const spy = sinon.spy();
+    const wrapper = mount(<PaginationFooter
+        showPrioritizedEnabled={true}
+        hasResults={true}
+        prioritizedEnabled={false}
+        displayedFields={displayedFields}
+        perPage={20}
+        canShowNext={true}
+        canShowPrevious={false}
+        toggleShowAllColumns={() => {}}
+        docs={[]}
+        pageStart={1}
+        pageEnd={20}
+        paginateNext={spy}
+      />);
+
+    wrapper.instance().nextClicked({ preventDefault: () => {} });
+    expect(spy.calledOnce).toBe(true);
+  });
+
+  it('does not call paginateNext when clicked and not available', () => {
+    const spy = sinon.spy();
+    const wrapper = mount(<PaginationFooter
+        showPrioritizedEnabled={true}
+        hasResults={true}
+        prioritizedEnabled={false}
+        displayedFields={displayedFields}
+        perPage={20}
+        canShowNext={false}
+        canShowPrevious={false}
+        toggleShowAllColumns={() => {}}
+        docs={[]}
+        pageStart={1}
+        pageEnd={20}
+        paginateNext={spy}
+      />);
+
+    wrapper.instance().nextClicked({ preventDefault: () => {} });
+    expect(spy.calledOnce).toBe(false);
+  });
+
+  it('calls paginatePrevious when clicked and available', () => {
+    const spy = sinon.spy();
+    const wrapper = mount(<PaginationFooter
+        showPrioritizedEnabled={true}
+        hasResults={true}
+        prioritizedEnabled={false}
+        displayedFields={displayedFields}
+        perPage={20}
+        canShowNext={false}
+        canShowPrevious={true}
+        toggleShowAllColumns={() => {}}
+        docs={[]}
+        pageStart={1}
+        pageEnd={20}
+        paginatePrevious={spy}
+      />);
+
+    wrapper.instance().previousClicked({ preventDefault: () => {} });
+    expect(spy.calledOnce).toBe(true);
+  });
+
+  it('does not call paginatePrevious when clicked and not available', () => {
+    const spy = sinon.spy();
+    const wrapper = mount(<PaginationFooter
+        showPrioritizedEnabled={true}
+        hasResults={true}
+        prioritizedEnabled={false}
+        displayedFields={displayedFields}
+        perPage={20}
+        canShowNext={false}
+        canShowPrevious={false}
+        toggleShowAllColumns={() => {}}
+        docs={[]}
+        pageStart={1}
+        pageEnd={20}
+        paginatePrevious={spy}
+      />);
+
+    wrapper.instance().previousClicked({ preventDefault: () => {} });
+    expect(spy.calledOnce).toBe(false);
+  });
+
+  it('renders custom text when no docs', () => {
+    const wrapper = mount(<PaginationFooter
+        showPrioritizedEnabled={true}
+        hasResults={true}
+        prioritizedEnabled={false}
+        displayedFields={displayedFields}
+        perPage={20}
+        canShowNext={false}
+        canShowPrevious={false}
+        toggleShowAllColumns={() => {}}
+        docs={[]}
+        pageStart={1}
+        pageEnd={20}
+      />);
+
+    expect(wrapper.find('.current-docs span').text()).toMatch('Showing 0 documents.');
+  });
+
+  it('renders text indicating range when docs', () => {
+    const wrapper = mount(<PaginationFooter
+        showPrioritizedEnabled={true}
+        hasResults={true}
+        prioritizedEnabled={false}
+        displayedFields={displayedFields}
+        perPage={20}
+        canShowNext={false}
+        canShowPrevious={false}
+        toggleShowAllColumns={() => {}}
+        docs={[{_id: 'foo'}]}
+        pageStart={1}
+        pageEnd={20}
+      />);
+
+    expect(wrapper.find('.current-docs span').text()).toMatch('Showing document 1 - 20.');
+  });
+});
diff --git a/app/addons/documents/__tests__/paging-controls.test.js b/app/addons/documents/__tests__/paging-controls.test.js
new file mode 100644
index 0000000..4fb93c2
--- /dev/null
+++ b/app/addons/documents/__tests__/paging-controls.test.js
@@ -0,0 +1,62 @@
+// 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 { mount } from 'enzyme';
+import PagingControls from '../index-results/components/pagination/PagingControls';
+
+describe('PagingControls', () => {
+  it('pagination controls disabled when canShowPrevious and canShowNext are false', () => {
+    const wrapper = mount(<PagingControls
+      canShowNext={false}
+      canShowPrevious={false}
+      nextClicked={() => {}}
+      previousClicked={() => {}}
+    />);
+
+    expect(wrapper.find('ul.pagination li.disabled').length).toBe(2);
+  });
+
+  it('pagination control disabled when canShowPrevious is false', () => {
+    const wrapper = mount(<PagingControls
+      canShowNext={true}
+      canShowPrevious={false}
+      nextClicked={() => {}}
+      previousClicked={() => {}}
+    />);
+
+    expect(wrapper.find('ul.pagination li.disabled #previous').length).toBe(1);
+  });
+
+  it('pagination control disabled when canShowNext is false', () => {
+    const wrapper = mount(<PagingControls
+      canShowNext={false}
+      canShowPrevious={true}
+      nextClicked={() => {}}
+      previousClicked={() => {}}
+    />);
+
+    expect(wrapper.find('ul.pagination li.disabled #next').length).toBe(1);
+  });
+
+  it('pagination controls enabled when canShowPrevious and canShowNext are true', () => {
+    const wrapper = mount(<PagingControls
+      canShowNext={true}
+      canShowPrevious={true}
+      nextClicked={() => {}}
+      previousClicked={() => {}}
+    />);
+
+    expect(wrapper.find('ul.pagination li.disabled').length).toBe(0);
+  });
+});
diff --git a/app/addons/documents/pagination/actiontypes.js b/app/addons/documents/__tests__/perpage-selector.test.js
similarity index 51%
copy from app/addons/documents/pagination/actiontypes.js
copy to app/addons/documents/__tests__/perpage-selector.test.js
index 7e47937..dc9fd0e 100644
--- a/app/addons/documents/pagination/actiontypes.js
+++ b/app/addons/documents/__tests__/perpage-selector.test.js
@@ -10,10 +10,18 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-export default {
-  COLLECTION_CHANGED: 'COLLECTION_CHANGED',
-  PER_PAGE_CHANGE: 'PER_PAGE_CHANGE',
-  PAGINATE_NEXT: 'PAGINATE_NEXT',
-  PAGINATE_PREVIOUS: 'PAGINATE_PREVIOUS',
-  SET_PAGINATION_DOCUMENT_LIMIT: 'SET_PAGINATION_DOCUMENT_LIMIT'
-};
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { mount } from 'enzyme';
+import PerPageSelector from '../index-results/components/pagination/PerPageSelector';
+import sinon from 'sinon';
+
+describe('PerPageSelector', () => {
+  it('calls perPageChange when value changes', () => {
+    const spy = sinon.spy();
+    const wrapper = mount(<PerPageSelector perPage={10} perPageChange={spy} />);
+
+    wrapper.find('#select-per-page').simulate('change');
+    expect(spy.calledOnce).toBe(true);
+  });
+});
diff --git a/app/addons/documents/pagination/actiontypes.js b/app/addons/documents/__tests__/query-buttons.test.js
similarity index 54%
copy from app/addons/documents/pagination/actiontypes.js
copy to app/addons/documents/__tests__/query-buttons.test.js
index 7e47937..225ccc3 100644
--- a/app/addons/documents/pagination/actiontypes.js
+++ b/app/addons/documents/__tests__/query-buttons.test.js
@@ -10,10 +10,18 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-export default {
-  COLLECTION_CHANGED: 'COLLECTION_CHANGED',
-  PER_PAGE_CHANGE: 'PER_PAGE_CHANGE',
-  PAGINATE_NEXT: 'PAGINATE_NEXT',
-  PAGINATE_PREVIOUS: 'PAGINATE_PREVIOUS',
-  SET_PAGINATION_DOCUMENT_LIMIT: 'SET_PAGINATION_DOCUMENT_LIMIT'
-};
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { mount } from 'enzyme';
+import QueryButtons from '../index-results/components/queryoptions/QueryButtons';
+import sinon from 'sinon';
+
+describe('QueryButtons', () => {
+  it('calls onCancel after click', () => {
+    const spy = sinon.spy();
+    const wrapper = mount(<QueryButtons onCancel={spy} />);
+
+    wrapper.find('a').simulate('click');
+    expect(spy.calledOnce).toBe(true);
+  });
+});
diff --git a/app/addons/documents/__tests__/query-options.test.js b/app/addons/documents/__tests__/query-options.test.js
new file mode 100644
index 0000000..fec4533
--- /dev/null
+++ b/app/addons/documents/__tests__/query-options.test.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 { shallow } from 'enzyme';
+import QueryOptions from '../index-results/components/queryoptions/QueryOptions';
+import sinon from 'sinon';
+import Constants from '../constants';
+
+describe('QueryOptions', () => {
+  const props = {
+    includeDocs: false,
+    queryOptionsToggleIncludeDocs: () => {},
+    reduce: false,
+    contentVisible: true
+  };
+
+  it('calls resetPagination and queryOptionsExecute on submit', () => {
+    const spy1 = sinon.spy();
+    const spy2 = sinon.spy();
+    const queryOptionsParams = {
+      include_docs: false
+    };
+
+    const wrapper = shallow(<QueryOptions
+      queryOptionsExecute={spy1}
+      resetPagination={spy2}
+      queryOptionsToggleVisibility={() => {}}
+      queryOptionsParams={queryOptionsParams}
+      selectedLayout={Constants.LAYOUT_ORIENTATION.METADATA}
+      changeLayout={() => {}}
+      {...props}
+    />);
+
+    wrapper.instance().executeQuery();
+    expect(spy1.calledOnce).toBe(true);
+    expect(spy2.calledOnce).toBe(true);
+  });
+
+  it('calls queryOptionsFilterOnlyDdocs if ddocsOnly is true', () => {
+    const spy = sinon.spy();
+    const queryOptionsParams = {
+      include_docs: false
+    };
+
+    shallow(<QueryOptions
+      ddocsOnly={true}
+      queryOptionsFilterOnlyDdocs={spy}
+      queryOptionsExecute={() => {}}
+      resetPagination={() => {}}
+      queryOptionsToggleVisibility={() => {}}
+      queryOptionsParams={queryOptionsParams}
+      selectedLayout={Constants.LAYOUT_ORIENTATION.METADATA}
+      changeLayout={() => {}}
+      {...props}
+    />);
+
+    expect(spy.calledOnce).toBe(true);
+  });
+
+  it('calls queryOptionsFilterOnlyDdocs if ddocsOnly switches to true on new props', () => {
+    const spy = sinon.spy();
+    const queryOptionsParams = {
+      include_docs: false
+    };
+
+    const wrapper = shallow(<QueryOptions
+      ddocsOnly={false}
+      queryOptionsFilterOnlyDdocs={spy}
+      queryOptionsExecute={() => {}}
+      resetPagination={() => {}}
+      queryOptionsToggleVisibility={() => {}}
+      queryOptionsParams={queryOptionsParams}
+      selectedLayout={Constants.LAYOUT_ORIENTATION.METADATA}
+      changeLayout={() => {}}
+      {...props}
+    />);
+
+    wrapper.instance().componentWillReceiveProps({
+      ddocsOnly: true
+    });
+    expect(spy.calledOnce).toBe(true);
+  });
+
+  it('calls resetState if ddocsOnly switches to false on new props', () => {
+    const spy = sinon.spy();
+    const queryOptionsParams = {
+      include_docs: false
+    };
+
+    const wrapper = shallow(<QueryOptions
+      ddocsOnly={true}
+      resetState={spy}
+      queryOptionsFilterOnlyDdocs={() => {}}
+      queryOptionsExecute={() => {}}
+      resetPagination={() => {}}
+      queryOptionsToggleVisibility={() => {}}
+      queryOptionsParams={queryOptionsParams}
+      selectedLayout={Constants.LAYOUT_ORIENTATION.METADATA}
+      changeLayout={() => {}}
+      {...props}
+    />);
+
+    wrapper.instance().componentWillReceiveProps({
+      ddocsOnly: false
+    });
+    expect(spy.calledOnce).toBe(true);
+  });
+});
diff --git a/app/addons/documents/__tests__/queryoptions-api.test.js b/app/addons/documents/__tests__/queryoptions-api.test.js
new file mode 100644
index 0000000..133feb2
--- /dev/null
+++ b/app/addons/documents/__tests__/queryoptions-api.test.js
@@ -0,0 +1,159 @@
+// 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 * as Api from '../index-results/apis/queryoptions';
+import ActionTypes from '../index-results/actiontypes';
+
+describe('Docs Query Options API', () => {
+  it('resetFetchParamsBeforeExecute returns proper fetch params', () => {
+    const perPage = 20;
+    expect(Api.resetFetchParamsBeforeExecute(perPage)).toEqual({
+      limit: 21,
+      skip: 0
+    });
+  });
+
+  it('queryOptionsToggleVisibility returns the proper event to dispatch', () => {
+    const newVisibility = true;
+    expect(Api.queryOptionsToggleVisibility(newVisibility)).toEqual({
+      type: ActionTypes.INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS,
+      options: {
+        isVisible: true
+      }
+    });
+  });
+
+  it('queryOptionsToggleReduce returns the proper event to dispatch', () => {
+    const previousReduce = true;
+    expect(Api.queryOptionsToggleReduce(previousReduce)).toEqual({
+      type: ActionTypes.INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS,
+      options: {
+        reduce: false
+      }
+    });
+  });
+
+  it('queryOptionsUpdateGroupLevel returns the proper event to dispatch', () => {
+    const newGroupLevel = 'exact';
+    expect(Api.queryOptionsUpdateGroupLevel(newGroupLevel)).toEqual({
+      type: ActionTypes.INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS,
+      options: {
+        groupLevel: 'exact'
+      }
+    });
+  });
+
+  it('queryOptionsToggleByKeys returns the proper event to dispatch', () => {
+    const previousShowByKeys = true;
+    expect(Api.queryOptionsToggleByKeys(previousShowByKeys)).toEqual({
+      type: ActionTypes.INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS,
+      options: {
+        showByKeys: false,
+        showBetweenKeys: true
+      }
+    });
+  });
+
+  it('queryOptionsToggleBetweenKeys returns the proper event to dispatch', () => {
+    const previousShowBetweenKeys = true;
+    expect(Api.queryOptionsToggleBetweenKeys(previousShowBetweenKeys)).toEqual({
+      type: ActionTypes.INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS,
+      options: {
+        showBetweenKeys: false,
+        showByKeys: true,
+      }
+    });
+  });
+
+  it('queryOptionsUpdateBetweenKeys returns the proper event to dispatch', () => {
+    const newBetweenKeys = {
+      include: true,
+      startkey: '\"_design\"',
+      endkey: '\"_design\"'
+    };
+    expect(Api.queryOptionsUpdateBetweenKeys(newBetweenKeys)).toEqual({
+      type: ActionTypes.INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS,
+      options: {
+        betweenKeys: {
+          include: true,
+          startkey: '\"_design\"',
+          endkey: '\"_design\"'
+        }
+      }
+    });
+  });
+
+  it('queryOptionsUpdateByKeys returns the proper event to dispatch', () => {
+    const newByKeys = ['foo', 'bar'];
+    expect(Api.queryOptionsUpdateByKeys(newByKeys)).toEqual({
+      type: ActionTypes.INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS,
+      options: {
+        byKeys: ['foo', 'bar']
+      }
+    });
+  });
+
+  it('queryOptionsToggleDescending returns the proper event to dispatch', () => {
+    const previousDescending = true;
+    expect(Api.queryOptionsToggleDescending(previousDescending)).toEqual({
+      type: ActionTypes.INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS,
+      options: {
+        descending: false
+      }
+    });
+  });
+
+  it('queryOptionsUpdateSkip returns the proper event to dispatch', () => {
+    const newSkip = 5;
+    expect(Api.queryOptionsUpdateSkip(newSkip)).toEqual({
+      type: ActionTypes.INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS,
+      options: {
+        skip: 5
+      }
+    });
+  });
+
+  it('queryOptionsUpdateLimit returns the proper event to dispatch', () => {
+    const newLimit = 50;
+    expect(Api.queryOptionsUpdateLimit(newLimit)).toEqual({
+      type: ActionTypes.INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS,
+      options: {
+        limit: 50
+      }
+    });
+  });
+
+  it('queryOptionsToggleIncludeDocs returns the proper event to dispatch', () => {
+    const previousIncludeDocs = true;
+    expect(Api.queryOptionsToggleIncludeDocs(previousIncludeDocs)).toEqual({
+      type: ActionTypes.INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS,
+      options: {
+        includeDocs: false
+      }
+    });
+  });
+
+  it('queryOptionsFilterOnlyDdocs returns the proper event to dispatch', () => {
+    expect(Api.queryOptionsFilterOnlyDdocs()).toEqual({
+      type: ActionTypes.INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS,
+      options: {
+        betweenKeys: {
+          include: false,
+          startkey: '\"_design\"',
+          endkey: '\"_design0\"'
+        },
+        showBetweenKeys: true,
+        showByKeys: false
+      }
+    });
+  });
+});
diff --git a/app/addons/documents/__tests__/reducers.test.js b/app/addons/documents/__tests__/reducers.test.js
new file mode 100644
index 0000000..b0e7a4f
--- /dev/null
+++ b/app/addons/documents/__tests__/reducers.test.js
@@ -0,0 +1,469 @@
+// 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 * as Reducers from '../index-results/reducers';
+import FauxtonAPI from '../../../core/api';
+import Constants from '../constants';
+import ActionTypes from '../index-results/actiontypes';
+
+describe('Docs Reducers', () => {
+  const initialState = {
+    docs: [],  // raw documents returned from couch
+    selectedDocs: [],  // documents selected for manipulation
+    isLoading: false,
+    tableView: {
+      selectedFieldsTableView: [],  // current columns to display
+      showAllFieldsTableView: false, // do we show all possible columns?
+    },
+    isEditable: true,  // can the user manipulate the results returned?
+    selectedLayout: Constants.LAYOUT_ORIENTATION.METADATA,
+    textEmptyIndex: 'No Documents Found',
+    typeOfIndex: 'view',
+    fetchParams: {
+      limit: FauxtonAPI.constants.MISC.DEFAULT_PAGE_SIZE + 1,
+      skip: 0
+    },
+    pagination: {
+      pageStart: 1,  // index of first doc in this page of results
+      currentPage: 1,  // what page of results are we showing?
+      perPage: FauxtonAPI.constants.MISC.DEFAULT_PAGE_SIZE,
+      canShowNext: false  // flag indicating if we can show a next page
+    },
+    queryOptionsPanel: {
+      isVisible: false,
+      showByKeys: false,
+      showBetweenKeys: false,
+      includeDocs: false,
+      betweenKeys: {
+        include: true,
+        startkey: '',
+        endkey: ''
+      },
+      byKeys: '',
+      descending: false,
+      skip: '',
+      limit: 'none',
+      reduce: false,
+      groupLevel: 'exact',
+      showReduce: false
+    }
+  };
+  const testDoc = {
+    _id: 'foo',
+    key: 'foo',
+    value: {
+      rev: '1-967a00dff5e02add41819138abb3284d'
+    }
+  };
+
+  it('getDocs returns the docs attribute from the state', () => {
+    const action = {
+      type: ActionTypes.INDEX_RESULTS_REDUX_NEW_RESULTS,
+      docs: [testDoc],
+      fetchPArams: {
+        limit: 21,
+        skip: 0
+      },
+      canShowNext: true
+    };
+
+    const newState = Reducers.default(initialState, action);
+    expect(Reducers.getDocs(newState)).toEqual([testDoc]);
+  });
+
+  it('getSelected returns the selectedDocs attribute from the state', () => {
+    const selectedDoc = {
+      _id: 'foo',
+      _rev: '1-967a00dff5e02add41819138abb3284d',
+      _deleted: true
+    };
+    const action = {
+      type: ActionTypes.INDEX_RESULTS_REDUX_NEW_SELECTED_DOCS,
+      selectedDocs: [selectedDoc]
+    };
+
+    const newState = Reducers.default(initialState, action);
+    expect(Reducers.getSelectedDocs(newState)).toEqual([selectedDoc]);
+  });
+
+  it('getIsLoading returns the isLoading attribute from the state', () => {
+    expect(Reducers.getIsLoading(initialState)).toBe(false);
+  });
+
+  it('getIsEditable returns the isEditable attribute from the state', () => {
+    expect(Reducers.getIsEditable(initialState)).toBe(true);
+  });
+
+  it('getSelectedLayout returns the selectedLayout attribute from the state', () => {
+    expect(Reducers.getSelectedLayout(initialState)).toMatch(Constants.LAYOUT_ORIENTATION.METADATA);
+  });
+
+  it('getTextEmptyIndex returns the textEmptyIndex attribute from the state', () => {
+    expect(Reducers.getTextEmptyIndex(initialState)).toMatch('No Documents Found');
+  });
+
+  it('getTypeOfIndex returns the typeOfIndex attribute from the state', () => {
+    expect(Reducers.getTypeOfIndex(initialState)).toMatch('view');
+  });
+
+  it('getFetchParams returns the fetchParams attribute from the state', () => {
+    expect(Reducers.getFetchParams(initialState)).toEqual({
+      limit: FauxtonAPI.constants.MISC.DEFAULT_PAGE_SIZE + 1,
+      skip: 0
+    });
+  });
+
+  it('getPageStart returns the pageStart attribute from the state', () => {
+    expect(Reducers.getPageStart(initialState)).toBe(1);
+  });
+
+  it('getPrioritizedEnabled returns the showAllFieldsTableView attribute from the state', () => {
+    expect(Reducers.getPrioritizedEnabled(initialState)).toBe(false);
+  });
+
+  it('getPerPage returns the perPage attribute from the state', () => {
+    expect(Reducers.getPerPage(initialState)).toBe(FauxtonAPI.constants.MISC.DEFAULT_PAGE_SIZE);
+  });
+
+  it('getCanShowNext returns the canShowNext attribute from the state', () => {
+    expect(Reducers.getCanShowNext(initialState)).toBe(false);
+  });
+
+  it('getQueryOptionsPanel returns the queryOptionsPanel attribute from the state', () => {
+    expect(Reducers.getQueryOptionsPanel(initialState)).toEqual({
+      isVisible: false,
+      showByKeys: false,
+      showBetweenKeys: false,
+      includeDocs: false,
+      betweenKeys: {
+        include: true,
+        startkey: '',
+        endkey: ''
+      },
+      byKeys: '',
+      descending: false,
+      skip: '',
+      limit: 'none',
+      reduce: false,
+      groupLevel: 'exact',
+      showReduce: false
+    });
+  });
+
+  describe('removeGeneratedMangoDocs', () => {
+    it('returns false when language is query', () => {
+      expect(Reducers.removeGeneratedMangoDocs({ language: 'query' })).toBe(false);
+    });
+
+    it('returns true when language is not query', () => {
+      expect(Reducers.removeGeneratedMangoDocs({ language: 'foo' })).toBe(true);
+    });
+  });
+
+  describe('getShowPrioritizedEnabled', () => {
+    it('returns false when not table layout', () => {
+      expect(Reducers.getShowPrioritizedEnabled(initialState)).toBe(false);
+    });
+
+    it('returns true when table layout', () => {
+      const action = {
+        type: ActionTypes.INDEX_RESULTS_REDUX_CHANGE_LAYOUT,
+        layout: Constants.LAYOUT_ORIENTATION.TABLE
+      };
+
+      const newState = Reducers.default(initialState, action);
+      expect(Reducers.getShowPrioritizedEnabled(newState)).toBe(true);
+    });
+  });
+
+  describe('getPageEnd', () => {
+    it('returns false when there are no results', () => {
+      expect(Reducers.getPageEnd(initialState)).toBe(false);
+    });
+
+    it('returns pageStart + results.length - 1 when there are results', () => {
+      const action = {
+        type: ActionTypes.INDEX_RESULTS_REDUX_NEW_RESULTS,
+        docs: [testDoc],
+        fetchPArams: {
+          limit: 21,
+          skip: 0
+        },
+        canShowNext: true
+      };
+
+      const newState = Reducers.default(initialState, action);
+      expect(Reducers.getPageEnd(newState)).toBe(1);
+    });
+  });
+
+  describe('getHasResults', () => {
+    it('returns false when state is loading', () => {
+      const action = {
+        type: ActionTypes.INDEX_RESULTS_REDUX_IS_LOADING
+      };
+
+      const newState = Reducers.default(initialState, action);
+      expect(Reducers.getHasResults(newState)).toBe(false);
+    });
+
+    it('returns false when docs.length is zero', () => {
+      expect(Reducers.getHasResults(initialState)).toBe(false);
+    });
+
+    it('returns true when not loading and there are results', () => {
+      const action = {
+        type: ActionTypes.INDEX_RESULTS_REDUX_NEW_RESULTS,
+        docs: [testDoc],
+        fetchPArams: {
+          limit: 21,
+          skip: 0
+        },
+        canShowNext: true
+      };
+
+      const newState = Reducers.default(initialState, action);
+      expect(Reducers.getHasResults(newState)).toBe(true);
+    });
+  });
+
+  describe('getAllDocsSelected', () => {
+    it('returns false if docs.length is zero', () => {
+      expect(Reducers.getAllDocsSelected(initialState)).toBe(false);
+    });
+
+    it('returns false if docs but selectedDocs.length is zero', () => {
+      const selectedDoc = {
+        _id: 'foo',
+        _rev: '1-967a00dff5e02add41819138abb3284d',
+        _deleted: true
+      };
+      const action = {
+        type: ActionTypes.INDEX_RESULTS_REDUX_NEW_SELECTED_DOCS,
+        selectedDocs: [selectedDoc]
+      };
+
+      const newState = Reducers.default(initialState, action);
+      expect(Reducers.getAllDocsSelected(newState)).toBe(false);
+    });
+
+    it('returns false there is a doc not in the selectedDocs array', () => {
+      const action = {
+        type: ActionTypes.INDEX_RESULTS_REDUX_NEW_RESULTS,
+        docs: [testDoc],
+        fetchPArams: {
+          limit: 21,
+          skip: 0
+        },
+        canShowNext: true
+      };
+
+      const newState = Reducers.default(initialState, action);
+      expect(Reducers.getAllDocsSelected(newState)).toBe(false);
+    });
+
+    it('returns true when all docs in the docs array are in the selectedDocs array', () => {
+      const newDocAction = {
+        type: ActionTypes.INDEX_RESULTS_REDUX_NEW_RESULTS,
+        docs: [testDoc],
+        fetchPArams: {
+          limit: 21,
+          skip: 0
+        },
+        canShowNext: true
+      };
+      const newState1 = Reducers.default(initialState, newDocAction);
+
+      const selectedDoc = {
+        _id: 'foo',
+        _rev: '1-967a00dff5e02add41819138abb3284d',
+        _deleted: true
+      };
+      const newSelectedDocAction = {
+        type: ActionTypes.INDEX_RESULTS_REDUX_NEW_SELECTED_DOCS,
+        selectedDocs: [selectedDoc]
+      };
+      const newState2 = Reducers.default(newState1, newSelectedDocAction);
+
+      expect(Reducers.getAllDocsSelected(newState2)).toBe(true);
+    });
+  });
+
+  describe('getHasDocsSelected', () => {
+    it('returns false when there are no docs in the selectedDocs array', () => {
+      expect(Reducers.getHasDocsSelected(initialState)).toBe(false);
+    });
+
+    it('returns true when there are docs in the selectedDocs array', () => {
+      const selectedDoc = {
+        _id: 'foo',
+        _rev: '1-967a00dff5e02add41819138abb3284d',
+        _deleted: true
+      };
+      const action = {
+        type: ActionTypes.INDEX_RESULTS_REDUX_NEW_SELECTED_DOCS,
+        selectedDocs: [selectedDoc]
+      };
+
+      const newState = Reducers.default(initialState, action);
+      expect(Reducers.getHasDocsSelected(newState)).toBe(true);
+    });
+  });
+
+  it('getNumDocsSelected returns the length of the selectedDocs array', () => {
+    expect(Reducers.getNumDocsSelected(initialState)).toBe(0);
+  });
+
+  describe('canShowPrevious', () => {
+    it('returns false when the current page is 1', () => {
+      expect(Reducers.getCanShowPrevious(initialState)).toBe(false);
+    });
+
+    it('returns true when the current page is greater than 1', () => {
+      const action = {
+        type: ActionTypes.INDEX_RESULTS_REDUX_PAGINATE_NEXT
+      };
+
+      const newState = Reducers.default(initialState, action);
+      expect(Reducers.getCanShowPrevious(newState)).toBe(true);
+    });
+  });
+
+  describe('getQueryOptionsParams', () => {
+    it('returns an empty object by default', () => {
+      expect(Reducers.getQueryOptionsParams(initialState)).toEqual({});
+    });
+
+    it('adds include_docs when set in queryOptionsPanel', () => {
+      const action = {
+        type: ActionTypes.INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS,
+        options: {
+          includeDocs: true
+        }
+      };
+
+      const newState = Reducers.default(initialState, action);
+      expect(Reducers.getQueryOptionsParams(newState)).toEqual({
+        include_docs: true
+      });
+    });
+
+    it('adds start_key, end_key, and inclusive end when set in queryOptionsPanel', () => {
+      const action = {
+        type: ActionTypes.INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS,
+        options: {
+          showBetweenKeys: true,
+          betweenKeys: {
+            include: true,
+            startkey: '\"_design\"',
+            endkey: '\"_design0\"'
+          }
+        }
+      };
+
+      const newState = Reducers.default(initialState, action);
+      expect(Reducers.getQueryOptionsParams(newState)).toEqual({
+        inclusive_end: true,
+        start_key: '\"_design\"',
+        end_key: '\"_design0\"'
+      });
+    });
+
+    it('adds keys if showByKeys is set in queryOptionsPanel', () => {
+      const action = {
+        type: ActionTypes.INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS,
+        options: {
+          showByKeys: true,
+          byKeys: "['_design', 'foo']"
+        }
+      };
+
+      const newState = Reducers.default(initialState, action);
+      expect(Reducers.getQueryOptionsParams(newState)).toEqual({
+        keys: "['_design', 'foo']"
+      });
+    });
+
+    it('adds limit if limit is set in the queryOptionsPanel', () => {
+      const action = {
+        type: ActionTypes.INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS,
+        options: {
+          limit: 50
+        }
+      };
+
+      const newState = Reducers.default(initialState, action);
+      expect(Reducers.getQueryOptionsParams(newState)).toEqual({
+        limit: 50
+      });
+    });
+
+    it('adds skip if skip is set in the queryOptionsPanel', () => {
+      const action = {
+        type: ActionTypes.INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS,
+        options: {
+          skip: 5
+        }
+      };
+
+      const newState = Reducers.default(initialState, action);
+      expect(Reducers.getQueryOptionsParams(newState)).toEqual({
+        skip: 5
+      });
+    });
+
+    it('adds descending if descending is set in the queryOptionsPanel', () => {
+      const action = {
+        type: ActionTypes.INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS,
+        options: {
+          descending: true
+        }
+      };
+
+      const newState = Reducers.default(initialState, action);
+      expect(Reducers.getQueryOptionsParams(newState)).toEqual({
+        descending: true
+      });
+    });
+
+    it('adds reduce if reduce is set in the queryOptionsPanel', () => {
+      const action = {
+        type: ActionTypes.INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS,
+        options: {
+          reduce: true
+        }
+      };
+
+      const newState = Reducers.default(initialState, action);
+      expect(Reducers.getQueryOptionsParams(newState)).toEqual({
+        reduce: true,
+        group: true
+      });
+    });
+
+    it('adds reduce and group_level if both are set in queryOptionsPanel', () => {
+      const action = {
+        type: ActionTypes.INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS,
+        options: {
+          reduce: true,
+          groupLevel: 2
+        }
+      };
+
+      const newState = Reducers.default(initialState, action);
+      expect(Reducers.getQueryOptionsParams(newState)).toEqual({
+        reduce: true,
+        group_level: 2
+      });
+    });
+  });
+});
diff --git a/app/addons/documents/__tests__/results-toolbar.test.js b/app/addons/documents/__tests__/results-toolbar.test.js
new file mode 100644
index 0000000..2fdd46f
--- /dev/null
+++ b/app/addons/documents/__tests__/results-toolbar.test.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 {ResultsToolBar} from "../components/results-toolbar";
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { mount } from 'enzyme';
+
+describe('Results Toolbar', () => {
+  const restProps = {
+    removeItem: () => {},
+    allDocumentsSelected: false,
+    hasSelectedItem: false,
+    toggleSelectAll: () => {},
+    isLoading: false
+  };
+
+  it('renders all content when there are results and they are deletable', () => {
+    const wrapper = mount(<ResultsToolBar hasResults={true} isListDeletable={true} {...restProps}/>);
+    expect(wrapper.find('.bulk-action-component').length).toBe(1);
+    expect(wrapper.find('.two-sides-toggle-button').length).toBe(1);
+    expect(wrapper.find('.document-result-screen__toolbar-create-btn').length).toBe(1);
+  });
+
+  it('does not render bulk action component when list is not deletable', () => {
+    const wrapper = mount(<ResultsToolBar hasResults={true} isListDeletable={false} {...restProps}/>);
+    expect(wrapper.find('.bulk-action-component').length).toBe(0);
+    expect(wrapper.find('.two-sides-toggle-button').length).toBe(1);
+    expect(wrapper.find('.document-result-screen__toolbar-create-btn').length).toBe(1);
+  });
+
+  it('only renders create button when there are no results', () => {
+    const wrapper = mount(<ResultsToolBar hasResults={false} {...restProps}/>);
+    expect(wrapper.find('.bulk-action-component').length).toBe(0);
+    expect(wrapper.find('.two-sides-toggle-button').length).toBe(0);
+    expect(wrapper.find('.document-result-screen__toolbar-create-btn').length).toBe(1);
+  });
+});
diff --git a/app/addons/documents/__tests__/shared-helpers.test.js b/app/addons/documents/__tests__/shared-helpers.test.js
new file mode 100644
index 0000000..a5e479b
--- /dev/null
+++ b/app/addons/documents/__tests__/shared-helpers.test.js
@@ -0,0 +1,170 @@
+// 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 {
+  getDocUrl,
+  isJSONDocEditable,
+  isJSONDocBulkDeletable,
+  hasBulkDeletableDoc
+} from '../index-results/helpers/shared-helpers';
+import FauxtonAPI from '../../../core/api';
+import '../base';
+import sinon from 'sinon';
+
+describe('Docs Shared Helpers', () => {
+  describe('getDocUrl', () => {
+    const context = 'server';
+    const id = 'foo';
+    const databaseName = 'testdb';
+    let spy;
+
+    beforeEach(() => {
+      spy = sinon.spy(FauxtonAPI, 'urls');
+    });
+
+    afterEach(() => {
+      FauxtonAPI.urls.restore();
+    });
+
+    it('requests the proper url with standard inputs', () => {
+      getDocUrl(context, id, databaseName);
+      expect(spy.calledWith('document', 'server', 'testdb', 'foo', '?conflicts=true'));
+    });
+
+    it('requests the proper url when context is undefined', () => {
+      let undefinedContext;
+      getDocUrl(undefinedContext, id, databaseName);
+      expect(spy.calledWith('document', 'server', 'testdb', 'foo', '?conflicts=true'));
+    });
+
+    it('requests the proper url when id is undefined', () => {
+      let undefinedId;
+      getDocUrl(context, undefinedId, databaseName);
+      expect(spy.calledWith('document', 'server', 'testdb', '', '?conflicts=true'));
+    });
+  });
+
+  describe('isJSONDocEditable', () => {
+    const doc = {
+      _id: "aardvark",
+      _rev: "5-717f5e88689af3ad191b47321de10c95",
+      min_weight: 40,
+      max_weight: 65,
+      min_length: 1,
+      max_length: 2.2,
+      latin_name: "Orycteropus afer",
+      wiki_page: "http://en.wikipedia.org/wiki/Aardvark",
+      class: "mammal",
+      diet: "omnivore"
+    };
+    let docType = 'view';
+    let testDoc = Object.assign({}, doc);
+
+    afterEach(() => {
+      docType = 'view';
+      testDoc = Object.assign({}, doc);
+    });
+
+    it('returns undefined when the doc is undefined', () => {
+      let undefinedDoc;
+      expect(isJSONDocEditable(undefinedDoc, docType)).toBe(undefined);
+    });
+
+    it('returns false when type is MangoIndex', () => {
+      docType = 'MangoIndex';
+      expect(isJSONDocEditable(testDoc, docType)).toBe(false);
+    });
+
+    it('returns false when the doc is empty', () => {
+      let emptyDoc = {};
+      expect(isJSONDocEditable(emptyDoc, docType)).toBe(false);
+    });
+
+    it('returns false if the doc does not have an _id', () => {
+      delete(testDoc._id);
+      expect(isJSONDocEditable(testDoc, docType)).toBe(false);
+    });
+
+    it('returns true otherwise', () => {
+      expect(isJSONDocEditable(testDoc, docType)).toBe(true);
+    });
+  });
+
+  describe('isJSONDocBulkDeletable', () => {
+    const doc = {
+      _id: "aardvark",
+      _rev: "5-717f5e88689af3ad191b47321de10c95",
+      min_weight: 40,
+      max_weight: 65,
+      min_length: 1,
+      max_length: 2.2,
+      latin_name: "Orycteropus afer",
+      wiki_page: "http://en.wikipedia.org/wiki/Aardvark",
+      class: "mammal",
+      diet: "omnivore"
+    };
+    let docType = 'view';
+    let testDoc = Object.assign({}, doc);
+
+    afterEach(() => {
+      testDoc = Object.assign({}, doc);
+      docType = 'view';
+    });
+
+    it('returns true for normal doc and views', () => {
+      expect(isJSONDocBulkDeletable(testDoc, docType)).toBe(true);
+    });
+
+    it('returns false if mango index and doc has type of special', () => {
+      docType = 'MangoIndex';
+      testDoc.type = 'special';
+      expect(isJSONDocBulkDeletable(testDoc, docType)).toBe(false);
+    });
+
+    it('returns false if doc does not have _id or id', () => {
+      delete(testDoc._id);
+      expect(isJSONDocBulkDeletable(testDoc, docType)).toBe(false);
+    });
+
+    it('returns false if doc does not have _rev or doc.value.rev', () => {
+      delete(testDoc._rev);
+      expect(isJSONDocBulkDeletable(testDoc, docType)).toBe(false);
+    });
+  });
+
+  describe('hasBulkDeletableDoc', () => {
+    const docs = [
+      {
+        _id: "aardvark",
+        _rev: "5-717f5e88689af3ad191b47321de10c95",
+        min_weight: 40,
+        max_weight: 65,
+        min_length: 1,
+        max_length: 2.2,
+        latin_name: "Orycteropus afer",
+        wiki_page: "http://en.wikipedia.org/wiki/Aardvark",
+        class: "mammal",
+        diet: "omnivore"
+      }
+    ];
+    let docType = 'MangoIndex';
+
+    it('returns true if any docs are bulk deletable', () => {
+      expect(hasBulkDeletableDoc(docs, docType)).toBe(true);
+    });
+
+    it('returns true when no docs are bulk deletable', () => {
+      docs[0].type = 'special';
+      expect(hasBulkDeletableDoc(docs, docType)).toBe(false);
+    });
+  });
+});
diff --git a/app/addons/documents/__tests__/table-controls.test.js b/app/addons/documents/__tests__/table-controls.test.js
new file mode 100644
index 0000000..e0249ba
--- /dev/null
+++ b/app/addons/documents/__tests__/table-controls.test.js
@@ -0,0 +1,51 @@
+// 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 { mount } from 'enzyme';
+import TableControls from '../index-results/components/pagination/TableControls';
+import sinon from 'sinon';
+
+describe('TableControls', () => {
+  it('shows range of columns when not all are shown', () => {
+    const wrapper = mount(<TableControls
+      prioritizedEnabled={false}
+      displayedFields={{shown: 5, allFieldCount: 10}}
+      toggleShowAllColumns={() => {}}
+    />);
+
+    expect(wrapper.find('.shown-fields').text()).toMatch('Showing 5 of 10 columns.');
+  });
+
+  it('shows custom text when all columns are shown', () => {
+    const wrapper = mount(<TableControls
+      prioritizedEnabled={false}
+      displayedFields={{shown: 5, allFieldCount: 5}}
+      toggleShowAllColumns={() => {}}
+    />);
+
+    expect(wrapper.find('.shown-fields').text()).toMatch('Showing 5 columns.');
+  });
+
+  it('shows custom text when all columns are shown', () => {
+    const spy = sinon.spy();
+    const wrapper = mount(<TableControls
+      prioritizedEnabled={false}
+      displayedFields={{shown: 5, allFieldCount: 5}}
+      toggleShowAllColumns={spy}
+    />);
+
+    wrapper.find('#footer-doc-control-prioritized').simulate('change');
+    expect(spy.calledOnce).toBe(true);
+  });
+});
diff --git a/app/addons/documents/__tests__/table-view.test.js b/app/addons/documents/__tests__/table-view.test.js
new file mode 100644
index 0000000..b15e345
--- /dev/null
+++ b/app/addons/documents/__tests__/table-view.test.js
@@ -0,0 +1,179 @@
+// 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 {
+  getPseudoSchema,
+  getPrioritizedFields,
+  sortByTwoFields,
+  getNotSelectedFields,
+  getMetaDataTableView,
+  getFullTableViewData
+} from '../index-results/helpers/table-view';
+
+describe('Docs Table View', () => {
+  const docs = [
+    {
+      _id: "badger",
+      _rev: "8-db03387de9cbd5c2814523b043566dfe",
+      wiki_page: "http://en.wikipedia.org/wiki/Badger",
+      min_weight: 7,
+      max_weight: 30,
+      min_length: 0.6,
+      max_length: 0.9,
+      latin_name: "Meles meles",
+      class: "mammal",
+      diet: "omnivore",
+      test: "xyz"
+    },
+    {
+      _id: "aardvark",
+      _rev: "5-717f5e88689af3ad191b47321de10c95",
+      min_weight: 40,
+      max_weight: 65,
+      min_length: 1,
+      max_length: 2.2,
+      latin_name: "Orycteropus afer",
+      wiki_page: "http://en.wikipedia.org/wiki/Aardvark",
+      class: "mammal",
+      diet: "omnivore",
+      foo: "bar"
+    }
+  ];
+
+  const schema = [
+    '_id',
+    '_rev',
+    'wiki_page',
+    'min_weight',
+    'max_weight',
+    'min_length',
+    'max_length',
+    'latin_name',
+    'class',
+    'diet',
+    'test',
+    'foo'
+  ];
+
+  describe('getPseudoSchema', () => {
+    it('returns array of unique keys with _id as the first element', () => {
+      expect(getPseudoSchema(docs)).toEqual(schema);
+    });
+  });
+
+  describe('getPrioritizedFields', () => {
+    it('returns the list of most popular attributes', () => {
+      const max = 5;
+      expect(getPrioritizedFields(docs, max)).toEqual([
+        '_id',
+        'class',
+        'diet',
+        'latin_name',
+        'max_length'
+      ]);
+    });
+  });
+
+  describe('sortByTowFields', () => {
+    it('returns proper sorted array for the input', () => {
+      const input = [[2, 'b'], [3, 'z'], [1, 'a'], [3, 'a']];
+      expect(sortByTwoFields(input)).toEqual([
+        [3, 'a'],
+        [3, 'z'],
+        [2, 'b'],
+        [1, 'a']
+      ]);
+    });
+  });
+
+  describe('getNotSelectedFields', () => {
+    it('returns a list of the remaining fields not currently selected', () => {
+      const selectedFields = ['_id', 'class', 'diet', 'latin_name', 'max_length'];
+      const allFields = ['_id', '_rev', 'wiki_page', 'min_weight', 'max_weight', 'min_length', 'max_length', 'latin_name', 'class', 'diet', 'test', 'foo'];
+      expect(getNotSelectedFields(selectedFields, allFields)).toEqual([
+        '_rev',
+        'wiki_page',
+        'min_weight',
+        'max_weight',
+        'min_length',
+        'test',
+        'foo'
+      ]);
+    });
+  });
+
+  describe('getFullTableViewData', () => {
+    let schemaWithoutMetaDataFields;
+    beforeEach(() => {
+      schemaWithoutMetaDataFields = _.without(schema, '_attachments');
+    });
+
+    it('returns json object with attributes necessary when selectedFieldsTableView is not set', () => {
+      const max = 5;
+      const selectedFieldsTableView = getPrioritizedFields(docs, max);
+      const notSelectedFieldsTableView = getNotSelectedFields(selectedFieldsTableView, schemaWithoutMetaDataFields);
+      const options = {
+        selectedFieldsTableView: [],
+        showAllFieldsTableView: false
+      };
+
+      expect(getFullTableViewData(docs, options)).toEqual({
+        schema,
+        normalizedDocs: docs,
+        selectedFieldsTableView,
+        notSelectedFieldsTableView
+      });
+    });
+
+    it('returns json object with attributes necessary when selectedFieldsTableView is set', () => {
+      const selectedFieldsTableView = ['_id', 'class', 'diet', 'latin_name', 'max_length'];
+      const notSelectedFieldsTableView = getNotSelectedFields(selectedFieldsTableView, schemaWithoutMetaDataFields);
+      const options = {
+        selectedFieldsTableView,
+        showAllFieldsTableView: false
+      };
+
+      expect(getFullTableViewData(docs, options)).toEqual({
+        schema,
+        normalizedDocs: docs,
+        selectedFieldsTableView,
+        notSelectedFieldsTableView
+      });
+    });
+
+    it('returns json object with attributes necessary when showAllFieldsTableView is set', () => {
+      const selectedFieldsTableView = ['_id', 'class', 'diet', 'latin_name', 'max_length'];
+      const options = {
+        selectedFieldsTableView,
+        showAllFieldsTableView: true
+      };
+
+      expect(getFullTableViewData(docs, options)).toEqual({
+        schema,
+        normalizedDocs: docs,
+        selectedFieldsTableView: schemaWithoutMetaDataFields,
+        notSelectedFieldsTableView: null
+      });
+    });
+  });
+
+  describe('getMetaDataTableView', () => {
+    it('returns json object with attributes necessary to build metadata table', () => {
+      expect(getMetaDataTableView(docs)).toEqual({
+        schema: schema,
+        normalizedDocs: docs,
+        selectedFieldsTableView: schema,
+        notSelectedFieldsTableView: null
+      });
+    });
+  });
+});
diff --git a/app/addons/documents/assets/less/header.less b/app/addons/documents/assets/less/header.less
index 0670cbb..1600b5c 100644
--- a/app/addons/documents/assets/less/header.less
+++ b/app/addons/documents/assets/less/header.less
@@ -26,3 +26,7 @@
     }
   }
 }
+
+.right-header-wrapper {
+  justify-content: flex-end;
+}
diff --git a/app/addons/documents/assets/less/index-results.less b/app/addons/documents/assets/less/index-results.less
index f53f9b9..51f296f 100644
--- a/app/addons/documents/assets/less/index-results.less
+++ b/app/addons/documents/assets/less/index-results.less
@@ -13,9 +13,6 @@
 @import "../../../../../assets/less/variables.less";
 
 .document-result-screen {
-  .bulk-action-component {
-    padding-bottom: 15px;
-  }
 
   .loading-lines-wrapper {
     margin-left: auto;
@@ -28,6 +25,32 @@
   }
 }
 
+.document-result-screen__toolbar {
+  display: flex;
+  padding-bottom: 20px;
+
+  .bulk-action-component {
+    min-width: 90px;
+    min-height: 26px;
+    padding: 8px 0;
+  }
+}
+
+.document-result-screen__toolbar-flex-container {
+  width: 100%;
+  display: flex;
+  justify-content: flex-end;
+}
+
+.document-result-screen__toolbar-create-btn {
+  height: 42px;
+}
+
+a.document-result-screen__toolbar-create-btn:active,
+a.document-result-screen__toolbar-create-btn:visited {
+  color: #fff;
+}
+
 .no-results-screen {
   position: absolute;
   margin: -15px;
@@ -57,12 +80,11 @@
 
 .table-view-docs {
   position: absolute;
-  margin-top: 30px;
 
 
   .bulk-action-component {
     padding-bottom: 0;
-    min-height: 0px;
+    min-height: 0;
   }
   .bulk-action-component-panel input {
     width: auto;
@@ -80,15 +102,23 @@
       overflow: hidden;
     }
   }
-
+  tbody tr {
+    cursor: pointer;
+    &:hover {
+      border-left: 2px solid @hoverHighlight;
+      td {
+        color: @hoverHighlight;
+        input[type="checkbox"] {
+          margin-left: 7px;
+        }
+      }
+    }
+  }
   td, th, td a {
     vertical-align: middle;
     line-height: 20px;
     font-size: 14px;
   }
-  td {
-    height: 49px;
-  }
   td, th {
     color: @defaultHTag;
     max-width: 160px;
@@ -97,7 +127,8 @@
     white-space: nowrap;
   }
   td.tableview-checkbox-cell, th.tableview-header-el-checkbox {
-    width: 68px;
+    width: 35px;
+    padding-left: 0px;
   }
   .tableview-conflict {
     color: #F00;
@@ -151,7 +182,6 @@
   .table-container-autocomplete .table-select-wrapper {
     width: inherit;
     overflow: visible;
-    min-height: 300px;
   }
 }
 
diff --git a/app/addons/documents/base.js b/app/addons/documents/base.js
index 10cf4bd..c1348ee 100644
--- a/app/addons/documents/base.js
+++ b/app/addons/documents/base.js
@@ -13,8 +13,13 @@
 import app from "../../app";
 import FauxtonAPI from "../../core/api";
 import Documents from "./routes";
+import reducers from "./index-results/reducers";
 import "./assets/less/documents.less";
 
+FauxtonAPI.addReducers({
+  indexResults: reducers
+});
+
 function getQueryParam (query) {
   if (!query) {
     query = '';
diff --git a/app/addons/documents/components/header-docs-right.js b/app/addons/documents/components/header-docs-right.js
index 2497823..4f03e50 100644
--- a/app/addons/documents/components/header-docs-right.js
+++ b/app/addons/documents/components/header-docs-right.js
@@ -12,12 +12,26 @@
 
 import React from 'react';
 import QueryOptions from '../queryoptions/queryoptions';
+import QueryOptionsContainer from '../index-results/containers/QueryOptionsContainer';
 import JumpToDoc from './jumptodoc';
 import Actions from './actions';
 
 const { QueryOptionsController } = QueryOptions;
 
-const RightAllDocsHeader = ({database, hideQueryOptions}) =>
+const getQueryOptionsComponent = (hideQueryOptions, isRedux, fetchUrl, ddocsOnly) => {
+  if (hideQueryOptions) {
+    return null;
+  }
+
+  let queryOptionsComponent = <QueryOptionsController />;
+  if (isRedux) {
+    queryOptionsComponent = <QueryOptionsContainer fetchUrl={fetchUrl} ddocsOnly={ddocsOnly} />;
+  }
+
+  return queryOptionsComponent;
+};
+
+const RightAllDocsHeader = ({database, hideQueryOptions, isRedux, fetchUrl, ddocsOnly}) =>
   <div className="header-right right-db-header flex-layout flex-row">
 
     <div className="faux-header__searchboxwrapper">
@@ -25,8 +39,7 @@ const RightAllDocsHeader = ({database, hideQueryOptions}) =>
         <JumpToDoc cache={false} loadOptions={Actions.fetchAllDocsWithKey(database)} database={database} />
       </div>
     </div>
-
-    {hideQueryOptions ? '' : <QueryOptionsController />}
+    {getQueryOptionsComponent(hideQueryOptions, isRedux, fetchUrl, ddocsOnly)}
   </div>;
 
 RightAllDocsHeader.propTypes = {
diff --git a/app/addons/documents/components/results-toolbar.js b/app/addons/documents/components/results-toolbar.js
new file mode 100644
index 0000000..e5c5a2a
--- /dev/null
+++ b/app/addons/documents/components/results-toolbar.js
@@ -0,0 +1,77 @@
+// 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 BulkDocumentHeaderController from "../header/header";
+import Stores from "../sidebar/stores";
+import Components from "../../components/react-components";
+
+const {BulkActionComponent} = Components;
+const store = Stores.sidebarStore;
+
+export class ResultsToolBar extends React.Component {
+  shouldComponentUpdate (nextProps) {
+    return nextProps.isListDeletable != undefined;
+  }
+
+  render () {
+    const dbName = store.getDatabase().id;
+    const {
+      hasResults,
+      isListDeletable,
+      removeItem,
+      allDocumentsSelected,
+      hasSelectedItem,
+      toggleSelectAll,
+      isLoading
+    } = this.props;
+
+    // Determine if we need to display the bulk action selector
+    let bulkAction = null;
+    if ((isListDeletable && hasResults) || isLoading) {
+      bulkAction = <BulkActionComponent
+        removeItem={removeItem}
+        isChecked={allDocumentsSelected}
+        hasSelectedItem={hasSelectedItem}
+        toggleSelect={toggleSelectAll}
+        disabled={isLoading}
+        title="Select all docs that can be..." />;
+    }
+
+    // Determine if we need to display the bulk doc header
+    let bulkHeader = null;
+    if (hasResults || isLoading) {
+      bulkHeader = <BulkDocumentHeaderController {...this.props} />;
+    }
+
+    return (
+      <div className="document-result-screen__toolbar">
+        {bulkAction}
+        {bulkHeader}
+        <div className="document-result-screen__toolbar-flex-container">
+          <a href={`#/database/${dbName}/new`} className="btn save document-result-screen__toolbar-create-btn btn-primary">
+            Create Document
+          </a>
+        </div>
+      </div>
+    );
+  }
+};
+
+ResultsToolBar.propTypes = {
+  removeItem: React.PropTypes.func.isRequired,
+  allDocumentsSelected: React.PropTypes.bool.isRequired,
+  hasSelectedItem: React.PropTypes.bool.isRequired,
+  toggleSelectAll: React.PropTypes.func.isRequired,
+  isLoading: React.PropTypes.bool.isRequired,
+  hasResults: React.PropTypes.bool.isRequired,
+  isListDeletable: React.PropTypes.bool
+};
diff --git a/app/addons/documents/header/header.actiontypes.js b/app/addons/documents/constants.js
similarity index 83%
copy from app/addons/documents/header/header.actiontypes.js
copy to app/addons/documents/constants.js
index b7fc6a6..a7063d3 100644
--- a/app/addons/documents/header/header.actiontypes.js
+++ b/app/addons/documents/constants.js
@@ -11,5 +11,9 @@
 // the License.
 
 export default {
-  TOGGLE_TABLEVIEW: 'TOGGLE_TABLEVIEW',
+  LAYOUT_ORIENTATION: {
+    TABLE: 'LAYOUT_TABLE',
+    METADATA: 'LAYOUT_METADATA',
+    JSON: 'LAYOUT_JSON'
+  }
 };
diff --git a/app/addons/documents/header/header.actions.js b/app/addons/documents/header/header.actions.js
index 3dc26b3..3baf943 100644
--- a/app/addons/documents/header/header.actions.js
+++ b/app/addons/documents/header/header.actions.js
@@ -33,11 +33,11 @@ export default {
     ActionsQueryOptions.runQuery(params);
   },
 
-  toggleTableView: function (state) {
+  toggleLayout: function (layout) {
     FauxtonAPI.dispatch({
-      type: ActionTypes.TOGGLE_TABLEVIEW,
+      type: ActionTypes.TOGGLE_LAYOUT,
       options: {
-        enable: state
+        layout: layout
       }
     });
   }
diff --git a/app/addons/documents/header/header.actiontypes.js b/app/addons/documents/header/header.actiontypes.js
index b7fc6a6..7d5d9b7 100644
--- a/app/addons/documents/header/header.actiontypes.js
+++ b/app/addons/documents/header/header.actiontypes.js
@@ -11,5 +11,5 @@
 // the License.
 
 export default {
-  TOGGLE_TABLEVIEW: 'TOGGLE_TABLEVIEW',
+  TOGGLE_LAYOUT: 'TOGGLE_LAYOUT',
 };
diff --git a/app/addons/documents/header/header.js b/app/addons/documents/header/header.js
index 0e76d2e..0464709 100644
--- a/app/addons/documents/header/header.js
+++ b/app/addons/documents/header/header.js
@@ -12,83 +12,126 @@
 
 import React from 'react';
 import Actions from './header.actions';
-import Components from '../../components/react-components';
+import Constants from '../constants';
 import IndexResultStores from '../index-results/stores';
 import QueryOptionsStore from '../queryoptions/stores';
 import { Button, ButtonGroup } from 'react-bootstrap';
 
 const { indexResultsStore } = IndexResultStores;
 const { queryOptionsStore } = QueryOptionsStore;
-const { ToggleHeaderButton } = Components;
 
-var BulkDocumentHeaderController = React.createClass({
+export default class BulkDocumentHeaderController extends React.Component {
+  constructor (props) {
+    super(props);
+    this.state = this.getStoreState();
+  }
+
   getStoreState () {
     return {
-      selectedView: indexResultsStore.getCurrentViewType(),
-      isTableView: indexResultsStore.getIsTableView(),
-      includeDocs: queryOptionsStore.getIncludeDocsEnabled(),
-      bulkDocCollection: indexResultsStore.getBulkDocCollection()
+      selectedLayout: indexResultsStore.getSelectedLayout(),
+      bulkDocCollection: indexResultsStore.getBulkDocCollection(),
+      isMango: indexResultsStore.getIsMangoResults()
     };
-  },
-
-  getInitialState () {
-    return this.getStoreState();
-  },
+  }
 
   componentDidMount () {
     indexResultsStore.on('change', this.onChange, this);
     queryOptionsStore.on('change', this.onChange, this);
 
-  },
+  }
 
   componentWillUnmount () {
     indexResultsStore.off('change', this.onChange);
     queryOptionsStore.off('change', this.onChange);
-  },
+  }
 
   onChange () {
     this.setState(this.getStoreState());
-  },
+  }
 
   render () {
-    var isTableViewSelected = this.state.isTableView;
+    const {
+      changeLayout,
+      selectedLayout,
+      typeOfIndex
+    } = this.props;
+
+    // If the changeLayout function is not undefined, default to using prop values
+    // because we're using our new redux store.
+    // TODO: migrate completely to redux and eliminate this check.
+    const layout = changeLayout ? selectedLayout : this.state.selectedLayout;
+    let metadata, json, table;
+    if ((typeOfIndex && typeOfIndex === 'view') || !this.state.isMango) {
+      metadata = <Button
+          className={layout === Constants.LAYOUT_ORIENTATION.METADATA ? 'active' : ''}
+          onClick={this.toggleLayout.bind(this, Constants.LAYOUT_ORIENTATION.METADATA)}
+        >
+          Metadata
+        </Button>;
+    }
+
+    // reduce doesn't allow for include_docs=true, so we'll prevent JSON and table
+    // views since they force include_docs=true when reduce is checked in the
+    // query options panel.
+    if (!queryOptionsStore.reduce()) {
+      table = <Button
+          className={layout === Constants.LAYOUT_ORIENTATION.TABLE ? 'active' : ''}
+          onClick={this.toggleLayout.bind(this, Constants.LAYOUT_ORIENTATION.TABLE)}
+        >
+          <i className="fonticon-table" /> Table
+        </Button>;
+
+      json = <Button
+          className={layout === Constants.LAYOUT_ORIENTATION.JSON ? 'active' : ''}
+          onClick={this.toggleLayout.bind(this, Constants.LAYOUT_ORIENTATION.JSON)}
+        >
+          <i className="fonticon-json" /> JSON
+        </Button>;
+    }
 
     return (
       <div className="alternative-header">
         <ButtonGroup className="two-sides-toggle-button">
-          <Button
-            className={isTableViewSelected ? '' : 'active'}
-            onClick={this.toggleTableView.bind(this, false)}
-          >
-            <i className="fonticon-json" /> JSON
-          </Button>
-          <Button
-            className={isTableViewSelected ? 'active' : ''}
-            onClick={this.toggleTableView.bind(this, true)}
-          >
-            <i className="fonticon-table" /> Table
-          </Button>
+          {table}
+          {metadata}
+          {json}
         </ButtonGroup>
-        {this.props.showIncludeAllDocs ? <ToggleHeaderButton
-          toggleCallback={this.toggleIncludeDocs}
-          containerClasses="header-control-box control-toggle-include-docs"
-          title="Enable/Disable include_docs"
-          fonticon={this.state.includeDocs ? 'icon-check' : 'icon-check-empty'}
-          iconDefaultClass="icon fontawesome"
-          text="" /> : null}  { /* text is set via responsive css */}
       </div>
     );
-  },
+  }
 
-  toggleIncludeDocs () {
-    Actions.toggleIncludeDocs(this.state.includeDocs, this.state.bulkDocCollection);
-  },
+  toggleLayout (newLayout) {
+    // this will be present when using redux stores
+    const {
+      changeLayout,
+      selectedLayout,
+      fetchAllDocs,
+      fetchParams,
+      queryOptionsParams,
+      queryOptionsToggleIncludeDocs
+    } = this.props;
 
-  toggleTableView: function (enable) {
-    Actions.toggleTableView(enable);
-  }
-});
+    if (changeLayout && newLayout !== selectedLayout) {
+      // change our layout to JSON, Table, or Metadata
+      changeLayout(newLayout);
 
-export default {
-  BulkDocumentHeaderController: BulkDocumentHeaderController
+      queryOptionsParams.include_docs = newLayout !== Constants.LAYOUT_ORIENTATION.METADATA;
+      if (newLayout === Constants.LAYOUT_ORIENTATION.TABLE) {
+        fetchParams.conflicts = true;
+      } else {
+        delete fetchParams.conflicts;
+      }
+
+      // tell the query options panel we're updating include_docs
+      queryOptionsToggleIncludeDocs(!queryOptionsParams.include_docs);
+      fetchAllDocs(fetchParams, queryOptionsParams);
+      return;
+    }
+
+    // fall back to old backbone style logic
+    Actions.toggleLayout(newLayout);
+    if (!this.state.isMango) {
+      Actions.toggleIncludeDocs(newLayout === Constants.LAYOUT_ORIENTATION.METADATA, this.state.bulkDocCollection);
+    }
+  }
 };
diff --git a/app/addons/documents/helpers.js b/app/addons/documents/helpers.js
index 1293843..1134e07 100644
--- a/app/addons/documents/helpers.js
+++ b/app/addons/documents/helpers.js
@@ -17,15 +17,15 @@ import ReactComponentsActions from "../components/actions";
 
 
 // sequence info is an array in couchdb2 with two indexes. On couch 1.x, it's just a string / number
-function getSeqNum (val) {
+const getSeqNum = (val) => {
   return _.isArray(val) ? val[1] : val;
-}
+};
 
-function getNewButtonLinks (databaseName) {
-  var addLinks = FauxtonAPI.getExtensions('sidebar:links');
-  var newUrlPrefix = '#' + FauxtonAPI.urls('databaseBaseURL', 'app', FauxtonAPI.url.encode(databaseName));
+const getNewButtonLinks = (databaseName) => {
+  const addLinks = FauxtonAPI.getExtensions('sidebar:links');
+  const newUrlPrefix = '#' + FauxtonAPI.urls('databaseBaseURL', 'app', FauxtonAPI.url.encode(databaseName));
 
-  var addNewLinks = _.reduce(addLinks, function (menuLinks, link) {
+  const addNewLinks = _.reduce(addLinks, function (menuLinks, link) {
     menuLinks.push({
       title: link.title,
       url: newUrlPrefix + '/' + link.url,
@@ -47,23 +47,23 @@ function getNewButtonLinks (databaseName) {
     title: 'Add New',
     links: addNewLinks
   }];
-}
+};
 
-function getMangoLink (databaseName) {
-  var newUrlPrefix = '#' + FauxtonAPI.urls('databaseBaseURL', 'app', FauxtonAPI.url.encode(databaseName));
+const getMangoLink = (databaseName) => {
+  const newUrlPrefix = '#' + FauxtonAPI.urls('databaseBaseURL', 'app', FauxtonAPI.url.encode(databaseName));
 
   return {
     title: app.i18n.en_US['new-mango-index'],
     url: newUrlPrefix + '/_index',
     icon: 'fonticon-plus-circled'
   };
-}
+};
 
-function parseJSON (str) {
+const parseJSON = (str) => {
   return JSON.parse('"' + str + '"');   // this ensures newlines are converted
-}
+};
 
-function getModifyDatabaseLinks (databaseName) {
+const getModifyDatabaseLinks = (databaseName) => {
   return [{
     title: 'Replicate Database',
     icon: 'fonticon-replicate',
@@ -73,11 +73,11 @@ function getModifyDatabaseLinks (databaseName) {
     icon: 'fonticon-trash',
     onClick: ReactComponentsActions.showDeleteDatabaseModal.bind(this, {showDeleteModal: true, dbId: databaseName})
   }];
-}
+};
 
-function truncateDoc (docString, maxRows) {
-  var lines = docString.split('\n');
-  var isTruncated = false;
+const truncateDoc = (docString, maxRows) => {
+  let lines = docString.split('\n');
+  let isTruncated = false;
   if (lines.length > maxRows) {
     isTruncated = true;
     lines = lines.slice(0, maxRows);
@@ -87,13 +87,12 @@ function truncateDoc (docString, maxRows) {
     isTruncated: isTruncated,
     content: docString
   };
-}
-
+};
 
 export default {
-  getSeqNum: getSeqNum,
-  getNewButtonLinks: getNewButtonLinks,
-  getModifyDatabaseLinks: getModifyDatabaseLinks,
-  parseJSON: parseJSON,
-  truncateDoc: truncateDoc
+  getSeqNum,
+  getNewButtonLinks,
+  getModifyDatabaseLinks,
+  parseJSON,
+  truncateDoc
 };
diff --git a/app/addons/documents/index-results/actions.js b/app/addons/documents/index-results/actions.js
index b484fad..619cfe3 100644
--- a/app/addons/documents/index-results/actions.js
+++ b/app/addons/documents/index-results/actions.js
@@ -51,8 +51,8 @@ export default {
     if (!options.collection.fetch) { return; }
 
     return options.collection.fetch({reset: true}).then(() => {
-      this.resultsListReset();
       this.sendMessageNewResultList(options);
+      this.resultsListReset();
     }, (collection, _xhr) => {
       //Make this more robust as sometimes the colection is passed through here.
       var xhr = collection.responseText ? collection : _xhr;
diff --git a/app/addons/documents/index-results/actiontypes.js b/app/addons/documents/index-results/actiontypes.js
index e39c17b..ddd17e6 100644
--- a/app/addons/documents/index-results/actiontypes.js
+++ b/app/addons/documents/index-results/actiontypes.js
@@ -19,5 +19,15 @@ export default {
   INDEX_RESULTS_SELECT_NEW_FIELD_IN_TABLE: 'INDEX_RESULTS_SELECT_NEW_FIELD_IN_TABLE',
   INDEX_RESULTS_CLEAR_SELECTED_ITEMS: 'INDEX_RESULTS_CLEAR_SELECTED_ITEMS',
   INDEX_RESULTS_TOGGLE_PRIORITIZED_TABLE_VIEW: 'INDEX_RESULTS_TOGGLE_PRIORITIZED_TABLE_VIEW',
-
+  INDEX_RESULTS_REDUX_NEW_RESULTS: 'INDEX_RESULTS_REDUX_NEW_RESULTS',
+  INDEX_RESULTS_REDUX_IS_LOADING: 'INDEX_RESULTS_REDUX_IS_LOADING',
+  INDEX_RESULTS_REDUX_CHANGE_LAYOUT: 'INDEX_RESULTS_REDUX_CHANGE_LAYOUT',
+  INDEX_RESULTS_REDUX_TOGGLE_SHOW_ALL_COLUMNS: 'INDEX_RESULTS_REDUX_TOGGLE_SHOW_ALL_COLUMNS',
+  INDEX_RESULTS_REDUX_SET_PER_PAGE: 'INDEX_RESULTS_REDUX_SET_PER_PAGE',
+  INDEX_RESULTS_REDUX_PAGINATE_NEXT: 'INDEX_RESULTS_REDUX_PAGINATE_NEXT',
+  INDEX_RESULTS_REDUX_PAGINATE_PREVIOUS: 'INDEX_RESULTS_REDUX_PAGINATE_PREVIOUS',
+  INDEX_RESULTS_REDUX_NEW_SELECTED_DOCS: 'INDEX_RESULTS_REDUX_NEW_SELECTED_DOCS',
+  INDEX_RESULTS_REDUX_CHANGE_TABLE_HEADER_ATTRIBUTE: 'INDEX_RESULTS_REDUX_CHANGE_TABLE_HEADER_ATTRIBUTE',
+  INDEX_RESULTS_REDUX_RESET_STATE: 'INDEX_RESULTS_REDUX_RESET_STATE',
+  INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS: 'INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS'
 };
diff --git a/app/addons/documents/index-results/apis/base.js b/app/addons/documents/index-results/apis/base.js
new file mode 100644
index 0000000..c0ce4c5
--- /dev/null
+++ b/app/addons/documents/index-results/apis/base.js
@@ -0,0 +1,100 @@
+// 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 ActionTypes from '../actiontypes';
+
+export const nowLoading = () => {
+  return {
+    type: ActionTypes.INDEX_RESULTS_REDUX_IS_LOADING
+  };
+};
+
+export const resetState = () => {
+  return {
+    type: ActionTypes.INDEX_RESULTS_REDUX_RESET_STATE
+  };
+};
+
+export const newResultsAvailable = (docs, params, canShowNext) => {
+  return {
+    type: ActionTypes.INDEX_RESULTS_REDUX_NEW_RESULTS,
+    docs: docs,
+    params: params,
+    canShowNext: canShowNext
+  };
+};
+
+export const newSelectedDocs = (selectedDocs = []) => {
+  return {
+    type: ActionTypes.INDEX_RESULTS_REDUX_NEW_SELECTED_DOCS,
+    selectedDocs: selectedDocs
+  };
+};
+
+export const selectDoc = (doc, selectedDocs) => {
+  // locate the doc in the selected docs array if it exists
+  const indexInSelectedDocs = selectedDocs.findIndex((selectedDoc) => {
+    return selectedDoc._id === doc._id;
+  });
+
+  // if the doc exists in the selectedDocs array, remove it. This occurs
+  // when a user has deselected or unchecked a doc from the list of results.
+  if (indexInSelectedDocs > -1) {
+    selectedDocs.splice(indexInSelectedDocs, 1);
+
+  // otherwise, add the _deleted: true flag and push it on to the array.
+  } else {
+    doc._deleted = true;
+    selectedDocs.push(doc);
+  }
+
+  return newSelectedDocs(selectedDocs);
+};
+
+export const bulkCheckOrUncheck = (docs, selectedDocs, allDocumentsSelected) => {
+  docs.forEach((doc) => {
+    // find the index of the doc in the selectedDocs array
+    const indexInSelectedDocs = selectedDocs.findIndex((selectedDoc) => {
+      return (doc._id || doc.id) === selectedDoc._id;
+    });
+
+    // remove the doc if we know all the documents are currently selected
+    if (allDocumentsSelected) {
+      selectedDocs.splice(indexInSelectedDocs, 1);
+    // otherwise, add the doc if it doesn't exist in the selectedDocs array
+    } else if (indexInSelectedDocs === -1) {
+      selectedDocs.push({
+        _id: doc._id || doc.id,
+        _rev: doc._rev || doc.rev || doc.value.rev,
+        _deleted: true
+      });
+    }
+  });
+
+  return newSelectedDocs(selectedDocs);
+};
+
+export const changeLayout = (newLayout) => {
+  return {
+    type: ActionTypes.INDEX_RESULTS_REDUX_CHANGE_LAYOUT,
+    layout: newLayout
+  };
+};
+
+export const changeTableHeaderAttribute = (newField, selectedFields) => {
+  selectedFields[newField.index] = newField.newSelectedRow;
+  return {
+    type: ActionTypes.INDEX_RESULTS_REDUX_CHANGE_TABLE_HEADER_ATTRIBUTE,
+    selectedFieldsTableView: selectedFields
+  };
+};
+
diff --git a/app/addons/documents/index-results/apis/fetch.js b/app/addons/documents/index-results/apis/fetch.js
new file mode 100644
index 0000000..1b580a3
--- /dev/null
+++ b/app/addons/documents/index-results/apis/fetch.js
@@ -0,0 +1,200 @@
+// 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 'url-polyfill';
+import 'whatwg-fetch';
+import FauxtonAPI from '../../../../core/api';
+import queryString from 'query-string';
+import SidebarActions from '../../sidebar/actions';
+import { nowLoading, newResultsAvailable, newSelectedDocs } from './base';
+
+const maxDocLimit = 10000;
+
+// This is a helper function to determine what params need to be sent to couch based
+// on what the user entered (i.e. queryOptionsParams) and what fauxton is using to
+// emulate pagination (i.e. fetchParams).
+export const mergeParams = (fetchParams, queryOptionsParams) => {
+  const params = {};
+
+  // determine the final "index" or "position" in the total result list based on the
+  // user's skip and limit inputs.  If queryOptionsParams.limit is empty,
+  // finalDocPosition will be NaN.  That's ok.
+  const finalDocPosition = (queryOptionsParams.skip || 0) + queryOptionsParams.limit;
+
+  // The skip value sent to couch will be the max of our current pagination skip
+  // (i.e. fetchParams.skip) and the user's original skip input (i.e. queryOptionsParams.skip).
+  // The limit will continue to be our pagination limit.
+  params.skip = Math.max(fetchParams.skip, queryOptionsParams.skip || 0);
+  params.limit = fetchParams.limit;
+  if (fetchParams.conflicts) {
+    params.conflicts = true;
+  }
+
+  // Determine the total number of documents remaining based on the user's skip and
+  // limit inputs.  Again, note that this will be NaN if queryOptionsParams.limit is
+  // empty.  That's ok.
+  const totalDocsRemaining = finalDocPosition - params.skip;
+
+  // return the merged params to send to couch and the num docs remaining.
+  return {
+    params: Object.assign({}, queryOptionsParams, params),
+    totalDocsRemaining: totalDocsRemaining
+  };
+};
+
+export const removeOverflowDocsAndCalculateHasNext = (docs, totalDocsRemaining, fetchLimit) => {
+  // Now is the time to determine if we have another page of results
+  // after this set of documents.  We also want to manipulate the array
+  // of docs because we always search with a limit larger than the desired
+  // number of results.  This is necessaary to emulate pagination.
+  let canShowNext = false;
+  if (totalDocsRemaining && docs.length > totalDocsRemaining) {
+    // We know the user manually entered a limit and we've reached the
+    // end of their desired results.  We need to remove any extra results
+    // that were returned because of our pagination emulation logic.
+    docs.splice(totalDocsRemaining);
+  } else if (docs.length === fetchLimit) {
+    // The number of docs returned is equal to our params.limit, which is
+    // one more than our perPage size.  We know that there is another
+    // page of results after this.
+    docs.splice(fetchLimit - 1);
+    canShowNext = true;
+  }
+
+  return {
+    finalDocList: docs,
+    canShowNext
+  };
+};
+
+// All the business logic for fetching docs from couch.
+// Arguments:
+// - fetchUrl -> the endpoint to fetch from
+// - fetchParams -> the internal params fauxton uses to emulate pagination
+// - queryOptionsParams -> manual query params entered by user
+export const fetchAllDocs = (fetchUrl, fetchParams, queryOptionsParams) => {
+  const { params, totalDocsRemaining } = mergeParams(fetchParams, queryOptionsParams);
+  params.limit = Math.min(params.limit, maxDocLimit);
+
+  return (dispatch) => {
+    // first, tell app state that we're loading
+    dispatch(nowLoading());
+
+    // now fetch the results
+    return queryEndpoint(fetchUrl, params).then((docs) => {
+      const {
+        finalDocList,
+        canShowNext
+      } = removeOverflowDocsAndCalculateHasNext(docs, totalDocsRemaining, params.limit);
+
+      // dispatch that we're all done
+      dispatch(newResultsAvailable(finalDocList, params, canShowNext));
+    });
+  };
+};
+
+export const queryEndpoint = (fetchUrl, params) => {
+  const query = queryString.stringify(params);
+  return fetch(`${fetchUrl}?${query}`, {
+    credentials: 'include',
+    headers: {
+      'Accept': 'application/json; charset=utf-8'
+    }
+  })
+  .then(res => res.json())
+  .then(res => res.error ? [] : res.rows);
+};
+
+export const errorMessage = (ids) => {
+  let msg = 'Failed to delete your document!';
+
+  if (ids) {
+    msg = 'Failed to delete: ' + ids.join(', ');
+  }
+
+  FauxtonAPI.addNotification({
+    msg: msg,
+    type: 'error',
+    clear:  true
+  });
+};
+
+export const validateBulkDelete = (docs) => {
+  const itemsLength = docs.length;
+
+  const msg = (itemsLength === 1) ? 'Are you sure you want to delete this doc?' :
+    'Are you sure you want to delete these ' + itemsLength + ' docs?';
+
+  if (itemsLength === 0) {
+    window.alert('Please select the document rows you want to delete.');
+    return false;
+  }
+
+  if (!window.confirm(msg)) {
+    return false;
+  }
+
+  return true;
+};
+
+export const bulkDeleteDocs = (databaseName, fetchUrl, docs, designDocs, fetchParams, queryOptionsParams) => {
+  if (!validateBulkDelete(docs)) {
+    return false;
+  }
+
+  return (dispatch) => {
+    const payload = {
+      docs: docs
+    };
+
+    return postToBulkDocs(databaseName, payload).then((res) => {
+      if (res.error) {
+        errorMessage();
+        return;
+      }
+      processBulkDeleteResponse(res, docs, designDocs);
+      dispatch(newSelectedDocs());
+      dispatch(fetchAllDocs(fetchUrl, fetchParams, queryOptionsParams));
+    });
+  };
+};
+
+export const postToBulkDocs = (databaseName, payload) => {
+  return fetch(`/${databaseName}/_bulk_docs`, {
+    method: 'POST',
+    credentials: 'include',
+    body: JSON.stringify(payload),
+    headers: {
+      'Accept': 'application/json; charset=utf-8',
+      'Content-Type': 'application/json'
+    }
+  })
+  .then(res => res.json());
+};
+
+export const processBulkDeleteResponse = (res, originalDocs, designDocs) => {
+  FauxtonAPI.addNotification({
+    msg: 'Successfully deleted your docs',
+    clear:  true
+  });
+
+  const failedDocs = res.filter(doc => !!doc.error).map(doc => doc.id);
+  const hasDesignDocs = !!originalDocs.map(d => d._id).find((_id) => /_design/.test(_id));
+
+  if (failedDocs.length > 0) {
+    errorMessage(failedDocs);
+  }
+
+  if (designDocs && hasDesignDocs) {
+    SidebarActions.updateDesignDocs(designDocs);
+  }
+};
diff --git a/app/addons/documents/index-results/apis/pagination.js b/app/addons/documents/index-results/apis/pagination.js
new file mode 100644
index 0000000..366f9e7
--- /dev/null
+++ b/app/addons/documents/index-results/apis/pagination.js
@@ -0,0 +1,88 @@
+// 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 FauxtonAPI from '../../../../core/api';
+import { fetchAllDocs } from './fetch';
+import ActionTypes from '../actiontypes';
+
+export const toggleShowAllColumns = () => {
+  return {
+    type: ActionTypes.INDEX_RESULTS_REDUX_TOGGLE_SHOW_ALL_COLUMNS
+  };
+};
+
+export const setPerPage = (amount) => {
+  return {
+    type: ActionTypes.INDEX_RESULTS_REDUX_SET_PER_PAGE,
+    perPage: amount
+  };
+};
+
+export const resetFetchParamsBeforePerPageChange = (fetchParams, queryOptionsParams, amount) => {
+  return Object.assign({}, fetchParams, {
+    limit: amount + 1,
+    skip: queryOptionsParams.skip || 0
+  });
+};
+
+export const updatePerPageResults = (databaseName, fetchParams, queryOptionsParams, amount) => {
+  // Set the query limit to the perPage + 1 so we know if there is
+  // a next page.  We also need to reset to the beginning of all
+  // possible pages since our logic to paginate backwards can't handle
+  // changing perPage amounts.
+  fetchParams = resetFetchParamsBeforePerPageChange(fetchParams, queryOptionsParams, amount);
+
+  return (dispatch) => {
+    dispatch(setPerPage(amount));
+    dispatch(fetchAllDocs(databaseName, fetchParams, queryOptionsParams));
+  };
+};
+
+export const incrementSkipForPageNext = (fetchParams, perPage) => {
+  return Object.assign({}, fetchParams, {
+    skip: fetchParams.skip + perPage
+  });
+};
+
+export const paginateNext = (databaseName, fetchParams, queryOptionsParams, perPage) => {
+  // add the perPage to the previous skip.
+  fetchParams = incrementSkipForPageNext(fetchParams, perPage);
+
+  return (dispatch) => {
+    dispatch({
+      type: ActionTypes.INDEX_RESULTS_REDUX_PAGINATE_NEXT
+    });
+    dispatch(fetchAllDocs(databaseName, fetchParams, queryOptionsParams));
+  };
+};
+
+export const decrementSkipForPagePrevious = (fetchParams, perPage) => {
+  return Object.assign({}, fetchParams, {
+    skip: Math.max(fetchParams.skip - perPage, 0)
+  });
+};
+
+export const paginatePrevious = (databaseName, fetchParams, queryOptionsParams, perPage) => {
+  // subtract the perPage to the previous skip.
+  fetchParams = decrementSkipForPagePrevious(fetchParams, perPage);
+
+  return (dispatch) => {
+    dispatch({
+      type: ActionTypes.INDEX_RESULTS_REDUX_PAGINATE_PREVIOUS
+    });
+    dispatch(fetchAllDocs(databaseName, fetchParams, queryOptionsParams));
+  };
+};
+
+export const resetPagination = (perPage = FauxtonAPI.constants.MISC.DEFAULT_PAGE_SIZE) => {
+  return setPerPage(perPage);
+};
diff --git a/app/addons/documents/index-results/apis/queryoptions.js b/app/addons/documents/index-results/apis/queryoptions.js
new file mode 100644
index 0000000..94e1620
--- /dev/null
+++ b/app/addons/documents/index-results/apis/queryoptions.js
@@ -0,0 +1,113 @@
+// 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 ActionTypes from '../actiontypes';
+import { fetchAllDocs } from './fetch';
+
+const updateQueryOptions = (queryOptions) => {
+  return {
+    type: ActionTypes.INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS,
+    options: queryOptions
+  };
+};
+
+export const resetFetchParamsBeforeExecute = (perPage) => {
+  return {
+    limit: perPage + 1,
+    skip: 0
+  };
+};
+
+export const queryOptionsExecute = (fetchUrl, queryOptionsParams, perPage) => {
+  const fetchParams = resetFetchParamsBeforeExecute(perPage);
+  return fetchAllDocs(fetchUrl, fetchParams, queryOptionsParams);
+};
+
+export const queryOptionsToggleVisibility = (newVisibility) => {
+  return updateQueryOptions({
+    isVisible: newVisibility
+  });
+};
+
+export const queryOptionsToggleReduce = (previousReduce) => {
+  return updateQueryOptions({
+    reduce: !previousReduce
+  });
+};
+
+export const queryOptionsUpdateGroupLevel = (newGroupLevel) => {
+  return updateQueryOptions({
+    groupLevel: newGroupLevel
+  });
+};
+
+export const queryOptionsToggleByKeys = (previousShowByKeys) => {
+  return updateQueryOptions({
+    showByKeys: !previousShowByKeys,
+    showBetweenKeys: !!previousShowByKeys
+  });
+};
+
+export const queryOptionsToggleBetweenKeys = (previousShowBetweenKeys) => {
+  return updateQueryOptions({
+    showBetweenKeys: !previousShowBetweenKeys,
+    showByKeys: !!previousShowBetweenKeys
+  });
+};
+
+export const queryOptionsUpdateBetweenKeys = (newBetweenKeys) => {
+  return updateQueryOptions({
+    betweenKeys: newBetweenKeys
+  });
+};
+
+export const queryOptionsUpdateByKeys = (newByKeys) => {
+  return updateQueryOptions({
+    byKeys: newByKeys
+  });
+};
+
+export const queryOptionsToggleDescending = (previousDescending) => {
+  return updateQueryOptions({
+    descending: !previousDescending
+  });
+};
+
+export const queryOptionsUpdateSkip = (newSkip) => {
+  return updateQueryOptions({
+    skip: newSkip
+  });
+};
+
+export const queryOptionsUpdateLimit = (newLimit) => {
+  return updateQueryOptions({
+    limit: newLimit
+  });
+};
+
+export const queryOptionsToggleIncludeDocs = (previousIncludeDocs) => {
+  return updateQueryOptions({
+    includeDocs: !previousIncludeDocs
+  });
+};
+
+export const queryOptionsFilterOnlyDdocs = () => {
+  return updateQueryOptions({
+    betweenKeys: {
+      include: false,
+      startkey: '\"_design\"',
+      endkey: '\"_design0\"'
+    },
+    showBetweenKeys: true,
+    showByKeys: false
+  });
+};
diff --git a/app/addons/documents/index-results/components/pagination/PaginationFooter.js b/app/addons/documents/index-results/components/pagination/PaginationFooter.js
new file mode 100644
index 0000000..9d00674
--- /dev/null
+++ b/app/addons/documents/index-results/components/pagination/PaginationFooter.js
@@ -0,0 +1,92 @@
+// 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 PagingControls from './PagingControls.js';
+import PerPageSelector from './PerPageSelector.js';
+import TableControls from './TableControls';
+
+export default class PaginationFooter extends React.Component {
+  constructor(props) {
+    super(props);
+  }
+
+  getPageNumberText () {
+    const { docs, pageStart, pageEnd } = this.props;
+
+    if (docs.length === 0) {
+      return <span>Showing 0 documents.</span>;
+    }
+
+    return <span>Showing document {pageStart} - {pageEnd}.</span>;
+  }
+
+  perPageChange (amount) {
+    const { updatePerPageResults, fetchParams, queryOptionsParams } = this.props;
+    updatePerPageResults(amount, fetchParams, queryOptionsParams);
+  }
+
+  nextClicked (event) {
+    event.preventDefault();
+
+    const { canShowNext, fetchParams, queryOptionsParams, paginateNext, perPage } = this.props;
+    if (canShowNext) {
+      paginateNext(fetchParams, queryOptionsParams, perPage);
+    }
+  }
+
+  previousClicked (event) {
+    event.preventDefault();
+
+    const { canShowPrevious, fetchParams, queryOptionsParams, paginatePrevious, perPage } = this.props;
+    if (canShowPrevious) {
+      paginatePrevious(fetchParams, queryOptionsParams, perPage);
+    }
+  }
+
+  render () {
+    const {
+      showPrioritizedEnabled,
+      hasResults,
+      prioritizedEnabled,
+      displayedFields,
+      perPage,
+      canShowNext,
+      canShowPrevious,
+      toggleShowAllColumns
+    } = this.props;
+
+    return (
+      <footer className="index-pagination pagination-footer">
+        <PagingControls
+          nextClicked={this.nextClicked.bind(this)}
+          previousClicked={this.previousClicked.bind(this)}
+          canShowNext={canShowNext}
+          canShowPrevious={canShowPrevious} />
+
+        <div className="footer-controls">
+          <div className="page-controls">
+            {showPrioritizedEnabled && hasResults ?
+              <TableControls
+                prioritizedEnabled={prioritizedEnabled}
+                displayedFields={displayedFields}
+                toggleShowAllColumns={toggleShowAllColumns} /> : null}
+          </div>
+          <PerPageSelector perPageChange={this.perPageChange.bind(this)} perPage={perPage} />
+          <div className="current-docs">
+            {this.getPageNumberText()}
+          </div>
+        </div>
+      </footer>
+    );
+  }
+};
diff --git a/app/addons/documents/index-results/components/pagination/PagingControls.js b/app/addons/documents/index-results/components/pagination/PagingControls.js
new file mode 100644
index 0000000..dd02c40
--- /dev/null
+++ b/app/addons/documents/index-results/components/pagination/PagingControls.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';
+
+export default function PagingControls ({ nextClicked, previousClicked, canShowPrevious, canShowNext }) {
+  let canShowPreviousClassName = '';
+  let canShowNextClassName = '';
+
+  if (!canShowPrevious) {
+    canShowPreviousClassName = 'disabled';
+  }
+
+  if (!canShowNext) {
+    canShowNextClassName = 'disabled';
+  }
+
+  return (
+    <div className="documents-pagination">
+      <ul className="pagination">
+        <li className={canShowPreviousClassName} >
+          <a id="previous" onClick={previousClicked} className="icon fonticon-left-open" href="#" data-bypass="true"></a>
+        </li>
+        <li className={canShowNextClassName} >
+          <a id="next" onClick={nextClicked} className="icon fonticon-right-open" href="#" data-bypass="true"></a>
+        </li>
+      </ul>
+    </div>
+  );
+};
+
+PagingControls.propTypes = {
+  nextClicked: React.PropTypes.func.isRequired,
+  previousClicked: React.PropTypes.func.isRequired,
+  canShowPrevious: React.PropTypes.bool.isRequired,
+  canShowNext: React.PropTypes.bool.isRequired
+};
diff --git a/app/addons/documents/index-results/components/pagination/PerPageSelector.js b/app/addons/documents/index-results/components/pagination/PerPageSelector.js
new file mode 100644
index 0000000..ba9657c
--- /dev/null
+++ b/app/addons/documents/index-results/components/pagination/PerPageSelector.js
@@ -0,0 +1,56 @@
+// 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';
+
+export default class PerPageSelector extends React.Component {
+  constructor (props) {
+    super(props);
+  }
+
+  perPageChange (e) {
+    const perPage = parseInt(e.target.value, 10);
+    this.props.perPageChange(perPage);
+  }
+
+  getOptions () {
+    return _.map(this.props.options, (i) => {
+      return (<option value={i} key={i}>{i}</option>);
+    });
+  }
+
+  render () {
+    return (
+      <div id="per-page">
+        <label htmlFor="select-per-page" className="drop-down inline">
+          {this.props.label} &nbsp;
+          <select id="select-per-page" onChange={this.perPageChange.bind(this)} value={this.props.perPage.toString()} className="input-small">
+            {this.getOptions()}
+          </select>
+        </label>
+      </div>
+    );
+  }
+
+};
+
+PerPageSelector.defaultProps = {
+  label: 'Documents per page: ',
+  options: [5, 10, 20, 30, 50, 100]
+};
+
+PerPageSelector.propTypes = {
+  perPage: React.PropTypes.number.isRequired,
+  perPageChange: React.PropTypes.func.isRequired,
+  label: React.PropTypes.string,
+  options: React.PropTypes.array
+};
diff --git a/app/addons/documents/index-results/components/pagination/TableControls.js b/app/addons/documents/index-results/components/pagination/TableControls.js
new file mode 100644
index 0000000..0023c20
--- /dev/null
+++ b/app/addons/documents/index-results/components/pagination/TableControls.js
@@ -0,0 +1,64 @@
+// 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';
+
+export default class TableControls extends React.Component {
+  constructor (props) {
+    super(props);
+  }
+
+  getAmountShownFields () {
+    const fields = this.props.displayedFields;
+
+    if (fields.shown === fields.allFieldCount) {
+      return (
+        <div className="pull-left shown-fields">
+          Showing {fields.shown} columns.
+        </div>
+      );
+    }
+
+    return (
+      <div className="pull-left shown-fields">
+        Showing {fields.shown} of {fields.allFieldCount} columns.
+      </div>
+    );
+  }
+
+  render () {
+    const { prioritizedEnabled, toggleShowAllColumns } = this.props;
+
+    return (
+      <div className="footer-table-control">
+        {this.getAmountShownFields()}
+        <div className="footer-doc-control-prioritized-wrapper pull-left">
+          <label htmlFor="footer-doc-control-prioritized">
+            <input
+              id="footer-doc-control-prioritized"
+              checked={prioritizedEnabled}
+              onChange={toggleShowAllColumns}
+              type="checkbox">
+            </input>
+            Show all columns.
+          </label>
+        </div>
+      </div>
+    );
+  }
+};
+
+TableControls.propTypes = {
+  prioritizedEnabled: React.PropTypes.bool.isRequired,
+  displayedFields: React.PropTypes.object.isRequired,
+  toggleShowAllColumns: React.PropTypes.func.isRequired
+};
diff --git a/app/addons/documents/index-results/components/queryoptions/AdditionalParams.js b/app/addons/documents/index-results/components/queryoptions/AdditionalParams.js
new file mode 100644
index 0000000..f3be5f0
--- /dev/null
+++ b/app/addons/documents/index-results/components/queryoptions/AdditionalParams.js
@@ -0,0 +1,78 @@
+// 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 FauxtonAPI from '../../../../../core/api';
+
+export default class AdditionalParams extends React.Component {
+  updateSkip (e) {
+    e.preventDefault();
+    let val = e.target.value;
+
+    //check skip is only numbers
+    if (!/^\d*$/.test(val)) {
+      FauxtonAPI.addNotification({
+        msg: 'Skip can only be a number',
+        type: 'error'
+      });
+      val = this.props.skip;
+    }
+
+    this.props.updateSkip(val);
+  }
+
+  updateLimit (e) {
+    e.preventDefault();
+    this.props.updateLimit(e.target.value);
+  }
+
+  toggleDescending () {
+    this.props.toggleDescending(this.props.descending);
+  }
+
+  render () {
+    return (
+      <div className="query-group" id="query-options-additional-params">
+        <div className="add-on additionalParams">Additional Parameters</div>
+        <div className="row-fluid fieldsets">
+          <div className="dropdown inline">
+            <label className="drop-down">
+              Limit
+              <select id="qoLimit" onChange={this.updateLimit.bind(this)} name="limit" value={this.props.limit} className="input-small">
+                <option value="none">None</option>
+                <option value={5}>5</option>
+                <option value={10}>10</option>
+                <option value={20}>20</option>
+                <option value={30}>30</option>
+                <option value={50}>50</option>
+                <option value={100}>100</option>
+                <option value={500}>500</option>
+              </select>
+            </label>
+          </div>
+        </div>
+        <div className="row-fluid fieldsets">
+          <div className="checkbox inline">
+            <input id="qoDescending" type="checkbox" onChange={this.toggleDescending.bind(this)} checked={this.props.descending} />
+            <label htmlFor="qoDescending">Descending</label>
+          </div>
+          <div className="dropdown inline">
+            <label htmlFor="qoSkip" className="drop-down">
+              Skip
+              <input value={this.props.skip} onChange={this.updateSkip.bind(this)} className="input-small" type="number" id="qoSkip" placeholder="# of rows" />
+            </label>
+          </div>
+        </div>
+      </div>
+    );
+  }
+};
diff --git a/app/addons/documents/index-results/components/queryoptions/KeySearchFields.js b/app/addons/documents/index-results/components/queryoptions/KeySearchFields.js
new file mode 100644
index 0000000..dddb63e
--- /dev/null
+++ b/app/addons/documents/index-results/components/queryoptions/KeySearchFields.js
@@ -0,0 +1,111 @@
+// 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 default class KeySearchFields extends React.Component {
+  constructor (props) {
+    super(props);
+  }
+
+  toggleByKeys () {
+    this.props.toggleByKeys();
+  }
+
+  toggleBetweenKeys () {
+    this.props.toggleBetweenKeys();
+  }
+
+  updateBetweenKeys () {
+    this.props.updateBetweenKeys({
+      startkey: ReactDOM.findDOMNode(this.refs.startkey).value,
+      endkey: ReactDOM.findDOMNode(this.refs.endkey).value,
+      include: this.props.betweenKeys.include
+    });
+  }
+
+  updateInclusiveEnd () {
+    this.props.updateBetweenKeys({
+      include: !this.props.betweenKeys.include,
+      startkey: this.props.betweenKeys.startkey,
+      endkey: this.props.betweenKeys.endkey
+    });
+  }
+
+  updateByKeys (e) {
+    this.props.updateByKeys(e.target.value);
+  }
+
+  render () {
+    let keysGroupClass = 'controls-group well js-query-keys-wrapper ';
+    let byKeysClass = 'row-fluid js-keys-section ';
+    let betweenKeysClass = byKeysClass;
+    let byKeysButtonClass = 'drop-down btn ';
+    let betweenKeysButtonClass = byKeysButtonClass;
+
+    if (!this.props.showByKeys && !this.props.showBetweenKeys) {
+      keysGroupClass += 'hide';
+    }
+
+    if (!this.props.showByKeys) {
+      byKeysClass += 'hide';
+    } else {
+      byKeysButtonClass += 'active';
+    }
+
+    if (!this.props.showBetweenKeys) {
+      betweenKeysClass += 'hide';
+    } else {
+      betweenKeysButtonClass += 'active';
+    }
+
+    return (
+      <div className="query-group" id="query-options-key-search">
+        <div className="add-on">Keys</div>
+        <div className="btn-group toggle-btns row-fluid">
+          <label style={{width: '101px'}} id="byKeys" onClick={this.toggleByKeys.bind(this)} className={byKeysButtonClass}>By Key(s)</label>
+          <label style={{width: '101px'}} id="betweenKeys" onClick={this.toggleBetweenKeys.bind(this)} className={betweenKeysButtonClass}>Between Keys</label>
+        </div>
+
+        <div className={keysGroupClass}>
+          <div className={byKeysClass} id="js-showKeys">
+            <div className="controls controls-row">
+              <label htmlFor="keys-input" className="drop-down">A key, or an array of keys.</label>
+              <textarea value={this.props.byKeys} onChange={this.updateByKeys.bind(this)} id="keys-input" className="input-xxlarge" rows="5" type="text"
+                placeholder='Enter either a single key ["123"] or an array of keys ["123", "456"]. A key value is the first parameter emitted in a map function. For example emit("123", 1) the key is "123".'></textarea>
+              <div id="keys-error" className="inline-block js-keys-error"></div>
+            </div>
+          </div>
+
+          <div className={betweenKeysClass} id="js-showStartEnd">
+            <div className="controls controls-row">
+              <div>
+                <label htmlFor="startkey" className="drop-down">Start key</label>
+                <input id="startkey" ref="startkey" type="text" onChange={this.updateBetweenKeys.bind(this)} value={this.props.betweenKeys.startkey} placeholder='e.g., "1234"' />
+              </div>
+              <div>
+                <label htmlFor="endkey" className="drop-down">End key</label>
+                <input id="endkey" ref="endkey" onChange={this.updateBetweenKeys.bind(this)} value={this.props.betweenKeys.endkey} type="text" placeholder='e.g., "1234"'/>
+                <div className="controls include-end-key-row checkbox controls-row inline">
+                  <input id="qoIncludeEndKeyInResults" ref="inclusive_end" type="checkbox" onChange={this.updateInclusiveEnd.bind(this)} checked={this.props.betweenKeys.include}/>
+                  <label htmlFor="qoIncludeEndKeyInResults">Include End Key in results</label>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+      </div>
+    );
+  }
+};
diff --git a/app/addons/documents/index-results/components/queryoptions/MainFieldsView.js b/app/addons/documents/index-results/components/queryoptions/MainFieldsView.js
new file mode 100644
index 0000000..735dd46
--- /dev/null
+++ b/app/addons/documents/index-results/components/queryoptions/MainFieldsView.js
@@ -0,0 +1,102 @@
+// 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';
+
+export default class MainFieldsView extends React.Component {
+  constructor(props) {
+    super(props);
+  }
+
+  toggleIncludeDocs () {
+    this.props.toggleIncludeDocs(this.props.includeDocs);
+  }
+
+  groupLevelChange (e) {
+    this.props.updateGroupLevel(e.target.value);
+  }
+
+  groupLevel () {
+    if (!this.props.reduce) {
+      return null;
+    }
+
+    return (
+      <label className="drop-down inline" id="qoGroupLevelGroup">
+        Group Level
+        <select onChange={this.groupLevelChange.bind(this)} id="qoGroupLevel" value={this.props.groupLevel} name="group_level" className="input-small">
+          <option value="0">None</option>
+          <option value="1">1</option>
+          <option value="2">2</option>
+          <option value="3">3</option>
+          <option value="4">4</option>
+          <option value="5">5</option>
+          <option value="6">6</option>
+          <option value="7">7</option>
+          <option value="8">8</option>
+          <option value="9">9</option>
+          <option value="exact">Exact</option>
+        </select>
+      </label>
+    );
+  }
+
+  reduce () {
+    if (!this.props.showReduce) {
+      return null;
+    }
+
+    return (
+      <span>
+        <div className="checkbox inline">
+          <input id="qoReduce" name="reduce" onChange={this.props.toggleReduce} type="checkbox" checked={this.props.reduce} />
+          <label htmlFor="qoReduce">Reduce</label>
+        </div>
+        {this.groupLevel()}
+      </span>
+    );
+  }
+
+  render () {
+    var includeDocs = this.props.includeDocs;
+    return (
+      <div className="query-group" id="query-options-main-fields">
+        <span className="add-on">
+          Query Options
+          <a className="help-link" href={this.props.docURL} target="_blank" data-bypass="true">
+            <i className="icon-question-sign" />
+          </a>
+        </span>
+        <div className="controls-group qo-main-fields-row">
+          <div className="row-fluid fieldsets">
+            <div className="checkbox inline">
+              <input disabled={this.props.reduce} onChange={this.toggleIncludeDocs.bind(this)} id="qoIncludeDocs"
+                 name="include_docs" type="checkbox" checked={includeDocs} />
+              <label className={this.props.reduce ? 'disabled' : ''} htmlFor="qoIncludeDocs" id="qoIncludeDocsLabel">Include Docs</label>
+            </div>
+            {this.reduce()}
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+};
+
+MainFieldsView.propTypes = {
+  toggleIncludeDocs: React.PropTypes.func.isRequired,
+  includeDocs: React.PropTypes.bool.isRequired,
+  reduce: React.PropTypes.bool.isRequired,
+  toggleReduce: React.PropTypes.func,
+  updateGroupLevel: React.PropTypes.func,
+  docURL: React.PropTypes.string.isRequired
+};
diff --git a/app/addons/documents/index-results/components/queryoptions/QueryButtons.js b/app/addons/documents/index-results/components/queryoptions/QueryButtons.js
new file mode 100644
index 0000000..2251c11
--- /dev/null
+++ b/app/addons/documents/index-results/components/queryoptions/QueryButtons.js
@@ -0,0 +1,38 @@
+// 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';
+
+export default class QueryButtons extends React.Component {
+  constructor (props) {
+    super(props);
+  }
+
+  hideTray () {
+    this.props.onCancel();
+  }
+
+  render () {
+    return (
+      <div className="controls-group query-group">
+        <div id="button-options" className="controls controls-row">
+          <button type="submit" className="btn btn-secondary">Run Query</button>
+          <a onClick={this.hideTray.bind(this)} className="btn btn-cancelDark">Cancel</a>
+        </div>
+      </div>
+    );
+  }
+};
+
+QueryButtons.propTypes = {
+  onCancel: React.PropTypes.func.isRequired
+};
diff --git a/app/addons/documents/index-results/components/queryoptions/QueryOptions.js b/app/addons/documents/index-results/components/queryoptions/QueryOptions.js
new file mode 100644
index 0000000..7d8b963
--- /dev/null
+++ b/app/addons/documents/index-results/components/queryoptions/QueryOptions.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 FauxtonAPI from '../../../../../core/api';
+import GeneralComponents from '../../../../components/react-components';
+import Constants from '../../../constants';
+import MainFieldsView from './MainFieldsView';
+import KeySearchFields from './KeySearchFields';
+import AdditionalParams from './AdditionalParams';
+import QueryButtons from './QueryButtons';
+
+const { ToggleHeaderButton, TrayContents } = GeneralComponents;
+
+export default class QueryOptions extends React.Component {
+  constructor(props) {
+    super(props);
+    const {
+      ddocsOnly,
+      queryOptionsFilterOnlyDdocs
+    } = props;
+
+    if (ddocsOnly) {
+      queryOptionsFilterOnlyDdocs();
+    }
+  }
+
+  componentWillReceiveProps (nextProps) {
+    const {
+      ddocsOnly,
+      queryOptionsFilterOnlyDdocs,
+      resetState
+    } = this.props;
+
+    if (!ddocsOnly && nextProps.ddocsOnly) {
+      queryOptionsFilterOnlyDdocs();
+    } else if (ddocsOnly && !nextProps.ddocsOnly) {
+      resetState();
+    }
+  }
+
+  executeQuery (e) {
+    if (e) { e.preventDefault(); }
+    this.closeTray();
+
+    const {
+      queryOptionsExecute,
+      queryOptionsParams,
+      perPage,
+      resetPagination,
+      selectedLayout,
+      changeLayout
+    } = this.props;
+
+    // reset pagination back to the beginning but hold on to the current perPage
+    resetPagination(perPage);
+
+    // We may have to change the layout based on include_docs.
+    const isMetadata = selectedLayout === Constants.LAYOUT_ORIENTATION.METADATA;
+    if (isMetadata && queryOptionsParams.include_docs) {
+      changeLayout(Constants.LAYOUT_ORIENTATION.TABLE);
+    } else if (!isMetadata && !queryOptionsParams.include_docs) {
+      changeLayout(Constants.LAYOUT_ORIENTATION.METADATA);
+    }
+
+    // finally, run the query
+    queryOptionsExecute(queryOptionsParams, perPage);
+  }
+
+  toggleTrayVisibility () {
+    this.props.queryOptionsToggleVisibility(!this.props.contentVisible);
+  }
+
+  closeTray () {
+    this.props.queryOptionsToggleVisibility(false);
+  }
+
+  getTray () {
+    return (
+      <TrayContents closeTray={this.closeTray.bind(this)} contentVisible={this.props.contentVisible}
+        className="query-options"
+        id="query-options-tray">
+
+        <form onSubmit={this.executeQuery.bind(this)} className="js-view-query-update custom-inputs">
+          <MainFieldsView
+            includeDocs={this.props.includeDocs}
+            toggleIncludeDocs={this.props.queryOptionsToggleIncludeDocs}
+            showReduce={this.props.showReduce}
+            reduce={this.props.reduce}
+            toggleReduce={this.props.queryOptionsToggleReduce}
+            groupLevel={this.props.groupLevel}
+            updateGroupLevel={this.props.queryOptionsUpdateGroupLevel}
+            docURL={FauxtonAPI.constants.DOC_URLS.GENERAL} />
+          <KeySearchFields
+            key={1}
+            showByKeys={this.props.showByKeys}
+            showBetweenKeys={this.props.showBetweenKeys}
+            toggleByKeys={this.props.queryOptionsToggleByKeys}
+            toggleBetweenKeys={this.props.queryOptionsToggleBetweenKeys}
+            betweenKeys={this.props.betweenKeys}
+            updateBetweenKeys={this.props.queryOptionsUpdateBetweenKeys}
+            byKeys={this.props.byKeys}
+            updateByKeys={this.props.queryOptionsUpdateByKeys} />
+          <AdditionalParams
+            descending={this.props.descending}
+            toggleDescending={this.props.queryOptionsToggleDescending}
+            skip={this.props.skip}
+            updateSkip={this.props.queryOptionsUpdateSkip}
+            updateLimit={this.props.queryOptionsUpdateLimit}
+            limit={this.props.limit} />
+          <QueryButtons onCancel={this.closeTray.bind(this)} />
+        </form>
+      </TrayContents>
+    );
+  }
+
+  render () {
+    return (
+      <div id="header-query-options">
+        <div id="query-options">
+          <div>
+            <ToggleHeaderButton
+              toggleCallback={this.toggleTrayVisibility.bind(this)}
+              containerClasses="header-control-box control-toggle-queryoptions"
+              title="Query Options"
+              fonticon="fonticon-gears"
+              text="Options" />
+              {this.getTray()}
+          </div>
+        </div>
+      </div>
+    );
+  }
+};
+
+QueryOptions.propTypes = {
+  contentVisible: React.PropTypes.bool.isRequired
+};
diff --git a/app/addons/documents/index-results/components/results/IndexResults.js b/app/addons/documents/index-results/components/results/IndexResults.js
new file mode 100644
index 0000000..58f17eb
--- /dev/null
+++ b/app/addons/documents/index-results/components/results/IndexResults.js
@@ -0,0 +1,100 @@
+// 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 ResultsScreen from './ResultsScreen';
+
+export default class IndexResults extends React.Component {
+  constructor (props) {
+    super(props);
+  }
+
+  componentDidMount () {
+    const {
+      fetchAllDocs,
+      fetchParams,
+      queryOptionsParams,
+    } = this.props;
+
+    // now get the docs!
+    fetchAllDocs(fetchParams, queryOptionsParams);
+  }
+
+  componentWillUpdate (nextProps) {
+    const {
+      fetchAllDocs,
+      fetchParams,
+      queryOptionsParams,
+      ddocsOnly
+    } = nextProps;
+
+    if (this.props.ddocsOnly !== ddocsOnly) {
+      fetchAllDocs(fetchParams, queryOptionsParams);
+    }
+  }
+
+  componentWillUnmount () {
+    const { resetState } = this.props;
+    resetState();
+  }
+
+  deleteSelectedDocs () {
+    const { bulkDeleteDocs, fetchParams, selectedDocs, queryOptionsParams } = this.props;
+    bulkDeleteDocs(selectedDocs, fetchParams, queryOptionsParams);
+  }
+
+  isSelected (id) {
+    const { selectedDocs } = this.props;
+
+    // check whether this id exists in our array of selected docs
+    return selectedDocs.findIndex((doc) => {
+      return id === doc._id;
+    }) > -1;
+  }
+
+  docChecked (_id, _rev) {
+    const { selectDoc, selectedDocs } = this.props;
+
+    // dispatch an action to push this doc on to the array of selected docs
+    const doc = {
+      _id: _id,
+      _rev: _rev
+    };
+
+    selectDoc(doc, selectedDocs);
+  }
+
+  toggleSelectAll () {
+    const {
+      docs,
+      selectedDocs,
+      allDocumentsSelected,
+      bulkCheckOrUncheck
+    } = this.props;
+
+    bulkCheckOrUncheck(docs, selectedDocs, allDocumentsSelected);
+  }
+
+  render () {
+    const { results } = this.props;
+
+    return (
+      <ResultsScreen
+        removeItem={this.deleteSelectedDocs.bind(this)}
+        isSelected={this.isSelected.bind(this)}
+        docChecked={this.docChecked.bind(this)}
+        isListDeletable={results.hasBulkDeletableDoc}
+        toggleSelectAll={this.toggleSelectAll.bind(this)}
+        {...this.props} />
+    );
+  }
+};
diff --git a/app/addons/documents/pagination/actiontypes.js b/app/addons/documents/index-results/components/results/NoResultsScreen.js
similarity index 65%
copy from app/addons/documents/pagination/actiontypes.js
copy to app/addons/documents/index-results/components/results/NoResultsScreen.js
index 7e47937..2279b28 100644
--- a/app/addons/documents/pagination/actiontypes.js
+++ b/app/addons/documents/index-results/components/results/NoResultsScreen.js
@@ -10,10 +10,17 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-export default {
-  COLLECTION_CHANGED: 'COLLECTION_CHANGED',
-  PER_PAGE_CHANGE: 'PER_PAGE_CHANGE',
-  PAGINATE_NEXT: 'PAGINATE_NEXT',
-  PAGINATE_PREVIOUS: 'PAGINATE_PREVIOUS',
-  SET_PAGINATION_DOCUMENT_LIMIT: 'SET_PAGINATION_DOCUMENT_LIMIT'
+import React from 'react';
+
+export default function NoResultsScreen ({ text }) {
+  return (
+    <div className="no-results-screen">
+      <div className="watermark-logo"></div>
+      <h3>{text}</h3>
+    </div>
+  );
+};
+
+NoResultsScreen.propTypes = {
+  text: React.PropTypes.string.isRequired
 };
diff --git a/app/addons/documents/index-results/components/results/ResultsScreen.js b/app/addons/documents/index-results/components/results/ResultsScreen.js
new file mode 100644
index 0000000..7975368
--- /dev/null
+++ b/app/addons/documents/index-results/components/results/ResultsScreen.js
@@ -0,0 +1,132 @@
+// 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 FauxtonAPI from '../../../../../core/api';
+import Constants from '../../../constants';
+import Components from "../../../../components/react-components";
+import {ResultsToolBar} from "../../../components/results-toolbar";
+import NoResultsScreen from './NoResultsScreen';
+import TableView from './TableView';
+
+const { LoadLines, Document } = Components;
+
+export default class ResultsScreen extends React.Component {
+  constructor (props) {
+    super(props);
+  }
+
+  componentDidMount () {
+    prettyPrint();
+  }
+
+  componentDidUpdate () {
+    prettyPrint();
+  }
+
+  onClick (id, doc) {
+    FauxtonAPI.navigate(doc.url);
+  }
+
+  getUrlFragment (url) {
+    if (!this.props.isEditable) {
+      return null;
+    }
+
+    return (
+      <a href={url}>
+        <i className="fonticon-pencil"></i>
+      </a>);
+  }
+
+  getDocumentList () {
+    let noop = () => {};
+    let data = this.props.results.results;
+
+    return _.map(data, function (doc, i) {
+      return (
+       <Document
+         key={doc.id + i}
+         doc={doc}
+         onClick={this.props.isEditable ? this.onClick : noop}
+         keylabel={doc.keylabel}
+         docContent={doc.content}
+         checked={this.props.isSelected(doc.id)}
+         header={doc.header}
+         docChecked={this.props.docChecked}
+         isDeletable={doc.isDeletable}
+         docIdentifier={doc.id} >
+         {doc.url ? this.getUrlFragment('#' + doc.url) : doc.url}
+       </Document>
+     );
+    }, this);
+  }
+
+  getDocumentStyleView () {
+    let classNames = 'view';
+
+    if (this.props.isListDeletable) {
+      classNames += ' show-select';
+    }
+
+    return (
+      <div className={classNames}>
+        <div id="doc-list">
+          {this.getDocumentList()}
+        </div>
+      </div>
+    );
+  }
+
+  getTableStyleView () {
+    return (
+      <div>
+        <TableView
+          onClick={this.onClick}
+          docChecked={this.props.docChecked}
+          isSelected={this.props.isSelected}
+          isListDeletable={this.props.isListDeletable}
+          data={this.props.results}
+          isLoading={this.props.isLoading}
+
+          removeItem={this.props.removeItem}
+          isChecked={this.props.allDocumentsSelected}
+          hasSelectedItem={this.props.hasSelectedItem}
+          toggleSelect={this.toggleSelectAll}
+          changeField={this.props.changeTableHeaderAttribute}
+          title="Select all docs that can be..." />
+      </div>
+    );
+  }
+
+  render () {
+    let mainView = null;
+
+    if (this.props.isLoading) {
+      mainView = <div className="loading-lines-wrapper"><LoadLines /></div>;
+    } else if (!this.props.hasResults) {
+      mainView = <NoResultsScreen text={this.props.textEmptyIndex}/>;
+    } else if (this.props.selectedLayout === Constants.LAYOUT_ORIENTATION.JSON) {
+      mainView = this.getDocumentStyleView();
+    } else {
+      mainView = this.getTableStyleView();
+    }
+
+    return (
+      <div className="document-result-screen">
+        <ResultsToolBar {...this.props} />
+        {mainView}
+      </div>
+    );
+  }
+
+};
diff --git a/app/addons/documents/index-results/components/results/TableRow.js b/app/addons/documents/index-results/components/results/TableRow.js
new file mode 100644
index 0000000..4db7d7c
--- /dev/null
+++ b/app/addons/documents/index-results/components/results/TableRow.js
@@ -0,0 +1,148 @@
+// 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 FauxtonAPI from '../../../../../core/api';
+import Components from '../../../../components/react-components';
+import uuid from 'uuid';
+
+const { Copy } = Components;
+
+export default class TableRow extends React.Component {
+  constructor (props) {
+    super(props);
+    this.state = {
+      checked: this.props.isSelected
+    };
+  }
+
+  onChange () {
+    this.props.docChecked(this.props.el.id, this.props.el._rev);
+  }
+
+  getRowContents (element, rowNumber) {
+    const el = element.content;
+
+    const row = this.props.data.selectedFields.map(function (k, i) {
+
+      const key = 'tableview-data-cell-' + rowNumber + k + i + el[k];
+      const stringified = typeof el[k] === 'object' ? JSON.stringify(el[k], null, '  ') : el[k];
+
+      return (
+        <td key={key} title={stringified} onClick={this.onClick.bind(this)}>
+          {stringified}
+        </td>
+      );
+    }.bind(this));
+
+    return row;
+  }
+
+  maybeGetCheckboxCell (el, i) {
+    return (
+      <td className="tableview-checkbox-cell" key={"tableview-checkbox-cell-" + i}>
+        {el.isDeletable ? <input
+          id={"checkbox-" + this.props.docIdentifier}
+          checked={this.props.isSelected}
+          type="checkbox"
+          onChange={this.onChange.bind(this)} /> : null}
+      </td>
+    );
+  }
+
+  getAdditionalInfoRow (el) {
+    const attachmentCount = Object.keys(el._attachments || {}).length;
+    let attachmentIndicator = null;
+    let textAttachments = null;
+
+    const conflictCount = Object.keys(el._conflicts || {}).length;
+    let conflictIndicator = null;
+    let textConflicts = null;
+
+
+    if (attachmentCount) {
+      textAttachments = attachmentCount === 1 ? attachmentCount + ' Attachment' : attachmentCount + ' Attachments';
+      attachmentIndicator = (
+        <div style={{display: 'inline', marginLeft: '5px'}} title={textAttachments}>
+          <i className="icon fonticon-paperclip"></i>{attachmentCount}
+        </div>
+      );
+    }
+
+    if (conflictCount) {
+      textConflicts = conflictCount === 1 ? conflictCount + ' Conflict' : conflictCount + ' Conflicts';
+      conflictIndicator = (
+        <div className="tableview-conflict" data-conflicts-indicator style={{display: 'inline'}} title={textConflicts}>
+          <i
+            style={{fontSize: '17px'}}
+            className="icon icon-code-fork"></i>{conflictCount}
+        </div>
+      );
+    }
+
+    return (
+      <td className="tableview-el-last" onClick={this.onClick}>
+        {conflictIndicator}
+        {attachmentIndicator}
+      </td>
+    );
+  }
+
+  getCopyButton (el) {
+    const text = JSON.stringify(el, null, '  ');
+    return (
+      <td title={text} className="tableview-el-copy">
+        <Copy
+          title={text}
+          text={text}
+          uniqueKey={uuid.v4()}
+          onClipboardClick={this.showCopiedMessage} />
+      </td>
+    );
+  }
+
+  showCopiedMessage () {
+    FauxtonAPI.addNotification({
+      msg: 'The document content has been copied to the clipboard.',
+      type: 'success',
+      clear: true
+    });
+  }
+
+  onClick (e) {
+    this.props.onClick(this.props.el._id, this.props.el, e);
+  }
+
+  render () {
+    const i = this.props.index;
+    const docContent = this.props.el.content;
+    const el = this.props.el;
+
+    return (
+      <tr key={"tableview-content-row-" + i}>
+        {this.maybeGetCheckboxCell(el, i)}
+        {this.getCopyButton(docContent)}
+        {this.getRowContents(el, i)}
+        {this.getAdditionalInfoRow(docContent)}
+      </tr>
+    );
+  }
+};
+
+TableRow.propTypes = {
+  docIdentifier: React.PropTypes.string.isRequired,
+  docChecked: React.PropTypes.func.isRequired,
+  isSelected: React.PropTypes.bool.isRequired,
+  index: React.PropTypes.number.isRequired,
+  data: React.PropTypes.object.isRequired,
+  onClick: React.PropTypes.func.isRequired
+};
diff --git a/app/addons/documents/index-results/components/results/TableView.js b/app/addons/documents/index-results/components/results/TableView.js
new file mode 100644
index 0000000..299ce71
--- /dev/null
+++ b/app/addons/documents/index-results/components/results/TableView.js
@@ -0,0 +1,105 @@
+// 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 TableRow from './TableRow';
+import WrappedAutocomplete from './WrappedAutocomplete';
+
+export default class TableView extends React.Component {
+  constructor (props) {
+    super(props);
+  }
+
+  getContentRows () {
+    const data = this.props.data.results;
+
+    return data.map(function (el, i) {
+
+      return (
+        <TableRow
+          onClick={this.props.onClick}
+          key={"tableview-row-component-" + i}
+          index={i}
+          el={el}
+          docIdentifier={el.id || "tableview-row-component-" + i}
+          docChecked={this.props.docChecked}
+          isSelected={this.props.isSelected(el.id)}
+          data={this.props.data} />
+      );
+    }.bind(this));
+  }
+
+  getOptionFieldRows (filtered) {
+    const notSelectedFields = this.props.data.notSelectedFields;
+
+    if (!notSelectedFields) {
+      return filtered.map(function (el, i) {
+        return <th key={'header-el-' + i}>{el}</th>;
+      });
+    }
+
+    return filtered.map(function (el, i) {
+      return (
+        <th key={'header-el-' + i}>
+          {this.getDropdown(
+            el,
+            this.props.data.schema,
+            i,
+            this.props.changeField,
+            this.props.data.selectedFields
+          )}
+        </th>
+      );
+    }.bind(this));
+  }
+
+  getDropdown (selectedField, notSelectedFields, i, changeField, selectedFields) {
+
+    return (
+      <WrappedAutocomplete
+        selectedField={selectedField}
+        notSelectedFields={notSelectedFields}
+        index={i}
+        changeField={changeField}
+        selectedFields={selectedFields} />
+    );
+  }
+
+  getHeader () {
+    const selectedFields = this.props.data.selectedFields;
+    const row = this.getOptionFieldRows(selectedFields);
+
+    return (
+      <tr key="tableview-content-row-header">
+        <th className="tableview-header-el-checkbox"></th>
+        <th className="tableview-el-copy"></th>
+        {row}
+        <th className="tableview-el-last"></th>
+      </tr>
+    );
+  }
+
+  render () {
+    return (
+      <div className="table-view-docs">
+        <table className="table table-striped">
+          <thead>
+            {this.getHeader()}
+          </thead>
+          <tbody>
+            {this.getContentRows()}
+          </tbody>
+        </table>
+      </div>
+    );
+  }
+};
diff --git a/app/addons/documents/index-results/components/results/WrappedAutocomplete.js b/app/addons/documents/index-results/components/results/WrappedAutocomplete.js
new file mode 100644
index 0000000..5b1c52a
--- /dev/null
+++ b/app/addons/documents/index-results/components/results/WrappedAutocomplete.js
@@ -0,0 +1,41 @@
+// 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 ReactSelect from "react-select";
+
+export default function WrappedAutocomplete ({
+  selectedField,
+  notSelectedFields,
+  index,
+  changeField,
+  selectedFields
+}) {
+  const options = notSelectedFields.map((el) => {
+    return {value: el, label: el};
+  });
+
+  return (
+    <div className="table-container-autocomplete">
+      <div className="table-select-wrapper">
+        <ReactSelect
+          value={selectedField}
+          options={options}
+          clearable={false}
+          onChange={(el) => {
+            changeField({newSelectedRow: el.value, index: index}, selectedFields);
+          }}
+        />
+      </div>
+    </div>
+  );
+};
diff --git a/app/addons/documents/index-results/containers/ApiBarContainer.js b/app/addons/documents/index-results/containers/ApiBarContainer.js
new file mode 100644
index 0000000..9767acb
--- /dev/null
+++ b/app/addons/documents/index-results/containers/ApiBarContainer.js
@@ -0,0 +1,36 @@
+// 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 queryString from 'query-string';
+import { connect } from 'react-redux';
+import { ApiBarWrapper } from '../../../components/layouts';
+import { getQueryOptionsParams } from '../reducers';
+import FauxtonAPI from '../../../../core/api';
+
+const urlRef = (databaseName, params) => {
+  let query = queryString.stringify(params);
+
+  if (query) {
+    query = `?${query}`;
+  }
+
+  return FauxtonAPI.urls('allDocs', "apiurl", encodeURIComponent(databaseName), query);
+};
+
+const mapStateToProps = ({indexResults}, ownProps) => {
+  return {
+    docUrl: FauxtonAPI.constants.DOC_URLS.GENERAL,
+    endpoint: urlRef(ownProps.databaseName, getQueryOptionsParams(indexResults))
+  };
+};
+
+export default connect (mapStateToProps)(ApiBarWrapper);
diff --git a/app/addons/documents/index-results/containers/IndexResultsContainer.js b/app/addons/documents/index-results/containers/IndexResultsContainer.js
new file mode 100644
index 0000000..d865401
--- /dev/null
+++ b/app/addons/documents/index-results/containers/IndexResultsContainer.js
@@ -0,0 +1,98 @@
+// 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 { connect } from 'react-redux';
+import IndexResults from '../components/results/IndexResults';
+import { fetchAllDocs, bulkDeleteDocs } from '../apis/fetch';
+import { queryOptionsToggleIncludeDocs } from '../apis/queryoptions';
+import {
+  selectDoc,
+  changeLayout,
+  bulkCheckOrUncheck,
+  changeTableHeaderAttribute,
+  resetState
+} from '../apis/base';
+import {
+  getDocs,
+  getSelectedDocs,
+  getIsLoading,
+  getHasResults,
+  getDataForRendering,
+  getIsEditable,
+  getSelectedLayout,
+  getAllDocsSelected,
+  getHasDocsSelected,
+  getNumDocsSelected,
+  getTextEmptyIndex,
+  getTypeOfIndex,
+  getFetchParams,
+  getQueryOptionsParams
+} from '../reducers';
+
+
+const mapStateToProps = ({indexResults}, ownProps) => {
+  return {
+    docs: getDocs(indexResults),
+    selectedDocs: getSelectedDocs(indexResults),
+    isLoading: getIsLoading(indexResults),
+    hasResults: getHasResults(indexResults),
+    results: getDataForRendering(indexResults, ownProps.databaseName),
+    isEditable: getIsEditable(indexResults),
+    selectedLayout: getSelectedLayout(indexResults),
+    allDocumentsSelected: getAllDocsSelected(indexResults),
+    hasSelectedItem: getHasDocsSelected(indexResults),
+    numDocsSelected: getNumDocsSelected(indexResults),
+    textEmptyIndex: getTextEmptyIndex(indexResults),
+    typeOfIndex: getTypeOfIndex(indexResults),
+    fetchParams: getFetchParams(indexResults),
+    queryOptionsParams: getQueryOptionsParams(indexResults)
+  };
+};
+
+const mapDispatchToProps = (dispatch, ownProps) => {
+  return {
+    fetchAllDocs: (fetchParams, queryOptionsParams) => {
+      dispatch(fetchAllDocs(ownProps.fetchUrl, fetchParams, queryOptionsParams));
+    },
+    selectDoc: (doc, selectedDocs) => {
+      dispatch(selectDoc(doc, selectedDocs));
+    },
+    bulkDeleteDocs: (docs, fetchParams, queryOptionsParams) => {
+      dispatch(bulkDeleteDocs(ownProps.databaseName,
+                              ownProps.fetchUrl,
+                              docs,
+                              ownProps.designDocs,
+                              fetchParams,
+                              queryOptionsParams));
+    },
+    changeLayout: (newLayout) => {
+      dispatch(changeLayout(newLayout));
+    },
+    bulkCheckOrUncheck: (docs, selectedDocs, allDocumentsSelected) => {
+      dispatch(bulkCheckOrUncheck(docs, selectedDocs, allDocumentsSelected));
+    },
+    changeTableHeaderAttribute: (newField, selectedFields) => {
+      dispatch(changeTableHeaderAttribute(newField, selectedFields));
+    },
+    resetState: () => {
+      dispatch(resetState());
+    },
+    queryOptionsToggleIncludeDocs: (previousIncludeDocs) => {
+      dispatch(queryOptionsToggleIncludeDocs(previousIncludeDocs));
+    }
+  };
+};
+
+export default connect (
+  mapStateToProps,
+  mapDispatchToProps
+)(IndexResults);
diff --git a/app/addons/documents/index-results/containers/PaginationContainer.js b/app/addons/documents/index-results/containers/PaginationContainer.js
new file mode 100644
index 0000000..27a7757
--- /dev/null
+++ b/app/addons/documents/index-results/containers/PaginationContainer.js
@@ -0,0 +1,76 @@
+// 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 { connect } from 'react-redux';
+import PaginationFooter from '../components/pagination/PaginationFooter';
+import {
+  toggleShowAllColumns,
+  updatePerPageResults,
+  paginateNext,
+  paginatePrevious
+} from '../apis/pagination';
+import {
+  getDocs,
+  getSelectedDocs,
+  getHasResults,
+  getFetchParams,
+  getPageStart,
+  getPageEnd,
+  getPerPage,
+  getPrioritizedEnabled,
+  getShowPrioritizedEnabled,
+  getDisplayedFields,
+  getCanShowNext,
+  getCanShowPrevious,
+  getQueryOptionsParams
+} from '../reducers';
+
+
+const mapStateToProps = ({indexResults}, ownProps) => {
+  return {
+    docs: getDocs(indexResults),
+    selectedDocs: getSelectedDocs(indexResults),
+    hasResults: getHasResults(indexResults),
+    pageStart: getPageStart(indexResults),
+    pageEnd: getPageEnd(indexResults),
+    perPage: getPerPage(indexResults),
+    prioritizedEnabled: getPrioritizedEnabled(indexResults),
+    showPrioritizedEnabled: getShowPrioritizedEnabled(indexResults),
+    displayedFields: getDisplayedFields(indexResults, ownProps.databaseName),
+    canShowNext: getCanShowNext(indexResults),
+    canShowPrevious: getCanShowPrevious(indexResults),
+    fetchParams: getFetchParams(indexResults),
+    queryOptionsParams: getQueryOptionsParams(indexResults)
+  };
+};
+
+const mapDispatchToProps = (dispatch, ownProps) => {
+  return {
+    toggleShowAllColumns: () => {
+      dispatch(toggleShowAllColumns());
+    },
+    updatePerPageResults: (amount, fetchParams, queryOptionsParams) => {
+      dispatch(updatePerPageResults(ownProps.fetchUrl, fetchParams, queryOptionsParams, amount));
+    },
+    paginateNext: (fetchParams, queryOptionsParams, perPage) => {
+      dispatch(paginateNext(ownProps.fetchUrl, fetchParams, queryOptionsParams, perPage));
+    },
+    paginatePrevious: (fetchParams, queryOptionsParams, perPage) => {
+      dispatch(paginatePrevious(ownProps.fetchUrl, fetchParams, queryOptionsParams, perPage));
+    }
+  };
+};
+
+export default connect (
+  mapStateToProps,
+  mapDispatchToProps
+)(PaginationFooter);
diff --git a/app/addons/documents/index-results/containers/QueryOptionsContainer.js b/app/addons/documents/index-results/containers/QueryOptionsContainer.js
new file mode 100644
index 0000000..eb08ae2
--- /dev/null
+++ b/app/addons/documents/index-results/containers/QueryOptionsContainer.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 { connect } from 'react-redux';
+import QueryOptions from '../components/queryoptions/QueryOptions';
+import { changeLayout, resetState } from '../apis/base';
+import { resetPagination } from '../apis/pagination';
+import {
+  queryOptionsExecute,
+  queryOptionsToggleReduce,
+  queryOptionsUpdateGroupLevel,
+  queryOptionsToggleByKeys,
+  queryOptionsToggleBetweenKeys,
+  queryOptionsUpdateBetweenKeys,
+  queryOptionsUpdateByKeys,
+  queryOptionsToggleDescending,
+  queryOptionsUpdateSkip,
+  queryOptionsUpdateLimit,
+  queryOptionsToggleIncludeDocs,
+  queryOptionsToggleVisibility,
+  queryOptionsFilterOnlyDdocs
+} from '../apis/queryoptions';
+import {
+  getQueryOptionsPanel,
+  getFetchParams,
+  getQueryOptionsParams,
+  getPerPage,
+  getSelectedLayout
+} from '../reducers';
+
+const mapStateToProps = ({indexResults}, ownProps) => {
+  const queryOptionsPanel = getQueryOptionsPanel(indexResults);
+  return {
+    contentVisible: queryOptionsPanel.isVisible,
+    includeDocs: queryOptionsPanel.includeDocs,
+    showReduce: queryOptionsPanel.showReduce,
+    reduce: queryOptionsPanel.reduce,
+    groupLevel: queryOptionsPanel.groupLevel,
+    showByKeys: queryOptionsPanel.showByKeys,
+    showBetweenKeys: queryOptionsPanel.showBetweenKeys,
+    betweenKeys: queryOptionsPanel.betweenKeys,
+    byKeys: queryOptionsPanel.byKeys,
+    descending: queryOptionsPanel.descending,
+    skip: queryOptionsPanel.skip,
+    limit: queryOptionsPanel.limit,
+    fetchParams: getFetchParams(indexResults),
+    queryOptionsParams: getQueryOptionsParams(indexResults),
+    perPage: getPerPage(indexResults),
+    selectedLayout: getSelectedLayout(indexResults),
+    ddocsOnly: ownProps.ddocsOnly
+  };
+};
+
+const mapDispatchToProps = (dispatch, ownProps) => {
+  return {
+    resetPagination: (perPage) => {
+      dispatch(resetPagination(perPage));
+    },
+    queryOptionsToggleReduce: (previousReduce) => {
+      dispatch(queryOptionsToggleReduce(previousReduce));
+    },
+    queryOptionsUpdateGroupLevel: (newGroupLevel) => {
+      dispatch(queryOptionsUpdateGroupLevel(newGroupLevel));
+    },
+    queryOptionsToggleByKeys: (previousShowByKeys) => {
+      dispatch(queryOptionsToggleByKeys(previousShowByKeys));
+    },
+    queryOptionsToggleBetweenKeys: (previousShowBetweenKeys) => {
+      dispatch(queryOptionsToggleBetweenKeys(previousShowBetweenKeys));
+    },
+    queryOptionsUpdateBetweenKeys: (newBetweenKeys) => {
+      dispatch(queryOptionsUpdateBetweenKeys(newBetweenKeys));
+    },
+    queryOptionsUpdateByKeys: (newByKeys) => {
+      dispatch(queryOptionsUpdateByKeys(newByKeys));
+    },
+    queryOptionsToggleDescending: (previousDescending) => {
+      dispatch(queryOptionsToggleDescending(previousDescending));
+    },
+    queryOptionsUpdateSkip: (newSkip) => {
+      dispatch(queryOptionsUpdateSkip(newSkip));
+    },
+    queryOptionsUpdateLimit: (newLimit) => {
+      dispatch(queryOptionsUpdateLimit(newLimit));
+    },
+    queryOptionsToggleIncludeDocs: (previousIncludeDocs) => {
+      dispatch(queryOptionsToggleIncludeDocs(previousIncludeDocs));
+    },
+    queryOptionsToggleVisibility: (newVisibility) => {
+      dispatch(queryOptionsToggleVisibility(newVisibility));
+    },
+    queryOptionsExecute: (queryOptionsParams, perPage) => {
+      dispatch(queryOptionsExecute(ownProps.fetchUrl, queryOptionsParams, perPage));
+    },
+    queryOptionsFilterOnlyDdocs: () => {
+      dispatch(queryOptionsFilterOnlyDdocs());
+    },
+    changeLayout: (newLayout) => {
+      dispatch(changeLayout(newLayout));
+    },
+    resetState: () => {
+      dispatch(resetState());
+    }
+  };
+};
+
+export default connect (
+  mapStateToProps,
+  mapDispatchToProps
+)(QueryOptions);
diff --git a/app/addons/documents/index-results/helpers/json-view.js b/app/addons/documents/index-results/helpers/json-view.js
new file mode 100644
index 0000000..89a04dd
--- /dev/null
+++ b/app/addons/documents/index-results/helpers/json-view.js
@@ -0,0 +1,35 @@
+// 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 { hasBulkDeletableDoc, getDocUrl } from "./shared-helpers";
+
+export const getJsonViewData = (docs, { databaseName, typeOfIndex }) => {
+  // expand on this when refactoring views and mango to use redux
+  const stagedResults = docs.map((doc) => {
+    return {
+      content: JSON.stringify(doc, null, ' '),
+      id: doc.id, //|| doc.key.toString(),
+      _rev: doc._rev || (doc.value && doc.value.rev),
+      header: doc.id, //|| doc.key.toString(),
+      keylabel: 'id', //doc.isFromView() ? 'key' : 'id',
+      url: doc.id ? getDocUrl('app', doc.id, databaseName) : null,
+      isDeletable: true,
+      isEditable: true
+    };
+  });
+
+  return {
+    displayedFields: null,
+    hasBulkDeletableDoc: hasBulkDeletableDoc(docs, typeOfIndex),
+    results: stagedResults
+  };
+};
diff --git a/app/addons/documents/index-results/helpers/shared-helpers.js b/app/addons/documents/index-results/helpers/shared-helpers.js
new file mode 100644
index 0000000..b21142d
--- /dev/null
+++ b/app/addons/documents/index-results/helpers/shared-helpers.js
@@ -0,0 +1,85 @@
+// 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 app from "../../../../app";
+import FauxtonAPI from "../../../../core/api";
+
+const getDocUrl = (context, id, databaseName) => {
+  if (context === undefined) {
+    context = 'server';
+  }
+
+  // new without id make a POST to the DB and not a PUT on a DB
+  let safeId = app.utils.getSafeIdForDoc(id);
+  if (!safeId) {
+    safeId = '';
+  }
+  const safeDatabaseName = encodeURIComponent(databaseName);
+
+  return FauxtonAPI.urls('document', context, safeDatabaseName, safeId, '?conflicts=true');
+};
+
+const isJSONDocEditable = (doc, docType) => {
+
+  if (!doc) {
+    return;
+  }
+
+  if (docType === 'MangoIndex') {
+    return false;
+  }
+
+  if (!Object.keys(doc).length) {
+    return false;
+  }
+
+  if (!doc._id) {
+    return false;
+  }
+
+  return true;
+};
+
+const isJSONDocBulkDeletable = (doc, docType) => {
+  if (docType === 'MangoIndex') {
+    return doc.type !== 'special';
+  }
+  const result = (doc._id || doc.id) && (doc._rev || (doc.value && doc.value.rev));
+  return !!result;
+};
+
+const hasBulkDeletableDoc = (docs, docType) => {
+  const doc = docs.find((doc) => {
+    return isJSONDocBulkDeletable(doc, docType);
+  });
+
+  return !!doc;
+};
+
+// if we've previously set the perPage in local storage, default to that.
+const getDefaultPerPage = () => {
+  if (window.localStorage) {
+    const storedPerPage = app.utils.localStorageGet('fauxton:perpageredux');
+    if (storedPerPage) {
+      return parseInt(storedPerPage, 10);
+    }
+  }
+  return FauxtonAPI.constants.MISC.DEFAULT_PAGE_SIZE;
+};
+
+export {
+  getDocUrl,
+  isJSONDocEditable,
+  isJSONDocBulkDeletable,
+  hasBulkDeletableDoc,
+  getDefaultPerPage
+};
diff --git a/app/addons/documents/index-results/helpers/table-view.js b/app/addons/documents/index-results/helpers/table-view.js
new file mode 100644
index 0000000..2bce347
--- /dev/null
+++ b/app/addons/documents/index-results/helpers/table-view.js
@@ -0,0 +1,179 @@
+// 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 Constants from "../../constants";
+import {
+    isJSONDocBulkDeletable,
+    isJSONDocEditable,
+    hasBulkDeletableDoc,
+    getDocUrl
+ } from "./shared-helpers";
+
+export const getPseudoSchema = (docs) => {
+  let cache = [];
+
+  docs.forEach((doc) => {
+    Object.keys(doc).forEach(function (k) {
+      cache.push(k);
+    });
+  });
+
+  cache = _.uniq(cache);
+
+  // always begin with _id
+  let i = cache.indexOf('_id');
+  if (i !== -1) {
+    cache.splice(i, 1);
+    cache.unshift('_id');
+  }
+
+  return cache;
+};
+
+export const getPrioritizedFields = (docs, max) => {
+  let res = docs.reduce((acc, el) => {
+    acc = acc.concat(Object.keys(el));
+    return acc;
+  }, []);
+
+  res = _.countBy(res, (el) => {
+    return el;
+  });
+
+  delete res.id;
+  delete res._rev;
+
+  res = Object.keys(res).reduce((acc, el) => {
+    acc.push([res[el], el]);
+    return acc;
+  }, []);
+
+  res = sortByTwoFields(res);
+  res = res.slice(0, max);
+
+  return res.reduce((acc, el) => {
+    acc.push(el[1]);
+    return acc;
+  }, []);
+};
+
+export const sortByTwoFields = (elements) => {
+  // given:
+  // var a = [[2, "b"], [3, "z"], [1, "a"], [3, "a"]]
+  // it sorts to:
+  // [[3, "a"], [3, "z"], [2, "b"], [1, "a"]]
+  // note that the arrays with 3 got the first two arrays
+  // _and_ that the second values in the array with 3 are also sorted
+
+  const _recursiveSort = (a, b, index) => {
+    if (a[index] === b[index]) {
+      return index < 2 ? _recursiveSort(a, b, index + 1) : 0;
+    }
+
+    // second elements asc
+    if (index === 1) {
+      return (a[index] < b[index]) ? -1 : 1;
+    }
+
+    // first elements desc
+    return (a[index] < b[index]) ? 1 : -1;
+  };
+
+  return elements.sort((a, b) => {
+    return _recursiveSort(a, b, 0);
+  });
+};
+
+export const getNotSelectedFields = (selectedFields, allFields) => {
+  const without = _.without.bind(this, allFields);
+  return without.apply(this, selectedFields);
+};
+
+export const getFullTableViewData = (docs, options) => {
+  let notSelectedFieldsTableView = null,
+      selectedFieldsTableView = options.selectedFieldsTableView,
+      showAllFieldsTableView = options.showAllFieldsTableView,
+      schema;  // array containing the unique attr keys in the results.  always begins with _id.
+
+  // only use the "doc" attribute as this resulted from an include_docs fetch
+  const normalizedDocs = docs.map((doc) => { return doc.doc || doc; });
+  // build the schema container based on the normalized data
+  schema = getPseudoSchema(normalizedDocs);
+
+  // if we don't know what attr/columns to display, build the list
+  if (selectedFieldsTableView && selectedFieldsTableView.length === 0) {
+    selectedFieldsTableView = getPrioritizedFields(normalizedDocs, 5);
+  }
+
+  // set the notSelectedFields to the subset excluding meta and selected attributes
+  const schemaWithoutMetaDataFields = _.without(schema, '_attachments');
+  notSelectedFieldsTableView = getNotSelectedFields(selectedFieldsTableView, schemaWithoutMetaDataFields);
+
+  // if we're showing all attr/columns, we revert the notSelectedFields to null and set
+  // the selected fields to everything excluding meta.
+  if (showAllFieldsTableView) {
+    notSelectedFieldsTableView = null;
+    selectedFieldsTableView = schemaWithoutMetaDataFields;
+  }
+
+  return {
+    schema,
+    normalizedDocs,
+    selectedFieldsTableView,
+    notSelectedFieldsTableView
+  };
+};
+
+export const getMetaDataTableView = (docs) => {
+  const schema = getPseudoSchema(docs);
+  return {
+    schema,
+    normalizedDocs: docs,  // no need to massage the docs for metadata
+    selectedFieldsTableView: schema,
+    notSelectedFieldsTableView: null
+  };
+};
+
+export const getTableViewData = (docs, options) => {
+  const isMetaData = Constants.LAYOUT_ORIENTATION.METADATA === options.selectedLayout;
+  const {
+    schema,
+    normalizedDocs,
+    selectedFieldsTableView,
+    notSelectedFieldsTableView
+  } = isMetaData ? getMetaDataTableView(docs) : getFullTableViewData(docs, options);
+
+  const res = normalizedDocs.map(function (doc) {
+    return {
+      content: doc,
+      id: doc._id || doc.id, // inconsistent apis for GET between mango and views
+      _rev: doc._rev || doc.value.rev,
+      header: '',
+      keylabel: '',
+      url: doc._id || doc.id ? getDocUrl('app', doc._id || doc.id, options.databaseName) : null,
+      isDeletable: isJSONDocBulkDeletable(doc, options.typeOfIndex),
+      isEditable: isJSONDocEditable(doc, options.typeOfIndex)
+    };
+  });
+
+  return {
+    notSelectedFields: notSelectedFieldsTableView,
+    selectedFields: selectedFieldsTableView,
+    hasBulkDeletableDoc: hasBulkDeletableDoc(normalizedDocs),
+    schema: schema,
+    results: res,
+    displayedFields: isMetaData ? null : {
+      shown: _.uniq(selectedFieldsTableView).length,
+      allFieldCount: _.without(schema, '_attachments').length
+    }
+  };
+};
diff --git a/app/addons/documents/index-results/index-results.components.js b/app/addons/documents/index-results/index-results.components.js
index 2b1563f..26dd135 100644
--- a/app/addons/documents/index-results/index-results.components.js
+++ b/app/addons/documents/index-results/index-results.components.js
@@ -15,11 +15,13 @@ import React from "react";
 import Stores from "./stores";
 import Actions from "./actions";
 import Components from "../../components/react-components";
+import Constants from "../constants";
 import ReactSelect from "react-select";
+import {ResultsToolBar} from "../components/results-toolbar";
 import "../../../../assets/js/plugins/prettify";
 import uuid from 'uuid';
 
-const {LoadLines, BulkActionComponent, Copy} = Components;
+const {LoadLines, Copy} = Components;
 const store  = Stores.indexResultsStore;
 
 var NoResultsScreen = React.createClass({
@@ -43,7 +45,8 @@ var TableRow = React.createClass({
     docChecked: React.PropTypes.func.isRequired,
     isSelected: React.PropTypes.bool.isRequired,
     index: React.PropTypes.number.isRequired,
-    data: React.PropTypes.object.isRequired
+    data: React.PropTypes.object.isRequired,
+    onClick: React.PropTypes.func.isRequired
   },
 
   onChange: function () {
@@ -65,7 +68,7 @@ var TableRow = React.createClass({
       var stringified = typeof el[k] === 'object' ? JSON.stringify(el[k], null, '  ') : el[k];
 
       return (
-        <td key={key} title={stringified}>
+        <td key={key} title={stringified} onClick={this.onClick}>
           {stringified}
         </td>
       );
@@ -74,33 +77,6 @@ var TableRow = React.createClass({
     return row;
   },
 
-  maybeGetSpecialField: function (element, i) {
-    if (!this.props.data.hasMetadata) {
-      return null;
-    }
-
-    var el = element.content;
-
-    return (
-      <td className="tableview-data-cell-id" key={'tableview-data-cell-id' + i}>
-        <div>{this.maybeGetUrl(element.url, el._id || el.id)}</div>
-        <div>{el._rev}</div>
-      </td>
-    );
-  },
-
-  maybeGetUrl: function (url, stringified) {
-    if (!url) {
-      return stringified;
-    }
-
-    return (
-      <a href={'#' + url}>
-        {stringified}
-      </a>
-    );
-  },
-
   maybeGetCheckboxCell: function (el, i) {
     return (
       <td className="tableview-checkbox-cell" key={"tableview-checkbox-cell-" + i}>
@@ -144,7 +120,7 @@ var TableRow = React.createClass({
     }
 
     return (
-      <td className="tableview-el-last">
+      <td className="tableview-el-last" onClick={this.onClick}>
         {conflictIndicator}
         {attachmentIndicator}
       </td>
@@ -172,6 +148,10 @@ var TableRow = React.createClass({
     });
   },
 
+  onClick: function (e) {
+    this.props.onClick(this.props.el._id, this.props.el, e);
+  },
+
   render: function () {
     var i = this.props.index;
     var docContent = this.props.el.content;
@@ -181,7 +161,6 @@ var TableRow = React.createClass({
       <tr key={"tableview-content-row-" + i}>
         {this.maybeGetCheckboxCell(el, i)}
         {this.getCopyButton(docContent)}
-        {this.maybeGetSpecialField(el, i)}
         {this.getRowContents(el, i)}
         {this.getAdditionalInfoRow(docContent)}
       </tr>
@@ -189,11 +168,25 @@ var TableRow = React.createClass({
   }
 });
 
-const WrappedAutocomplete = ({selectedField, notSelectedFields, index}) => {
+const WrappedAutocomplete = ({selectedField, notSelectedFields, index, changeField, selectedFields}) => {
   const options = notSelectedFields.map((el) => {
     return {value: el, label: el};
   });
 
+  const onChange = (el) => {
+    const newField = {
+      newSelectedRow: el.value,
+      index: index
+    };
+
+    // changeField will be undefined for non-redux components
+    if (changeField) {
+      changeField(newField, selectedFields);
+    } else {
+      Actions.changeField(newField);
+    }
+  };
+
   return (
     <div className="table-container-autocomplete">
       <div className="table-select-wrapper">
@@ -201,12 +194,7 @@ const WrappedAutocomplete = ({selectedField, notSelectedFields, index}) => {
           value={selectedField}
           options={options}
           clearable={false}
-          onChange={(el) => {
-            Actions.changeField({
-              newSelectedRow: el.value,
-              index: index
-            });
-          }} />
+          onChange={onChange} />
       </div>
     </div>
   );
@@ -222,6 +210,7 @@ var TableView = React.createClass({
 
       return (
         <TableRow
+          onClick={this.props.onClick}
           key={"tableview-row-component-" + i}
           index={i}
           el={el}
@@ -234,7 +223,7 @@ var TableView = React.createClass({
   },
 
   getOptionFieldRows: function (filtered) {
-    var notSelectedFields = this.props.data.notSelectedFields;
+    const notSelectedFields = this.props.data.notSelectedFields;
 
     if (!notSelectedFields) {
       return filtered.map(function (el, i) {
@@ -245,50 +234,38 @@ var TableView = React.createClass({
     return filtered.map(function (el, i) {
       return (
         <th key={'header-el-' + i}>
-          {this.getDropdown(el, this.props.data.schema, i)}
+          {this.getDropdown(
+            el,
+            this.props.data.schema,
+            i,
+            this.props.changeField,
+            this.props.data.selectedFields
+          )}
         </th>
       );
     }.bind(this));
   },
 
-  getDropdown: function (selectedField, notSelectedFields, i) {
+  getDropdown: function (selectedField, notSelectedFields, i, changeField, selectedFields) {
 
     return (
       <WrappedAutocomplete
         selectedField={selectedField}
         notSelectedFields={notSelectedFields}
-        index={i} />
+        index={i}
+        changeField={changeField}
+        selectedFields={selectedFields} />
     );
   },
 
   getHeader: function () {
     var selectedFields = this.props.data.selectedFields;
-
-    var specialField = null;
-    if (this.props.data.hasMetadata) {
-      specialField = (<th key="header-el-metadata" title="Metadata">Metadata</th>);
-    }
-
     var row = this.getOptionFieldRows(selectedFields);
 
-    var box = (
-      <th className="tableview-header-el-checkbox" key="tableview-header-el-checkbox">
-        {this.props.isListDeletable ? <BulkActionComponent
-          disabled={this.props.isLoading}
-          removeItem={this.props.removeItem}
-          isChecked={this.props.isChecked}
-          hasSelectedItem={this.props.hasSelectedItem}
-          toggleSelect={this.props.toggleSelect}
-          title="Select all docs that can be..." /> : null}
-      </th>
-    );
-
-
     return (
       <tr key="tableview-content-row-header">
-        {box}
+        <th className="tableview-header-el-checkbox"></th>
         <th className="tableview-el-copy"></th>
-        {specialField}
         {row}
         <th className="tableview-el-last"></th>
       </tr>
@@ -317,7 +294,7 @@ var TableView = React.createClass({
 
 var ResultsScreen = React.createClass({
 
-  onDoubleClick: function (id, doc) {
+  onClick: function (id, doc) {
     FauxtonAPI.navigate(doc.url);
   },
 
@@ -341,7 +318,7 @@ var ResultsScreen = React.createClass({
        <Components.Document
          key={doc.id + i}
          doc={doc}
-         onDoubleClick={this.props.isEditable ? this.onDoubleClick : noop}
+         onClick={this.props.isEditable ? this.onClick : noop}
          keylabel={doc.keylabel}
          docContent={doc.content}
          checked={this.props.isSelected(doc.id)}
@@ -355,9 +332,8 @@ var ResultsScreen = React.createClass({
     }, this);
   },
 
-  getDocumentStyleView: function (loadLines) {
-    var classNames = 'view';
-    var isDeletable = this.props.isListDeletable;
+  getDocumentStyleView: function () {
+    let classNames = 'view';
 
     if (this.props.isListDeletable) {
       classNames += ' show-select';
@@ -365,33 +341,18 @@ var ResultsScreen = React.createClass({
 
     return (
       <div className={classNames}>
-        <div className="loading-lines-wrapper">
-          {loadLines}
-        </div>
-
         <div id="doc-list">
-          {isDeletable ? <BulkActionComponent
-            removeItem={this.props.removeItem}
-            isChecked={this.props.allDocumentsSelected}
-            hasSelectedItem={this.props.hasSelectedItem}
-            toggleSelect={this.toggleSelectAll}
-            disabled={this.props.isLoading}
-            title="Select all docs that can be..." /> : null}
-
-            {this.getDocumentList()}
+          {this.getDocumentList()}
         </div>
       </div>
     );
   },
 
-  getTableStyleView: function (loadLines) {
+  getTableStyleView: function () {
     return (
       <div>
-        <div className="loading-lines-wrapper">
-          {loadLines}
-        </div>
-
         <TableView
+          onClick={this.onClick}
           docChecked={this.props.docChecked}
           isSelected={this.props.isSelected}
           isListDeletable={this.props.isListDeletable}
@@ -402,23 +363,30 @@ var ResultsScreen = React.createClass({
           isChecked={this.props.allDocumentsSelected}
           hasSelectedItem={this.props.hasSelectedItem}
           toggleSelect={this.toggleSelectAll}
+          changeField={this.props.changeTableHeaderAttribute}
           title="Select all docs that can be..." />
       </div>
     );
   },
 
   render: function () {
-
-    var loadLines = null;
-    var isTableView = this.props.isTableView;
+    let mainView = null;
+    const { toggleSelectAll } = this.props;
+    let toolbar = <ResultsToolBar toggleSelectAll={toggleSelectAll || this.toggleSelectAll} {...this.props} />;
 
     if (this.props.isLoading) {
-      loadLines = <LoadLines />;
+      mainView = <div className="loading-lines-wrapper"><LoadLines /></div>;
+    } else if (!this.props.hasResults) {
+      mainView = <NoResultsScreen text={this.props.textEmptyIndex}/>;
+    } else if (this.props.selectedLayout === Constants.LAYOUT_ORIENTATION.JSON) {
+      mainView = this.getDocumentStyleView();
+    } else {
+      mainView = this.getTableStyleView();
     }
 
-    var mainView = isTableView ? this.getTableStyleView(loadLines) : this.getDocumentStyleView(loadLines);
     return (
       <div className="document-result-screen">
+        {toolbar}
         {mainView}
       </div>
     );
@@ -446,14 +414,15 @@ var ViewResultListController = React.createClass({
   },
 
   getStoreState: function () {
-    var selectedItemsLength = store.getSelectedItemsLength();
+    const selectedItemsLength = store.getSelectedItemsLength();
+    const isLoading = store.isLoading();
     return {
       hasResults: store.hasResults(),
-      results: store.getResults(),
-      isLoading: store.isLoading(),
+      results: isLoading ? {} : store.getResults(),
+      isLoading: isLoading,
       isEditable: store.isEditable(),
       textEmptyIndex: store.getTextEmptyIndex(),
-      isTableView: store.getIsTableView(),
+      selectedLayout: store.getSelectedLayout(),
       allDocumentsSelected: store.areAllDocumentsSelected(),
       hasSelectedItem: !!selectedItemsLength,
       selectedItemsLength: selectedItemsLength,
@@ -493,10 +462,8 @@ var ViewResultListController = React.createClass({
   },
 
   render: function () {
-    var view = <NoResultsScreen text={this.state.textEmptyIndex}/>;
-
-    if (this.state.hasResults) {
-      view = <ResultsScreen
+    return (
+      <ResultsScreen
         removeItem={this.removeItem}
         hasSelectedItem={this.state.hasSelectedItem}
         allDocumentsSelected={this.state.allDocumentsSelected}
@@ -505,17 +472,14 @@ var ViewResultListController = React.createClass({
         isListDeletable={this.state.results.hasBulkDeletableDoc}
         docChecked={this.docChecked}
         isLoading={this.state.isLoading}
+        hasResults={this.state.hasResults}
+        textEmptyIndex={this.state.textEmptyIndex}
         results={this.state.results}
-        isTableView={this.state.isTableView} />;
-    }
-
-    return (
-      view
+        selectedLayout={this.state.selectedLayout} />
     );
   }
 });
 
-
 export default {
   List: ViewResultListController,
   NoResultsScreen: NoResultsScreen,
diff --git a/app/addons/documents/index-results/reducers.js b/app/addons/documents/index-results/reducers.js
new file mode 100644
index 0000000..6c38011
--- /dev/null
+++ b/app/addons/documents/index-results/reducers.js
@@ -0,0 +1,306 @@
+// 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 app from "../../../app";
+import ActionTypes from './actiontypes';
+import Constants from '../constants';
+import { getJsonViewData } from './helpers/json-view';
+import { getTableViewData } from './helpers/table-view';
+import { getDefaultPerPage } from './helpers/shared-helpers';
+
+const initialState = {
+  docs: [],  // raw documents returned from couch
+  selectedDocs: [],  // documents selected for manipulation
+  isLoading: false,
+  tableView: {
+    selectedFieldsTableView: [],  // current columns to display
+    showAllFieldsTableView: false, // do we show all possible columns?
+  },
+  isEditable: true,  // can the user manipulate the results returned?
+  selectedLayout: Constants.LAYOUT_ORIENTATION.METADATA,
+  textEmptyIndex: 'No Documents Found',
+  typeOfIndex: 'view',
+  fetchParams: {
+    limit: getDefaultPerPage() + 1,
+    skip: 0
+  },
+  pagination: {
+    pageStart: 1,  // index of first doc in this page of results
+    currentPage: 1,  // what page of results are we showing?
+    perPage: getDefaultPerPage(),
+    canShowNext: false  // flag indicating if we can show a next page
+  },
+  queryOptionsPanel: {
+    isVisible: false,
+    showByKeys: false,
+    showBetweenKeys: false,
+    includeDocs: false,
+    betweenKeys: {
+      include: true,
+      startkey: '',
+      endkey: ''
+    },
+    byKeys: '',
+    descending: false,
+    skip: '',
+    limit: 'none',
+    reduce: false,
+    groupLevel: 'exact',
+    showReduce: false
+  }
+};
+
+export default function resultsState (state = initialState, action) {
+  switch (action.type) {
+
+    case ActionTypes.INDEX_RESULTS_REDUX_RESET_STATE:
+      return Object.assign({}, initialState, {
+        fetchParams: {
+          limit: getDefaultPerPage() + 1,
+          skip: 0
+        },
+        pagination: Object.assign({}, initialState.pagination, {
+          perPage: state.pagination.perPage
+        })
+      });
+
+    case ActionTypes.INDEX_RESULTS_REDUX_IS_LOADING:
+      return Object.assign({}, state, {
+        isLoading: true
+      });
+
+    case ActionTypes.INDEX_RESULTS_REDUX_NEW_SELECTED_DOCS:
+      return Object.assign({}, state, {
+        selectedDocs: action.selectedDocs
+      });
+
+    case ActionTypes.INDEX_RESULTS_REDUX_NEW_RESULTS:
+      return Object.assign({}, state, {
+        docs: action.docs,
+        isLoading: false,
+        isEditable: true, //TODO: determine logic for this
+        fetchParams: Object.assign({}, state.fetchParams, action.params),
+        pagination: Object.assign({}, state.pagination, {
+          canShowNext: action.canShowNext
+        })
+      });
+
+    case ActionTypes.INDEX_RESULTS_REDUX_CHANGE_LAYOUT:
+      return Object.assign({}, state, {
+        selectedLayout: action.layout
+      });
+
+    case ActionTypes.INDEX_RESULTS_REDUX_TOGGLE_SHOW_ALL_COLUMNS:
+      return Object.assign({}, state, {
+        tableView: Object.assign({}, state.tableView, {
+          showAllFieldsTableView: !state.tableView.showAllFieldsTableView,
+          cachedFieldsTableView: state.tableView.selectedFieldsTableView
+        })
+      });
+
+    case ActionTypes.INDEX_RESULTS_REDUX_CHANGE_TABLE_HEADER_ATTRIBUTE:
+      return Object.assign({}, state, {
+        tableView: Object.assign({}, state.tableView, {
+          selectedFieldsTableView: action.selectedFieldsTableView
+        })
+      });
+
+    case ActionTypes.INDEX_RESULTS_REDUX_SET_PER_PAGE:
+      app.utils.localStorageSet('fauxton:perpageredux', action.perPage);
+      return Object.assign({}, state, {
+        pagination: Object.assign({}, initialState.pagination, {
+          perPage: action.perPage
+        })
+      });
+
+    case ActionTypes.INDEX_RESULTS_REDUX_PAGINATE_NEXT:
+      return Object.assign({}, state, {
+        pagination: Object.assign({}, state.pagination, {
+          pageStart: state.pagination.pageStart + state.pagination.perPage,
+          currentPage: state.pagination.currentPage + 1
+        })
+      });
+
+    case ActionTypes.INDEX_RESULTS_REDUX_PAGINATE_PREVIOUS:
+      return Object.assign({}, state, {
+        pagination: Object.assign({}, state.pagination, {
+          pageStart: state.pagination.pageStart - state.pagination.perPage,
+          currentPage: state.pagination.currentPage - 1
+        })
+      });
+
+    case ActionTypes.INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS:
+      return Object.assign({}, state, {
+        queryOptionsPanel: Object.assign({}, state.queryOptionsPanel, action.options)
+      });
+
+    default:
+      return state;
+  }
+};
+
+// we don't want to muddy the waters with autogenerated mango docs
+export const removeGeneratedMangoDocs = (doc) => {
+  return doc.language !== 'query';
+};
+
+// transform the docs in to a state ready for rendering on the page
+export const getDataForRendering = (state, databaseName) => {
+  const { docs } = state;
+  const options = {
+    databaseName: databaseName,
+    selectedLayout: state.selectedLayout,
+    selectedFieldsTableView: state.tableView.selectedFieldsTableView,
+    showAllFieldsTableView: state.tableView.showAllFieldsTableView,
+    typeOfIndex: state.typeOfIndex
+  };
+
+  const docsWithoutGeneratedMangoDocs = docs.filter(removeGeneratedMangoDocs);
+
+  if (Constants.LAYOUT_ORIENTATION.JSON === options.selectedLayout) {
+    return getJsonViewData(docsWithoutGeneratedMangoDocs, options);
+  } else {
+    return getTableViewData(docsWithoutGeneratedMangoDocs, options);
+  }
+};
+
+// Should we show the input checkbox where the user can elect to display
+// all possible columns in the table view?
+export const getShowPrioritizedEnabled = (state) => {
+  return state.selectedLayout === Constants.LAYOUT_ORIENTATION.TABLE;
+};
+
+// returns the index of the last result in the total possible results.
+export const getPageEnd = (state) => {
+  if (!getHasResults(state)) {
+    return false;
+  }
+  return state.pagination.pageStart + state.docs.length - 1;
+};
+
+// do we have any docs in the state tree currently?
+export const getHasResults = (state) => {
+  return !state.isLoading && state.docs.length > 0;
+};
+
+// helper function to determine if all the docs on the current page are selected.
+export const getAllDocsSelected = (state) => {
+  if (state.docs.length === 0 || state.selectedDocs.length === 0) {
+    return false;
+  }
+
+  // Iterate over the results and determine if each one is included
+  // in the selectedDocs array.
+  //
+  // This is O(n^2) which makes me unhappy.  We know
+  // that the number of docs will never be that large due to the
+  // per page limitations we force on the user.
+  //
+  // We need to use a for loop here instead of a forEach since there
+  // is no way to short circuit Array.prototype.forEach.
+
+  for (let i = 0; i < state.docs.length; i++) {
+    const doc = state.docs[i];
+
+    // Helper function for finding index of a doc in the current
+    // selected docs list.
+    const exists = (selectedDoc) => {
+      return doc._id || doc.id === selectedDoc._id;
+    };
+
+    if (!state.selectedDocs.some(exists)) {
+      return false;
+    }
+  }
+  return true;
+};
+
+// are there any documents selected in the state tree?
+export const getHasDocsSelected = (state) => {
+  return state.selectedDocs.length > 0;
+};
+
+// how many documents are selected in the state tree?
+export const getNumDocsSelected = (state) => {
+  return state.selectedDocs.length;
+};
+
+// is there a previous page of results?  We only care if the current page
+// of results is greater than 1 (i.e. the first page of results).
+export const getCanShowPrevious = (state) => {
+  return state.pagination.currentPage > 1;
+};
+
+export const getDisplayedFields = (state, databaseName)  => {
+  return getDataForRendering(state, databaseName).displayedFields || {};
+};
+
+export const getQueryOptionsParams = (state) => {
+  const { queryOptionsPanel } = state;
+  const params = {};
+
+  if (queryOptionsPanel.includeDocs) {
+    params.include_docs = queryOptionsPanel.includeDocs;
+  }
+
+  if (queryOptionsPanel.showBetweenKeys) {
+    const betweenKeys = queryOptionsPanel.betweenKeys;
+    params.inclusive_end = betweenKeys.include;
+    if (betweenKeys.startkey && betweenKeys.startkey != '') {
+      params.start_key = betweenKeys.startkey;
+    }
+    if (betweenKeys.endkey && betweenKeys.endkey != '') {
+      params.end_key = betweenKeys.endkey;
+    }
+  } else if (queryOptionsPanel.showByKeys) {
+    params.keys = queryOptionsPanel.byKeys.replace(/\r?\n/g, '');
+  }
+
+  if (queryOptionsPanel.limit !== 'none') {
+    params.limit = parseInt(queryOptionsPanel.limit, 10);
+  }
+
+  if (queryOptionsPanel.skip) {
+    params.skip = parseInt(queryOptionsPanel.skip, 10);
+  }
+
+  if (queryOptionsPanel.descending) {
+    params.descending = queryOptionsPanel.descending;
+  }
+
+  if (queryOptionsPanel.reduce) {
+    params.reduce = true;
+
+    if (queryOptionsPanel.groupLevel === 'exact') {
+      params.group = true;
+    } else {
+      params.group_level = queryOptionsPanel.groupLevel;
+    }
+  }
+
+  return params;
+};
+
+// Here be simple getters
+export const getDocs = state => state.docs;
+export const getSelectedDocs = state => state.selectedDocs;
+export const getIsLoading = state => state.isLoading;
+export const getIsEditable = state => state.isEditable;
+export const getSelectedLayout = state => state.selectedLayout;
+export const getTextEmptyIndex = state => state.textEmptyIndex;
+export const getTypeOfIndex = state => state.typeOfIndex;
+export const getPageStart = state => state.pagination.pageStart;
+export const getPrioritizedEnabled = state => state.tableView.showAllFieldsTableView;
+export const getCanShowNext = state => state.pagination.canShowNext;
+export const getQueryOptionsPanel = state => state.queryOptionsPanel;
+export const getPerPage = state => state.pagination.perPage;
+export const getFetchParams = state => state.fetchParams;
diff --git a/app/addons/documents/index-results/stores.js b/app/addons/documents/index-results/stores.js
index eb1856f..821e70c 100644
--- a/app/addons/documents/index-results/stores.js
+++ b/app/addons/documents/index-results/stores.js
@@ -17,6 +17,7 @@ import HeaderActionTypes from "../header/header.actiontypes";
 import PaginationActionTypes from "../pagination/actiontypes";
 import MangoHelper from "../mango/mango.helper";
 import Resources from "../resources";
+import Constants from "../constants";
 
 var Stores = {};
 
@@ -45,7 +46,7 @@ Stores.IndexResultsStore = FauxtonAPI.Store.extend({
     this._isPrioritizedEnabled = false;
 
     this._tableSchema = [];
-    this._tableView = false;
+    this._selectedLayout = Constants.LAYOUT_ORIENTATION.METADATA;
 
     this.resetPagination();
   },
@@ -73,7 +74,6 @@ Stores.IndexResultsStore = FauxtonAPI.Store.extend({
   canShowPrevious: function () {
     if (!this._enabled) { return false; }
     if (!this._collection || !this._collection.hasPrevious) { return false; }
-
     return this._collection.hasPrevious();
   },
 
@@ -161,6 +161,10 @@ Stores.IndexResultsStore = FauxtonAPI.Store.extend({
     return this._collection.length;
   },
 
+  setPageStart: function (options) {
+    this._pageStart = options.start + 1;
+  },
+
   getPageStart: function () {
     return this._pageStart;
   },
@@ -187,6 +191,15 @@ Stores.IndexResultsStore = FauxtonAPI.Store.extend({
       this._typeOfIndex = options.typeOfIndex;
     }
 
+    // layout shifting magic to support refreshes, query options, and results toolbar
+    if (this.getIsMetadataView() && (this.isIncludeDocsEnabled() || this.getIsMangoResults())) {
+      this._selectedLayout = Constants.LAYOUT_ORIENTATION.TABLE;
+    }
+
+    if (!this.getIsMetadataView() && !this.isIncludeDocsEnabled() && !this.getIsMangoResults()) {
+      this._selectedLayout = Constants.LAYOUT_ORIENTATION.METADATA;
+    }
+
     this._cachedSelected = [];
 
     this._filteredCollection = this._collection.filter(filterOutGeneratedMangoDocs);
@@ -246,6 +259,22 @@ Stores.IndexResultsStore = FauxtonAPI.Store.extend({
     return this._bulkDeleteDocCollection;
   },
 
+  setCachedOffset: function (options) {
+    this._cachedOffset = options.offset;
+  },
+
+  getCachedOffset: function () {
+    return this._cachedOffset;
+  },
+
+  hasCachedOffset: function () {
+    return !!this._cachedOffset;
+  },
+
+  deleteCachedOffset: function () {
+    delete this._cachedOffset;
+  },
+
   getDocContent: function (originalDoc) {
     var doc = originalDoc.toJSON();
 
@@ -305,16 +334,16 @@ Stores.IndexResultsStore = FauxtonAPI.Store.extend({
   },
 
   getResults: function () {
-    var hasBulkDeletableDoc;
-    var res;
-
-    // Table sytle view
-    if (this.getIsTableView()) {
+    if (this._selectedLayout === Constants.LAYOUT_ORIENTATION.JSON) {
+      return this.getJsonViewData();
+    } else {
       return this.getTableViewData();
     }
+  },
 
+  getJsonViewData: function () {
     // JSON style views
-    res = this._filteredCollection
+    const res = this._filteredCollection
       .map(function (doc, i) {
         if (doc.get('def') || this.isGhostDoc(doc)) {
           return this.getMangoDoc(doc, i);
@@ -331,11 +360,9 @@ Stores.IndexResultsStore = FauxtonAPI.Store.extend({
         };
       }, this);
 
-    hasBulkDeletableDoc = this.hasBulkDeletableDoc(this._filteredCollection);
-
     return {
       displayedFields: this.getDisplayCountForTableView(),
-      hasBulkDeletableDoc: hasBulkDeletableDoc,
+      hasBulkDeletableDoc: this.hasBulkDeletableDoc(this._filteredCollection),
       results: res
     };
   },
@@ -388,7 +415,6 @@ Stores.IndexResultsStore = FauxtonAPI.Store.extend({
       return el;
     });
 
-    delete res._id;
     delete res.id;
     delete res._rev;
 
@@ -453,38 +479,16 @@ Stores.IndexResultsStore = FauxtonAPI.Store.extend({
       return null;
     }
 
-    if (!this.isIncludeDocsEnabled()) {
-      return null;
-    }
-
     shownCount = _.uniq(this._tableViewSelectedFields).length;
-
-    allFieldCount = this._tableSchema.length;
-    if (_.includes(this._tableSchema, '_id', '_rev')) {
-      allFieldCount = allFieldCount - 1;
-    }
-
-    if (_.includes(this._tableSchema, '_id', '_rev')) {
-      shownCount = shownCount + 1;
-    }
+    allFieldCount = _.without(this._tableSchema, '_attachments').length;
 
     return {shown: shownCount, allFieldCount: allFieldCount};
   },
 
   getTableViewData: function () {
-    var res;
-    var schema;
-    var hasIdOrRev;
-    var hasIdOrRev;
-    var prioritizedFields;
-    var hasBulkDeletableDoc;
-    var database = this.getDatabase();
-    var isView = !!this._collection.view;
-
     // softmigration remove backbone
-    var data;
-    var collectionType = this._collection.collectionType;
-    data = this._filteredCollection.map(function (el) {
+    const collectionType = this._collection.collectionType;
+    let data = this._filteredCollection.map(el => {
       return fixDocIdForMango(el.toJSON(), collectionType);
     });
 
@@ -528,50 +532,53 @@ Stores.IndexResultsStore = FauxtonAPI.Store.extend({
 
     // softmigration end
 
-    var isIncludeDocsEnabled = this.isIncludeDocsEnabled();
-    var notSelectedFields = null;
-    if (isIncludeDocsEnabled) {
+    let notSelectedFields = null;
+    let schema;  // array containing the unique attr keys in the results.  always begins with _id.
 
+    if (this.isIncludeDocsEnabled() || this.getIsMangoResults()) {
+      const isView = !!this._collection.view;
+      // remove "cruft" we don't want to display in the results
       data = this.normalizeTableData(data, isView);
+      // build the schema container based on the normalized data
       schema = this.getPseudoSchema(data);
-      hasIdOrRev = this.hasIdOrRev(schema);
 
+      // if we're showing a subset of the attr/columns in the table, set the selected fields
+      // to the previously cached fields if they exist.
       if (!this._isPrioritizedEnabled) {
         this._tableViewSelectedFields = this._cachedSelected || [];
       }
 
+      // if we still don't know what attr/columns to display, build the list and update the
+      // cached fields for the next time.
       if (this._tableViewSelectedFields.length === 0) {
-        prioritizedFields = this.getPrioritizedFields(data, hasIdOrRev ? 4 : 5);
-        this._tableViewSelectedFields = prioritizedFields;
+        this._tableViewSelectedFields = this.getPrioritizedFields(data, 5);
         this._cachedSelected = this._tableViewSelectedFields;
       }
 
-      var schemaWithoutMetaDataFields = _.without(schema, '_id', '_rev', '_attachment');
+      // set the notSelectedFields to the subset excluding meta and selected attributes
+      const schemaWithoutMetaDataFields = _.without(schema, '_attachments');
       notSelectedFields = this.getNotSelectedFields(this._tableViewSelectedFields, schemaWithoutMetaDataFields);
 
+      // if we're showing all attr/columns, we revert the notSelectedFields to null and set
+      // the selected fields to everything excluding meta.
       if (this._isPrioritizedEnabled) {
         notSelectedFields = null;
         this._tableViewSelectedFields = schemaWithoutMetaDataFields;
       }
 
-
     } else {
+      // METADATA view.
+      // Build the schema based on the original data and then remove _attachment and value meta
+      // attributes.
       schema = this.getPseudoSchema(data);
-      this._tableViewSelectedFields = _.without(schema, '_id', '_rev', '_attachment');
+      this._tableViewSelectedFields = _.without(schema, '_attachments');
     }
 
     this._notSelectedFields = notSelectedFields;
     this._tableSchema = schema;
 
-    var dbId = database.safeID();
-
-    res = data.map(function (doc) {
-      var safeId = app.utils.getSafeIdForDoc(doc._id || doc.id); // inconsistent apis for GET between mango and views
-      var url;
-
-      if (safeId) {
-        url = FauxtonAPI.urls('document', 'app', dbId, safeId);
-      }
+    const res = data.map(function (doc) {
+      const safeId = app.utils.getSafeIdForDoc(doc._id || doc.id); // inconsistent apis for GET between mango and views
 
       return {
         content: doc,
@@ -579,19 +586,16 @@ Stores.IndexResultsStore = FauxtonAPI.Store.extend({
         _rev: doc._rev,
         header: '',
         keylabel: '',
-        url: url,
+        url: safeId ? FauxtonAPI.urls('document', 'app', this.getDatabase().safeID(), safeId) : '',
         isDeletable: isJSONDocBulkDeletable(doc, collectionType),
         isEditable: isJSONDocEditable(doc, collectionType)
       };
     }.bind(this));
 
-    hasBulkDeletableDoc = this.hasBulkDeletableDoc(this._filteredCollection);
-
     return {
       notSelectedFields: notSelectedFields,
-      hasMetadata: this.getHasMetadata(schema),
       selectedFields: this._tableViewSelectedFields,
-      hasBulkDeletableDoc: hasBulkDeletableDoc,
+      hasBulkDeletableDoc: this.hasBulkDeletableDoc(this._filteredCollection),
       schema: schema,
       results: res,
       displayedFields: this.getDisplayCountForTableView(),
@@ -626,7 +630,7 @@ Stores.IndexResultsStore = FauxtonAPI.Store.extend({
   },
 
   hasResults: function () {
-    if (this.isLoading()) { return this.isLoading(); }
+    if (this.isLoading()) { return !this.isLoading(); }
     return this._collection.length > 0;
   },
 
@@ -727,19 +731,24 @@ Stores.IndexResultsStore = FauxtonAPI.Store.extend({
     return this.getSelectedItemsLength() > 0;
   },
 
-  toggleTableView: function (options) {
-    var enableTableView = options.enable;
-
-    if (enableTableView) {
-      this._tableView = true;
-      return;
-    }
+  toggleLayout: function (options) {
+    this._selectedLayout = options.layout;
+  },
 
-    this._tableView = false;
+  getSelectedLayout: function () {
+    return this._selectedLayout;
   },
 
   getIsTableView: function () {
-    return this._tableView;
+    return this._selectedLayout === Constants.LAYOUT_ORIENTATION.TABLE;
+  },
+
+  getIsMetadataView: function () {
+    return this._selectedLayout === Constants.LAYOUT_ORIENTATION.METADATA;
+  },
+
+  getIsMangoResults: function () {
+    return this._typeOfIndex === 'mango';
   },
 
   getIsPrioritizedEnabled: function () {
@@ -756,7 +765,7 @@ Stores.IndexResultsStore = FauxtonAPI.Store.extend({
   },
 
   getShowPrioritizedFieldToggler: function () {
-    return this.isIncludeDocsEnabled() && this.getIsTableView();
+    return (this.isIncludeDocsEnabled() || this.getIsMangoResults()) && this.getIsTableView();
   },
 
   clearResultsBeforeFetch: function () {
@@ -794,8 +803,8 @@ Stores.IndexResultsStore = FauxtonAPI.Store.extend({
         this.togglePrioritizedTableView();
       break;
 
-      case HeaderActionTypes.TOGGLE_TABLEVIEW:
-        this.toggleTableView(action.options);
+      case HeaderActionTypes.TOGGLE_LAYOUT:
+        this.toggleLayout(action.options);
       break;
 
       case PaginationActionTypes.SET_PAGINATION_DOCUMENT_LIMIT:
@@ -811,6 +820,18 @@ Stores.IndexResultsStore = FauxtonAPI.Store.extend({
         this.resetPagination();
         this.setPerPage(action.perPage);
       break;
+      case PaginationActionTypes.SET_CACHED_OFFSET:
+        this.setCachedOffset(action.options);
+      break;
+      case PaginationActionTypes.DELETE_CACHED_OFFSET:
+        this.deleteCachedOffset();
+      break;
+      case PaginationActionTypes.SET_PAGE_START:
+        this.setPageStart(action.options);
+      break;
+      case PaginationActionTypes.RESET_PAGINATION:
+        this.resetPagination();
+      break;
 
       default:
       return;
diff --git a/app/addons/documents/index-results/tests/index-results.componentsSpec.js b/app/addons/documents/index-results/tests/index-results.componentsSpec.js
index 9de02e7..08713de 100644
--- a/app/addons/documents/index-results/tests/index-results.componentsSpec.js
+++ b/app/addons/documents/index-results/tests/index-results.componentsSpec.js
@@ -14,6 +14,7 @@ import Views from "../index-results.components";
 import IndexResultsActions from "../actions";
 import Stores from "../stores";
 import Documents from "../../resources";
+import Constants from "../../constants";
 import documentTestHelper from "../../tests/document-test-helper";
 import utils from "../../../../../test/mocha/testUtils";
 import React from "react";
@@ -49,7 +50,7 @@ describe('Index Results', function () {
       IndexResultsActions.resultsListReset();
 
       instance = TestUtils.renderIntoDocument(<Views.List />, container);
-      var $el = $(ReactDOM.findDOMNode(instance));
+      var $el = $(ReactDOM.findDOMNode(instance)).find('.no-results-screen');
       assert.equal($el.text(), 'No Documents Found');
     });
 
@@ -72,7 +73,7 @@ describe('Index Results', function () {
 
 
       instance = TestUtils.renderIntoDocument(<Views.List />, container);
-      var $el = $(ReactDOM.findDOMNode(instance));
+      var $el = $(ReactDOM.findDOMNode(instance)).find('.no-results-screen');
       assert.equal($el.text(), 'I <3 Hamburg');
     });
   });
@@ -91,10 +92,11 @@ describe('Index Results', function () {
     it('does not render checkboxes for elements with just the special index (Mango Index List)', function () {
       IndexResultsActions.sendMessageNewResultList({
         collection: createMangoIndexDocColumn([{foo: 'testId1', type: 'special'}]),
+        typeOfIndex: 'mango',
         bulkCollection: new Documents.BulkDeleteDocCollection([], {databaseId: '1'}),
       });
 
-      store.toggleTableView({enable: true});
+      store.toggleLayout({layout: Constants.LAYOUT_ORIENTATION.TABLE});
 
       IndexResultsActions.resultsListReset();
 
@@ -122,10 +124,11 @@ describe('Index Results', function () {
           type: 'special',
           def: {fields: [{_id: 'desc'}]}
         }]),
+        typeOfIndex: 'mango',
         bulkCollection: new Documents.BulkDeleteDocCollection([], {databaseId: '1'}),
       });
 
-      store.toggleTableView({enable: true});
+      store.toggleLayout({layout: Constants.LAYOUT_ORIENTATION.TABLE});
 
       IndexResultsActions.resultsListReset();
 
@@ -147,10 +150,11 @@ describe('Index Results', function () {
           type: 'special',
           def: {fields: [{_id: 'desc'}]}
         }]),
+        typeOfIndex: 'view',
         bulkCollection: new Documents.BulkDeleteDocCollection([], {databaseId: '1'}),
       });
 
-      store.toggleTableView({enable: true});
+      store.toggleLayout({layout: Constants.LAYOUT_ORIENTATION.TABLE});
 
       IndexResultsActions.resultsListReset();
 
@@ -167,10 +171,11 @@ describe('Index Results', function () {
     it('does not render checkboxes for elements with no rev in a table (usual docs)', function () {
       IndexResultsActions.sendMessageNewResultList({
         collection: createDocColumn([{id: '1', foo: 'testId1'}, {id: '1', bar: 'testId1'}]),
+        typeOfIndex: 'view',
         bulkCollection: new Documents.BulkDeleteDocCollection([], {databaseId: '1'}),
       });
 
-      store.toggleTableView({enable: true});
+      store.toggleLayout({layout: Constants.LAYOUT_ORIENTATION.TABLE});
 
       IndexResultsActions.resultsListReset();
 
@@ -187,10 +192,11 @@ describe('Index Results', function () {
     it('renders checkboxes for elements with an id and rev in a table (usual docs)', function () {
       IndexResultsActions.sendMessageNewResultList({
         collection: createDocColumn([{id: '1', foo: 'testId1', rev: 'foo'}, {bar: 'testId1', rev: 'foo'}]),
+        typeOfIndex: 'view',
         bulkCollection: new Documents.BulkDeleteDocCollection([], {databaseId: '1'}),
       });
 
-      store.toggleTableView({enable: true});
+      store.toggleLayout({layout: Constants.LAYOUT_ORIENTATION.TABLE});
 
       IndexResultsActions.resultsListReset();
 
@@ -207,12 +213,13 @@ describe('Index Results', function () {
     it('renders checkboxes for elements with an id and rev in a json view (usual docs)', function () {
       IndexResultsActions.sendMessageNewResultList({
         collection: createDocColumn([{id: '1', emma: 'testId1', rev: 'foo'}, {bar: 'testId1'}]),
+        typeOfIndex: 'view',
         bulkCollection: new Documents.BulkDeleteDocCollection([], {databaseId: '1'}),
       });
 
       IndexResultsActions.resultsListReset();
 
-      store.toggleTableView({enable: false});
+      store.toggleLayout({layout: Constants.LAYOUT_ORIENTATION.JSON});
 
       instance = TestUtils.renderIntoDocument(
         <Views.List />,
@@ -226,12 +233,13 @@ describe('Index Results', function () {
     it('does not render checkboxes for elements with that are not deletable in a json view (usual docs)', function () {
       IndexResultsActions.sendMessageNewResultList({
         collection: createDocColumn([{foo: 'testId1', rev: 'foo'}, {bar: 'testId1'}]),
+        typeOfIndex: 'view',
         bulkCollection: new Documents.BulkDeleteDocCollection([], {databaseId: '1'}),
       });
 
       IndexResultsActions.resultsListReset();
 
-      store.toggleTableView({enable: false});
+      store.toggleLayout({layout: Constants.LAYOUT_ORIENTATION.JSON});
 
       instance = TestUtils.renderIntoDocument(
         <Views.List />,
@@ -261,10 +269,11 @@ describe('Index Results', function () {
 
       IndexResultsActions.sendMessageNewResultList({
         collection: createDocColumn([doc]),
+        typeOfIndex: 'view',
         bulkCollection: new Documents.BulkDeleteDocCollection([], {databaseId: '1'}),
       });
 
-      store.toggleTableView({enable: true});
+      store.toggleLayout({layout: Constants.LAYOUT_ORIENTATION.TABLE});
 
       IndexResultsActions.resultsListReset();
 
@@ -294,7 +303,7 @@ describe('Index Results', function () {
     });
 
     it('should show loading component', function () {
-      var results = {results: []};
+      var results = {results: [], selectedFields: []};
       instance = TestUtils.renderIntoDocument(
         <Views.ResultsScreen results={results} isLoading={true} />,
         container
@@ -306,7 +315,7 @@ describe('Index Results', function () {
     });
 
     it('should not show loading component', function () {
-      var results = {results: []};
+      var results = {results: [], selectedFields: []};
       instance = TestUtils.renderIntoDocument(
         <Views.ResultsScreen results={results} isLoading={false} />,
         container
diff --git a/app/addons/documents/index-results/tests/index-results.storesSpec.js b/app/addons/documents/index-results/tests/index-results.storesSpec.js
index 2ac3de2..fd8a5cd 100644
--- a/app/addons/documents/index-results/tests/index-results.storesSpec.js
+++ b/app/addons/documents/index-results/tests/index-results.storesSpec.js
@@ -13,6 +13,7 @@
 import FauxtonAPI from "../../../../core/api";
 import Stores from "../stores";
 import Documents from "../../resources";
+import Constants from "../../constants";
 import documentTestHelper from "../../tests/document-test-helper";
 import testUtils from "../../../../../test/mocha/testUtils";
 import sinon from "sinon";
@@ -101,6 +102,7 @@ describe('Index Results Store', function () {
         {_id: 'testId', _rev: '1', 'value': 'one'},
       ])
     });
+    store.toggleLayout({layout: Constants.LAYOUT_ORIENTATION.JSON});
 
     var doc = store.getResults().results[0];
     assert.equal(doc.id, 'testId');
@@ -157,14 +159,14 @@ describe('Index Results Store', function () {
     assert.deepEqual(res[0], {"_rev": "1", "ente": "gans", "fuchs": "hase"});
   });
 
-  it('returns the fields that occure the most without id and rev', function () {
+  it('returns the fields that occur the most without id and rev', function () {
     var doclist = [
       {_rev: '1', _id: '1', id: 'testId2', foo: 'one'},
       {_rev: '1', _id: '1', id: 'testId3', foo: 'two'}
     ];
 
     var res = store.getPrioritizedFields(doclist, 10);
-    assert.deepEqual(res, ['foo']);
+    assert.deepEqual(res, ['_id', 'foo']);
   });
 
   it('sorts the fields that occure the most', function () {
@@ -253,47 +255,26 @@ describe('Index Results Store', function () {
     assert.ok(store.areAllDocumentsSelected());
   });
 
-  it('does not count multiple fields in the prioritzed table', function () {
+  it('does not count multiple fields in the prioritized table', function () {
     store.newResults({
       collection: createDocColumn([
         {a: '1', 'value': 'one', b: '1'},
         {a: '1', 'value': 'one', b: '1'},
         {a: '1', 'value': 'one', b: '1'}
-      ])
+      ]),
+      typeOfIndex: 'view'
     });
 
-    store.getResults();
-
-    store.toggleTableView({enable: true});
+    store.toggleLayout({layout: Constants.LAYOUT_ORIENTATION.TABLE});
+    const stub = sinon.stub(store, 'isIncludeDocsEnabled');
+    stub.returns(true);
     store.getResults();
 
     store.changeTableViewFields({index: 0, newSelectedRow: 'value'});
 
-    var stub = sinon.stub(store, 'isIncludeDocsEnabled');
-    stub.returns(true);
-
     assert.deepEqual(store.getDisplayCountForTableView(), { shown: 2, allFieldCount: 3 });
   });
 
-  it('id and rev count as one field, because of the combined metadata field', function () {
-    store.newResults({
-      collection: createDocColumn([
-        {_id: 'foo1', _rev: 'bar', a: '1', 'value': 'one', b: '1'},
-        {_id: 'foo2', _rev: 'bar', a: '1', 'value': 'one', b: '1'},
-        {_id: 'foo3', _rev: 'bar', a: '1', 'value': 'one', b: '1'}
-      ]),
-      bulkCollection: new Documents.BulkDeleteDocCollection([], { databaseId: '1' })
-    });
-
-    store.toggleTableView({enable: true});
-
-    var stub = sinon.stub(store, 'isIncludeDocsEnabled');
-    stub.returns(true);
-    store.getResults();
-
-    assert.deepEqual(store.getDisplayCountForTableView(), { shown: 4, allFieldCount: 4 });
-  });
-
   it('selectDoc selects doc if not already selected', function () {
     store._collection = new createDocColumn([
       {_id: 'id', _rev: '1', 'value': 'one'},
diff --git a/app/addons/documents/layouts.js b/app/addons/documents/layouts.js
index f5af05a..3b83b58 100644
--- a/app/addons/documents/layouts.js
+++ b/app/addons/documents/layouts.js
@@ -13,7 +13,6 @@
 import React from 'react';
 import IndexResultsComponents from './index-results/index-results.components';
 import ReactPagination from './pagination/pagination';
-import ReactHeader from './header/header';
 import {NotificationCenterButton} from '../fauxton/notifications/notifications';
 import {ApiBarWrapper} from '../components/layouts';
 import SidebarComponents from "./sidebar/sidebar";
@@ -22,16 +21,20 @@ import Changes from './changes/components';
 import IndexEditorComponents from "./index-editor/components";
 import DesignDocInfoComponents from './designdocinfo/components';
 import RightAllDocsHeader from './components/header-docs-right';
+import IndexResultsContainer from './index-results/containers/IndexResultsContainer';
+import PaginationContainer from './index-results/containers/PaginationContainer';
+import ApiBarContainer from './index-results/containers/ApiBarContainer';
 
 export const TabsSidebarHeader = ({
   hideQueryOptions,
-  hideHeaderBar,
   database,
   dbName,
   dropDownLinks,
-  showIncludeAllDocs,
   docURL,
-  endpoint
+  endpoint,
+  isRedux = false,
+  fetchUrl,
+  ddocsOnly
 }) => {
   return (
     <header className="two-panel-header">
@@ -43,13 +46,16 @@ export const TabsSidebarHeader = ({
             />
         </div>
         <div className="right-header-wrapper flex-layout flex-row flex-body">
-          <div id="react-headerbar" className="flex-body">
-              {hideHeaderBar ? null : <ReactHeader.BulkDocumentHeaderController showIncludeAllDocs={showIncludeAllDocs} />}
-          </div>
           <div id="right-header" className="flex-fill">
-            <RightAllDocsHeader hideQueryOptions={hideQueryOptions} database={database} />
+            <RightAllDocsHeader
+              hideQueryOptions={hideQueryOptions}
+              database={database}
+              isRedux={isRedux}
+              fetchUrl={fetchUrl}
+              ddocsOnly={ddocsOnly} />
           </div>
-          <ApiBarWrapper docURL={docURL} endpoint={endpoint} />
+          { isRedux ? <ApiBarContainer databaseName={dbName} /> :
+                      <ApiBarWrapper docURL={docURL} endpoint={endpoint} /> }
           <div id="notification-center-btn" className="flex-fill">
             <NotificationCenterButton />
           </div>
@@ -73,7 +79,14 @@ TabsSidebarHeader.defaultProps = {
   hideHeaderBar: false
 };
 
-export const TabsSidebarContent = ({hideFooter, lowerContent, upperContent}) => {
+export const TabsSidebarContent = ({
+  hideFooter,
+  lowerContent,
+  upperContent,
+  isRedux = false,
+  fetchUrl,
+  databaseName
+}) => {
   return (
     <div className="with-sidebar tabs-with-sidebar content-area">
       <aside id="sidebar-content" className="scrollable">
@@ -87,7 +100,10 @@ export const TabsSidebarContent = ({hideFooter, lowerContent, upperContent}) =>
           {lowerContent}
         </div>
         <div id="footer">
-          {hideFooter ? null : <ReactPagination.Footer />}
+          {isRedux && !hideFooter ? <PaginationContainer
+                                      databaseName={databaseName}
+                                      fetchUrl={fetchUrl} /> : null}
+          {!isRedux && !hideFooter ? <ReactPagination.Footer /> : null}
         </div>
       </section>
     </div>
@@ -103,19 +119,45 @@ TabsSidebarContent.propTypes = {
   upperContent: React.PropTypes.object,
 };
 
-export const DocsTabsSidebarLayout = ({database, designDocs, showIncludeAllDocs, docURL, endpoint, dbName, dropDownLinks}) => {
+export const DocsTabsSidebarLayout = ({
+  database,
+  designDocs,
+  docURL,
+  endpoint,
+  dbName,
+  dropDownLinks,
+  isRedux = false,
+  fetchUrl,
+  ddocsOnly
+}) => {
+  let lowerContent;
+  if (isRedux) {
+    lowerContent = <IndexResultsContainer
+                      fetchUrl={fetchUrl}
+                      designDocs={designDocs}
+                      ddocsOnly={ddocsOnly}
+                      databaseName={dbName} />;
+  } else {
+    lowerContent = <IndexResultsComponents.List designDocs={designDocs} />;
+  }
+
   return (
     <div id="dashboard" className="with-sidebar">
       <TabsSidebarHeader
-        showIncludeAllDocs={showIncludeAllDocs}
         docURL={docURL}
         endpoint={endpoint}
         dbName={dbName}
         dropDownLinks={dropDownLinks}
         database={database}
+        isRedux={isRedux}
+        fetchUrl={fetchUrl}
+        ddocsOnly={ddocsOnly}
       />
       <TabsSidebarContent
-        lowerContent={<IndexResultsComponents.List designDocs={designDocs} />}
+        lowerContent={lowerContent}
+        isRedux={isRedux}
+        fetchUrl={fetchUrl}
+        databaseName={dbName}
       />
     </div>
   );
@@ -125,7 +167,6 @@ export const ChangesSidebarLayout = ({docURL, database, endpoint, dbName, dropDo
   return (
     <div id="dashboard" className="with-sidebar">
       <TabsSidebarHeader
-        hideHeaderBar={true}
         docURL={docURL}
         endpoint={endpoint}
         dbName={dbName}
@@ -146,7 +187,6 @@ export const ViewsTabsSidebarLayout = ({showEditView, database, docURL, endpoint
   return (
     <div id="dashboard" className="with-sidebar">
       <TabsSidebarHeader
-        hideHeaderBar={true}
         endpoint={endpoint}
         docURL={docURL}
         dbName={dbName}
diff --git a/app/addons/documents/mango/mango.components.js b/app/addons/documents/mango/mango.components.js
index 00fdb93..2931783 100644
--- a/app/addons/documents/mango/mango.components.js
+++ b/app/addons/documents/mango/mango.components.js
@@ -104,8 +104,7 @@ var MangoQueryEditorController = React.createClass({
 
     IndexResultActions.runMangoFindQuery({
       database: this.state.database,
-      queryCode: this.getMangoEditor().getEditorValue(),
-
+      queryCode: this.getMangoEditor().getEditorValue()
     });
   }
 });
diff --git a/app/addons/documents/mangolayout.js b/app/addons/documents/mangolayout.js
index df8c99d..105fd26 100644
--- a/app/addons/documents/mangolayout.js
+++ b/app/addons/documents/mangolayout.js
@@ -13,7 +13,6 @@
 import React from 'react';
 import app from "../../app";
 import ReactPagination from "./pagination/pagination";
-import ReactHeader from "./header/header";
 import {Breadcrumbs} from '../components/header-breadcrumbs';
 import {NotificationCenterButton} from '../fauxton/notifications/notifications';
 import {ApiBarWrapper} from '../components/layouts';
@@ -21,12 +20,9 @@ import MangoComponents from "./mango/mango.components";
 import IndexResultsComponents from "./index-results/index-results.components";
 
 
-export const RightHeader = ({showIncludeAllDocs, docURL, endpoint}) => {
+export const RightHeader = ({docURL, endpoint}) => {
   return (
     <div className="right-header-wrapper flex-layout flex-row flex-body">
-      <div id="react-headerbar" className="flex-body">
-        <ReactHeader.BulkDocumentHeaderController showIncludeAllDocs={showIncludeAllDocs} />
-      </div>
       <div id="right-header" className="flex-body">
       </div>
       <ApiBarWrapper docURL={docURL} endpoint={endpoint} />
@@ -45,14 +41,13 @@ export const MangoFooter = () => {
   );
 };
 
-export const MangoHeader = ({showIncludeAllDocs, crumbs, docURL, endpoint}) => {
+export const MangoHeader = ({crumbs, docURL, endpoint}) => {
   return (
     <div className="header-wrapper flex-layout flex-row">
       <div className='flex-body faux__breadcrumbs-mango-header'>
         <Breadcrumbs crumbs={crumbs}/>
       </div>
       <RightHeader
-        showIncludeAllDocs={showIncludeAllDocs}
         docURL={docURL}
         endpoint={endpoint}
       />
@@ -91,11 +86,10 @@ const MangoContent = ({edit, designDocs}) => {
 };
 
 
-export const MangoLayout = ({edit, showIncludeAllDocs, docURL, endpoint, crumbs, designDocs}) => {
+export const MangoLayout = ({edit, docURL, endpoint, crumbs, designDocs}) => {
   return (
     <div id="dashboard" className="two-pane flex-layout flex-col">
       <MangoHeader
-        showIncludeAllDocs={showIncludeAllDocs}
         docURL={docURL}
         endpoint={endpoint}
         crumbs={crumbs}
diff --git a/app/addons/documents/pagination/actions.js b/app/addons/documents/pagination/actions.js
index 3827465..695e913 100644
--- a/app/addons/documents/pagination/actions.js
+++ b/app/addons/documents/pagination/actions.js
@@ -14,66 +14,110 @@ import FauxtonAPI from "../../../core/api";
 import ActionTypes from "./actiontypes";
 import IndexResultsActions from "../index-results/actions";
 
-export default {
+const updatePerPage = (perPage, collection, bulkCollection) => {
 
-  updatePerPage: function (perPage, collection, bulkCollection) {
+  FauxtonAPI.dispatch({
+    type: ActionTypes.PER_PAGE_CHANGE,
+    perPage: perPage
+  });
 
-    FauxtonAPI.dispatch({
-      type: ActionTypes.PER_PAGE_CHANGE,
-      perPage: perPage
+  IndexResultsActions.clearResults();
+  collection.fetch().then(function () {
+    IndexResultsActions.resultsListReset();
+    IndexResultsActions.sendMessageNewResultList({
+      collection: collection,
+      bulkCollection: bulkCollection
     });
+  });
+};
 
-    IndexResultsActions.clearResults();
-    collection.fetch().then(function () {
-      IndexResultsActions.resultsListReset();
-      IndexResultsActions.sendMessageNewResultList({
-        collection: collection,
-        bulkCollection: bulkCollection
-      });
-    });
-  },
+const setDocumentLimit = (docLimit) => {
+  FauxtonAPI.dispatch({
+    type: ActionTypes.SET_PAGINATION_DOCUMENT_LIMIT,
+    docLimit: docLimit
+  });
+};
 
-  setDocumentLimit: function (docLimit) {
-    FauxtonAPI.dispatch({
-      type: ActionTypes.SET_PAGINATION_DOCUMENT_LIMIT,
-      docLimit: docLimit
-    });
-  },
+const paginateNext = (collection, bulkCollection) => {
+  FauxtonAPI.dispatch({
+    type: ActionTypes.PAGINATE_NEXT,
+  });
 
-  paginateNext: function (collection, bulkCollection) {
-    FauxtonAPI.dispatch({
-      type: ActionTypes.PAGINATE_NEXT,
-    });
+  IndexResultsActions.clearResults();
+  collection.next().then(function () {
+    // update the cached offset for improved UX between layouts
+    setCachedOffset(collection.paging.params.skip);
 
-    IndexResultsActions.clearResults();
-    collection.next().then(function () {
-      IndexResultsActions.resultsListReset();
+    IndexResultsActions.resultsListReset();
 
-      IndexResultsActions.sendMessageNewResultList({
-        collection: collection,
-        bulkCollection: bulkCollection
-      });
+    IndexResultsActions.sendMessageNewResultList({
+      collection: collection,
+      bulkCollection: bulkCollection
     });
-  },
+  });
+};
 
-  paginatePrevious: function (collection, bulkCollection) {
-    FauxtonAPI.dispatch({
-      type: ActionTypes.PAGINATE_PREVIOUS,
-    });
+const paginatePrevious = (collection, bulkCollection) => {
+  FauxtonAPI.dispatch({
+    type: ActionTypes.PAGINATE_PREVIOUS,
+  });
 
-    IndexResultsActions.clearResults();
-    collection.previous().then(function () {
-      IndexResultsActions.resultsListReset();
+  IndexResultsActions.clearResults();
+  collection.previous().then(function () {
+    // update the cached offset for improved UX between layouts
+    setCachedOffset(collection.paging.params.skip);
 
-      IndexResultsActions.sendMessageNewResultList({
-        collection: collection,
-        bulkCollection: bulkCollection
-      });
+    IndexResultsActions.resultsListReset();
+
+    IndexResultsActions.sendMessageNewResultList({
+      collection: collection,
+      bulkCollection: bulkCollection
     });
-  },
+  });
+};
 
-  toggleTableViewType: function () {
-    IndexResultsActions.togglePrioritizedTableView();
-  }
+const toggleTableViewType = () => {
+  IndexResultsActions.togglePrioritizedTableView();
+};
+
+const deleteCachedOffset = () => {
+  FauxtonAPI.dispatch({
+    type: ActionTypes.DELETE_CACHED_OFFSET
+  });
+};
+
+const setCachedOffset = (offset) => {
+  FauxtonAPI.dispatch({
+    type: ActionTypes.SET_CACHED_OFFSET,
+    options: {
+      offset: offset
+    }
+  });
+};
 
+const setPageStart = (start) => {
+  FauxtonAPI.dispatch({
+    type: ActionTypes.SET_PAGE_START,
+    options: {
+      start: start
+    }
+  });
+};
+
+const resetPagination = () => {
+  FauxtonAPI.dispatch({
+    type: ActionTypes.RESET_PAGINATION
+  });
+};
+
+export default {
+  updatePerPage,
+  setDocumentLimit,
+  paginateNext,
+  paginatePrevious,
+  toggleTableViewType,
+  deleteCachedOffset,
+  setCachedOffset,
+  setPageStart,
+  resetPagination
 };
diff --git a/app/addons/documents/pagination/actiontypes.js b/app/addons/documents/pagination/actiontypes.js
index 7e47937..d752dba 100644
--- a/app/addons/documents/pagination/actiontypes.js
+++ b/app/addons/documents/pagination/actiontypes.js
@@ -15,5 +15,9 @@ export default {
   PER_PAGE_CHANGE: 'PER_PAGE_CHANGE',
   PAGINATE_NEXT: 'PAGINATE_NEXT',
   PAGINATE_PREVIOUS: 'PAGINATE_PREVIOUS',
-  SET_PAGINATION_DOCUMENT_LIMIT: 'SET_PAGINATION_DOCUMENT_LIMIT'
+  SET_PAGINATION_DOCUMENT_LIMIT: 'SET_PAGINATION_DOCUMENT_LIMIT',
+  SET_CACHED_OFFSET: 'SET_CACHED_OFFSET',
+  DELETE_CACHED_OFFSET: 'DELETE_CACHED_OFFSET',
+  SET_PAGE_START: 'SET_PAGE_START',
+  RESET_PAGINATION: 'RESET_PAGINATION'
 };
diff --git a/app/addons/documents/pagination/pagination.js b/app/addons/documents/pagination/pagination.js
index 883bc3f..47ce4b4 100644
--- a/app/addons/documents/pagination/pagination.js
+++ b/app/addons/documents/pagination/pagination.js
@@ -36,6 +36,11 @@ var IndexPaginationController = React.createClass({
 
   componentWillUnmount: function () {
     indexResultsStore.off('change', this.onChange);
+
+    // Since we're migrating away from a paginated result list, don't forget
+    // to delete the cached offset used for an improved UX when switching
+    // between layouts.
+    Actions.deleteCachedOffset();
   },
 
   onChange: function () {
@@ -133,14 +138,16 @@ var PerPageSelector = React.createClass({
 var AllDocsNumberController = React.createClass({
 
   getStoreState: function () {
+    const isLoading = indexResultsStore.isLoading();
     return {
+      hasResults: indexResultsStore.hasResults(),
       totalRows: indexResultsStore.getTotalRows(),
       pageStart: indexResultsStore.getPageStart(),
       pageEnd: indexResultsStore.getPageEnd(),
       perPage: indexResultsStore.getPerPage(),
       prioritizedEnabled: indexResultsStore.getIsPrioritizedEnabled(),
       showPrioritizedFieldToggler: indexResultsStore.getShowPrioritizedFieldToggler(),
-      displayedFields: indexResultsStore.getResults().displayedFields,
+      displayedFields: isLoading ? {} : indexResultsStore.getResults().displayedFields,
       collection: indexResultsStore.getCollection(),
       bulkCollection: indexResultsStore.getBulkDocCollection(),
     };
@@ -177,13 +184,14 @@ var AllDocsNumberController = React.createClass({
   },
 
   render: function () {
-    var showTableControls = this.state.showPrioritizedFieldToggler;
+    const showTableControls = this.state.showPrioritizedFieldToggler;
+    const hasResults = this.state.hasResults;
 
     return (
       <div className="footer-controls">
 
         <div className="page-controls">
-          {showTableControls ?
+          {showTableControls && hasResults ?
             <TableControls
               prioritizedEnabled={this.state.prioritizedEnabled}
               displayedFields={this.state.displayedFields} /> : null}
diff --git a/app/addons/documents/queryoptions/queryoptions.js b/app/addons/documents/queryoptions/queryoptions.js
index 80fa0f5..34e3fe9 100644
--- a/app/addons/documents/queryoptions/queryoptions.js
+++ b/app/addons/documents/queryoptions/queryoptions.js
@@ -15,6 +15,7 @@ import React from 'react';
 import ReactDOM from 'react-dom';
 import QueryOptionsStores from './stores';
 import Actions from './actions';
+import PaginationActions from '../pagination/actions';
 import Components from '../../components/react-components';
 
 const { connectToStores, TrayWrapper, ToggleHeaderButton, TrayContents } = Components;
@@ -352,6 +353,8 @@ var QueryTray = React.createClass({
   runQuery: function (e) {
     e.preventDefault();
 
+    // we're going to have a fresh collection, purge the cached offset!
+    PaginationActions.deleteCachedOffset();
     Actions.runQuery(this.props.queryParams);
     this.toggleTrayVisibility();
   },
@@ -368,6 +371,13 @@ var QueryTray = React.createClass({
     Actions.toggleIncludeDocs();
   },
 
+  toggleReduce: function () {
+    if (this.props.includeDocs) {
+      this.toggleIncludeDocs();
+    }
+    Actions.toggleReduce();
+  },
+
   getTray: function () {
     return (
       <TrayContents closeTray={this.closeTray} contentVisible={this.props.contentVisible}
@@ -380,7 +390,7 @@ var QueryTray = React.createClass({
             toggleIncludeDocs={this.toggleIncludeDocs}
             showReduce={this.props.showReduce}
             reduce={this.props.reduce}
-            toggleReduce={Actions.toggleReduce}
+            toggleReduce={this.toggleReduce}
             groupLevel={this.props.groupLevel}
             updateGroupLevel={Actions.updateGroupLevel}
             docURL={FauxtonAPI.constants.DOC_URLS.GENERAL} />
diff --git a/app/addons/documents/resources.js b/app/addons/documents/resources.js
index 5349e16..7976a66 100644
--- a/app/addons/documents/resources.js
+++ b/app/addons/documents/resources.js
@@ -251,8 +251,8 @@ Documents.MangoDocumentCollection = PagingCollection.extend({
 
   fetch: function () {
     var url = this.urlRef(),
-              promise = FauxtonAPI.Deferred(),
-              query = this.getPaginatedQuery();
+        promise = FauxtonAPI.Deferred(),
+        query = this.getPaginatedQuery();
 
     $.ajax({
       type: 'POST',
@@ -282,8 +282,7 @@ Documents.MangoDocumentCollection = PagingCollection.extend({
       update_seq: resp.update_seq
     };
 
-    var skipLimit = this.paging.defaultParams.skip || 0;
-    if (this.paging.params.skip > skipLimit) {
+    if (this.paging.params.skip > 0) {
       this.paging.hasPrevious = true;
     }
 
diff --git a/app/addons/documents/routes-documents.js b/app/addons/documents/routes-documents.js
index 583cfda..1a8dae1 100644
--- a/app/addons/documents/routes-documents.js
+++ b/app/addons/documents/routes-documents.js
@@ -10,16 +10,16 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-import app from '../../app';
+//import app from '../../app';
 import React from 'react';
 import FauxtonAPI from '../../core/api';
 import BaseRoute from './shared-routes';
-import Documents from './resources';
+//import Documents from './resources';
 import ChangesActions from './changes/actions';
 import Databases from '../databases/base';
 import Resources from './resources';
-import IndexResultStores from './index-results/stores';
-import IndexResultsActions from './index-results/actions';
+//import IndexResultStores from './index-results/stores';
+//import IndexResultsActions from './index-results/actions';
 import SidebarActions from './sidebar/actions';
 import DesignDocInfoActions from './designdocinfo/actions';
 import ComponentsActions from '../components/actions';
@@ -78,66 +78,42 @@ var DocumentsRouteObject = BaseRoute.extend({
   },
 
   /*
-  * docParams are the options collection uses to fetch from the server
+  * docParams are the options fauxton uses to fetch from the server
   * urlParams are what are shown in the url and to the user
   * They are not the same when paginating
   */
   allDocs: function (databaseName, options) {
-    var params = this.createParams(options),
-        urlParams = params.urlParams,
-        docParams = params.docParams,
-        collection;
-
-    // includes_docs = true if you are visiting the _replicator/_users databases
-    if (['_replicator', '_users'].indexOf(databaseName) > -1) {
-      docParams.include_docs = true;
-      urlParams = params.docParams;
-      var updatedURL = FauxtonAPI.urls('allDocs', 'app', databaseName, '?' + $.param(urlParams));
-      FauxtonAPI.navigate(updatedURL, {trigger: false, replace: true});
-    }
+    const params = this.createParams(options),
+          urlParams = params.urlParams,
+          docParams = params.docParams;
 
+    const url = `/${encodeURIComponent(databaseName)}/_all_docs`;
+    // this is used for the header and sidebar
     this.database.buildAllDocs(docParams);
-    collection = this.database.allDocs;
 
-    var tab = 'all-docs';
-    if (docParams.startkey && docParams.startkey.indexOf("_design") > -1) {
+    const onlyShowDdocs = !!(docParams.startkey && docParams.startkey.indexOf("_design") > -1);
+    let tab = 'all-docs';
+    if (onlyShowDdocs) {
       tab = 'design-docs';
     }
 
     SidebarActions.selectNavItem(tab);
     ComponentsActions.showDeleteDatabaseModal({showDeleteModal: false, dbId: ''});
 
-    if (!docParams) {
-      docParams = {};
-    }
-
-    const frozenCollection = app.utils.localStorageGet('include_docs_bulkdocs');
-    window.localStorage.removeItem('include_docs_bulkdocs');
-
-    IndexResultsActions.newResultsList({
-      collection: collection,
-      textEmptyIndex: 'No Documents Found',
-      bulkCollection: new Documents.BulkDeleteDocCollection(frozenCollection, { databaseId: this.database.safeID() }),
-    });
-
-    this.database.allDocs.paging.pageSize = IndexResultStores.indexResultsStore.getPerPage();
-
     const endpoint = this.database.allDocs.urlRef("apiurl", urlParams);
     const docURL = this.database.allDocs.documentation();
 
-    // update the query options with the latest & greatest info
-    QueryOptionsActions.reset({queryParams: urlParams});
-    QueryOptionsActions.showQueryOptions();
-
     const dropDownLinks = this.getCrumbs(this.database);
     return <DocsTabsSidebarLayout
-      showIncludeAllDocs={true}
       docURL={docURL}
       endpoint={endpoint}
       dbName={this.database.id}
       dropDownLinks={dropDownLinks}
       database={this.database}
       designDocs={this.designDocs}
+      fetchUrl={url}
+      ddocsOnly={onlyShowDdocs}
+      isRedux={true}
     />;
   },
 
diff --git a/app/addons/documents/routes-index-editor.js b/app/addons/documents/routes-index-editor.js
index d204bb5..ea2fdf6 100644
--- a/app/addons/documents/routes-index-editor.js
+++ b/app/addons/documents/routes-index-editor.js
@@ -50,10 +50,20 @@ const IndexEditorAndResults = BaseRoute.extend({
   },
 
   showView: function (databaseName, ddoc, viewName) {
-    var params = this.createParams(),
-        urlParams = params.urlParams,
-        docParams = params.docParams,
-        decodeDdoc = decodeURIComponent(ddoc);
+    const params = this.createParams(),
+          urlParams = params.urlParams,
+          docParams = params.docParams,
+          decodeDdoc = decodeURIComponent(ddoc),
+          store = IndexResultsStores.indexResultsStore;
+
+    // if the user is simply switching the layout style (i.e. metadata, json, or table),
+    // there will be a cached offset value.  Use that offset when getting the "new"
+    // collection so data stays the same.
+    if (docParams.skip && store.hasCachedOffset()) {
+      docParams.skip = Math.max(store.getCachedOffset(), docParams.skip);
+    } else if (store.hasCachedOffset()) {
+      docParams.skip = store.getCachedOffset();
+    }
 
     viewName = viewName.replace(/\?.*$/, '');
     this.indexedDocs = new Documents.IndexCollection(null, {
@@ -62,7 +72,7 @@ const IndexEditorAndResults = BaseRoute.extend({
       view: viewName,
       params: docParams,
       paging: {
-        pageSize: IndexResultsStores.indexResultsStore.getPerPage()
+        pageSize: store.getPerPage()
       }
     });
 
@@ -70,6 +80,7 @@ const IndexEditorAndResults = BaseRoute.extend({
 
     IndexResultsActions.newResultsList({
       collection: this.indexedDocs,
+      typeOfIndex: 'view',
       bulkCollection: new Documents.BulkDeleteDocCollection([], { databaseId: this.database.safeID() }),
     });
 
@@ -94,7 +105,6 @@ const IndexEditorAndResults = BaseRoute.extend({
 
     const dropDownLinks = this.getCrumbs(this.database);
     return <DocsTabsSidebarLayout
-      showIncludeAllDocs={true}
       docURL={docURL}
       endpoint={endpoint}
       dbName={this.database.id}
diff --git a/app/addons/documents/routes-mango.js b/app/addons/documents/routes-mango.js
index 8778823..e26711d 100644
--- a/app/addons/documents/routes-mango.js
+++ b/app/addons/documents/routes-mango.js
@@ -17,6 +17,7 @@ import Databases from "../databases/resources";
 import Resources from "./resources";
 import IndexResultsActions from "./index-results/actions";
 import IndexResultStores from "./index-results/stores";
+import PaginationActions from "./pagination/actions";
 import Documents from "./shared-resources";
 import MangoActions from "./mango/mango.actions";
 import SidebarActions from "./sidebar/actions";
@@ -48,10 +49,16 @@ const MangoIndexEditorAndQueryEditor = FauxtonAPI.RouteObject.extend({
   },
 
   findUsingIndex: function (database) {
+    PaginationActions.resetPagination();
+
+    const pageSize = IndexResultStores.indexResultsStore.getPerPage();
     const mangoResultCollection = new Resources.MangoDocumentCollection(null, {
       database: this.database,
+      params: {
+        limit: pageSize
+      },
       paging: {
-        pageSize: IndexResultStores.indexResultsStore.getPerPage()
+        pageSize: pageSize
       }
     });
 
@@ -59,7 +66,7 @@ const MangoIndexEditorAndQueryEditor = FauxtonAPI.RouteObject.extend({
       database: this.database,
       params: null,
       paging: {
-        pageSize: IndexResultStores.indexResultsStore.getPerPage()
+        pageSize: pageSize
       }
     });
 
@@ -68,6 +75,7 @@ const MangoIndexEditorAndQueryEditor = FauxtonAPI.RouteObject.extend({
     IndexResultsActions.newMangoResultsList({
       collection: mangoResultCollection,
       textEmptyIndex: 'No Results',
+      typeOfIndex: 'mango',
       bulkCollection: new Resources.BulkDeleteDocCollection([], { databaseId: this.database.safeID() }),
     });
 
@@ -85,7 +93,6 @@ const MangoIndexEditorAndQueryEditor = FauxtonAPI.RouteObject.extend({
     ];
 
     return <MangoLayout
-      showIncludeAllDocs={false}
       crumbs={crumbs}
       docURL={FauxtonAPI.constants.DOC_URLS.MANGO_SEARCH}
       endpoint={mangoResultCollection.urlRef('query-apiurl', '')}
diff --git a/app/addons/documents/shared-routes.js b/app/addons/documents/shared-routes.js
index 141f11a..15f5b44 100644
--- a/app/addons/documents/shared-routes.js
+++ b/app/addons/documents/shared-routes.js
@@ -77,15 +77,24 @@ var BaseRoute = FauxtonAPI.RouteObject.extend({
   },
 
   createParams: function (options) {
-    var urlParams = app.getParams(options),
-        params = Documents.QueryParams.parse(urlParams);
+    const urlParams = app.getParams(options),
+          params = Documents.QueryParams.parse(urlParams),
+          store = IndexResultStores.indexResultsStore;
 
+    let start = 0;
+    if (urlParams.skip && store.hasCachedOffset()) {
+      start = Math.max(store.getCachedOffset(), parseInt(urlParams.skip, 10));
+    } else if (urlParams.skip) {
+      start = parseInt(urlParams.skip, 10);
+    } else if (store.hasCachedOffset()) {
+      start = store.getCachedOffset();
+    }
+    PaginationActions.setPageStart(start);
     PaginationActions.setDocumentLimit(parseInt(urlParams.limit, 10));
 
-    var limit = IndexResultStores.indexResultsStore.getPerPage();
     return {
       urlParams: urlParams,
-      docParams: _.extend(params, {limit: limit})
+      docParams: _.extend(params, {limit: store.getPerPage()})
     };
   }
 });
diff --git a/app/addons/documents/tests/nightwatch/bulkDelete.js b/app/addons/documents/tests/nightwatch/bulkDelete.js
index 0efa3c3..f5df763 100644
--- a/app/addons/documents/tests/nightwatch/bulkDelete.js
+++ b/app/addons/documents/tests/nightwatch/bulkDelete.js
@@ -14,7 +14,7 @@
 
 module.exports = {
 
-  'Bulk deletes': function (client) {
+  'Bulk deletes on json view': function (client) {
     var waitTime = client.globals.maxWaitTime,
       newDatabaseName = client.globals.testDatabaseName,
       newDocumentName1 = 'bulktest1',
@@ -26,6 +26,7 @@ module.exports = {
       .createDocument(newDocumentName1, newDatabaseName)
       .createDocument(newDocumentName2, newDatabaseName)
       .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+      .clickWhenVisible('.fonticon-json')
       .waitForElementPresent('.bulk-action-component-selector-group', waitTime, false)
 
       // ensures page content has loaded before proceeding
@@ -48,6 +49,40 @@ module.exports = {
       .end();
   },
 
+  'Bulk deletes on table/metadata view': function (client) {
+    var waitTime = client.globals.maxWaitTime,
+      newDatabaseName = client.globals.testDatabaseName,
+      newDocumentName1 = 'bulktest1',
+      newDocumentName2 = 'bulktest2',
+      baseUrl = client.globals.test_settings.launch_url;
+
+    client
+      .loginToGUI()
+      .createDocument(newDocumentName1, newDatabaseName)
+      .createDocument(newDocumentName2, newDatabaseName)
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+      .waitForElementPresent('.bulk-action-component-selector-group', waitTime, false)
+
+      // ensures page content has loaded before proceeding
+      .waitForElementVisible('.table-view-docs', waitTime, false)
+
+      .clickWhenVisible('.bulk-action-component-selector-group input[type="checkbox"]')
+      .clickWhenVisible('.bulk-action-component-selector-group button.fonticon-trash', waitTime, false)
+      .acceptAlert()
+      .waitForElementVisible('#global-notifications .alert.alert-info', waitTime, false)
+      .waitForElementNotPresent('.table-view-docs ', waitTime, false)
+      .getText('body', function (result) {
+        var data = result.value,
+          isPresentFirstDoc = data.indexOf(newDocumentName1) !== -1,
+          isPresentSecondDoc = data.indexOf(newDocumentName2) !== -1,
+          bothMissing = !isPresentFirstDoc && !isPresentSecondDoc;
+
+        this.verify.ok(bothMissing,
+          'Checking if documents were deleted');
+      })
+      .end();
+  },
+
   'Select all works after changing the page': function (client) {
     var waitTime = client.globals.maxWaitTime,
       newDatabaseName = client.globals.testDatabaseName,
@@ -57,6 +92,7 @@ module.exports = {
       .loginToGUI()
       .createManyDocuments(25, newDatabaseName)
       .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+      .clickWhenVisible('.fonticon-json')
 
       // ensures page content has loaded before proceeding
       .waitForElementVisible('.prettyprint', waitTime, false)
diff --git a/app/addons/documents/tests/nightwatch/changes.js b/app/addons/documents/tests/nightwatch/changes.js
index b2cbabb..257ad64 100644
--- a/app/addons/documents/tests/nightwatch/changes.js
+++ b/app/addons/documents/tests/nightwatch/changes.js
@@ -14,21 +14,6 @@
 
 module.exports = {
 
-  'Does not display the View-Selector-Button': function (client) {
-    var waitTime = client.globals.maxWaitTime,
-        newDatabaseName = client.globals.testDatabaseName,
-        baseUrl = client.globals.test_settings.launch_url;
-
-    client
-      .loginToGUI()
-      .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
-      .waitForElementPresent('.two-sides-toggle-button', waitTime, false)
-      .clickWhenVisible('#changes')
-      .waitForElementPresent('.js-changes-view', waitTime, false)
-      .assert.elementNotPresent('.two-sides-toggle-button')
-      .end();
-  },
-
   'Check doc link in Changes feed links properly': function (client) {
     var waitTime = client.globals.maxWaitTime,
         newDatabaseName = client.globals.testDatabaseName,
diff --git a/app/addons/documents/tests/nightwatch/createsDocument.js b/app/addons/documents/tests/nightwatch/createsDocument.js
index 1894567..7fae788 100644
--- a/app/addons/documents/tests/nightwatch/createsDocument.js
+++ b/app/addons/documents/tests/nightwatch/createsDocument.js
@@ -13,12 +13,12 @@
 
 
 module.exports = {
-  'Creates a document' : function (client) {
+  'Creates a document' : (client) => {
     /*jshint multistr: true */
-    var waitTime = client.globals.maxWaitTime,
-        newDatabaseName = client.globals.testDatabaseName,
-        newDocumentName = 'create_doc_document',
-        baseUrl = client.globals.test_settings.launch_url;
+    const waitTime = client.globals.maxWaitTime,
+          newDatabaseName = client.globals.testDatabaseName,
+          newDocumentName = 'create_doc_document',
+          baseUrl = client.globals.test_settings.launch_url;
 
     client
       .createDatabase(newDatabaseName)
@@ -44,6 +44,50 @@ module.exports = {
       .clickWhenVisible('#doc-editor-actions-panel .save-doc')
       .checkForDocumentCreated(newDocumentName)
       .url(baseUrl + '#/database/' + newDatabaseName + '/_all_docs')
+      .clickWhenVisible('.fonticon-json')
+      .waitForElementVisible('.prettyprint', waitTime, false)
+      .getText('.prettyprint', function (result) {
+        const data = result.value;
+        const createdDocIsPresent = data.indexOf(newDocumentName) !== -1;
+
+        this.verify.ok(
+          createdDocIsPresent,
+          'Checking if new document shows up in _all_docs.'
+        );
+      })
+    .end();
+  },
+
+  'Creates a Document through Create Document toolbar button': (client) => {
+    const waitTime = client.globals.maxWaitTime,
+          newDatabaseName = client.globals.testDatabaseName,
+          newDocumentName = 'create_doc_document',
+          baseUrl = client.globals.test_settings.launch_url;
+
+    client
+      .createDatabase(newDatabaseName)
+      .loginToGUI()
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+      .clickWhenVisible('.document-result-screen__toolbar-create-btn')
+      .waitForElementPresent('#editor-container', waitTime, false)
+      .verify.urlEquals(baseUrl + '/#/database/' + newDatabaseName + '/new')
+      .waitForElementPresent('.ace_gutter-active-line', waitTime, false)
+
+      // confirm the header elements are showing up
+      .waitForElementVisible('.faux-header__breadcrumbs', waitTime, true)
+      .waitForElementVisible('.faux__jsondoc-wrapper', waitTime, true)
+
+      .execute('\
+        var editor = ace.edit("doc-editor");\
+        editor.gotoLine(2,10);\
+        editor.removeWordRight();\
+        editor.insert("' + newDocumentName + '");\
+      ')
+
+      .clickWhenVisible('#doc-editor-actions-panel .save-doc')
+      .checkForDocumentCreated(newDocumentName)
+      .url(baseUrl + '#/database/' + newDatabaseName + '/_all_docs')
+      .clickWhenVisible('.fonticon-json')
       .waitForElementVisible('.prettyprint', waitTime, false)
       .getText('.prettyprint', function (result) {
         const data = result.value;
diff --git a/app/addons/documents/tests/nightwatch/deletesDocuments.js b/app/addons/documents/tests/nightwatch/deletesDocuments.js
index 6cc3cbc..2960a39 100644
--- a/app/addons/documents/tests/nightwatch/deletesDocuments.js
+++ b/app/addons/documents/tests/nightwatch/deletesDocuments.js
@@ -13,7 +13,7 @@
 
 
 module.exports = {
-  'Deletes a document': function (client) {
+  'Deletes a document on json view': function (client) {
     var waitTime = client.globals.maxWaitTime,
         newDatabaseName = client.globals.testDatabaseName,
         newDocumentName = 'delete_doc_doc',
@@ -28,6 +28,7 @@ module.exports = {
       .url(baseUrl)
       .waitForElementPresent('#dashboard-content a[href="database/' + newDatabaseName + '/_all_docs"]', waitTime, false)
       .clickWhenVisible('#dashboard-content a[href="database/' + newDatabaseName + '/_all_docs"]', waitTime, false)
+      .clickWhenVisible('.fonticon-json')
       .waitForElementVisible('label[for="checkbox-' + newDocumentName + '"]', waitTime, false)
       .clickWhenVisible('label[for="checkbox-' + newDocumentName + '"]', waitTime, false)
       .clickWhenVisible('.bulk-action-component-selector-group button.fonticon-trash', waitTime, false)
@@ -55,6 +56,46 @@ module.exports = {
     .end();
   },
 
+  'Deletes a document on table/metadata view': function (client) {
+    var waitTime = client.globals.maxWaitTime,
+        newDatabaseName = client.globals.testDatabaseName,
+        newDocumentName = 'delete_doc_doc',
+        baseUrl = client.globals.test_settings.launch_url;
+
+    client
+      .createDocument(newDocumentName, newDatabaseName)
+      .createDocument(newDocumentName + '2', newDatabaseName)
+      .loginToGUI()
+      .checkForDocumentCreated(newDocumentName)
+      .checkForDocumentCreated(newDocumentName + '2')
+      .url(baseUrl)
+      .waitForElementPresent('#dashboard-content a[href="database/' + newDatabaseName + '/_all_docs"]', waitTime, false)
+      .clickWhenVisible('#dashboard-content a[href="database/' + newDatabaseName + '/_all_docs"]', waitTime, false)
+      .clickWhenVisible('#checkbox-' + newDocumentName, waitTime, false)
+      .clickWhenVisible('.bulk-action-component-selector-group button.fonticon-trash', waitTime, false)
+      .acceptAlert()
+      .waitForElementVisible('.alert.alert-info', waitTime, false)
+
+      .clickWhenVisible('#checkbox-' + newDocumentName + '2', waitTime, false)
+      .clickWhenVisible('.bulk-action-component-selector-group button.fonticon-trash', waitTime, false)
+      .acceptAlert()
+
+      .checkForStringNotPresent(newDatabaseName + '/_all_docs', newDocumentName)
+      .checkForStringNotPresent(newDatabaseName + '/_all_docs', newDocumentName + '2')
+      .url(baseUrl + '/' + newDatabaseName + '/_all_docs')
+
+      .waitForElementPresent('pre', waitTime, false)
+      .getText('pre', function (result) {
+        var data = result.value,
+            createdDocumentANotPresent = data.indexOf(newDocumentName) === -1,
+            createdDocumentBNotPresent = data.indexOf(newDocumentName + '2') === -1;
+
+        this.verify.ok(createdDocumentANotPresent && createdDocumentBNotPresent,
+          'Checking if new documents no longer shows up in _all_docs.');
+      })
+    .end();
+  },
+
   'Deleting a new Design Doc automatically removes it from the sidebar': function (client) {
     var waitTime = client.globals.maxWaitTime;
     var newDatabaseName = client.globals.testDatabaseName;
@@ -75,13 +116,14 @@ module.exports = {
       .populateDatabase(newDatabaseName)
       .createDocument(designDoc._id, newDatabaseName, designDoc)
       .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+      .clickWhenVisible('.fonticon-json')
       .waitForElementPresent('.prettyprint', waitTime, false)
       .waitForElementNotPresent('.loading-lines', waitTime, false)
 
       // confirm the design doc appears in the sidebar
       .waitForElementPresent('#sidebar-content span[title="_design/sidebar-update"]', waitTime, false)
       .waitForElementPresent('label[for="checkbox-_design/sidebar-update"]', waitTime, false)
-      .execute('$("label[for=\'checkbox-_design/sidebar-update\']")[0].scrollIntoView();')
+      .execute('$("div[data-id=\'_design/sidebar-update\']")[0].scrollIntoView();')
       .clickWhenVisible('label[for="checkbox-_design/sidebar-update"]', waitTime, false)
 
       .waitForElementPresent('.bulk-action-component-selector-group .fonticon-trash', waitTime, false)
diff --git a/app/addons/documents/tests/nightwatch/doubleEmitResults.js b/app/addons/documents/tests/nightwatch/doubleEmitResults.js
index 6ac759a..9319af7 100644
--- a/app/addons/documents/tests/nightwatch/doubleEmitResults.js
+++ b/app/addons/documents/tests/nightwatch/doubleEmitResults.js
@@ -23,6 +23,7 @@ module.exports = {
     .loginToGUI()
     .populateDatabase(newDatabaseName)
     .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/testdesigndoc/_view/stubview')
+    .clickWhenVisible('.fonticon-json')
     .waitForElementPresent('.clearfix', waitTime, false)
     .waitForElementPresent('.doc-row', waitTime, false)
     .execute(function () {
diff --git a/app/addons/documents/tests/nightwatch/editDocumentsFromView.js b/app/addons/documents/tests/nightwatch/editDocumentsFromView.js
index 7332db1..4f1303f 100644
--- a/app/addons/documents/tests/nightwatch/editDocumentsFromView.js
+++ b/app/addons/documents/tests/nightwatch/editDocumentsFromView.js
@@ -33,6 +33,7 @@ module.exports = {
 
       .loginToGUI()
       .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/abc/_view/evens')
+      .clickWhenVisible('.fonticon-json')
 
       //navigate to 'evens' view (declared above), then click on first document's pencil icon
       .clickWhenVisible('a[href="#/database/' + newDatabaseName + '/document_10"]')
diff --git a/app/addons/documents/tests/nightwatch/fixRegressionTableView.js b/app/addons/documents/tests/nightwatch/fixRegressionTableView.js
index 8c3e4ce..8e25616 100644
--- a/app/addons/documents/tests/nightwatch/fixRegressionTableView.js
+++ b/app/addons/documents/tests/nightwatch/fixRegressionTableView.js
@@ -27,13 +27,13 @@ module.exports = {
       .checkForDocumentCreated(newDocumentName1)
       .checkForDocumentCreated(newDocumentName2)
       .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+      .clickWhenVisible('.fonticon-json')
 
       .clickWhenVisible('.fonticon-pencil', client.globals.maxWaitTime, false)
       .clickWhenVisible('.faux-header__breadcrumbs-link')
       .clickWhenVisible('.fonticon-table', client.globals.maxWaitTime, false)
       .waitForElementVisible('.tableview-checkbox-cell', client.globals.maxWaitTime, false)
-      .waitForElementVisible('.tableview-data-cell-id', client.globals.maxWaitTime, false)
-      .clickWhenVisible('.tableview-data-cell-id a', client.globals.maxWaitTime, false)
+      .clickWhenVisible('td[title=\'bulktest1\'', client.globals.maxWaitTime, false)
       .waitForElementVisible('#doc-editor-actions-panel', client.globals.maxWaitTime, false)
       .end();
   },
diff --git a/app/addons/documents/tests/nightwatch/jsonView.js b/app/addons/documents/tests/nightwatch/jsonView.js
index 6372fb6..92ca33d 100644
--- a/app/addons/documents/tests/nightwatch/jsonView.js
+++ b/app/addons/documents/tests/nightwatch/jsonView.js
@@ -17,18 +17,9 @@ module.exports = {
       .checkForDocumentCreated(newDocumentName)
 
       .url(baseUrl + '#/database/' + newDatabaseName + '/_all_docs')
+      .clickWhenVisible('.fonticon-json')
       .waitForElementPresent('.doc-item', client.globals.maxWaitTime, false)
-      // by default include_docs is off, so check "American Bittern" doesn't exist in the DOM
-      .getText('body', function (result) {
-        var birdNameNotPresent = result.value.indexOf('"American Bittern"') === -1;
-        this.verify.ok(birdNameNotPresent, 'Checking doc content doesn\'t show up in results.');
-      })
-
-      // now enable ?include_docs and try again
-      .url(baseUrl + '#/database/' + newDatabaseName + '/_find')
-      .waitForElementPresent('.watermark-logo', client.globals.maxWaitTime, false)
-      .url(baseUrl + '#/database/' + newDatabaseName + '/_all_docs?include_docs=true')
-
+      // by default include_docs is on, so check "American Bittern" does exist in the DOM
       .waitForElementPresent('.prettyprint', client.globals.maxWaitTime, false)
       .assert.containsText('.prettyprint', 'American Bittern')
       .end();
diff --git a/app/addons/documents/tests/nightwatch/mangoIndex.js b/app/addons/documents/tests/nightwatch/mangoIndex.js
index 424466b..b7fa3b1 100644
--- a/app/addons/documents/tests/nightwatch/mangoIndex.js
+++ b/app/addons/documents/tests/nightwatch/mangoIndex.js
@@ -25,6 +25,7 @@ module.exports = {
       .populateDatabase(newDatabaseName)
       .loginToGUI()
       .url(baseUrl + '/#/database/' + newDatabaseName + '/_index')
+      .clickWhenVisible('.fonticon-json')
       .waitForElementPresent('.prettyprint', waitTime, false)
       .waitForElementNotPresent('.loading-lines', waitTime, false)
       .execute('\
diff --git a/app/addons/documents/tests/nightwatch/mangoQuery.js b/app/addons/documents/tests/nightwatch/mangoQuery.js
index e19f861..596e810 100644
--- a/app/addons/documents/tests/nightwatch/mangoQuery.js
+++ b/app/addons/documents/tests/nightwatch/mangoQuery.js
@@ -36,6 +36,7 @@ module.exports = {
       ')
       .execute('$("#create-index-btn")[0].scrollIntoView();')
       .clickWhenVisible('#create-index-btn')
+      .clickWhenVisible('.fonticon-json')
 
       .waitForElementPresent('.prettyprint', waitTime, false)
       .assert.containsText('#dashboard-lower-content', 'number')
diff --git a/app/addons/documents/tests/nightwatch/paginateAllDocs.js b/app/addons/documents/tests/nightwatch/paginateAllDocs.js
index e79a8ab..38965ac 100644
--- a/app/addons/documents/tests/nightwatch/paginateAllDocs.js
+++ b/app/addons/documents/tests/nightwatch/paginateAllDocs.js
@@ -24,6 +24,7 @@ module.exports = {
       .populateDatabase(newDatabaseName)
       .loginToGUI()
       .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+      .clickWhenVisible('.fonticon-json')
 
       // ensures the main body (results list) has been rendered
       .waitForElementPresent('.prettyprint', waitTime, false)
@@ -55,6 +56,7 @@ module.exports = {
       .populateDatabase(newDatabaseName)
       .loginToGUI()
       .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+      .clickWhenVisible('.fonticon-json')
 
       // ensures the main body (results list) has been rendered
       .waitForElementPresent('.prettyprint', waitTime, false)
@@ -80,6 +82,7 @@ module.exports = {
       .populateDatabase(newDatabaseName)
       .loginToGUI()
       .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+      .clickWhenVisible('.fonticon-json')
 
       // ensures the main body (results list) has been rendered
       .waitForElementPresent('.prettyprint', waitTime, false)
@@ -95,5 +98,34 @@ module.exports = {
       .keys(['\uE013', '\uE006'])
       .waitForElementPresent('div[data-id="document_1"]', waitTime)
       .end();
-  }
+  },
+
+  'paginate to page two and switch to json view': function (client) {
+    /*jshint multistr: true */
+    var waitTime = client.globals.maxWaitTime,
+        newDatabaseName = client.globals.testDatabaseName,
+        baseUrl = client.globals.test_settings.launch_url;
+
+    client
+      .populateDatabase(newDatabaseName)
+      .loginToGUI()
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+
+      // ensures the main body (results list) has been rendered
+      .waitForElementPresent('.table-view-docs', waitTime)
+
+      .clickWhenVisible('#select-per-page', waitTime, false)
+      // http://www.w3.org/TR/2012/WD-webdriver-20120710/
+      .keys(['\uE013', '\uE006'])
+      .waitForElementNotPresent('td[title="document_16"]', waitTime)
+      .clickWhenVisible('#next', waitTime, false)
+      .waitForElementPresent('td[title="document_17"]', waitTime)
+
+      .clickWhenVisible('.fonticon-json')
+      .waitForElementPresent('.prettyprint', waitTime, false)
+      .waitForElementPresent('div[data-id="document_17"]', waitTime)
+      .clickWhenVisible('#previous', waitTime, false)
+      .waitForElementPresent('div[data-id="document_1"]', waitTime)
+      .end();
+  },
 };
diff --git a/app/addons/documents/tests/nightwatch/paginateView.js b/app/addons/documents/tests/nightwatch/paginateView.js
index 5cfe02b..ba32bb2 100644
--- a/app/addons/documents/tests/nightwatch/paginateView.js
+++ b/app/addons/documents/tests/nightwatch/paginateView.js
@@ -27,6 +27,7 @@ module.exports = {
       .waitForElementVisible('#dashboard-content table.databases', waitTime, false)
 
       .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/keyview/_view/keyview')
+      .clickWhenVisible('.fonticon-json')
 
       .waitForElementPresent('.control-toggle-queryoptions', waitTime, false)
 
@@ -62,6 +63,7 @@ module.exports = {
       .waitForElementVisible('#dashboard-content table.databases', waitTime, false)
 
       .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/keyview/_view/keyview')
+      .clickWhenVisible('.fonticon-json')
       .waitForElementPresent('.control-toggle-queryoptions', waitTime, false)
 
       // ensure the page content has loaded
@@ -97,6 +99,7 @@ module.exports = {
       .waitForElementVisible('#dashboard-content table.databases', waitTime, false)
 
       .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/keyview/_view/keyview')
+      .clickWhenVisible('.fonticon-json')
       .waitForElementPresent('.control-toggle-queryoptions', waitTime, false)
 
       // ensure the page content has loaded
@@ -117,5 +120,44 @@ module.exports = {
 
       .waitForElementPresent('div[data-id="document_1"]', waitTime)
       .end();
-  }
+  },
+
+  'paginate to page two and switch to json view': function (client) {
+    var waitTime = client.globals.maxWaitTime,
+        newDatabaseName = client.globals.testDatabaseName,
+        baseUrl = client.globals.test_settings.launch_url;
+
+    client
+      .populateDatabase(newDatabaseName)
+      .loginToGUI()
+
+      // wait for the db page to fully load
+      .waitForElementVisible('#dashboard-content table.databases', waitTime, false)
+
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/keyview/_view/keyview')
+      .waitForElementPresent('.control-toggle-queryoptions', waitTime, false)
+
+      // ensure the page content has loaded
+      .waitForElementPresent('.table-view-docs', waitTime)
+
+      .clickWhenVisible('#select-per-page', waitTime, false)
+
+      // http://www.w3.org/TR/2012/WD-webdriver-20120710/
+      .keys(['\uE013', '\uE006'])
+      .waitForElementNotPresent('.loading-lines', waitTime, false)
+      .waitForElementPresent('#next', waitTime, false)
+      .clickWhenVisible('#next', waitTime, false)
+      .waitForElementNotPresent('td[title="document_1"]', waitTime)
+      .waitForElementNotPresent('.loading-lines', waitTime, false)
+      .waitForElementPresent('td[title="document_19"]', waitTime)
+
+      .clickWhenVisible('.fonticon-json')
+      .waitForElementPresent('.prettyprint', waitTime, false)
+      .waitForElementPresent('div[data-id="document_19"]', waitTime)
+      .clickWhenVisible('#previous', waitTime, false)
+      .waitForElementNotPresent('div[data-id="document_19"]', waitTime)
+      .waitForElementNotPresent('.loading-lines', waitTime, false)
+      .waitForElementPresent('div[data-id="document_1"]', waitTime)
+      .end();
+  },
 };
diff --git a/app/addons/documents/tests/nightwatch/queryOptions.js b/app/addons/documents/tests/nightwatch/queryOptions.js
index ce800c9..a934d32 100644
--- a/app/addons/documents/tests/nightwatch/queryOptions.js
+++ b/app/addons/documents/tests/nightwatch/queryOptions.js
@@ -28,6 +28,7 @@ module.exports = {
       .clickWhenVisible('#betweenKeys', waitTime, false)
       .setValue('#startkey', '"document_2"')
       .clickWhenVisible('.query-options .btn-secondary')
+      .clickWhenVisible('.fonticon-json')
       .waitForElementNotPresent('#doc-list [data-id="document_1"]', waitTime, false)
       .waitForElementPresent('#doc-list [data-id="document_2"]', waitTime, false)
       .assert.elementPresent('#doc-list [data-id="document_2"]')
diff --git a/app/addons/documents/tests/nightwatch/resultsToolbar.js b/app/addons/documents/tests/nightwatch/resultsToolbar.js
new file mode 100644
index 0000000..6a6c95b
--- /dev/null
+++ b/app/addons/documents/tests/nightwatch/resultsToolbar.js
@@ -0,0 +1,72 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+module.exports = {
+  'Defaults to metadata layout when displaying results': (client) => {
+    const waitTime = client.globals.maxWaitTime;
+    const newDatabaseName = client.globals.testDatabaseName;
+    const baseUrl = client.globals.test_settings.launch_url;
+    const newDocumentName = 'resultsToolbarTest';
+    const docContent = {
+      "foo": "bar"
+    };
+
+    client
+      .createDocument(newDocumentName, newDatabaseName, docContent)
+      .loginToGUI()
+      .checkForDocumentCreated(newDocumentName)
+
+      .url(`${baseUrl}#/database/${newDatabaseName}/_all_docs`)
+      .waitForElementPresent('.two-sides-toggle-button', waitTime, false)
+      .assert.containsText('.two-sides-toggle-button button.active', 'Metadata')
+      .assert.elementNotPresent('.table-container-autocomplete')
+      .end();
+  },
+
+  'Layouts update on manual url change/refresh and query options': (client) => {
+    const waitTime = client.globals.maxWaitTime;
+    const newDatabaseName = client.globals.testDatabaseName;
+    const baseUrl = client.globals.test_settings.launch_url;
+    const newDocumentName = 'resultsToolbarTest';
+    const docContent = {
+      "foo": "bar"
+    };
+
+    client
+      .createDocument(newDocumentName, newDatabaseName, docContent)
+      .loginToGUI()
+      .checkForDocumentCreated(newDocumentName)
+      .url(`${baseUrl}#/database/${newDatabaseName}/_all_docs`)
+      .waitForElementPresent('.two-sides-toggle-button', waitTime, false)
+      .assert.containsText('.two-sides-toggle-button button.active', 'Metadata')
+
+      // turn include_docs on through query options
+      .clickWhenVisible('.control-toggle-queryoptions')
+      .waitForElementPresent('#qoIncludeDocsLabel', waitTime, false)
+      .clickWhenVisible('#qoIncludeDocsLabel')
+      .clickWhenVisible('.query-options .btn-secondary')
+      .waitForElementPresent('.two-sides-toggle-button', waitTime, false)
+      .assert.containsText('.two-sides-toggle-button button.active', 'Table')
+
+      // switch to json view and then turn off include_docs
+      .clickWhenVisible('.fonticon-json')
+      .assert.containsText('.two-sides-toggle-button button.active', 'JSON')
+      .clickWhenVisible('.control-toggle-queryoptions')
+      .waitForElementPresent('#qoIncludeDocsLabel', waitTime, false)
+      .assert.attributeEquals('#qoIncludeDocs', 'checked', 'true')
+      .clickWhenVisible('#qoIncludeDocsLabel')
+      .clickWhenVisible('.query-options .btn-secondary')
+      .waitForElementPresent('.two-sides-toggle-button', waitTime, false)
+      .assert.containsText('.two-sides-toggle-button button.active', 'Metadata')
+      .end();
+  },
+};
diff --git a/app/addons/documents/tests/nightwatch/revBrowser.js b/app/addons/documents/tests/nightwatch/revBrowser.js
index 189cab2..003a8f4 100644
--- a/app/addons/documents/tests/nightwatch/revBrowser.js
+++ b/app/addons/documents/tests/nightwatch/revBrowser.js
@@ -44,6 +44,7 @@ module.exports = {
       .clickWhenVisible('[data-id="button-select-theirs"]')
       .clickWhenVisible('.modal-footer input[type="checkbox"]')
       .clickWhenVisible('.modal-footer button.btn-danger')
+      .clickWhenVisible('.fonticon-json')
 
       .clickWhenVisible('[data-id="zebra"] a')
 
diff --git a/app/addons/documents/tests/nightwatch/selectDocViaTypeahead.js b/app/addons/documents/tests/nightwatch/selectDocViaTypeahead.js
index 0a083f2..ac64681 100644
--- a/app/addons/documents/tests/nightwatch/selectDocViaTypeahead.js
+++ b/app/addons/documents/tests/nightwatch/selectDocViaTypeahead.js
@@ -23,6 +23,7 @@ module.exports = {
       .populateDatabase(newDatabaseName, 3)
       .loginToGUI()
       .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+      .clickWhenVisible('.fonticon-json')
       .waitForElementPresent('.jump-to-doc', waitTime, false)
       .keys(['\uE00C'])
       .waitForElementPresent('.prettyprint', waitTime, false)
@@ -45,6 +46,7 @@ module.exports = {
       .createDocument('MY_CAP_DOC_ID', newDatabaseName, {value: 1, value: 2})
       .loginToGUI()
       .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+      .clickWhenVisible('.fonticon-json')
       .waitForElementPresent('.jump-to-doc', waitTime, false)
       .keys(['\uE00C'])
       .waitForElementPresent('.prettyprint', waitTime, false)
diff --git a/app/addons/documents/tests/nightwatch/tableView.js b/app/addons/documents/tests/nightwatch/tableView.js
index 70e0479..4c6ce2a 100644
--- a/app/addons/documents/tests/nightwatch/tableView.js
+++ b/app/addons/documents/tests/nightwatch/tableView.js
@@ -14,7 +14,7 @@
 
 module.exports = {
 
-  'Shows data in the table for all docs (include docs enabled)': function (client) {
+  'Shows data in the full table for all docs (include docs enabled)': function (client) {
     var waitTime = client.globals.maxWaitTime,
         newDatabaseName = client.globals.testDatabaseName,
         newDocumentName1 = 'bulktest1',
@@ -27,11 +27,10 @@ module.exports = {
       .loginToGUI()
       .checkForDocumentCreated(newDocumentName1)
       .checkForDocumentCreated(newDocumentName2)
-      .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs?include_docs=true')
-      .waitForElementVisible('.prettyprint', waitTime, false)
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+      .clickWhenVisible('.fonticon-table')
 
-      .clickWhenVisible('.alternative-header .two-sides-toggle-button button:last-child')
-      .waitForElementVisible('.tableview-checkbox-cell', client.globals.maxWaitTime, false)
+      .waitForElementVisible('.tableview-checkbox-cell', waitTime, false)
       .getText('.table', function (result) {
         var data = result.value;
 
@@ -46,7 +45,7 @@ module.exports = {
       .end();
   },
 
-  'Shows data in the table for all docs (include docs disabled)': function (client) {
+  'Shows data in the metadata table for all docs (include docs disabled)': function (client) {
     var waitTime = client.globals.maxWaitTime,
         newDatabaseName = client.globals.testDatabaseName,
         newDocumentName1 = 'bulktest1',
@@ -60,10 +59,8 @@ module.exports = {
       .checkForDocumentCreated(newDocumentName1)
       .checkForDocumentCreated(newDocumentName2)
       .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
-      .waitForElementVisible('.prettyprint', waitTime, false)
-
-      .clickWhenVisible('.alternative-header .two-sides-toggle-button button:last-child')
-      .waitForElementVisible('.tableview-checkbox-cell', client.globals.maxWaitTime, false)
+      .assert.containsText('button.active', 'Metadata')
+      .waitForElementVisible('.tableview-checkbox-cell', waitTime, false)
       .getText('.table', function (result) {
         var data = result.value;
 
diff --git a/app/addons/documents/tests/nightwatch/tableViewConflicts.js b/app/addons/documents/tests/nightwatch/tableViewConflicts.js
index b644f16..ed4222a 100644
--- a/app/addons/documents/tests/nightwatch/tableViewConflicts.js
+++ b/app/addons/documents/tests/nightwatch/tableViewConflicts.js
@@ -24,13 +24,10 @@ module.exports = {
       .checkForDocumentCreated('outfit1')
       .loginToGUI()
       .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
-      .waitForElementVisible('.prettyprint', waitTime, false)
 
-      .clickWhenVisible('.alternative-header .two-sides-toggle-button button:last-child')
+      .clickWhenVisible('.fonticon-table')
       .waitForElementVisible('.table', waitTime, false)
 
-      .clickWhenVisible('.control-toggle-include-docs')
-
       .waitForElementVisible('.table-container-autocomplete', waitTime, false)
       .waitForElementVisible('.tableview-conflict', waitTime, false)
 
diff --git a/app/addons/documents/tests/nightwatch/viewClone.js b/app/addons/documents/tests/nightwatch/viewClone.js
index 882f592..74f1793 100644
--- a/app/addons/documents/tests/nightwatch/viewClone.js
+++ b/app/addons/documents/tests/nightwatch/viewClone.js
@@ -24,6 +24,7 @@ module.exports = {
       .populateDatabase(newDatabaseName)
       .loginToGUI()
       .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/testdesigndoc/_view/stubview')
+      .clickWhenVisible('.fonticon-json')
       .waitForElementPresent('.prettyprint', waitTime, false)
       .assert.containsText('.prettyprint', 'stub')
       .clickWhenVisible('.index-list .active span', waitTime, true)
diff --git a/app/addons/documents/tests/nightwatch/viewCreate.js b/app/addons/documents/tests/nightwatch/viewCreate.js
index 2597b5d..6ce4d0b 100644
--- a/app/addons/documents/tests/nightwatch/viewCreate.js
+++ b/app/addons/documents/tests/nightwatch/viewCreate.js
@@ -30,6 +30,7 @@ module.exports = {
       .execute('$("#save-view")[0].scrollIntoView();')
       .waitForElementPresent('#save-view', waitTime, false)
       .clickWhenVisible('#save-view', waitTime, false)
+      .clickWhenVisible('.fonticon-json')
       .checkForDocumentCreated('_design/test_design_doc-selenium-0')
       .waitForElementPresent('.prettyprint', waitTime, false)
       .waitForElementNotPresent('.loading-lines', waitTime, false)
@@ -53,6 +54,7 @@ module.exports = {
       .execute('$("#save-view")[0].scrollIntoView();')
       .waitForElementPresent('#save-view', waitTime, false)
       .clickWhenVisible('#save-view', waitTime, false)
+      .clickWhenVisible('.fonticon-json')
       .checkForDocumentCreated('_design/test_design_doc-selenium-1')
       .waitForElementPresent('.prettyprint', waitTime, false)
       .waitForElementNotPresent('.loading-lines', waitTime, false)
@@ -76,6 +78,7 @@ module.exports = {
       .execute('$("#save-view")[0].scrollIntoView();')
       .waitForElementPresent('#save-view', waitTime, false)
       .clickWhenVisible('#save-view', waitTime, false)
+      .clickWhenVisible('.fonticon-json')
       .checkForDocumentCreated('_design/test_design_doc-selenium-3')
       .waitForElementPresent('.prettyprint', waitTime, false)
       .waitForElementNotPresent('.loading-lines', waitTime, false)
@@ -101,6 +104,7 @@ module.exports = {
       .execute('$("#save-view")[0].scrollIntoView();')
       .waitForElementPresent('#save-view', waitTime, false)
       .clickWhenVisible('#save-view')
+      .clickWhenVisible('.fonticon-json')
       .checkForDocumentCreated('_design/test_design_doc-selenium-2')
       .waitForElementPresent('.prettyprint', waitTime, false)
       .waitForElementNotPresent('.loading-lines', waitTime, false)
@@ -129,6 +133,7 @@ module.exports = {
       ')
       .execute('$("#save-view")[0].scrollIntoView();')
       .clickWhenVisible('#save-view')
+      .clickWhenVisible('.fonticon-json')
       .checkForDocumentCreated('_design/testdesigndoc/_view/test-new-view')
       .waitForElementPresent('.prettyprint', waitTime, false)
       .waitForElementNotPresent('.loading-lines', waitTime, false)
diff --git a/app/addons/documents/tests/nightwatch/viewCreateBadView.js b/app/addons/documents/tests/nightwatch/viewCreateBadView.js
index 8f79e4e..fc46ebc 100644
--- a/app/addons/documents/tests/nightwatch/viewCreateBadView.js
+++ b/app/addons/documents/tests/nightwatch/viewCreateBadView.js
@@ -42,7 +42,7 @@ module.exports = {
       .clickWhenVisible('.control-toggle-queryoptions', waitTime, false)
       .clickWhenVisible('label[for="qoReduce"]', waitTime, false)
       .clickWhenVisible('.query-options .btn-secondary', waitTime, false)
-      .waitForAttribute('.doc-item', 'textContent', function (docContents) {
+      .waitForAttribute('.table-view-docs td:nth-child(4)', 'title', function (docContents) {
         return (/_sum function requires/).test(docContents);
       })
       .end();
@@ -60,7 +60,7 @@ module.exports = {
       .clickWhenVisible('.control-toggle-queryoptions', waitTime, false)
       .clickWhenVisible('label[for="qoReduce"]', waitTime, false)
       .clickWhenVisible('.query-options .btn-secondary', waitTime, false)
-      .waitForAttribute('.doc-item', 'textContent', function (docContents) {
+      .waitForAttribute('.table-view-docs td:nth-child(4)', 'title', function (docContents) {
         return (/_sum function requires/).test(docContents);
       })
       .end();
diff --git a/app/addons/documents/tests/nightwatch/viewDelete.js b/app/addons/documents/tests/nightwatch/viewDelete.js
index d88fff5..d82bb12 100644
--- a/app/addons/documents/tests/nightwatch/viewDelete.js
+++ b/app/addons/documents/tests/nightwatch/viewDelete.js
@@ -24,6 +24,7 @@ module.exports = {
       .populateDatabase(newDatabaseName)
       .loginToGUI()
       .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/testdesigndoc/_view/stubview')
+      .clickWhenVisible('.fonticon-json')
       .waitForElementPresent('.prettyprint', waitTime, false)
       .assert.containsText('.prettyprint', 'stub')
 
diff --git a/app/addons/documents/tests/nightwatch/viewEdit.js b/app/addons/documents/tests/nightwatch/viewEdit.js
index 7deab4c..18f5ee5 100644
--- a/app/addons/documents/tests/nightwatch/viewEdit.js
+++ b/app/addons/documents/tests/nightwatch/viewEdit.js
@@ -87,6 +87,7 @@ module.exports = {
       .checkForStringPresent(viewUrl, 'hasehase6000')
       .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/testdesigndoc/_view/stubview')
       .waitForElementNotPresent('.loading-lines', waitTime, false)
+      .clickWhenVisible('.fonticon-json')
       .waitForElementVisible('.prettyprint', waitTime, false)
       .waitForElementPresent('.faux-header__doc-header-title', waitTime, false)
       .waitForAttribute('.faux-header__doc-header-title', 'textContent', function (docContents) {
@@ -196,6 +197,7 @@ module.exports = {
       .populateDatabase(newDatabaseName)
       .loginToGUI()
       .url(baseUrl + '/#/database/' + newDatabaseName + '/_design/testdesigndoc/_view/stubview')
+      .clickWhenVisible('.fonticon-json')
       .waitForElementPresent('.prettyprint', waitTime, false)
 
       // confirm the sidebar shows the testdesigndoc design doc
diff --git a/app/addons/documents/tests/nightwatch/viewQueryOptions.js b/app/addons/documents/tests/nightwatch/viewQueryOptions.js
index dbd4db0..d8c419d 100644
--- a/app/addons/documents/tests/nightwatch/viewQueryOptions.js
+++ b/app/addons/documents/tests/nightwatch/viewQueryOptions.js
@@ -27,6 +27,7 @@ module.exports = {
       .clickWhenVisible('#byKeys', waitTime, false)
       .setValue('#keys-input', '["document_1"]')
       .clickWhenVisible('.query-options .btn-secondary')
+      .clickWhenVisible('.fonticon-json')
       .waitForElementNotPresent('#doc-list [data-id="document_2"]', waitTime, false)
       .assert.elementNotPresent('#doc-list [data-id="document_2"]')
       .assert.elementNotPresent('#doc-list [data-id="document_0"]')
@@ -48,6 +49,7 @@ module.exports = {
       .clickWhenVisible('#byKeys', waitTime, false)
       .setValue('#keys-input', '["document_1",\n"document_2"]')
       .clickWhenVisible('.query-options .btn-secondary')
+      .clickWhenVisible('.fonticon-json')
       .waitForElementNotPresent('#doc-list [data-id="document_0"]', waitTime, false)
       .assert.elementNotPresent('#doc-list [data-id="document_0"]')
       .assert.elementPresent('#doc-list [data-id="document_1"]')
diff --git a/app/addons/fauxton/tests/nightwatch/notificationCenter.js b/app/addons/fauxton/tests/nightwatch/notificationCenter.js
index 0c192bd..1f17fdc 100644
--- a/app/addons/fauxton/tests/nightwatch/notificationCenter.js
+++ b/app/addons/fauxton/tests/nightwatch/notificationCenter.js
@@ -25,6 +25,7 @@ module.exports = {
       .waitForElementNotPresent('#notification-center-btn div.fonticon-bell', waitTime, false)
 
       .loginToGUI()
+      .waitForElementNotPresent('.notification-wrapper', waitTime, false)
       .waitForElementPresent('#notification-center-btn', waitTime, false)
       .assert.cssClassNotPresent('.notification-center-panel', 'visible')
       .clickWhenVisible('#notification-center-btn', waitTime, false)
diff --git a/app/addons/fauxton/tests/nightwatch/updatesUrlsSameRouteobject.js b/app/addons/fauxton/tests/nightwatch/updatesUrlsSameRouteobject.js
index 39c3ca0..96a347e 100644
--- a/app/addons/fauxton/tests/nightwatch/updatesUrlsSameRouteobject.js
+++ b/app/addons/fauxton/tests/nightwatch/updatesUrlsSameRouteobject.js
@@ -24,6 +24,7 @@ module.exports = {
       .waitForElementVisible('.faux__jsonlink-link', waitTime, false)
       .assert.attributeContains('.faux__jsonlink-link', 'href', newDatabaseName + '/_find')
       .clickWhenVisible('.edit-link')
+      .clickWhenVisible('.fonticon-json')
       .waitForElementVisible('.prettyprint', waitTime, false)
       .waitForElementVisible('.faux__jsonlink-link', waitTime, false)
       .assert.attributeContains('.faux__jsonlink-link', 'href', newDatabaseName + '/_index')
diff --git a/assets/js/plugins/cloudant.pagingcollection.js b/assets/js/plugins/cloudant.pagingcollection.js
index f18d78f..6d66805 100644
--- a/assets/js/plugins/cloudant.pagingcollection.js
+++ b/assets/js/plugins/cloudant.pagingcollection.js
@@ -52,7 +52,7 @@ export const PagingCollection = Backbone.Collection.extend({
     params.skip = (parseInt(currentParams.skip, 10) || 0) + skipIncrement;
 
     // guard against hard limits
-    if(this.paging.defaultParams.limit) {
+    if (this.paging.defaultParams.limit) {
       params.limit = Math.min(this.paging.defaultParams.limit, params.limit);
     }
     // request an extra row so we know that there are more results
@@ -102,14 +102,14 @@ export const PagingCollection = Backbone.Collection.extend({
 
   // `next` is called with the number of items for the next page.
   // It returns the fetch promise.
-  next: function(options){
+  next: function(options) {
     this.paging.direction = "next";
     return this._iterate(this.paging.pageSize, options);
   },
 
   // `previous` is called with the number of items for the previous page.
   // It returns the fetch promise.
-  previous: function(options){
+  previous: function(options) {
     this.paging.direction = "previous";
     return this._iterate(0 - this.paging.pageSize, options);
   },
@@ -118,7 +118,7 @@ export const PagingCollection = Backbone.Collection.extend({
     try {
       JSON.parse(val);
       return false;
-    } catch(e) {
+    } catch (e) {
       return true;
     }
   },
@@ -159,12 +159,11 @@ export const PagingCollection = Backbone.Collection.extend({
       update_seq: resp.update_seq
     };
 
-    var skipLimit = this.paging.defaultParams.skip || 0;
-    if(this.paging.params.skip > skipLimit) {
+    if (this.paging.params.skip > 0) {
       this.paging.hasPrevious = true;
     }
 
-    if(rows.length === this.paging.pageSize + 1) {
+    if (rows.length === this.paging.pageSize + 1) {
       this.paging.hasNext = true;
 
       // remove the next page marker result
diff --git a/assets/less/formstyles.less b/assets/less/formstyles.less
index 4618adb..0c870d0 100644
--- a/assets/less/formstyles.less
+++ b/assets/less/formstyles.less
@@ -59,7 +59,7 @@ button:focus {
   text-shadow: none;
   background-repeat: no-repeat;
   padding: 10px;
-  margin-top: 0px;
+  margin-top: 0;
   .icon {
     margin-right: 0.2em;
   }
@@ -264,7 +264,7 @@ form.view-query-update, form.view-query-save {
     margin-right: 10px;
     position: absolute;
     left: 0;
-    bottom: 0px;
+    bottom: 0;
     background-color: #7C8085;
   }
 
@@ -290,7 +290,7 @@ form.view-query-update, form.view-query-save {
 div.add-dropdown {
   position: absolute;
   top: 2px;
-  right: 0px;
+  right: 0;
   .dropdown-menu {
     left: -110px;
     padding-bottom: 0;
@@ -332,7 +332,7 @@ input.errorHighlight {
 
 .two-sides-toggle-button {
   font-size: 15px;
-  padding: 11px;
+  padding: 0 11px;
 
   button.btn {
     padding: 10px 15px;
diff --git a/assets/less/templates.less b/assets/less/templates.less
index f42de00..711711b 100644
--- a/assets/less/templates.less
+++ b/assets/less/templates.less
@@ -280,7 +280,7 @@
 
   #right-content {
     .view {
-      padding: 0 20px 40px;
+      padding: 0 0 40px;
     }
     border-left: 1px solid #999;
     .box-shadow(-6px 0 rgba(0, 0, 0, 0.1));

-- 
To stop receiving notification emails like this one, please contact
['"commits@couchdb.apache.org" <co...@couchdb.apache.org>'].