diff --git a/app/addons/components/assets/less/docs.less b/app/addons/components/assets/less/docs.less index 988b7f0e6..473a30572 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 1bf6ced3e..a0cdde8f3 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 ( -
+
{this.getCheckbox()}
-
+
{this.props.keylabel} diff --git a/app/addons/components/tests/docSpec.js b/app/addons/components/tests/docSpec.js index 57919354b..442e7e5b0 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( - , + , 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 000000000..1231e3a2e --- /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(); + + wrapper.find('#qoSkip').simulate('change'); + expect(spy.calledOnce).toBe(true); + }); + + it('updateLimit is called after change', () => { + const spy = sinon.spy(); + const wrapper = mount(); + + wrapper.find('#qoLimit').simulate('change'); + expect(spy.calledOnce).toBe(true); + }); + + it('toggleDescending is called after change', () => { + const spy = sinon.spy(); + const wrapper = mount(); + + 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 000000000..c966316d3 --- /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 000000000..34d936fb1 --- /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 000000000..574db687e --- /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(); + + wrapper.instance().componentDidMount(); + expect(spy.calledOnce).toBe(true); + }); + + it('calls fetchAllDocs on update if ddocsOnly switches', () => { + const spy = sinon.spy(); + const wrapper = shallow( {}} + 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( {}} + 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( {}} + 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( {}} + results={[]} + />); + + expect(wrapper.instance().isSelected('foo')).toBe(false); + }); + + it('docChecked calls selectDoc', () => { + const spy = sinon.spy(); + const wrapper = shallow( {}} + 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( {}} + 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 000000000..0e3c2a2c4 --- /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 000000000..d337ab1ef --- /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(); + + 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(); + + 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(); + + 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(); + + wrapper.find('#byKeys').simulate('click'); + expect(spy.calledOnce).toBe(true); + }); + + it('calls toggleBetweenKeys onClick', () => { + const spy = sinon.spy(); + const wrapper = mount(); + + wrapper.find('#betweenKeys').simulate('click'); + expect(spy.calledOnce).toBe(true); + }); + + it('calls updateBetweenKeys onChange', () => { + const spy = sinon.spy(); + const wrapper = mount(); + + 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(); + + wrapper.find('#qoIncludeEndKeyInResults').simulate('change'); + expect(spy.calledOnce).toBe(true); + }); + + it('calls updateByKeys onChange', () => { + const spy = sinon.spy(); + const wrapper = mount(); + + 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 000000000..80b0c45fd --- /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( {}} + 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( {}} + 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(); + + wrapper.find('#qoIncludeDocs').simulate('change'); + expect(spy.calledOnce).toBe(true); + }); + + it('calls groupLevelChange onChange', () => { + const spy = sinon.spy(); + const wrapper = mount( {}} + 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( {}} + 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 000000000..75de7e63f --- /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 000000000..97c0d95d4 --- /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( {}} + 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( {}} + 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( {}} + 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( {}} + 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( {}} + 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( {}} + 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( {}} + 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( {}} + 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( {}} + 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 000000000..4fb93c2dc --- /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( {}} + previousClicked={() => {}} + />); + + expect(wrapper.find('ul.pagination li.disabled').length).toBe(2); + }); + + it('pagination control disabled when canShowPrevious is false', () => { + const wrapper = mount( {}} + previousClicked={() => {}} + />); + + expect(wrapper.find('ul.pagination li.disabled #previous').length).toBe(1); + }); + + it('pagination control disabled when canShowNext is false', () => { + const wrapper = mount( {}} + 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( {}} + previousClicked={() => {}} + />); + + expect(wrapper.find('ul.pagination li.disabled').length).toBe(0); + }); +}); diff --git a/app/addons/documents/__tests__/perpage-selector.test.js b/app/addons/documents/__tests__/perpage-selector.test.js new file mode 100644 index 000000000..dc9fd0ea0 --- /dev/null +++ b/app/addons/documents/__tests__/perpage-selector.test.js @@ -0,0 +1,27 @@ +// 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 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(); + + wrapper.find('#select-per-page').simulate('change'); + expect(spy.calledOnce).toBe(true); + }); +}); diff --git a/app/addons/documents/__tests__/query-buttons.test.js b/app/addons/documents/__tests__/query-buttons.test.js new file mode 100644 index 000000000..225ccc30b --- /dev/null +++ b/app/addons/documents/__tests__/query-buttons.test.js @@ -0,0 +1,27 @@ +// 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 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(); + + 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 000000000..fec4533de --- /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( {}} + 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( {}} + 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( {}} + 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( {}} + 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 000000000..133feb252 --- /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 000000000..b0e7a4f5a --- /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 000000000..2fdd46f52 --- /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(); + 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(); + 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(); + 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 000000000..a5e479b83 --- /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 000000000..e0249ba1a --- /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( {}} + />); + + expect(wrapper.find('.shown-fields').text()).toMatch('Showing 5 of 10 columns.'); + }); + + it('shows custom text when all columns are shown', () => { + const wrapper = mount( {}} + />); + + 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(); + + 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 000000000..b15e3455f --- /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 0670cbbf0..1600b5c19 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 f53f9b938..51f296f3f 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 10cf4bdfd..c1348ee0a 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 249782336..4f03e507d 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 = ; + if (isRedux) { + queryOptionsComponent = ; + } + + return queryOptionsComponent; +}; + +const RightAllDocsHeader = ({database, hideQueryOptions, isRedux, fetchUrl, ddocsOnly}) =>
@@ -25,8 +39,7 @@ const RightAllDocsHeader = ({database, hideQueryOptions}) =>
- - {hideQueryOptions ? '' : } + {getQueryOptionsComponent(hideQueryOptions, isRedux, fetchUrl, ddocsOnly)}
; 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 000000000..e5c5a2a01 --- /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 = ; + } + + // Determine if we need to display the bulk doc header + let bulkHeader = null; + if (hasResults || isLoading) { + bulkHeader = ; + } + + return ( +
+ {bulkAction} + {bulkHeader} + +
+ ); + } +}; + +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/constants.js b/app/addons/documents/constants.js new file mode 100644 index 000000000..a7063d3e9 --- /dev/null +++ b/app/addons/documents/constants.js @@ -0,0 +1,19 @@ +// 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. + +export default { + 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 3dc26b3ed..3baf9430d 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 b7fc6a69a..7d5d9b7e8 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 0e76d2ec9..046470975 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 = ; + } + + // 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 = ; + + json = ; + } return (
- - + {table} + {metadata} + {json} - {this.props.showIncludeAllDocs ? : null} { /* text is set via responsive css */}
); - }, + } - 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 12938439e..1134e0797 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 b484fad22..619cfe33e 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 e39c17bba..ddd17e680 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 000000000..c0ce4c5d8 --- /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 000000000..1b580a390 --- /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 000000000..366f9e726 --- /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 000000000..94e16205f --- /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 000000000..9d006747d --- /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 Showing 0 documents.; + } + + return Showing document {pageStart} - {pageEnd}.; + } + + 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 ( +
+ + +
+
+ {showPrioritizedEnabled && hasResults ? + : null} +
+ +
+ {this.getPageNumberText()} +
+
+
+ ); + } +}; 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 000000000..dd02c400f --- /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 ( +
+
    +
  • + +
  • +
  • + +
  • +
+
+ ); +}; + +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 000000000..ba9657c8d --- /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 (); + }); + } + + render () { + return ( +
+ +
+ ); + } + +}; + +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 000000000..0023c2076 --- /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 ( +
+ Showing {fields.shown} columns. +
+ ); + } + + return ( +
+ Showing {fields.shown} of {fields.allFieldCount} columns. +
+ ); + } + + render () { + const { prioritizedEnabled, toggleShowAllColumns } = this.props; + + return ( +
+ {this.getAmountShownFields()} +
+ +
+
+ ); + } +}; + +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 000000000..f3be5f0f9 --- /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 ( +
+
Additional Parameters
+
+
+ +
+
+
+
+ + +
+
+ +
+
+
+ ); + } +}; 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 000000000..dddb63e38 --- /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 ( + + ); + } +}; 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 000000000..735dd4680 --- /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 ( + + ); + } + + reduce () { + if (!this.props.showReduce) { + return null; + } + + return ( + +
+ + +
+ {this.groupLevel()} +
+ ); + } + + render () { + var includeDocs = this.props.includeDocs; + return ( +
+ + Query Options + + + + +
+
+
+ + +
+ {this.reduce()} +
+
+
+ ); + } + +}; + +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 000000000..2251c1179 --- /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 ( +
+
+ + Cancel +
+
+ ); + } +}; + +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 000000000..7d8b9630b --- /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 ( + + +
+ + + + + +
+ ); + } + + render () { + return ( +
+
+
+ + {this.getTray()} +
+
+
+ ); + } +}; + +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 000000000..58f17eb7b --- /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 ( + + ); + } +}; diff --git a/app/addons/documents/index-results/components/results/NoResultsScreen.js b/app/addons/documents/index-results/components/results/NoResultsScreen.js new file mode 100644 index 000000000..2279b28d8 --- /dev/null +++ b/app/addons/documents/index-results/components/results/NoResultsScreen.js @@ -0,0 +1,26 @@ +// 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 NoResultsScreen ({ text }) { + return ( +
+
+

