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}
+ <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>'].