{text}

+
+ ); +}; + +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 000000000..797536870 --- /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 ( + + + ); + } + + getDocumentList () { + let noop = () => {}; + let data = this.props.results.results; + + return _.map(data, function (doc, i) { + return ( + + {doc.url ? this.getUrlFragment('#' + doc.url) : doc.url} + + ); + }, this); + } + + getDocumentStyleView () { + let classNames = 'view'; + + if (this.props.isListDeletable) { + classNames += ' show-select'; + } + + return ( +
+
+ {this.getDocumentList()} +
+
+ ); + } + + getTableStyleView () { + return ( +
+ +
+ ); + } + + render () { + let mainView = null; + + if (this.props.isLoading) { + mainView =
; + } else if (!this.props.hasResults) { + mainView = ; + } else if (this.props.selectedLayout === Constants.LAYOUT_ORIENTATION.JSON) { + mainView = this.getDocumentStyleView(); + } else { + mainView = this.getTableStyleView(); + } + + return ( +
+ + {mainView} +
+ ); + } + +}; 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 000000000..4db7d7cda --- /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 ( + + {stringified} + + ); + }.bind(this)); + + return row; + } + + maybeGetCheckboxCell (el, i) { + return ( + + {el.isDeletable ? : null} + + ); + } + + 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 = ( +
+ {attachmentCount} +
+ ); + } + + if (conflictCount) { + textConflicts = conflictCount === 1 ? conflictCount + ' Conflict' : conflictCount + ' Conflicts'; + conflictIndicator = ( +
+ {conflictCount} +
+ ); + } + + return ( + + {conflictIndicator} + {attachmentIndicator} + + ); + } + + getCopyButton (el) { + const text = JSON.stringify(el, null, ' '); + return ( + + + + ); + } + + 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 ( + + {this.maybeGetCheckboxCell(el, i)} + {this.getCopyButton(docContent)} + {this.getRowContents(el, i)} + {this.getAdditionalInfoRow(docContent)} + + ); + } +}; + +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 000000000..299ce7118 --- /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 ( + + ); + }.bind(this)); + } + + getOptionFieldRows (filtered) { + const notSelectedFields = this.props.data.notSelectedFields; + + if (!notSelectedFields) { + return filtered.map(function (el, i) { + return {el}; + }); + } + + return filtered.map(function (el, i) { + return ( + + {this.getDropdown( + el, + this.props.data.schema, + i, + this.props.changeField, + this.props.data.selectedFields + )} + + ); + }.bind(this)); + } + + getDropdown (selectedField, notSelectedFields, i, changeField, selectedFields) { + + return ( + + ); + } + + getHeader () { + const selectedFields = this.props.data.selectedFields; + const row = this.getOptionFieldRows(selectedFields); + + return ( + + + + {row} + + + ); + } + + render () { + return ( +
+ + + {this.getHeader()} + + + {this.getContentRows()} + +
+
+ ); + } +}; 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 000000000..5b1c52aad --- /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 ( +
+
+ { + changeField({newSelectedRow: el.value, index: index}, selectedFields); + }} + /> +
+
+ ); +}; 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 000000000..9767acb1c --- /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 000000000..d865401ff --- /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 000000000..27a77573f --- /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 000000000..eb08ae295 --- /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 000000000..89a04dd77 --- /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 000000000..b21142ddc --- /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 000000000..2bce347ce --- /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 2b1563fcc..26dd13593 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 ( - + {stringified} ); @@ -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 ( - -
{this.maybeGetUrl(element.url, el._id || el.id)}
-
{el._rev}
- - ); - }, - - maybeGetUrl: function (url, stringified) { - if (!url) { - return stringified; - } - - return ( - - {stringified} - - ); - }, - maybeGetCheckboxCell: function (el, i) { return ( @@ -144,7 +120,7 @@ var TableRow = React.createClass({ } return ( - + {conflictIndicator} {attachmentIndicator} @@ -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({ {this.maybeGetCheckboxCell(el, i)} {this.getCopyButton(docContent)} - {this.maybeGetSpecialField(el, i)} {this.getRowContents(el, i)} {this.getAdditionalInfoRow(docContent)} @@ -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 (
@@ -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} />
); @@ -222,6 +210,7 @@ var TableView = React.createClass({ return ( - {this.getDropdown(el, this.props.data.schema, i)} + {this.getDropdown( + el, + this.props.data.schema, + i, + this.props.changeField, + this.props.data.selectedFields + )} ); }.bind(this)); }, - getDropdown: function (selectedField, notSelectedFields, i) { + getDropdown: function (selectedField, notSelectedFields, i, changeField, selectedFields) { return ( + index={i} + changeField={changeField} + selectedFields={selectedFields} /> ); }, getHeader: function () { var selectedFields = this.props.data.selectedFields; - - var specialField = null; - if (this.props.data.hasMetadata) { - specialField = (Metadata); - } - var row = this.getOptionFieldRows(selectedFields); - var box = ( - - {this.props.isListDeletable ? : null} - - ); - - return ( - {box} + - {specialField} {row} @@ -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({ -
- {loadLines} -
-
- {isDeletable ? : null} - - {this.getDocumentList()} + {this.getDocumentList()}
); }, - getTableStyleView: function (loadLines) { + getTableStyleView: function () { return (
-
- {loadLines} -
-
); }, render: function () { - - var loadLines = null; - var isTableView = this.props.isTableView; + let mainView = null; + const { toggleSelectAll } = this.props; + let toolbar = ; if (this.props.isLoading) { - loadLines = ; + mainView =
; + } else if (!this.props.hasResults) { + mainView = ; + } 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 (
+ {toolbar} {mainView}
); @@ -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 = ; - - if (this.state.hasResults) { - view = ; - } - - 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 000000000..6c38011cc --- /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 eb1856f80..821e70cf2 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 9de02e7ab..08713de3a 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(, 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(, 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( , @@ -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( , @@ -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( , 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( , 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 2ac3de2e4..fd8a5cd04 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 f5af05a1e..3b83b585d 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 (
@@ -43,13 +46,16 @@ export const TabsSidebarHeader = ({ />
-
- {hideHeaderBar ? null : } -
- +
- + { isRedux ? : + }
@@ -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 (
@@ -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 = ; + } else { + lowerContent = ; + } + return (
} + lowerContent={lowerContent} + isRedux={isRedux} + fetchUrl={fetchUrl} + databaseName={dbName} />
); @@ -125,7 +167,6 @@ export const ChangesSidebarLayout = ({docURL, database, endpoint, dbName, dropDo return (
{ +export const RightHeader = ({docURL, endpoint}) => { return (
-
- -
@@ -45,14 +41,13 @@ export const MangoFooter = () => { ); }; -export const MangoHeader = ({showIncludeAllDocs, crumbs, docURL, endpoint}) => { +export const MangoHeader = ({crumbs, docURL, endpoint}) => { return (
@@ -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 (
{ - 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 7e47937d5..d752dba75 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 883bc3fb7..47ce4b4a5 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 (
- {showTableControls ? + {showTableControls && hasResults ? : null} diff --git a/app/addons/documents/queryoptions/queryoptions.js b/app/addons/documents/queryoptions/queryoptions.js index 80fa0f50d..34e3fe9a1 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 ( diff --git a/app/addons/documents/resources.js b/app/addons/documents/resources.js index 5349e16fc..7976a66ea 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 583cfdab3..1a8dae1fd 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 ; }, diff --git a/app/addons/documents/routes-index-editor.js b/app/addons/documents/routes-index-editor.js index d204bb5da..ea2fdf62e 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 { /*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 6cc3cbca7..2960a3934 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 6ac759a4e..9319af744 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 7332db173..4f1303f80 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 8c3e4ce6a..8e256167d 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 6372fb66f..92ca33d0d 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 424466bec..b7fa3b1c3 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 e19f8619f..596e8103e 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 e79a8ab8a..38965ac86 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 5cfe02bd9..ba32bb20b 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 ce800c953..a934d32f5 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 000000000..6a6c95b45 --- /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 189cab2ea..003a8f4bf 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 0a083f206..ac64681ed 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 70e047979..4c6ce2a7d 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 b644f16c3..ed4222a36 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 882f59212..74f179366 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 2597b5da4..6ce4d0bc3 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 8f79e4e97..fc46ebc49 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 d88fff55c..d82bb12bb 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 7deab4c96..18f5ee590 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 dbd4db022..d8c419d59 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 0c192bd0f..1f17fdccc 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 39c3ca052..96a347e62 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 f18d78fd4..6d6680501 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 4618adb5a..0c870d0f9 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 f42de0078..711711b42 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));