From dce51574166b18e62e8c65c8438909b56c4c063c Mon Sep 17 00:00:00 2001 From: Ryan Millay Date: Mon, 17 Apr 2017 08:30:37 -0400 Subject: [PATCH 01/25] initial tool bar. limited view logic --- app/addons/documents/assets/less/header.less | 4 ++ .../documents/assets/less/index-results.less | 32 ++++++++++-- app/addons/documents/components/results-toolbar.js | 41 +++++++++++++++ app/addons/documents/constants.js | 19 +++++++ app/addons/documents/header/header.actions.js | 6 +-- app/addons/documents/header/header.actiontypes.js | 2 +- app/addons/documents/header/header.js | 35 +++++++------ .../index-results/index-results.components.js | 59 ++++++++++------------ app/addons/documents/index-results/stores.js | 46 ++++++++++------- app/addons/documents/layouts.js | 10 +--- app/addons/documents/routes-documents.js | 1 - app/addons/documents/routes-index-editor.js | 1 - assets/less/formstyles.less | 2 +- 13 files changed, 171 insertions(+), 87 deletions(-) create mode 100644 app/addons/documents/components/results-toolbar.js create mode 100644 app/addons/documents/constants.js 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..2c79c93ed 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 0px; + } +} + +.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,7 +80,6 @@ .table-view-docs { position: absolute; - margin-top: 30px; .bulk-action-component { @@ -97,7 +119,7 @@ white-space: nowrap; } td.tableview-checkbox-cell, th.tableview-header-el-checkbox { - width: 68px; + width: 45px; } .tableview-conflict { color: #F00; diff --git a/app/addons/documents/components/results-toolbar.js b/app/addons/documents/components/results-toolbar.js new file mode 100644 index 000000000..0eb357a31 --- /dev/null +++ b/app/addons/documents/components/results-toolbar.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 Header from "../header/header.react"; +import Stores from "../sidebar/stores.react"; +import Components from "../../components/react-components.react"; + +const {BulkDocumentHeaderController} = Header; +const {BulkActionComponent} = Components; +const store = Stores.sidebarStore; + +export const ResultsToolBar = ({removeItem, allDocumentsSelected, hasSelectedItem, toggleSelectAll, isLoading, isListDeletable}) => { + const dbName = store.getDatabase().id; + + return ( +
+ {isListDeletable ? : null} + +
+ + Create Document + +
+
+ ); +}; 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..6f0466a59 100644 --- a/app/addons/documents/header/header.js +++ b/app/addons/documents/header/header.js @@ -13,19 +13,19 @@ 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({ getStoreState () { return { selectedView: indexResultsStore.getCurrentViewType(), - isTableView: indexResultsStore.getIsTableView(), + selectedLayout: indexResultsStore.getSelectedLayout(), includeDocs: queryOptionsStore.getIncludeDocsEnabled(), bulkDocCollection: indexResultsStore.getBulkDocCollection() }; @@ -51,31 +51,30 @@ var BulkDocumentHeaderController = React.createClass({ }, render () { - var isTableViewSelected = this.state.isTableView; + const layout = this.state.selectedLayout; return (
+ - {this.props.showIncludeAllDocs ? : null} { /* text is set via responsive css */}
); }, @@ -84,8 +83,8 @@ var BulkDocumentHeaderController = React.createClass({ Actions.toggleIncludeDocs(this.state.includeDocs, this.state.bulkDocCollection); }, - toggleTableView: function (enable) { - Actions.toggleTableView(enable); + toggleLayout: function (layout) { + Actions.toggleLayout(layout); } }); diff --git a/app/addons/documents/index-results/index-results.components.js b/app/addons/documents/index-results/index-results.components.js index 2b1563fcc..15179c3dc 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({ @@ -271,22 +273,9 @@ var TableView = React.createClass({ var row = this.getOptionFieldRows(selectedFields); - var box = ( - - {this.props.isListDeletable ? : null} - - ); - - return ( - {box} + {specialField} {row} @@ -356,8 +345,7 @@ var ResultsScreen = React.createClass({ }, getDocumentStyleView: function (loadLines) { - var classNames = 'view'; - var isDeletable = this.props.isListDeletable; + let classNames = 'view'; if (this.props.isListDeletable) { classNames += ' show-select'; @@ -370,15 +358,7 @@ var ResultsScreen = React.createClass({
- {isDeletable ? : null} - - {this.getDocumentList()} + {this.getDocumentList()}
); @@ -409,16 +389,33 @@ var ResultsScreen = React.createClass({ render: function () { - var loadLines = null; - var isTableView = this.props.isTableView; + let loadLines = null; + let mainView = null; if (this.props.isLoading) { loadLines = ; } - var mainView = isTableView ? this.getTableStyleView(loadLines) : this.getDocumentStyleView(loadLines); + switch (this.props.selectedLayout) { + case Constants.LAYOUT_ORIENTATION.TABLE: + mainView = this.getTableStyleView(loadLines); + break; + + case Constants.LAYOUT_ORIENTATION.METADATA: + mainView = this.getTableStyleView(loadLines); + break; + + case Constants.LAYOUT_ORIENTATION.JSON: + mainView = this.getDocumentStyleView(loadLines); + break; + + default: + mainView = this.getTableStyleView(loadLines); + }; + return (
+ {mainView}
); @@ -453,7 +450,7 @@ var ViewResultListController = React.createClass({ isLoading: store.isLoading(), isEditable: store.isEditable(), textEmptyIndex: store.getTextEmptyIndex(), - isTableView: store.getIsTableView(), + selectedLayout: store.getSelectedLayout(), allDocumentsSelected: store.areAllDocumentsSelected(), hasSelectedItem: !!selectedItemsLength, selectedItemsLength: selectedItemsLength, @@ -506,7 +503,7 @@ var ViewResultListController = React.createClass({ docChecked={this.docChecked} isLoading={this.state.isLoading} results={this.state.results} - isTableView={this.state.isTableView} />; + selectedLayout={this.state.selectedLayout} />; } return ( diff --git a/app/addons/documents/index-results/stores.js b/app/addons/documents/index-results/stores.js index eb1856f80..8a1219d70 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.JSON; this.resetPagination(); }, @@ -305,14 +306,28 @@ Stores.IndexResultsStore = FauxtonAPI.Store.extend({ }, getResults: function () { + switch (this._selectedLayout) { + case Constants.LAYOUT_ORIENTATION.TABLE: + return this.getTableViewData(); + break; + + case Constants.LAYOUT_ORIENTATION.METADATA: + return this.getTableViewData(); + break; + + case Constants.LAYOUT_ORIENTATION.JSON: + return this.getJsonViewData(); + break; + + default: + return this.getJsonViewData(); + }; + }, + + getJsonViewData: function () { var hasBulkDeletableDoc; var res; - // Table sytle view - if (this.getIsTableView()) { - return this.getTableViewData(); - } - // JSON style views res = this._filteredCollection .map(function (doc, i) { @@ -727,19 +742,16 @@ 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; }, getIsPrioritizedEnabled: function () { @@ -794,8 +806,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: diff --git a/app/addons/documents/layouts.js b/app/addons/documents/layouts.js index f5af05a1e..b16e2f59e 100644 --- a/app/addons/documents/layouts.js +++ b/app/addons/documents/layouts.js @@ -25,11 +25,9 @@ import RightAllDocsHeader from './components/header-docs-right'; export const TabsSidebarHeader = ({ hideQueryOptions, - hideHeaderBar, database, dbName, dropDownLinks, - showIncludeAllDocs, docURL, endpoint }) => { @@ -43,9 +41,6 @@ export const TabsSidebarHeader = ({ />
-
- {hideHeaderBar ? null : } -
@@ -103,11 +98,10 @@ 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}) => { return (
Date: Mon, 24 Apr 2017 11:10:27 -0400 Subject: [PATCH 02/25] metadata default and include docs flag tied to view --- app/addons/documents/components/results-toolbar.js | 6 +++--- app/addons/documents/header/header.js | 7 +------ app/addons/documents/index-results/actions.js | 2 +- .../documents/index-results/index-results.components.js | 11 ++++++++--- app/addons/documents/index-results/stores.js | 4 ++-- app/addons/documents/layouts.js | 1 - app/addons/documents/pagination/pagination.js | 3 ++- app/addons/documents/routes-documents.js | 9 ++++++--- 8 files changed, 23 insertions(+), 20 deletions(-) diff --git a/app/addons/documents/components/results-toolbar.js b/app/addons/documents/components/results-toolbar.js index 0eb357a31..8d2cf1f01 100644 --- a/app/addons/documents/components/results-toolbar.js +++ b/app/addons/documents/components/results-toolbar.js @@ -10,9 +10,9 @@ // License for the specific language governing permissions and limitations under // the License. import React from 'react'; -import Header from "../header/header.react"; -import Stores from "../sidebar/stores.react"; -import Components from "../../components/react-components.react"; +import Header from "../header/header"; +import Stores from "../sidebar/stores"; +import Components from "../../components/react-components"; const {BulkDocumentHeaderController} = Header; const {BulkActionComponent} = Components; diff --git a/app/addons/documents/header/header.js b/app/addons/documents/header/header.js index 6f0466a59..131a2bdb1 100644 --- a/app/addons/documents/header/header.js +++ b/app/addons/documents/header/header.js @@ -12,7 +12,6 @@ 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'; @@ -26,7 +25,6 @@ var BulkDocumentHeaderController = React.createClass({ return { selectedView: indexResultsStore.getCurrentViewType(), selectedLayout: indexResultsStore.getSelectedLayout(), - includeDocs: queryOptionsStore.getIncludeDocsEnabled(), bulkDocCollection: indexResultsStore.getBulkDocCollection() }; }, @@ -79,12 +77,9 @@ var BulkDocumentHeaderController = React.createClass({ ); }, - toggleIncludeDocs () { - Actions.toggleIncludeDocs(this.state.includeDocs, this.state.bulkDocCollection); - }, - toggleLayout: function (layout) { Actions.toggleLayout(layout); + Actions.toggleIncludeDocs(layout === Constants.LAYOUT_ORIENTATION.METADATA, this.state.bulkDocCollection); } }); 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/index-results.components.js b/app/addons/documents/index-results/index-results.components.js index 15179c3dc..758b099b4 100644 --- a/app/addons/documents/index-results/index-results.components.js +++ b/app/addons/documents/index-results/index-results.components.js @@ -443,11 +443,12 @@ 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(), selectedLayout: store.getSelectedLayout(), @@ -490,6 +491,10 @@ var ViewResultListController = React.createClass({ }, render: function () { + if (this.state.isLoading) { + return ; + } + var view = ; if (this.state.hasResults) { diff --git a/app/addons/documents/index-results/stores.js b/app/addons/documents/index-results/stores.js index 8a1219d70..410dfa3c7 100644 --- a/app/addons/documents/index-results/stores.js +++ b/app/addons/documents/index-results/stores.js @@ -46,7 +46,7 @@ Stores.IndexResultsStore = FauxtonAPI.Store.extend({ this._isPrioritizedEnabled = false; this._tableSchema = []; - this._selectedLayout = Constants.LAYOUT_ORIENTATION.JSON; + this._selectedLayout = Constants.LAYOUT_ORIENTATION.METADATA; this.resetPagination(); }, @@ -641,7 +641,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; }, diff --git a/app/addons/documents/layouts.js b/app/addons/documents/layouts.js index b16e2f59e..395bd4ddf 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"; diff --git a/app/addons/documents/pagination/pagination.js b/app/addons/documents/pagination/pagination.js index 883bc3fb7..b16e546e6 100644 --- a/app/addons/documents/pagination/pagination.js +++ b/app/addons/documents/pagination/pagination.js @@ -133,6 +133,7 @@ var PerPageSelector = React.createClass({ var AllDocsNumberController = React.createClass({ getStoreState: function () { + const isLoading = indexResultsStore.isLoading(); return { totalRows: indexResultsStore.getTotalRows(), pageStart: indexResultsStore.getPageStart(), @@ -140,7 +141,7 @@ var AllDocsNumberController = React.createClass({ perPage: indexResultsStore.getPerPage(), prioritizedEnabled: indexResultsStore.getIsPrioritizedEnabled(), showPrioritizedFieldToggler: indexResultsStore.getShowPrioritizedFieldToggler(), - displayedFields: indexResultsStore.getResults().displayedFields, + displayedFields: isLoading ? {} : indexResultsStore.getResults().displayedFields, collection: indexResultsStore.getCollection(), bulkCollection: indexResultsStore.getBulkDocCollection(), }; diff --git a/app/addons/documents/routes-documents.js b/app/addons/documents/routes-documents.js index eb9d4beaa..dea10da2e 100644 --- a/app/addons/documents/routes-documents.js +++ b/app/addons/documents/routes-documents.js @@ -24,6 +24,7 @@ import SidebarActions from './sidebar/actions'; import DesignDocInfoActions from './designdocinfo/actions'; import ComponentsActions from '../components/actions'; import QueryOptionsActions from './queryoptions/actions'; +import Constants from './constants'; import {DocsTabsSidebarLayout, ViewsTabsSidebarLayout, ChangesSidebarLayout} from './layouts'; var DocumentsRouteObject = BaseRoute.extend({ @@ -88,8 +89,10 @@ var DocumentsRouteObject = BaseRoute.extend({ docParams = params.docParams, collection; - // includes_docs = true if you are visiting the _replicator/_users databases - if (['_replicator', '_users'].indexOf(databaseName) > -1) { + const indexResultsStore = IndexResultStores.indexResultsStore; + + // includes_docs = true if using table or JSON view + if (indexResultsStore.getSelectedLayout() !== Constants.LAYOUT_ORIENTATION.METADATA) { docParams.include_docs = true; urlParams = params.docParams; var updatedURL = FauxtonAPI.urls('allDocs', 'app', databaseName, '?' + $.param(urlParams)); @@ -120,7 +123,7 @@ var DocumentsRouteObject = BaseRoute.extend({ bulkCollection: new Documents.BulkDeleteDocCollection(frozenCollection, { databaseId: this.database.safeID() }), }); - this.database.allDocs.paging.pageSize = IndexResultStores.indexResultsStore.getPerPage(); + this.database.allDocs.paging.pageSize = indexResultsStore.getPerPage(); const endpoint = this.database.allDocs.urlRef("apiurl", urlParams); const docURL = this.database.allDocs.documentation(); From 48c03d95f37075f10a42145f58a0ad66007144d3 Mon Sep 17 00:00:00 2001 From: Ryan Millay Date: Wed, 26 Apr 2017 07:05:35 -0400 Subject: [PATCH 03/25] all docs and view behavior cleaned up --- app/addons/components/assets/less/docs.less | 1 + .../documents/assets/less/index-results.less | 20 ++++-- app/addons/documents/base.js | 4 ++ app/addons/documents/header/header.js | 2 +- .../index-results/index-results.components.js | 55 ++++------------ app/addons/documents/index-results/stores.js | 74 +++++++++++----------- app/addons/documents/routes-documents.js | 9 --- 7 files changed, 71 insertions(+), 94 deletions(-) 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/documents/assets/less/index-results.less b/app/addons/documents/assets/less/index-results.less index 2c79c93ed..31793be72 100644 --- a/app/addons/documents/assets/less/index-results.less +++ b/app/addons/documents/assets/less/index-results.less @@ -102,15 +102,23 @@ a.document-result-screen__toolbar-create-btn:visited { 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; @@ -119,7 +127,8 @@ a.document-result-screen__toolbar-create-btn:visited { white-space: nowrap; } td.tableview-checkbox-cell, th.tableview-header-el-checkbox { - width: 45px; + width: 35px; + padding-left: 0px; } .tableview-conflict { color: #F00; @@ -173,7 +182,6 @@ a.document-result-screen__toolbar-create-btn:visited { .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..15dcdf2c0 100644 --- a/app/addons/documents/base.js +++ b/app/addons/documents/base.js @@ -109,6 +109,10 @@ FauxtonAPI.registerUrls('view', { fragment: function (database, designDoc, viewName) { return 'database/' + database + designDoc + '/_view/' + viewName; + }, + + query: function (database, designDoc, viewName, query) { + return 'database/' + database + '/_design/' + designDoc + '/_view/' + viewName + getQueryParam(query); } }); diff --git a/app/addons/documents/header/header.js b/app/addons/documents/header/header.js index 131a2bdb1..0ddd59d77 100644 --- a/app/addons/documents/header/header.js +++ b/app/addons/documents/header/header.js @@ -79,7 +79,7 @@ var BulkDocumentHeaderController = React.createClass({ toggleLayout: function (layout) { Actions.toggleLayout(layout); - Actions.toggleIncludeDocs(layout === Constants.LAYOUT_ORIENTATION.METADATA, this.state.bulkDocCollection); + Actions.toggleIncludeDocs(layout !== Constants.LAYOUT_ORIENTATION.TABLE, this.state.bulkDocCollection); } }); diff --git a/app/addons/documents/index-results/index-results.components.js b/app/addons/documents/index-results/index-results.components.js index 758b099b4..8c90cc528 100644 --- a/app/addons/documents/index-results/index-results.components.js +++ b/app/addons/documents/index-results/index-results.components.js @@ -45,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, + onDoubleClick: React.PropTypes.func.isRequired }, onChange: function () { @@ -76,21 +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; @@ -174,16 +160,19 @@ var TableRow = React.createClass({ }); }, + onDoubleClick: function (e) { + this.props.onDoubleClick(this.props.el._id, this.props.el, e); + }, + render: function () { var i = this.props.index; var docContent = this.props.el.content; var el = this.props.el; return ( - + {this.maybeGetCheckboxCell(el, i)} {this.getCopyButton(docContent)} - {this.maybeGetSpecialField(el, i)} {this.getRowContents(el, i)} {this.getAdditionalInfoRow(docContent)} @@ -224,6 +213,7 @@ var TableView = React.createClass({ return ( Metadata); - } - var row = this.getOptionFieldRows(selectedFields); return ( - {specialField} {row} @@ -372,6 +355,7 @@ var ResultsScreen = React.createClass({
; } - switch (this.props.selectedLayout) { - case Constants.LAYOUT_ORIENTATION.TABLE: - mainView = this.getTableStyleView(loadLines); - break; - - case Constants.LAYOUT_ORIENTATION.METADATA: - mainView = this.getTableStyleView(loadLines); - break; - - case Constants.LAYOUT_ORIENTATION.JSON: - mainView = this.getDocumentStyleView(loadLines); - break; - - default: - mainView = this.getTableStyleView(loadLines); - }; + if (this.props.selectedLayout === Constants.LAYOUT_ORIENTATION.JSON) { + mainView = this.getDocumentStyleView(loadLines); + } else { + mainView = this.getTableStyleView(loadLines); + } return (
diff --git a/app/addons/documents/index-results/stores.js b/app/addons/documents/index-results/stores.js index 410dfa3c7..731e673b1 100644 --- a/app/addons/documents/index-results/stores.js +++ b/app/addons/documents/index-results/stores.js @@ -325,6 +325,12 @@ Stores.IndexResultsStore = FauxtonAPI.Store.extend({ }, getJsonViewData: function () { + // switch to metadata if the include docs flag was manually removed + /*if (!this.isIncludeDocsEnabled()) { + this.toggleLayout({layout: Constants.LAYOUT_ORIENTATION.METADATA}); + return this.getResults(); + }*/ + var hasBulkDeletableDoc; var res; @@ -403,8 +409,6 @@ Stores.IndexResultsStore = FauxtonAPI.Store.extend({ return el; }); - delete res._id; - delete res.id; delete res._rev; res = Object.keys(res).reduce(function (acc, el) { @@ -487,19 +491,9 @@ Stores.IndexResultsStore = FauxtonAPI.Store.extend({ }, 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(function (el) { return fixDocIdForMango(el.toJSON(), collectionType); }); @@ -543,50 +537,59 @@ 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()) { + // ensure the right layout state in case user manually added include_docs + this.toggleLayout({layout: Constants.LAYOUT_ORIENTATION.TABLE}); + + 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, '_id', '_rev', '_attachment'); 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. + + // ensure the right layout in case user manually removed include_docs + this.toggleLayout({layout: Constants.LAYOUT_ORIENTATION.METADATA}); schema = this.getPseudoSchema(data); - this._tableViewSelectedFields = _.without(schema, '_id', '_rev', '_attachment'); + this._tableViewSelectedFields = _.without(schema, '_attachment'); } 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, @@ -594,19 +597,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(), diff --git a/app/addons/documents/routes-documents.js b/app/addons/documents/routes-documents.js index dea10da2e..7963f4f96 100644 --- a/app/addons/documents/routes-documents.js +++ b/app/addons/documents/routes-documents.js @@ -24,7 +24,6 @@ import SidebarActions from './sidebar/actions'; import DesignDocInfoActions from './designdocinfo/actions'; import ComponentsActions from '../components/actions'; import QueryOptionsActions from './queryoptions/actions'; -import Constants from './constants'; import {DocsTabsSidebarLayout, ViewsTabsSidebarLayout, ChangesSidebarLayout} from './layouts'; var DocumentsRouteObject = BaseRoute.extend({ @@ -91,14 +90,6 @@ var DocumentsRouteObject = BaseRoute.extend({ const indexResultsStore = IndexResultStores.indexResultsStore; - // includes_docs = true if using table or JSON view - if (indexResultsStore.getSelectedLayout() !== Constants.LAYOUT_ORIENTATION.METADATA) { - docParams.include_docs = true; - urlParams = params.docParams; - var updatedURL = FauxtonAPI.urls('allDocs', 'app', databaseName, '?' + $.param(urlParams)); - FauxtonAPI.navigate(updatedURL, {trigger: false, replace: true}); - } - this.database.buildAllDocs(docParams); collection = this.database.allDocs; From 1c732e27cad6cbf0dfcc4ebc79adb331f3ab6dee Mon Sep 17 00:00:00 2001 From: Ryan Millay Date: Wed, 26 Apr 2017 08:48:05 -0400 Subject: [PATCH 04/25] mango support for new toolbar --- .../documents/assets/less/index-results.less | 4 +-- app/addons/documents/header/header.js | 21 +++++++----- app/addons/documents/index-results/stores.js | 37 +++++++--------------- app/addons/documents/mangolayout.js | 12 ++----- app/addons/documents/pagination/pagination.js | 6 ++-- app/addons/documents/routes-documents.js | 1 + app/addons/documents/routes-mango.js | 2 +- assets/less/formstyles.less | 8 ++--- assets/less/templates.less | 2 +- 9 files changed, 41 insertions(+), 52 deletions(-) diff --git a/app/addons/documents/assets/less/index-results.less b/app/addons/documents/assets/less/index-results.less index 31793be72..51f296f3f 100644 --- a/app/addons/documents/assets/less/index-results.less +++ b/app/addons/documents/assets/less/index-results.less @@ -32,7 +32,7 @@ .bulk-action-component { min-width: 90px; min-height: 26px; - padding: 8px 0px; + padding: 8px 0; } } @@ -84,7 +84,7 @@ a.document-result-screen__toolbar-create-btn:visited { .bulk-action-component { padding-bottom: 0; - min-height: 0px; + min-height: 0; } .bulk-action-component-panel input { width: auto; diff --git a/app/addons/documents/header/header.js b/app/addons/documents/header/header.js index 0ddd59d77..9140a6f8b 100644 --- a/app/addons/documents/header/header.js +++ b/app/addons/documents/header/header.js @@ -25,7 +25,8 @@ var BulkDocumentHeaderController = React.createClass({ return { selectedView: indexResultsStore.getCurrentViewType(), selectedLayout: indexResultsStore.getSelectedLayout(), - bulkDocCollection: indexResultsStore.getBulkDocCollection() + bulkDocCollection: indexResultsStore.getBulkDocCollection(), + isMango: indexResultsStore.getIsMangoResults() }; }, @@ -50,6 +51,13 @@ var BulkDocumentHeaderController = React.createClass({ render () { const layout = this.state.selectedLayout; + const metadata = this.state.isMango ? null : + ; return (
@@ -60,12 +68,7 @@ var BulkDocumentHeaderController = React.createClass({ > Table - + {metadata}
); - }, + } + + toggleLayout (newLayout) { + // this will be present when using redux stores + const { changeLayout, selectedLayout, fetchAllDocs, queryParams } = this.props; + if (changeLayout && newLayout !== selectedLayout) { + changeLayout(newLayout); + + if (newLayout === Constants.LAYOUT_ORIENTATION.METADATA) { + delete queryParams.docParams.include_docs; + } else { + queryParams.docParams.include_docs = true; + } - toggleLayout: function (layout) { - Actions.toggleLayout(layout); + fetchAllDocs(queryParams.docParams); + return; + } + + // fall back to old backbone style logic + Actions.toggleLayout(newLayout); if (!this.state.isMango) { - Actions.toggleIncludeDocs(layout === Constants.LAYOUT_ORIENTATION.METADATA, this.state.bulkDocCollection); + Actions.toggleIncludeDocs(newLayout === Constants.LAYOUT_ORIENTATION.METADATA, this.state.bulkDocCollection); } } -}); - -export default { - BulkDocumentHeaderController: BulkDocumentHeaderController }; diff --git a/app/addons/documents/index-results/actiontypes.js b/app/addons/documents/index-results/actiontypes.js index 06fbdabaf..d5c2ddb24 100644 --- a/app/addons/documents/index-results/actiontypes.js +++ b/app/addons/documents/index-results/actiontypes.js @@ -22,5 +22,12 @@ export default { INDEX_RESULTS_REDUX_NEW_RESULTS: 'INDEX_RESULTS_REDUX_NEW_RESULTS', INDEX_RESULTS_REDUX_INITIALIZE: 'INDEX_RESULTS_REDUX_INITIALIZE', INDEX_RESULTS_REDUX_IS_LOADING: 'INDEX_RESULTS_REDUX_IS_LOADING', - INDEX_RESULTS_REDUX_SELECT_DOC: 'INDEX_RESULTS_REDUX_SELECT_DOC' + 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' }; diff --git a/app/addons/documents/index-results/components/IndexResults.js b/app/addons/documents/index-results/components/IndexResults.js index 110441c40..13208b7bc 100644 --- a/app/addons/documents/index-results/components/IndexResults.js +++ b/app/addons/documents/index-results/components/IndexResults.js @@ -16,9 +16,6 @@ import Components from '../index-results.components'; export default class IndexResults extends React.Component { constructor (props) { super(props); - } - - componentWillMount () { const { fetchAllDocs, queryParams, initialize } = this.props; // save the prop params to the state tree as an initialization step @@ -28,9 +25,14 @@ export default class IndexResults extends React.Component { fetchAllDocs(queryParams.docParams); } + componentWillUnmount () { + const { resetState } = this.props; + resetState(); + } + deleteSelectedDocs () { - const { bulkDeleteDocs, fetchAllDocs, queryParams, selectedDocs } = this.props; - bulkDeleteDocs(selectedDocs).then(fetchAllDocs(queryParams)); + const { bulkDeleteDocs, queryParams, selectedDocs } = this.props; + bulkDeleteDocs(selectedDocs, queryParams.docParams); } isSelected (id) { @@ -38,35 +40,45 @@ export default class IndexResults extends React.Component { // check whether this id exists in our array of selected docs return selectedDocs.findIndex((doc) => { - return id === doc.id; + return id === doc._id; }) > -1; } docChecked (_id, _rev) { - const { selectDoc } = this.props; + const { selectDoc, selectedDocs } = this.props; // dispatch an action to push this doc on to the array of selected docs - selectDoc({ + const doc = { _id: _id, - _rev: _rev - }); + _rev: _rev, + _deleted: true + }; + + selectDoc(doc, selectedDocs); + } + + toggleSelectAll () { + const { + docs, + selectedDocs, + allDocumentsSelected, + bulkCheckOrUncheck + } = this.props; + + bulkCheckOrUncheck(docs, selectedDocs, allDocumentsSelected); } render () { + const { results } = this.props; + return ( + docChecked={this.docChecked.bind(this)} + isListDeletable={results.hasBulkDeletableDoc} + toggleSelectAll={this.toggleSelectAll.bind(this)} + {...this.props} /> ); } -} +}; 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..d41932647 --- /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, queryParams } = this.props; + updatePerPageResults(amount, queryParams.docParams); + } + + nextClicked (event) { + event.preventDefault(); + + const { canShowNext, queryParams, paginateNext, perPage } = this.props; + if (canShowNext) { + paginateNext(queryParams.docParams, perPage); + } + } + + previousClicked (event) { + event.preventDefault(); + + const { canShowPrevious, queryParams, paginatePrevious, perPage } = this.props; + if (canShowPrevious) { + paginatePrevious(queryParams.docParams, 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/containers/IndexResultsContainer.js b/app/addons/documents/index-results/containers/IndexResultsContainer.js index 18e379dc7..e76b07bcc 100644 --- a/app/addons/documents/index-results/containers/IndexResultsContainer.js +++ b/app/addons/documents/index-results/containers/IndexResultsContainer.js @@ -16,7 +16,11 @@ import { fetchAllDocs, initialize, selectDoc, - bulkDeleteDocs + bulkDeleteDocs, + changeLayout, + bulkCheckOrUncheck, + changeTableHeaderAttribute, + resetState } from '../../api'; import { getDocs, @@ -35,17 +39,17 @@ import { } from '../../reducers'; -const mapStateToProps = ({indexResults}) => { +const mapStateToProps = ({indexResults}, ownProps) => { return { docs: getDocs(indexResults), selectedDocs: getSelectedDocs(indexResults), isLoading: getIsLoading(indexResults), hasResults: getHasResults(indexResults), - dataForRendering: getDataForRendering(indexResults), + results: getDataForRendering(indexResults, ownProps.databaseName), isEditable: getIsEditable(indexResults), selectedLayout: getSelectedLayout(indexResults), - allDocsSelected: getAllDocsSelected(indexResults), - hasDocSelected: getHasDocsSelected(indexResults), + allDocumentsSelected: getAllDocsSelected(indexResults), + hasSelectedItem: getHasDocsSelected(indexResults), numDocsSelected: getNumDocsSelected(indexResults), textEmptyIndex: getTextEmptyIndex(indexResults), typeOfIndex: getTypeOfIndex(indexResults), @@ -57,8 +61,18 @@ const mapDispatchToProps = (dispatch, ownProps) => { return { fetchAllDocs: (params) => { dispatch(fetchAllDocs(ownProps.databaseName, params)); }, initialize: () => { dispatch(initialize(ownProps.params)); }, - selectDoc: (doc) => { dispatch(selectDoc(doc)); }, - bulkDeleteDocs: (docs) => { bulkDeleteDocs(ownProps.databaseName, docs, ownProps.designDocs); } + selectDoc: (doc, selectedDocs) => { dispatch(selectDoc(doc, selectedDocs)); }, + bulkDeleteDocs: (docs, params) => { + dispatch(bulkDeleteDocs(ownProps.databaseName, docs, ownProps.designDocs, params)); + }, + changeLayout: (newLayout) => { dispatch(changeLayout(newLayout)); }, + bulkCheckOrUncheck: (docs, selectedDocs, allDocumentsSelected) => { + dispatch(bulkCheckOrUncheck(docs, selectedDocs, allDocumentsSelected)); + }, + changeTableHeaderAttribute: (newField, selectedFields) => { + dispatch(changeTableHeaderAttribute(newField, selectedFields)); + }, + resetState: () => { dispatch(resetState()); } }; }; 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..77f56335c --- /dev/null +++ b/app/addons/documents/index-results/containers/PaginationContainer.js @@ -0,0 +1,66 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import { connect } from 'react-redux'; +import PaginationFooter from '../components/pagination/PaginationFooter'; +import { + toggleShowAllColumns, + updatePerPageResults, + paginateNext, + paginatePrevious +} from '../../api'; +import { + getDocs, + getSelectedDocs, + getHasResults, + getQueryParams, + getPageStart, + getPageEnd, + getPerPage, + getPrioritizedEnabled, + getShowPrioritizedEnabled, + getDisplayedFields, + getCanShowNext, + getCanShowPrevious +} 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), + queryParams: getQueryParams(indexResults) + }; +}; + +const mapDispatchToProps = (dispatch, ownProps) => { + return { + toggleShowAllColumns: () => { dispatch(toggleShowAllColumns()); }, + updatePerPageResults: (amount, params) => { dispatch(updatePerPageResults(ownProps.databaseName, amount, params)); }, + paginateNext: (params, perPage) => { dispatch(paginateNext(ownProps.databaseName, params, perPage)); }, + paginatePrevious: (params, perPage) => { dispatch(paginatePrevious(ownProps.databaseName, params, perPage)); } + }; +}; + +export default connect ( + mapStateToProps, + mapDispatchToProps +)(PaginationFooter); diff --git a/app/addons/documents/index-results/helpers/table-view.js b/app/addons/documents/index-results/helpers/table-view.js index c7b587262..059d78a9a 100644 --- a/app/addons/documents/index-results/helpers/table-view.js +++ b/app/addons/documents/index-results/helpers/table-view.js @@ -105,19 +105,13 @@ const getFullTableViewData = (docs, options) => { 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 - docs = docs.map((doc) => { return doc.doc || doc; }); + const normalizedDocs = docs.map((doc) => { return doc.doc || doc; }); // build the schema container based on the normalized data - schema = getPseudoSchema(docs); + schema = getPseudoSchema(normalizedDocs); - // 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 (!showAllFieldsTableView) { - selectedFieldsTableView = options.cachedFieldsTableView || []; - } - - // if we still don't know what attr/columns to display, build the list + // if we don't know what attr/columns to display, build the list if (selectedFieldsTableView && selectedFieldsTableView.length === 0) { - selectedFieldsTableView = getPrioritizedFields(docs, 5); + selectedFieldsTableView = getPrioritizedFields(normalizedDocs, 5); } // set the notSelectedFields to the subset excluding meta and selected attributes @@ -133,6 +127,7 @@ const getFullTableViewData = (docs, options) => { return { schema, + normalizedDocs, selectedFieldsTableView, notSelectedFieldsTableView }; @@ -142,6 +137,7 @@ const getMetaDataTableView = (docs) => { const schema = getPseudoSchema(docs); return { schema, + normalizedDocs: docs, // no need to massage the docs for metadata selectedFieldsTableView: schema, notSelectedFieldsTableView: null }; @@ -151,11 +147,12 @@ 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 = docs.map(function (doc) { + const res = normalizedDocs.map(function (doc) { return { content: doc, id: doc._id || doc.id, // inconsistent apis for GET between mango and views @@ -171,7 +168,7 @@ export const getTableViewData = (docs, options) => { return { notSelectedFields: notSelectedFieldsTableView, selectedFields: selectedFieldsTableView, - hasBulkDeletableDoc: hasBulkDeletableDoc(docs), + hasBulkDeletableDoc: hasBulkDeletableDoc(normalizedDocs), schema: schema, results: res, displayedFields: isMetaData ? null : { diff --git a/app/addons/documents/index-results/index-results.components.js b/app/addons/documents/index-results/index-results.components.js index f9c1511f9..538b777e7 100644 --- a/app/addons/documents/index-results/index-results.components.js +++ b/app/addons/documents/index-results/index-results.components.js @@ -168,7 +168,7 @@ 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}; }); @@ -181,11 +181,13 @@ const WrappedAutocomplete = ({selectedField, notSelectedFields, index}) => { options={options} clearable={false} onChange={(el) => { - Actions.changeField({ - newSelectedRow: el.value, - index: index - }); - }} /> + const newField = { + newSelectedRow: el.value, + index: index + }; + changeField(newField, selectedFields) || Actions.changeField(newField); + } + } />
); @@ -214,7 +216,7 @@ var TableView = React.createClass({ }, getOptionFieldRows: function (filtered) { - var notSelectedFields = this.props.data.notSelectedFields; + const notSelectedFields = this.props.data.notSelectedFields; if (!notSelectedFields) { return filtered.map(function (el, i) { @@ -225,19 +227,27 @@ var TableView = React.createClass({ return filtered.map(function (el, i) { 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} /> ); }, @@ -346,6 +356,7 @@ var ResultsScreen = React.createClass({ isChecked={this.props.allDocumentsSelected} hasSelectedItem={this.props.hasSelectedItem} toggleSelect={this.toggleSelectAll} + changeField={this.props.changeTableHeaderAttribute} title="Select all docs that can be..." /> ); @@ -353,7 +364,8 @@ var ResultsScreen = React.createClass({ render: function () { let mainView = null; - let toolbar = ; + const { toggleSelectAll } = this.props; + let toolbar = ; if (this.props.isLoading) { mainView =
; diff --git a/app/addons/documents/layouts.js b/app/addons/documents/layouts.js index 4c71d0be5..91e331a40 100644 --- a/app/addons/documents/layouts.js +++ b/app/addons/documents/layouts.js @@ -22,6 +22,7 @@ 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'; export const TabsSidebarHeader = ({ hideQueryOptions, @@ -68,7 +69,13 @@ TabsSidebarHeader.defaultProps = { hideHeaderBar: false }; -export const TabsSidebarContent = ({hideFooter, lowerContent, upperContent}) => { +export const TabsSidebarContent = ({ + hideFooter, + lowerContent, + upperContent, + databaseName, + isRedux = false +}) => { return (
@@ -126,6 +134,8 @@ export const DocsTabsSidebarLayout = ({ /> ); diff --git a/app/addons/documents/reducers.js b/app/addons/documents/reducers.js index a186618bc..669768f39 100644 --- a/app/addons/documents/reducers.js +++ b/app/addons/documents/reducers.js @@ -10,45 +10,57 @@ // License for the specific language governing permissions and limitations under // the License. +import FauxtonAPI from '../../core/api'; import ActionTypes from './index-results/actiontypes'; import Constants from './constants'; import { getJsonViewData } from './index-results/helpers/json-view'; import { getTableViewData } from './index-results/helpers/table-view'; const initialState = { - docs: [], - selectedDocs: [], + docs: [], // raw documents returned from couch + selectedDocs: [], // documents selected for manipulation isLoading: false, - dataForRendering: {}, tableView: { - selectedFieldsTableView: [], - showAllFieldsTableView: false, - cachedFieldsTableView: [] + selectedFieldsTableView: [], // current columns to display + showAllFieldsTableView: false, // do we show all possible columns? }, - isEditable: true, + isEditable: true, // can the user manipulate the results returned? selectedLayout: Constants.LAYOUT_ORIENTATION.METADATA, textEmptyIndex: 'No Documents Found', typeOfIndex: 'view', - queryParams: {} -}; - -const removeGeneratedMangoDocs = (doc) => { - return doc.language !== 'query'; -}; - -const buildDataForRendering = (docs, options) => { - const docsWithoutGeneratedMangoDocs = docs.filter(removeGeneratedMangoDocs); - - if (Constants.LAYOUT_ORIENTATION.JSON === options.selectedLayout) { - return getJsonViewData(docsWithoutGeneratedMangoDocs, options); - } else { - return getTableViewData(docsWithoutGeneratedMangoDocs, options); + queryParams: { + docParams: { // params fauxton uses to fetch results from couch + limit: FauxtonAPI.constants.MISC.DEFAULT_PAGE_SIZE + 1 + }, + urlParams: { // params representing what is visible to the user + limit: FauxtonAPI.constants.MISC.DEFAULT_PAGE_SIZE + } + }, + 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 } }; export default function resultsState (state = initialState, action) { switch (action.type) { + case ActionTypes.INDEX_RESULTS_REDUX_RESET_STATE: + return Object.assign({}, initialState, { + // deeply assign these values to ensure they're reset + queryParams: { + docParams: { + limit: FauxtonAPI.constants.MISC.DEFAULT_PAGE_SIZE + 1 + }, + urlParams: { + limit: FauxtonAPI.constants.MISC.DEFAULT_PAGE_SIZE + } + } + }); + break; + case ActionTypes.INDEX_RESULTS_REDUX_INITIALIZE: return Object.assign({}, state, { queryParams: action.params @@ -57,93 +69,200 @@ export default function resultsState (state = initialState, action) { case ActionTypes.INDEX_RESULTS_REDUX_IS_LOADING: return Object.assign({}, state, { - isLoading: true, - docs: [], - selectedDocs: [], - dataForRendering: [] + isLoading: true }); break; - case ActionTypes.INDEX_RESULTS_REDUX_SELECT_DOC: + case ActionTypes.INDEX_RESULTS_REDUX_NEW_SELECTED_DOCS: return Object.assign({}, state, { - selectedDocs: state.selectedDocs.push(action.doc) + selectedDocs: action.selectedDocs }); break; case ActionTypes.INDEX_RESULTS_REDUX_NEW_RESULTS: return Object.assign({}, state, { - docs: action.docs, - dataForRendering: buildDataForRendering(action.docs, { - databaseName: action.databaseName, - selectedLayout: state.selectedLayout, - selectedFieldsTableView: state.selectedFieldsTableView, - showAllFieldsTableView: state.showAllFieldsTableVie, - cachedFieldsTableView: state.cachedFieldsTableView, - typeOfIndex: state.typeOfIndex - }), + docs: removeOverflowDoc(action.docs, state.pagination.perPage), isLoading: false, - selectedDocs: [], isEditable: true, //TODO: determine logic for this - queryParams: { - urlParams: state.queryParams.urlParams, + queryParams: Object.assign({}, state.queryParams, { docParams: action.params - } + }), + pagination: Object.assign({}, state.pagination, { + canShowNext: action.docs.length > state.pagination.perPage + }) + }); + break; + + case ActionTypes.INDEX_RESULTS_REDUX_CHANGE_LAYOUT: + return Object.assign({}, state, { + selectedLayout: action.layout + }); + break; + + 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 + }) + }); + break; + + case ActionTypes.INDEX_RESULTS_REDUX_CHANGE_TABLE_HEADER_ATTRIBUTE: + return Object.assign({}, state, { + tableView: Object.assign({}, state.tableView, { + selectedFieldsTableView: action.selectedFieldsTableView + }) + }); + break; + + case ActionTypes.INDEX_RESULTS_REDUX_SET_PER_PAGE: + return Object.assign({}, state, { + pagination: Object.assign({}, state.pagination, { + perPage: action.perPage, + currentPage: 1, + pageStart: 1 + }) + }); + break; + + 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 + }) + }); + break; + + 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 + }) }); + break; default: return state; } }; -export const getDocs = state => state.docs; -export const getSelectedDocs = state => state.selectedDocs; -export const getIsLoading = state => state.isLoading; -export const getDataForRendering = state => state.dataForRendering; -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 getQueryParams = state => state.queryParams; +// fauxton always requests one extra doc as a sneaky way to determine if +// there is another page of results. We need to remove that extra doc so +// we don't confuse users. +const removeOverflowDoc = (docs, limit) => { + return docs.length <= limit ? docs : docs.slice(0, limit); +}; + +// we don't want to muddy the waters with autogenerated mango docs +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 && state.docs.length > 0; + 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 || !state.selectedDocs || state.docs.length === 0 || state.selectedDocs.length === 0) { + 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 slowly shrink the - // selectedDocsCopy array to improve this slightly and we know + // 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. - const selectedDocsCopy = state.selectedDocs; - state.docs.forEach((doc) => { + // + // 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 selected doc in the current - // results list. - const indexOfDoc = (selectedDoc) => { - return doc._id === selectedDoc._id; + // 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 (selectedDocsCopy.findIndex(indexOfDoc) === -1) { + if (!state.selectedDocs.some(exists)) { return false; } - - selectedDocsCopy.splice(indexOfDoc, 1); - }); - + } return true; }; +// are there any documents selected in the state tree? export const getHasDocsSelected = (state) => { - return state.selectedDocs && state.selectedDocs.length > 0; + return state.selectedDocs.length > 0; }; +// how many documents are selected in the state tree? export const getNumDocsSelected = (state) => { - return state.selectedDocs && state.selectedDocs.length; + 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 || {}; }; + +// 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 getQueryParams = state => state.queryParams; +export const getPageStart = state => state.pagination.pageStart; +export const getPrioritizedEnabled = state => state.tableView.showAllFieldsTableView; +export const getPerPage = state => state.pagination.perPage; +export const getCanShowNext = state => state.pagination.canShowNext; From b4576b1bad9e3667d0281d4589313b0f96a6b592 Mon Sep 17 00:00:00 2001 From: Ryan Millay Date: Fri, 19 May 2017 09:07:07 -0400 Subject: [PATCH 16/25] all_docs and queryoptions stable with redux --- app/addons/documents/api.js | 270 --------------------- app/addons/documents/base.js | 2 +- .../documents/components/header-docs-right.js | 7 +- app/addons/documents/components/results-toolbar.js | 4 +- app/addons/documents/header/header.js | 19 +- app/addons/documents/index-results/actiontypes.js | 4 +- .../documents/index-results/apis/base-api.js | 94 +++++++ .../documents/index-results/apis/fetch-api.js | 180 ++++++++++++++ .../documents/index-results/apis/pagination-api.js | 70 ++++++ .../index-results/apis/queryoptions-api.js | 97 ++++++++ .../components/pagination/PaginationFooter.js | 12 +- .../components/queryoptions/AdditionalParams.js | 78 ++++++ .../components/queryoptions/KeySearchFields.js | 111 +++++++++ .../components/queryoptions/MainFieldsView.js | 102 ++++++++ .../components/queryoptions/QueryButtons.js | 38 +++ .../components/queryoptions/QueryOptions.js | 125 ++++++++++ .../components/{ => results}/IndexResults.js | 15 +- .../components/results/NoResultsScreen.js | 26 ++ .../components/results/ResultsScreen.js | 132 ++++++++++ .../index-results/components/results/TableRow.js | 148 +++++++++++ .../index-results/components/results/TableView.js | 105 ++++++++ .../components/results/WrappedAutocomplete.js | 41 ++++ .../containers/IndexResultsContainer.js | 37 ++- .../containers/PaginationContainer.js | 28 ++- .../containers/QueryOptionsContainer.js | 111 +++++++++ .../documents/{ => index-results}/reducers.js | 129 ++++++---- app/addons/documents/layouts.js | 10 +- app/addons/documents/routes-documents.js | 24 -- 28 files changed, 1631 insertions(+), 388 deletions(-) delete mode 100644 app/addons/documents/api.js create mode 100644 app/addons/documents/index-results/apis/base-api.js create mode 100644 app/addons/documents/index-results/apis/fetch-api.js create mode 100644 app/addons/documents/index-results/apis/pagination-api.js create mode 100644 app/addons/documents/index-results/apis/queryoptions-api.js create mode 100644 app/addons/documents/index-results/components/queryoptions/AdditionalParams.js create mode 100644 app/addons/documents/index-results/components/queryoptions/KeySearchFields.js create mode 100644 app/addons/documents/index-results/components/queryoptions/MainFieldsView.js create mode 100644 app/addons/documents/index-results/components/queryoptions/QueryButtons.js create mode 100644 app/addons/documents/index-results/components/queryoptions/QueryOptions.js rename app/addons/documents/index-results/components/{ => results}/IndexResults.js (82%) create mode 100644 app/addons/documents/index-results/components/results/NoResultsScreen.js create mode 100644 app/addons/documents/index-results/components/results/ResultsScreen.js create mode 100644 app/addons/documents/index-results/components/results/TableRow.js create mode 100644 app/addons/documents/index-results/components/results/TableView.js create mode 100644 app/addons/documents/index-results/components/results/WrappedAutocomplete.js create mode 100644 app/addons/documents/index-results/containers/QueryOptionsContainer.js rename app/addons/documents/{ => index-results}/reducers.js (75%) diff --git a/app/addons/documents/api.js b/app/addons/documents/api.js deleted file mode 100644 index cfe2b7a7e..000000000 --- a/app/addons/documents/api.js +++ /dev/null @@ -1,270 +0,0 @@ -// Licensed under the Apache License, Version 2.0 (the "License"); you may not -// use this file except in compliance with the License. You may obtain a copy of -// the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations under -// the License. - -import 'url-polyfill'; -//import app from '../../app'; -import FauxtonAPI from '../../core/api'; -//import base64 from 'base-64'; -//import _ from 'lodash'; -import 'whatwg-fetch'; -import queryString from 'query-string'; -import ActionTypes from './index-results/actiontypes'; -import SidebarActions from './sidebar/actions'; - -const maxDocLimit = 10000; - -const nowLoading = () => { - return { - type: ActionTypes.INDEX_RESULTS_REDUX_IS_LOADING - }; -}; - -export const resetState = () => { - return { - type: ActionTypes.INDEX_RESULTS_REDUX_RESET_STATE - }; -}; - -const newResultsAvailable = (docs, databaseName, params) => { - return { - type: ActionTypes.INDEX_RESULTS_REDUX_NEW_RESULTS, - docs: docs, - databaseName: databaseName, - params: params - }; -}; - -export const fetchAllDocs = (databaseName, params) => { - params.limit = Math.min(params.limit, maxDocLimit); - - return (dispatch) => { - // first, tell app state that we're loading - dispatch(nowLoading()); - - // now fetch the results - const query = queryString.stringify(params); - return fetch(`/${databaseName}/_all_docs?${query}`, { - credentials: 'include', - headers: { - 'Accept': 'application/json; charset=utf-8' - } - }) - .then(res => res.json()) - .then((res) => { - - // dispatch that we're all done - dispatch(newResultsAvailable(res.error ? [] : res.rows, databaseName, params)); - }); - }; -}; - -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 - }); -}; - -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, docs, designDocs, params) => { - if (!validateBulkDelete(docs)) { - return false; - } - - return (dispatch) => { - const payload = { - docs: docs - }; - - 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()) - .then((res) => { - if (res.error) { - errorMessage(); - return; - } - processBulkDeleteResponse(res, docs, designDocs); - dispatch(newSelectedDocs()); - dispatch(fetchAllDocs(databaseName, params)); - }); - }; -}; - -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); - } -}; - -export const initialize = (params) => { - return { - type: ActionTypes.INDEX_RESULTS_REDUX_INITIALIZE, - params: params - }; -}; - -const newSelectedDocs = (selectedDocs = []) => { - return { - type: ActionTypes.INDEX_RESULTS_REDUX_NEW_SELECTED_DOCS, - selectedDocs: selectedDocs - }; -}; - -export const selectDoc = (doc, selectedDocs) => { - const indexInSelectedDocs = selectedDocs.findIndex((selectedDoc) => { - return selectedDoc._id === doc._id; - }); - - if (indexInSelectedDocs > -1) { - selectedDocs.splice(indexInSelectedDocs, 1); - } else { - 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, - _deleted: true - }); - } - }); - - return newSelectedDocs(selectedDocs); -}; - -export const changeLayout = (newLayout) => { - return { - type: ActionTypes.INDEX_RESULTS_REDUX_CHANGE_LAYOUT, - layout: newLayout - }; -}; - -export const toggleShowAllColumns = () => { - return { - type: ActionTypes.INDEX_RESULTS_REDUX_TOGGLE_SHOW_ALL_COLUMNS - }; -}; - -const setPerPage = (amount) => { - return { - type: ActionTypes.INDEX_RESULTS_REDUX_SET_PER_PAGE, - perPage: amount - }; -}; - -export const updatePerPageResults = (databaseName, amount, params) => { - // 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. - params.limit = amount + 1; - params.skip = 0; - - return (dispatch) => { - dispatch(setPerPage(amount)); - dispatch(fetchAllDocs(databaseName, params)); - }; -}; - -export const paginateNext = (databaseName, params, perPage) => { - // add the perPage to the previous skip. - if (!params.skip) { - params.skip = 0; - } - params.skip += perPage; - - return (dispatch) => { - dispatch({ - type: ActionTypes.INDEX_RESULTS_REDUX_PAGINATE_NEXT - }); - dispatch(fetchAllDocs(databaseName, params)); - }; -}; - -export const paginatePrevious = (databaseName, params, perPage) => { - // subtract the perPage to the previous skip. - params.skip = Math.max(params.skip - perPage, 0); - - return (dispatch) => { - dispatch({ - type: ActionTypes.INDEX_RESULTS_REDUX_PAGINATE_PREVIOUS - }); - dispatch(fetchAllDocs(databaseName, params)); - }; -}; - -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/base.js b/app/addons/documents/base.js index 81a02b3c3..c1348ee0a 100644 --- a/app/addons/documents/base.js +++ b/app/addons/documents/base.js @@ -13,7 +13,7 @@ import app from "../../app"; import FauxtonAPI from "../../core/api"; import Documents from "./routes"; -import reducers from "./reducers"; +import reducers from "./index-results/reducers"; import "./assets/less/documents.less"; FauxtonAPI.addReducers({ diff --git a/app/addons/documents/components/header-docs-right.js b/app/addons/documents/components/header-docs-right.js index 249782336..2544ff590 100644 --- a/app/addons/documents/components/header-docs-right.js +++ b/app/addons/documents/components/header-docs-right.js @@ -12,12 +12,13 @@ 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 RightAllDocsHeader = ({database, hideQueryOptions, isRedux, dbName}) =>
@@ -25,8 +26,8 @@ const RightAllDocsHeader = ({database, hideQueryOptions}) =>
- - {hideQueryOptions ? '' : } + {!hideQueryOptions && isRedux ? : ''} + {!hideQueryOptions && !isRedux ? : ''} ; RightAllDocsHeader.propTypes = { diff --git a/app/addons/documents/components/results-toolbar.js b/app/addons/documents/components/results-toolbar.js index 2827ca4e7..95ffacbea 100644 --- a/app/addons/documents/components/results-toolbar.js +++ b/app/addons/documents/components/results-toolbar.js @@ -36,14 +36,14 @@ export class ResultsToolBar extends React.Component { return (
- {isListDeletable && hasResults ? : null} - {hasResults ? : null} + {hasResults || isLoading ? : null}
Create Document diff --git a/app/addons/documents/header/header.js b/app/addons/documents/header/header.js index 9afddccba..8adc35364 100644 --- a/app/addons/documents/header/header.js +++ b/app/addons/documents/header/header.js @@ -88,17 +88,28 @@ export default class BulkDocumentHeaderController extends React.Component { toggleLayout (newLayout) { // this will be present when using redux stores - const { changeLayout, selectedLayout, fetchAllDocs, queryParams } = this.props; + const { + changeLayout, + selectedLayout, + fetchAllDocs, + fetchParams, + queryOptionsParams, + queryOptionsToggleIncludeDocs + } = this.props; + if (changeLayout && newLayout !== selectedLayout) { + // change our layout to JSON, Table, or Metadata changeLayout(newLayout); if (newLayout === Constants.LAYOUT_ORIENTATION.METADATA) { - delete queryParams.docParams.include_docs; + queryOptionsParams.include_docs = false; } else { - queryParams.docParams.include_docs = true; + queryOptionsParams.include_docs = true; } - fetchAllDocs(queryParams.docParams); + // tell the query options panel we're updating include_docs + queryOptionsToggleIncludeDocs(!queryOptionsParams.include_docs); + fetchAllDocs(fetchParams, queryOptionsParams); return; } diff --git a/app/addons/documents/index-results/actiontypes.js b/app/addons/documents/index-results/actiontypes.js index d5c2ddb24..ddd17e680 100644 --- a/app/addons/documents/index-results/actiontypes.js +++ b/app/addons/documents/index-results/actiontypes.js @@ -20,7 +20,6 @@ export default { 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_INITIALIZE: 'INDEX_RESULTS_REDUX_INITIALIZE', 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', @@ -29,5 +28,6 @@ export default { 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_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-api.js b/app/addons/documents/index-results/apis/base-api.js new file mode 100644 index 000000000..d23f88595 --- /dev/null +++ b/app/addons/documents/index-results/apis/base-api.js @@ -0,0 +1,94 @@ +// 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) => { + const indexInSelectedDocs = selectedDocs.findIndex((selectedDoc) => { + return selectedDoc._id === doc._id; + }); + + if (indexInSelectedDocs > -1) { + selectedDocs.splice(indexInSelectedDocs, 1); + } else { + 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, + _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-api.js b/app/addons/documents/index-results/apis/fetch-api.js new file mode 100644 index 000000000..950d4b120 --- /dev/null +++ b/app/addons/documents/index-results/apis/fetch-api.js @@ -0,0 +1,180 @@ +// 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-api'; + +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). +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; + + // 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 + }; +}; + +// All the business logic for fetching docs from couch. +// Arguments: +// - databaseName -> the name of the database to fetch from +// - fetchParams -> the internal params fauxton uses to emulate pagination +// - queryOptionsParams -> manual query params entered by user +export const fetchAllDocs = (databaseName, 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 + const query = queryString.stringify(params); + return fetch(`/${databaseName}/_all_docs?${query}`, { + credentials: 'include', + headers: { + 'Accept': 'application/json; charset=utf-8' + } + }) + .then(res => res.json()) + .then((res) => { + const docs = res.error ? [] : res.rows; + + // 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 === params.limit) { + // 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(params.limit - 1); + canShowNext = true; + } + + // dispatch that we're all done + dispatch(newResultsAvailable(docs, params, canShowNext)); + }); + }; +}; + +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 + }); +}; + +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, docs, designDocs, params) => { + if (!validateBulkDelete(docs)) { + return false; + } + + return (dispatch) => { + const payload = { + docs: docs + }; + + 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()) + .then((res) => { + if (res.error) { + errorMessage(); + return; + } + processBulkDeleteResponse(res, docs, designDocs); + dispatch(newSelectedDocs()); + dispatch(fetchAllDocs(databaseName, params)); + }); + }; +}; + +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-api.js b/app/addons/documents/index-results/apis/pagination-api.js new file mode 100644 index 000000000..490ef46a2 --- /dev/null +++ b/app/addons/documents/index-results/apis/pagination-api.js @@ -0,0 +1,70 @@ +// 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-api'; +import ActionTypes from '../actiontypes'; + +export const toggleShowAllColumns = () => { + return { + type: ActionTypes.INDEX_RESULTS_REDUX_TOGGLE_SHOW_ALL_COLUMNS + }; +}; + +const setPerPage = (amount) => { + return { + type: ActionTypes.INDEX_RESULTS_REDUX_SET_PER_PAGE, + perPage: amount + }; +}; + +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.limit = amount + 1; + fetchParams.skip = queryOptionsParams.skip || 0; + + return (dispatch) => { + dispatch(setPerPage(amount)); + dispatch(fetchAllDocs(databaseName, fetchParams, queryOptionsParams)); + }; +}; + +export const paginateNext = (databaseName, fetchParams, queryOptionsParams, perPage) => { + // add the perPage to the previous skip. + fetchParams.skip += perPage; + + return (dispatch) => { + dispatch({ + type: ActionTypes.INDEX_RESULTS_REDUX_PAGINATE_NEXT + }); + dispatch(fetchAllDocs(databaseName, fetchParams, queryOptionsParams)); + }; +}; + +export const paginatePrevious = (databaseName, fetchParams, queryOptionsParams, perPage) => { + // subtract the perPage to the previous skip. + fetchParams.skip = Math.max(fetchParams.skip - perPage, 0); + + 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-api.js b/app/addons/documents/index-results/apis/queryoptions-api.js new file mode 100644 index 000000000..abb8f1556 --- /dev/null +++ b/app/addons/documents/index-results/apis/queryoptions-api.js @@ -0,0 +1,97 @@ +// 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-api'; + +const updateQueryOptions = (queryOptions) => { + return { + type: ActionTypes.INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS, + options: queryOptions + }; +}; + +export const queryOptionsExecute = (databaseName, queryOptionsParams, perPage) => { + const fetchParams = { + limit: perPage + 1, + skip: 0 + }; + return fetchAllDocs(databaseName, 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 + }); +}; diff --git a/app/addons/documents/index-results/components/pagination/PaginationFooter.js b/app/addons/documents/index-results/components/pagination/PaginationFooter.js index d41932647..9d006747d 100644 --- a/app/addons/documents/index-results/components/pagination/PaginationFooter.js +++ b/app/addons/documents/index-results/components/pagination/PaginationFooter.js @@ -31,25 +31,25 @@ export default class PaginationFooter extends React.Component { } perPageChange (amount) { - const { updatePerPageResults, queryParams } = this.props; - updatePerPageResults(amount, queryParams.docParams); + const { updatePerPageResults, fetchParams, queryOptionsParams } = this.props; + updatePerPageResults(amount, fetchParams, queryOptionsParams); } nextClicked (event) { event.preventDefault(); - const { canShowNext, queryParams, paginateNext, perPage } = this.props; + const { canShowNext, fetchParams, queryOptionsParams, paginateNext, perPage } = this.props; if (canShowNext) { - paginateNext(queryParams.docParams, perPage); + paginateNext(fetchParams, queryOptionsParams, perPage); } } previousClicked (event) { event.preventDefault(); - const { canShowPrevious, queryParams, paginatePrevious, perPage } = this.props; + const { canShowPrevious, fetchParams, queryOptionsParams, paginatePrevious, perPage } = this.props; if (canShowPrevious) { - paginatePrevious(queryParams.docParams, perPage); + paginatePrevious(fetchParams, queryOptionsParams, perPage); } } 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..3be40d070 --- /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..16ec78340 --- /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..ea12b07eb --- /dev/null +++ b/app/addons/documents/index-results/components/queryoptions/QueryOptions.js @@ -0,0 +1,125 @@ +// 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); + } + + executeQuery (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/IndexResults.js b/app/addons/documents/index-results/components/results/IndexResults.js similarity index 82% rename from app/addons/documents/index-results/components/IndexResults.js rename to app/addons/documents/index-results/components/results/IndexResults.js index 13208b7bc..65173b8a7 100644 --- a/app/addons/documents/index-results/components/IndexResults.js +++ b/app/addons/documents/index-results/components/results/IndexResults.js @@ -11,18 +11,15 @@ // the License. import React from 'react'; -import Components from '../index-results.components'; +import ResultsScreen from './ResultsScreen'; export default class IndexResults extends React.Component { constructor (props) { super(props); - const { fetchAllDocs, queryParams, initialize } = this.props; - - // save the prop params to the state tree as an initialization step - initialize(); + const { fetchAllDocs, fetchParams, queryOptionsParams } = this.props; // now get the docs! - fetchAllDocs(queryParams.docParams); + fetchAllDocs(fetchParams, queryOptionsParams); } componentWillUnmount () { @@ -31,8 +28,8 @@ export default class IndexResults extends React.Component { } deleteSelectedDocs () { - const { bulkDeleteDocs, queryParams, selectedDocs } = this.props; - bulkDeleteDocs(selectedDocs, queryParams.docParams); + const { bulkDeleteDocs, fetchParams, selectedDocs } = this.props; + bulkDeleteDocs(selectedDocs, fetchParams); } isSelected (id) { @@ -72,7 +69,7 @@ export default class IndexResults extends React.Component { const { results } = this.props; 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/IndexResultsContainer.js b/app/addons/documents/index-results/containers/IndexResultsContainer.js index e76b07bcc..0ed048b86 100644 --- a/app/addons/documents/index-results/containers/IndexResultsContainer.js +++ b/app/addons/documents/index-results/containers/IndexResultsContainer.js @@ -11,17 +11,16 @@ // the License. import { connect } from 'react-redux'; -import IndexResults from '../components/IndexResults'; +import IndexResults from '../components/results/IndexResults'; +import { fetchAllDocs, bulkDeleteDocs } from '../apis/fetch-api'; +import { queryOptionsToggleIncludeDocs } from '../apis/queryoptions-api'; import { - fetchAllDocs, - initialize, selectDoc, - bulkDeleteDocs, changeLayout, bulkCheckOrUncheck, changeTableHeaderAttribute, resetState -} from '../../api'; +} from '../apis/base-api'; import { getDocs, getSelectedDocs, @@ -35,8 +34,9 @@ import { getNumDocsSelected, getTextEmptyIndex, getTypeOfIndex, - getQueryParams -} from '../../reducers'; + getFetchParams, + getQueryOptionsParams +} from '../reducers'; const mapStateToProps = ({indexResults}, ownProps) => { @@ -53,26 +53,37 @@ const mapStateToProps = ({indexResults}, ownProps) => { numDocsSelected: getNumDocsSelected(indexResults), textEmptyIndex: getTextEmptyIndex(indexResults), typeOfIndex: getTypeOfIndex(indexResults), - queryParams: getQueryParams(indexResults) + fetchParams: getFetchParams(indexResults), + queryOptionsParams: getQueryOptionsParams(indexResults) }; }; const mapDispatchToProps = (dispatch, ownProps) => { return { - fetchAllDocs: (params) => { dispatch(fetchAllDocs(ownProps.databaseName, params)); }, - initialize: () => { dispatch(initialize(ownProps.params)); }, - selectDoc: (doc, selectedDocs) => { dispatch(selectDoc(doc, selectedDocs)); }, + fetchAllDocs: (fetchParams, queryOptionsParams) => { + dispatch(fetchAllDocs(ownProps.databaseName, fetchParams, queryOptionsParams)); + }, + selectDoc: (doc, selectedDocs) => { + dispatch(selectDoc(doc, selectedDocs)); + }, bulkDeleteDocs: (docs, params) => { dispatch(bulkDeleteDocs(ownProps.databaseName, docs, ownProps.designDocs, params)); }, - changeLayout: (newLayout) => { dispatch(changeLayout(newLayout)); }, + changeLayout: (newLayout) => { + dispatch(changeLayout(newLayout)); + }, bulkCheckOrUncheck: (docs, selectedDocs, allDocumentsSelected) => { dispatch(bulkCheckOrUncheck(docs, selectedDocs, allDocumentsSelected)); }, changeTableHeaderAttribute: (newField, selectedFields) => { dispatch(changeTableHeaderAttribute(newField, selectedFields)); }, - resetState: () => { dispatch(resetState()); } + resetState: () => { + dispatch(resetState()); + }, + queryOptionsToggleIncludeDocs: (previousIncludeDocs) => { + dispatch(queryOptionsToggleIncludeDocs(previousIncludeDocs)); + } }; }; diff --git a/app/addons/documents/index-results/containers/PaginationContainer.js b/app/addons/documents/index-results/containers/PaginationContainer.js index 77f56335c..ee22c35fe 100644 --- a/app/addons/documents/index-results/containers/PaginationContainer.js +++ b/app/addons/documents/index-results/containers/PaginationContainer.js @@ -17,12 +17,12 @@ import { updatePerPageResults, paginateNext, paginatePrevious -} from '../../api'; +} from '../apis/pagination-api'; import { getDocs, getSelectedDocs, getHasResults, - getQueryParams, + getFetchParams, getPageStart, getPageEnd, getPerPage, @@ -30,8 +30,9 @@ import { getShowPrioritizedEnabled, getDisplayedFields, getCanShowNext, - getCanShowPrevious -} from '../../reducers'; + getCanShowPrevious, + getQueryOptionsParams +} from '../reducers'; const mapStateToProps = ({indexResults}, ownProps) => { @@ -47,16 +48,25 @@ const mapStateToProps = ({indexResults}, ownProps) => { displayedFields: getDisplayedFields(indexResults, ownProps.databaseName), canShowNext: getCanShowNext(indexResults), canShowPrevious: getCanShowPrevious(indexResults), - queryParams: getQueryParams(indexResults) + fetchParams: getFetchParams(indexResults), + queryOptionsParams: getQueryOptionsParams(indexResults) }; }; const mapDispatchToProps = (dispatch, ownProps) => { return { - toggleShowAllColumns: () => { dispatch(toggleShowAllColumns()); }, - updatePerPageResults: (amount, params) => { dispatch(updatePerPageResults(ownProps.databaseName, amount, params)); }, - paginateNext: (params, perPage) => { dispatch(paginateNext(ownProps.databaseName, params, perPage)); }, - paginatePrevious: (params, perPage) => { dispatch(paginatePrevious(ownProps.databaseName, params, perPage)); } + toggleShowAllColumns: () => { + dispatch(toggleShowAllColumns()); + }, + updatePerPageResults: (amount, fetchParams, queryOptionsParams) => { + dispatch(updatePerPageResults(ownProps.databaseName, fetchParams, queryOptionsParams, amount)); + }, + paginateNext: (fetchParams, queryOptionsParams, perPage) => { + dispatch(paginateNext(ownProps.databaseName, fetchParams, queryOptionsParams, perPage)); + }, + paginatePrevious: (fetchParams, queryOptionsParams, perPage) => { + dispatch(paginatePrevious(ownProps.databaseName, fetchParams, queryOptionsParams, perPage)); + } }; }; 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..6b139c7f4 --- /dev/null +++ b/app/addons/documents/index-results/containers/QueryOptionsContainer.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 { connect } from 'react-redux'; +import QueryOptions from '../components/queryoptions/QueryOptions'; +import { changeLayout } from '../apis/base-api'; +import { resetPagination } from '../apis/pagination-api'; +import { + queryOptionsExecute, + queryOptionsToggleReduce, + queryOptionsUpdateGroupLevel, + queryOptionsToggleByKeys, + queryOptionsToggleBetweenKeys, + queryOptionsUpdateBetweenKeys, + queryOptionsUpdateByKeys, + queryOptionsToggleDescending, + queryOptionsUpdateSkip, + queryOptionsUpdateLimit, + queryOptionsToggleIncludeDocs, + queryOptionsToggleVisibility +} from '../apis/queryoptions-api'; +import { + getQueryOptionsPanel, + getFetchParams, + getQueryOptionsParams, + getPerPage, + getSelectedLayout +} from '../reducers'; + +const mapStateToProps = ({indexResults}) => { + 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) + }; +}; + +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.databaseName, queryOptionsParams, perPage)); + }, + changeLayout: (newLayout) => { + dispatch(changeLayout(newLayout)); + } + }; +}; + +export default connect ( + mapStateToProps, + mapDispatchToProps +)(QueryOptions); diff --git a/app/addons/documents/reducers.js b/app/addons/documents/index-results/reducers.js similarity index 75% rename from app/addons/documents/reducers.js rename to app/addons/documents/index-results/reducers.js index 669768f39..4683ec2e8 100644 --- a/app/addons/documents/reducers.js +++ b/app/addons/documents/index-results/reducers.js @@ -10,11 +10,11 @@ // License for the specific language governing permissions and limitations under // the License. -import FauxtonAPI from '../../core/api'; -import ActionTypes from './index-results/actiontypes'; -import Constants from './constants'; -import { getJsonViewData } from './index-results/helpers/json-view'; -import { getTableViewData } from './index-results/helpers/table-view'; +import FauxtonAPI from '../../../core/api'; +import ActionTypes from './actiontypes'; +import Constants from '../constants'; +import { getJsonViewData } from './helpers/json-view'; +import { getTableViewData } from './helpers/table-view'; const initialState = { docs: [], // raw documents returned from couch @@ -28,19 +28,31 @@ const initialState = { selectedLayout: Constants.LAYOUT_ORIENTATION.METADATA, textEmptyIndex: 'No Documents Found', typeOfIndex: 'view', - queryParams: { - docParams: { // params fauxton uses to fetch results from couch - limit: FauxtonAPI.constants.MISC.DEFAULT_PAGE_SIZE + 1 - }, - urlParams: { // params representing what is visible to the user - limit: FauxtonAPI.constants.MISC.DEFAULT_PAGE_SIZE - } + 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 + }, + byKeys: '', + descending: false, + skip: '', + limit: 'none', + reduce: false, + groupLevel: 'exact', + showReduce: false } }; @@ -49,24 +61,13 @@ export default function resultsState (state = initialState, action) { case ActionTypes.INDEX_RESULTS_REDUX_RESET_STATE: return Object.assign({}, initialState, { - // deeply assign these values to ensure they're reset - queryParams: { - docParams: { - limit: FauxtonAPI.constants.MISC.DEFAULT_PAGE_SIZE + 1 - }, - urlParams: { - limit: FauxtonAPI.constants.MISC.DEFAULT_PAGE_SIZE - } + fetchParams: { + limit: FauxtonAPI.constants.MISC.DEFAULT_PAGE_SIZE + 1, + skip: 0 } }); break; - case ActionTypes.INDEX_RESULTS_REDUX_INITIALIZE: - return Object.assign({}, state, { - queryParams: action.params - }); - break; - case ActionTypes.INDEX_RESULTS_REDUX_IS_LOADING: return Object.assign({}, state, { isLoading: true @@ -81,14 +82,12 @@ export default function resultsState (state = initialState, action) { case ActionTypes.INDEX_RESULTS_REDUX_NEW_RESULTS: return Object.assign({}, state, { - docs: removeOverflowDoc(action.docs, state.pagination.perPage), + docs: action.docs, isLoading: false, isEditable: true, //TODO: determine logic for this - queryParams: Object.assign({}, state.queryParams, { - docParams: action.params - }), + fetchParams: Object.assign({}, state.fetchParams, action.params), pagination: Object.assign({}, state.pagination, { - canShowNext: action.docs.length > state.pagination.perPage + canShowNext: action.canShowNext }) }); break; @@ -118,10 +117,8 @@ export default function resultsState (state = initialState, action) { case ActionTypes.INDEX_RESULTS_REDUX_SET_PER_PAGE: return Object.assign({}, state, { - pagination: Object.assign({}, state.pagination, { - perPage: action.perPage, - currentPage: 1, - pageStart: 1 + pagination: Object.assign({}, initialState.pagination, { + perPage: action.perPage }) }); break; @@ -144,19 +141,18 @@ export default function resultsState (state = initialState, action) { }); break; + case ActionTypes.INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS: + return Object.assign({}, state, { + queryOptionsPanel: Object.assign({}, state.queryOptionsPanel, action.options) + }); + break; + default: return state; } }; -// fauxton always requests one extra doc as a sneaky way to determine if -// there is another page of results. We need to remove that extra doc so -// we don't confuse users. -const removeOverflowDoc = (docs, limit) => { - return docs.length <= limit ? docs : docs.slice(0, limit); -}; - // we don't want to muddy the waters with autogenerated mango docs const removeGeneratedMangoDocs = (doc) => { return doc.language !== 'query'; @@ -253,6 +249,52 @@ 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; @@ -261,8 +303,9 @@ 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 getQueryParams = state => state.queryParams; +export const getFetchParams = state => state.fetchParams; export const getPageStart = state => state.pagination.pageStart; export const getPrioritizedEnabled = state => state.tableView.showAllFieldsTableView; export const getPerPage = state => state.pagination.perPage; export const getCanShowNext = state => state.pagination.canShowNext; +export const getQueryOptionsPanel = state => state.queryOptionsPanel; diff --git a/app/addons/documents/layouts.js b/app/addons/documents/layouts.js index 91e331a40..71c82ad2c 100644 --- a/app/addons/documents/layouts.js +++ b/app/addons/documents/layouts.js @@ -30,7 +30,8 @@ export const TabsSidebarHeader = ({ dbName, dropDownLinks, docURL, - endpoint + endpoint, + isRedux = false }) => { return (
@@ -43,7 +44,11 @@ export const TabsSidebarHeader = ({
- +
@@ -131,6 +136,7 @@ export const DocsTabsSidebarLayout = ({ dbName={dbName} dropDownLinks={dropDownLinks} database={database} + isRedux={isRedux} /> -1) { @@ -109,18 +98,6 @@ var DocumentsRouteObject = BaseRoute.extend({ SidebarActions.selectNavItem(tab); ComponentsActions.showDeleteDatabaseModal({showDeleteModal: false, dbId: ''}); - /*const frozenCollection = app.utils.localStorageGet('include_docs_bulkdocs'); - window.localStorage.removeItem('include_docs_bulkdocs'); - - IndexResultsActions.newResultsList({ - collection: collection, - textEmptyIndex: 'No Documents Found', - typeOfIndex: 'view', - bulkCollection: new Documents.BulkDeleteDocCollection(frozenCollection, { databaseId: this.database.safeID() }), - }); - - this.database.allDocs.paging.pageSize = store.getPerPage();*/ - const endpoint = this.database.allDocs.urlRef("apiurl", urlParams); const docURL = this.database.allDocs.documentation(); @@ -137,7 +114,6 @@ var DocumentsRouteObject = BaseRoute.extend({ database={this.database} designDocs={this.designDocs} isRedux={true} - params={params} />; }, From 4e4432bbf0e5f970be8108c7aba11c59178de6f0 Mon Sep 17 00:00:00 2001 From: Ryan Millay Date: Fri, 19 May 2017 13:52:55 -0400 Subject: [PATCH 17/25] switching fetch logic to depend on url instead of dbname --- .../documents/components/header-docs-right.js | 4 +- .../documents/index-results/apis/fetch-api.js | 55 +++++++++++++--------- .../index-results/apis/queryoptions-api.js | 4 +- .../containers/IndexResultsContainer.js | 2 +- .../containers/PaginationContainer.js | 6 +-- .../containers/QueryOptionsContainer.js | 2 +- app/addons/documents/layouts.js | 23 ++++++--- app/addons/documents/routes-documents.js | 6 +-- 8 files changed, 60 insertions(+), 42 deletions(-) diff --git a/app/addons/documents/components/header-docs-right.js b/app/addons/documents/components/header-docs-right.js index 2544ff590..b4472a207 100644 --- a/app/addons/documents/components/header-docs-right.js +++ b/app/addons/documents/components/header-docs-right.js @@ -18,7 +18,7 @@ import Actions from './actions'; const { QueryOptionsController } = QueryOptions; -const RightAllDocsHeader = ({database, hideQueryOptions, isRedux, dbName}) => +const RightAllDocsHeader = ({database, hideQueryOptions, isRedux, fetchUrl}) =>
@@ -26,7 +26,7 @@ const RightAllDocsHeader = ({database, hideQueryOptions, isRedux, dbName}) =>
- {!hideQueryOptions && isRedux ? : ''} + {!hideQueryOptions && isRedux ? : ''} {!hideQueryOptions && !isRedux ? : ''}
; diff --git a/app/addons/documents/index-results/apis/fetch-api.js b/app/addons/documents/index-results/apis/fetch-api.js index 950d4b120..101dd4200 100644 --- a/app/addons/documents/index-results/apis/fetch-api.js +++ b/app/addons/documents/index-results/apis/fetch-api.js @@ -48,12 +48,37 @@ const mergeParams = (fetchParams, queryOptionsParams) => { }; }; +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: -// - databaseName -> the name of the database to fetch from +// - 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 = (databaseName, fetchParams, queryOptionsParams) => { +export const fetchAllDocs = (fetchUrl, fetchParams, queryOptionsParams) => { const { params, totalDocsRemaining } = mergeParams(fetchParams, queryOptionsParams); params.limit = Math.min(params.limit, maxDocLimit); @@ -63,7 +88,7 @@ export const fetchAllDocs = (databaseName, fetchParams, queryOptionsParams) => { // now fetch the results const query = queryString.stringify(params); - return fetch(`/${databaseName}/_all_docs?${query}`, { + return fetch(`${fetchUrl}?${query}`, { credentials: 'include', headers: { 'Accept': 'application/json; charset=utf-8' @@ -72,27 +97,13 @@ export const fetchAllDocs = (databaseName, fetchParams, queryOptionsParams) => { .then(res => res.json()) .then((res) => { const docs = res.error ? [] : res.rows; - - // 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 === params.limit) { - // 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(params.limit - 1); - canShowNext = true; - } + const { + finalDocList, + canShowNext + } = removeOverflowDocsAndCalculateHasNext(docs, totalDocsRemaining, params.limit); // dispatch that we're all done - dispatch(newResultsAvailable(docs, params, canShowNext)); + dispatch(newResultsAvailable(finalDocList, params, canShowNext)); }); }; }; diff --git a/app/addons/documents/index-results/apis/queryoptions-api.js b/app/addons/documents/index-results/apis/queryoptions-api.js index abb8f1556..972af77d5 100644 --- a/app/addons/documents/index-results/apis/queryoptions-api.js +++ b/app/addons/documents/index-results/apis/queryoptions-api.js @@ -20,12 +20,12 @@ const updateQueryOptions = (queryOptions) => { }; }; -export const queryOptionsExecute = (databaseName, queryOptionsParams, perPage) => { +export const queryOptionsExecute = (fetchUrl, queryOptionsParams, perPage) => { const fetchParams = { limit: perPage + 1, skip: 0 }; - return fetchAllDocs(databaseName, fetchParams, queryOptionsParams); + return fetchAllDocs(fetchUrl, fetchParams, queryOptionsParams); }; export const queryOptionsToggleVisibility = (newVisibility) => { diff --git a/app/addons/documents/index-results/containers/IndexResultsContainer.js b/app/addons/documents/index-results/containers/IndexResultsContainer.js index 0ed048b86..439e1d1e7 100644 --- a/app/addons/documents/index-results/containers/IndexResultsContainer.js +++ b/app/addons/documents/index-results/containers/IndexResultsContainer.js @@ -61,7 +61,7 @@ const mapStateToProps = ({indexResults}, ownProps) => { const mapDispatchToProps = (dispatch, ownProps) => { return { fetchAllDocs: (fetchParams, queryOptionsParams) => { - dispatch(fetchAllDocs(ownProps.databaseName, fetchParams, queryOptionsParams)); + dispatch(fetchAllDocs(ownProps.fetchUrl, fetchParams, queryOptionsParams)); }, selectDoc: (doc, selectedDocs) => { dispatch(selectDoc(doc, selectedDocs)); diff --git a/app/addons/documents/index-results/containers/PaginationContainer.js b/app/addons/documents/index-results/containers/PaginationContainer.js index ee22c35fe..f70970532 100644 --- a/app/addons/documents/index-results/containers/PaginationContainer.js +++ b/app/addons/documents/index-results/containers/PaginationContainer.js @@ -59,13 +59,13 @@ const mapDispatchToProps = (dispatch, ownProps) => { dispatch(toggleShowAllColumns()); }, updatePerPageResults: (amount, fetchParams, queryOptionsParams) => { - dispatch(updatePerPageResults(ownProps.databaseName, fetchParams, queryOptionsParams, amount)); + dispatch(updatePerPageResults(ownProps.fetchUrl, fetchParams, queryOptionsParams, amount)); }, paginateNext: (fetchParams, queryOptionsParams, perPage) => { - dispatch(paginateNext(ownProps.databaseName, fetchParams, queryOptionsParams, perPage)); + dispatch(paginateNext(ownProps.fetchUrl, fetchParams, queryOptionsParams, perPage)); }, paginatePrevious: (fetchParams, queryOptionsParams, perPage) => { - dispatch(paginatePrevious(ownProps.databaseName, fetchParams, queryOptionsParams, perPage)); + dispatch(paginatePrevious(ownProps.fetchUrl, fetchParams, queryOptionsParams, perPage)); } }; }; diff --git a/app/addons/documents/index-results/containers/QueryOptionsContainer.js b/app/addons/documents/index-results/containers/QueryOptionsContainer.js index 6b139c7f4..2ab9a2e8a 100644 --- a/app/addons/documents/index-results/containers/QueryOptionsContainer.js +++ b/app/addons/documents/index-results/containers/QueryOptionsContainer.js @@ -97,7 +97,7 @@ const mapDispatchToProps = (dispatch, ownProps) => { dispatch(queryOptionsToggleVisibility(newVisibility)); }, queryOptionsExecute: (queryOptionsParams, perPage) => { - dispatch(queryOptionsExecute(ownProps.databaseName, queryOptionsParams, perPage)); + dispatch(queryOptionsExecute(ownProps.fetchUrl, queryOptionsParams, perPage)); }, changeLayout: (newLayout) => { dispatch(changeLayout(newLayout)); diff --git a/app/addons/documents/layouts.js b/app/addons/documents/layouts.js index 71c82ad2c..0d708c098 100644 --- a/app/addons/documents/layouts.js +++ b/app/addons/documents/layouts.js @@ -31,7 +31,8 @@ export const TabsSidebarHeader = ({ dropDownLinks, docURL, endpoint, - isRedux = false + isRedux = false, + fetchUrl }) => { return (
@@ -48,7 +49,7 @@ export const TabsSidebarHeader = ({ hideQueryOptions={hideQueryOptions} database={database} isRedux={isRedux} - dbName={dbName} /> + fetchUrl={fetchUrl} />
@@ -78,8 +79,9 @@ export const TabsSidebarContent = ({ hideFooter, lowerContent, upperContent, - databaseName, - isRedux = false + isRedux = false, + fetchUrl, + databaseName }) => { return (
@@ -94,7 +96,9 @@ export const TabsSidebarContent = ({ {lowerContent}
@@ -119,11 +123,14 @@ export const DocsTabsSidebarLayout = ({ dbName, dropDownLinks, isRedux = false, - params = {} + fetchUrl }) => { let lowerContent; if (isRedux) { - lowerContent = ; + lowerContent = ; } else { lowerContent = ; } @@ -137,10 +144,12 @@ export const DocsTabsSidebarLayout = ({ dropDownLinks={dropDownLinks} database={database} isRedux={isRedux} + fetchUrl={fetchUrl} />
diff --git a/app/addons/documents/routes-documents.js b/app/addons/documents/routes-documents.js index bfffb3536..7aa151df6 100644 --- a/app/addons/documents/routes-documents.js +++ b/app/addons/documents/routes-documents.js @@ -87,6 +87,7 @@ var DocumentsRouteObject = BaseRoute.extend({ urlParams = params.urlParams, docParams = params.docParams; + const url = `/${databaseName}/_all_docs`; // this is used for the header and sidebar this.database.buildAllDocs(docParams); @@ -101,10 +102,6 @@ var DocumentsRouteObject = BaseRoute.extend({ 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 ; }, From 69b83a790e2877a3b75caf93b58b3d89c577eaef Mon Sep 17 00:00:00 2001 From: Ryan Millay Date: Tue, 23 May 2017 13:36:36 -0400 Subject: [PATCH 18/25] ddocs only filter and apibar support --- .../documents/components/header-docs-right.js | 4 +-- .../index-results/apis/queryoptions-api.js | 16 ++++++++-- .../components/queryoptions/QueryButtons.js | 2 +- .../components/queryoptions/QueryOptions.js | 24 ++++++++++++++- .../components/results/IndexResults.js | 19 +++++++++++- .../index-results/containers/ApiBarContainer.js | 36 ++++++++++++++++++++++ .../containers/QueryOptionsContainer.js | 16 +++++++--- app/addons/documents/index-results/reducers.js | 17 +++------- app/addons/documents/layouts.js | 15 ++++++--- app/addons/documents/routes-documents.js | 4 ++- 10 files changed, 124 insertions(+), 29 deletions(-) create mode 100644 app/addons/documents/index-results/containers/ApiBarContainer.js diff --git a/app/addons/documents/components/header-docs-right.js b/app/addons/documents/components/header-docs-right.js index b4472a207..4a94ef3ee 100644 --- a/app/addons/documents/components/header-docs-right.js +++ b/app/addons/documents/components/header-docs-right.js @@ -18,7 +18,7 @@ import Actions from './actions'; const { QueryOptionsController } = QueryOptions; -const RightAllDocsHeader = ({database, hideQueryOptions, isRedux, fetchUrl}) => +const RightAllDocsHeader = ({database, hideQueryOptions, isRedux, fetchUrl, ddocsOnly}) =>
@@ -26,7 +26,7 @@ const RightAllDocsHeader = ({database, hideQueryOptions, isRedux, fetchUrl}) =>
- {!hideQueryOptions && isRedux ? : ''} + {!hideQueryOptions && isRedux ? : ''} {!hideQueryOptions && !isRedux ? : ''} ; diff --git a/app/addons/documents/index-results/apis/queryoptions-api.js b/app/addons/documents/index-results/apis/queryoptions-api.js index 972af77d5..828803a81 100644 --- a/app/addons/documents/index-results/apis/queryoptions-api.js +++ b/app/addons/documents/index-results/apis/queryoptions-api.js @@ -49,14 +49,14 @@ export const queryOptionsUpdateGroupLevel = (newGroupLevel) => { export const queryOptionsToggleByKeys = (previousShowByKeys) => { return updateQueryOptions({ showByKeys: !previousShowByKeys, - showBetweenKeys: previousShowByKeys + showBetweenKeys: !!previousShowByKeys }); }; export const queryOptionsToggleBetweenKeys = (previousShowBetweenKeys) => { return updateQueryOptions({ showBetweenKeys: !previousShowBetweenKeys, - showByKeys: previousShowBetweenKeys + showByKeys: !!previousShowBetweenKeys }); }; @@ -95,3 +95,15 @@ export const queryOptionsToggleIncludeDocs = (previousIncludeDocs) => { 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/queryoptions/QueryButtons.js b/app/addons/documents/index-results/components/queryoptions/QueryButtons.js index 16ec78340..2251c1179 100644 --- a/app/addons/documents/index-results/components/queryoptions/QueryButtons.js +++ b/app/addons/documents/index-results/components/queryoptions/QueryButtons.js @@ -26,7 +26,7 @@ export default class QueryButtons extends React.Component {
- Cancel + Cancel
); diff --git a/app/addons/documents/index-results/components/queryoptions/QueryOptions.js b/app/addons/documents/index-results/components/queryoptions/QueryOptions.js index ea12b07eb..7d8b9630b 100644 --- a/app/addons/documents/index-results/components/queryoptions/QueryOptions.js +++ b/app/addons/documents/index-results/components/queryoptions/QueryOptions.js @@ -24,10 +24,32 @@ 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) { - e.preventDefault(); + if (e) { e.preventDefault(); } this.closeTray(); const { diff --git a/app/addons/documents/index-results/components/results/IndexResults.js b/app/addons/documents/index-results/components/results/IndexResults.js index 65173b8a7..6e3fc99cc 100644 --- a/app/addons/documents/index-results/components/results/IndexResults.js +++ b/app/addons/documents/index-results/components/results/IndexResults.js @@ -16,12 +16,29 @@ import ResultsScreen from './ResultsScreen'; export default class IndexResults extends React.Component { constructor (props) { super(props); - const { fetchAllDocs, fetchParams, queryOptionsParams } = this.props; + 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(); 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/QueryOptionsContainer.js b/app/addons/documents/index-results/containers/QueryOptionsContainer.js index 2ab9a2e8a..622cd9e82 100644 --- a/app/addons/documents/index-results/containers/QueryOptionsContainer.js +++ b/app/addons/documents/index-results/containers/QueryOptionsContainer.js @@ -12,7 +12,7 @@ import { connect } from 'react-redux'; import QueryOptions from '../components/queryoptions/QueryOptions'; -import { changeLayout } from '../apis/base-api'; +import { changeLayout, resetState } from '../apis/base-api'; import { resetPagination } from '../apis/pagination-api'; import { queryOptionsExecute, @@ -26,7 +26,8 @@ import { queryOptionsUpdateSkip, queryOptionsUpdateLimit, queryOptionsToggleIncludeDocs, - queryOptionsToggleVisibility + queryOptionsToggleVisibility, + queryOptionsFilterOnlyDdocs } from '../apis/queryoptions-api'; import { getQueryOptionsPanel, @@ -36,7 +37,7 @@ import { getSelectedLayout } from '../reducers'; -const mapStateToProps = ({indexResults}) => { +const mapStateToProps = ({indexResults}, ownProps) => { const queryOptionsPanel = getQueryOptionsPanel(indexResults); return { contentVisible: queryOptionsPanel.isVisible, @@ -54,7 +55,8 @@ const mapStateToProps = ({indexResults}) => { fetchParams: getFetchParams(indexResults), queryOptionsParams: getQueryOptionsParams(indexResults), perPage: getPerPage(indexResults), - selectedLayout: getSelectedLayout(indexResults) + selectedLayout: getSelectedLayout(indexResults), + ddocsOnly: ownProps.ddocsOnly }; }; @@ -99,8 +101,14 @@ const mapDispatchToProps = (dispatch, ownProps) => { queryOptionsExecute: (queryOptionsParams, perPage) => { dispatch(queryOptionsExecute(ownProps.fetchUrl, queryOptionsParams, perPage)); }, + queryOptionsFilterOnlyDdocs: () => { + dispatch(queryOptionsFilterOnlyDdocs()); + }, changeLayout: (newLayout) => { dispatch(changeLayout(newLayout)); + }, + resetState: () => { + dispatch(resetState()); } }; }; diff --git a/app/addons/documents/index-results/reducers.js b/app/addons/documents/index-results/reducers.js index 4683ec2e8..7203d9ee5 100644 --- a/app/addons/documents/index-results/reducers.js +++ b/app/addons/documents/index-results/reducers.js @@ -44,7 +44,9 @@ const initialState = { showBetweenKeys: false, includeDocs: false, betweenKeys: { - include: true + include: true, + startkey: '', + endkey: '' }, byKeys: '', descending: false, @@ -66,19 +68,16 @@ export default function resultsState (state = initialState, action) { skip: 0 } }); - break; case ActionTypes.INDEX_RESULTS_REDUX_IS_LOADING: return Object.assign({}, state, { isLoading: true }); - break; case ActionTypes.INDEX_RESULTS_REDUX_NEW_SELECTED_DOCS: return Object.assign({}, state, { selectedDocs: action.selectedDocs }); - break; case ActionTypes.INDEX_RESULTS_REDUX_NEW_RESULTS: return Object.assign({}, state, { @@ -90,13 +89,11 @@ export default function resultsState (state = initialState, action) { canShowNext: action.canShowNext }) }); - break; case ActionTypes.INDEX_RESULTS_REDUX_CHANGE_LAYOUT: return Object.assign({}, state, { selectedLayout: action.layout }); - break; case ActionTypes.INDEX_RESULTS_REDUX_TOGGLE_SHOW_ALL_COLUMNS: return Object.assign({}, state, { @@ -105,7 +102,6 @@ export default function resultsState (state = initialState, action) { cachedFieldsTableView: state.tableView.selectedFieldsTableView }) }); - break; case ActionTypes.INDEX_RESULTS_REDUX_CHANGE_TABLE_HEADER_ATTRIBUTE: return Object.assign({}, state, { @@ -113,7 +109,6 @@ export default function resultsState (state = initialState, action) { selectedFieldsTableView: action.selectedFieldsTableView }) }); - break; case ActionTypes.INDEX_RESULTS_REDUX_SET_PER_PAGE: return Object.assign({}, state, { @@ -121,7 +116,6 @@ export default function resultsState (state = initialState, action) { perPage: action.perPage }) }); - break; case ActionTypes.INDEX_RESULTS_REDUX_PAGINATE_NEXT: return Object.assign({}, state, { @@ -130,7 +124,6 @@ export default function resultsState (state = initialState, action) { currentPage: state.pagination.currentPage + 1 }) }); - break; case ActionTypes.INDEX_RESULTS_REDUX_PAGINATE_PREVIOUS: return Object.assign({}, state, { @@ -139,13 +132,11 @@ export default function resultsState (state = initialState, action) { currentPage: state.pagination.currentPage - 1 }) }); - break; case ActionTypes.INDEX_RESULTS_REDUX_NEW_QUERY_OPTIONS: return Object.assign({}, state, { queryOptionsPanel: Object.assign({}, state.queryOptionsPanel, action.options) }); - break; default: return state; @@ -263,7 +254,7 @@ export const getQueryOptionsParams = (state) => { if (betweenKeys.startkey && betweenKeys.startkey != '') { params.start_key = betweenKeys.startkey; } - if (betweenKeys.endKey && betweenKeys.endKey != '') { + if (betweenKeys.endkey && betweenKeys.endkey != '') { params.end_key = betweenKeys.endkey; } } else if (queryOptionsPanel.showByKeys) { diff --git a/app/addons/documents/layouts.js b/app/addons/documents/layouts.js index 0d708c098..3b83b585d 100644 --- a/app/addons/documents/layouts.js +++ b/app/addons/documents/layouts.js @@ -23,6 +23,7 @@ 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, @@ -32,7 +33,8 @@ export const TabsSidebarHeader = ({ docURL, endpoint, isRedux = false, - fetchUrl + fetchUrl, + ddocsOnly }) => { return (
@@ -49,9 +51,11 @@ export const TabsSidebarHeader = ({ hideQueryOptions={hideQueryOptions} database={database} isRedux={isRedux} - fetchUrl={fetchUrl} /> + fetchUrl={fetchUrl} + ddocsOnly={ddocsOnly} /> - + { isRedux ? : + }
@@ -123,13 +127,15 @@ export const DocsTabsSidebarLayout = ({ dbName, dropDownLinks, isRedux = false, - fetchUrl + fetchUrl, + ddocsOnly }) => { let lowerContent; if (isRedux) { lowerContent = ; } else { lowerContent = ; @@ -145,6 +151,7 @@ export const DocsTabsSidebarLayout = ({ database={database} isRedux={isRedux} fetchUrl={fetchUrl} + ddocsOnly={ddocsOnly} /> -1); let tab = 'all-docs'; - if (docParams.startkey && docParams.startkey.indexOf("_design") > -1) { + if (onlyShowDdocs) { tab = 'design-docs'; } @@ -111,6 +112,7 @@ var DocumentsRouteObject = BaseRoute.extend({ database={this.database} designDocs={this.designDocs} fetchUrl={url} + ddocsOnly={onlyShowDdocs} isRedux={true} />; }, From cf9063ffe26efd95af9be0b63fa4c676b1a5eedd Mon Sep 17 00:00:00 2001 From: Ryan Millay Date: Fri, 26 May 2017 13:00:09 -0400 Subject: [PATCH 19/25] mid test writing --- app/addons/documents/__tests__/base-api.test.js | 175 ++++++++ app/addons/documents/__tests__/fetch-api.test.js | 346 ++++++++++++++++ app/addons/documents/__tests__/json-view.test.js | 128 ++++++ .../documents/__tests__/pagination-api.test.js | 110 +++++ .../documents/__tests__/queryoptions-api.test.js | 159 +++++++ app/addons/documents/__tests__/reducers.test.js | 459 +++++++++++++++++++++ .../documents/__tests__/shared-helpers.test.js | 170 ++++++++ app/addons/documents/__tests__/table-view.test.js | 109 +++++ app/addons/documents/header/header.js | 7 +- .../documents/index-results/apis/base-api.js | 5 +- .../documents/index-results/apis/fetch-api.js | 65 +-- .../documents/index-results/apis/pagination-api.js | 28 +- .../index-results/apis/queryoptions-api.js | 8 +- .../components/results/IndexResults.js | 7 +- .../containers/IndexResultsContainer.js | 9 +- .../documents/index-results/helpers/json-view.js | 2 +- .../index-results/helpers/shared-helpers.js | 3 +- .../documents/index-results/helpers/table-view.js | 12 +- app/addons/documents/routes-documents.js | 2 +- .../documents/tests/nightwatch/resultsToolbar.js | 11 +- app/addons/documents/tests/nightwatch/tableView.js | 4 +- 21 files changed, 1755 insertions(+), 64 deletions(-) create mode 100644 app/addons/documents/__tests__/base-api.test.js create mode 100644 app/addons/documents/__tests__/fetch-api.test.js create mode 100644 app/addons/documents/__tests__/json-view.test.js create mode 100644 app/addons/documents/__tests__/pagination-api.test.js create mode 100644 app/addons/documents/__tests__/queryoptions-api.test.js create mode 100644 app/addons/documents/__tests__/reducers.test.js create mode 100644 app/addons/documents/__tests__/shared-helpers.test.js create mode 100644 app/addons/documents/__tests__/table-view.test.js 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..d3d09fdab --- /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-api'; +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..4b6813dcf --- /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-api'; +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__/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__/pagination-api.test.js b/app/addons/documents/__tests__/pagination-api.test.js new file mode 100644 index 000000000..abc0d6955 --- /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-api'; +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__/queryoptions-api.test.js b/app/addons/documents/__tests__/queryoptions-api.test.js new file mode 100644 index 000000000..6e4aefc6d --- /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-api'; +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..f950623f0 --- /dev/null +++ b/app/addons/documents/__tests__/reducers.test.js @@ -0,0 +1,459 @@ +// 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('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__/shared-helpers.test.js b/app/addons/documents/__tests__/shared-helpers.test.js new file mode 100644 index 000000000..b8eb398ee --- /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-view.test.js b/app/addons/documents/__tests__/table-view.test.js new file mode 100644 index 000000000..1ed80b8f3 --- /dev/null +++ b/app/addons/documents/__tests__/table-view.test.js @@ -0,0 +1,109 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import { + getPseudoSchema, + getPrioritizedFields, + sortByTwoFields, + getNotSelectedFields +} 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" + } + ]; + + describe('getPseudoSchema', () => { + it('returns array of unique keys with _id as the first element', () => { + expect(getPseudoSchema(docs)).toEqual([ + '_id', + '_rev', + 'wiki_page', + 'min_weight', + 'max_weight', + 'min_length', + 'max_length', + 'latin_name', + 'class', + 'diet', + 'test', + 'foo' + ]); + }); + }); + + 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' + ]); + }); + }); +}); diff --git a/app/addons/documents/header/header.js b/app/addons/documents/header/header.js index 8adc35364..543c83421 100644 --- a/app/addons/documents/header/header.js +++ b/app/addons/documents/header/header.js @@ -101,10 +101,11 @@ export default class BulkDocumentHeaderController extends React.Component { // change our layout to JSON, Table, or Metadata changeLayout(newLayout); - if (newLayout === Constants.LAYOUT_ORIENTATION.METADATA) { - queryOptionsParams.include_docs = false; + queryOptionsParams.include_docs = newLayout !== Constants.LAYOUT_ORIENTATION.METADATA; + if (newLayout === Constants.LAYOUT_ORIENTATION.TABLE) { + fetchParams.conflicts = true; } else { - queryOptionsParams.include_docs = true; + delete fetchParams.conflicts; } // tell the query options panel we're updating include_docs diff --git a/app/addons/documents/index-results/apis/base-api.js b/app/addons/documents/index-results/apis/base-api.js index d23f88595..026a1643a 100644 --- a/app/addons/documents/index-results/apis/base-api.js +++ b/app/addons/documents/index-results/apis/base-api.js @@ -48,6 +48,7 @@ export const selectDoc = (doc, selectedDocs) => { if (indexInSelectedDocs > -1) { selectedDocs.splice(indexInSelectedDocs, 1); } else { + doc._deleted = true; selectedDocs.push(doc); } @@ -58,7 +59,7 @@ 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; + return (doc._id || doc.id) === selectedDoc._id; }); // remove the doc if we know all the documents are currently selected @@ -68,7 +69,7 @@ export const bulkCheckOrUncheck = (docs, selectedDocs, allDocumentsSelected) => } else if (indexInSelectedDocs === -1) { selectedDocs.push({ _id: doc._id || doc.id, - _rev: doc._rev || doc.rev, + _rev: doc._rev || doc.rev || doc.value.rev, _deleted: true }); } diff --git a/app/addons/documents/index-results/apis/fetch-api.js b/app/addons/documents/index-results/apis/fetch-api.js index 101dd4200..8659a981b 100644 --- a/app/addons/documents/index-results/apis/fetch-api.js +++ b/app/addons/documents/index-results/apis/fetch-api.js @@ -22,7 +22,7 @@ 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). -const mergeParams = (fetchParams, queryOptionsParams) => { +export const mergeParams = (fetchParams, queryOptionsParams) => { const params = {}; // determine the final "index" or "position" in the total result list based on the @@ -35,6 +35,9 @@ const mergeParams = (fetchParams, queryOptionsParams) => { // 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 @@ -48,7 +51,7 @@ const mergeParams = (fetchParams, queryOptionsParams) => { }; }; -const removeOverflowDocsAndCalculateHasNext = (docs, totalDocsRemaining, fetchLimit) => { +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 @@ -87,16 +90,7 @@ export const fetchAllDocs = (fetchUrl, fetchParams, queryOptionsParams) => { dispatch(nowLoading()); // now fetch the results - const query = queryString.stringify(params); - return fetch(`${fetchUrl}?${query}`, { - credentials: 'include', - headers: { - 'Accept': 'application/json; charset=utf-8' - } - }) - .then(res => res.json()) - .then((res) => { - const docs = res.error ? [] : res.rows; + return queryEndpoint(fetchUrl, params).then((docs) => { const { finalDocList, canShowNext @@ -108,7 +102,19 @@ export const fetchAllDocs = (fetchUrl, fetchParams, queryOptionsParams) => { }; }; -const errorMessage = (ids) => { +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) { @@ -122,7 +128,7 @@ const errorMessage = (ids) => { }); }; -const validateBulkDelete = (docs) => { +export const validateBulkDelete = (docs) => { const itemsLength = docs.length; const msg = (itemsLength === 1) ? 'Are you sure you want to delete this doc?' : @@ -140,7 +146,7 @@ const validateBulkDelete = (docs) => { return true; }; -export const bulkDeleteDocs = (databaseName, docs, designDocs, params) => { +export const bulkDeleteDocs = (databaseName, fetchUrl, docs, designDocs, fetchParams, queryOptionsParams) => { if (!validateBulkDelete(docs)) { return false; } @@ -150,29 +156,32 @@ export const bulkDeleteDocs = (databaseName, docs, designDocs, params) => { docs: docs }; - 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()) - .then((res) => { + return postToBulkDocs(databaseName, payload).then((res) => { if (res.error) { errorMessage(); return; } processBulkDeleteResponse(res, docs, designDocs); dispatch(newSelectedDocs()); - dispatch(fetchAllDocs(databaseName, params)); + dispatch(fetchAllDocs(fetchUrl, fetchParams, queryOptionsParams)); }); }; }; -const processBulkDeleteResponse = (res, originalDocs, designDocs) => { +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 diff --git a/app/addons/documents/index-results/apis/pagination-api.js b/app/addons/documents/index-results/apis/pagination-api.js index 490ef46a2..37069bc2b 100644 --- a/app/addons/documents/index-results/apis/pagination-api.js +++ b/app/addons/documents/index-results/apis/pagination-api.js @@ -20,20 +20,26 @@ export const toggleShowAllColumns = () => { }; }; -const setPerPage = (amount) => { +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.limit = amount + 1; - fetchParams.skip = queryOptionsParams.skip || 0; + fetchParams = resetFetchParamsBeforePerPageChange(fetchParams, queryOptionsParams, amount); return (dispatch) => { dispatch(setPerPage(amount)); @@ -41,9 +47,15 @@ export const updatePerPageResults = (databaseName, fetchParams, queryOptionsPara }; }; +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.skip += perPage; + fetchParams = incrementSkipForPageNext(fetchParams, perPage); return (dispatch) => { dispatch({ @@ -53,9 +65,15 @@ export const paginateNext = (databaseName, fetchParams, queryOptionsParams, perP }; }; +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.skip = Math.max(fetchParams.skip - perPage, 0); + fetchParams = decrementSkipForPagePrevious(fetchParams, perPage); return (dispatch) => { dispatch({ diff --git a/app/addons/documents/index-results/apis/queryoptions-api.js b/app/addons/documents/index-results/apis/queryoptions-api.js index 828803a81..392703a46 100644 --- a/app/addons/documents/index-results/apis/queryoptions-api.js +++ b/app/addons/documents/index-results/apis/queryoptions-api.js @@ -20,11 +20,15 @@ const updateQueryOptions = (queryOptions) => { }; }; -export const queryOptionsExecute = (fetchUrl, queryOptionsParams, perPage) => { - const fetchParams = { +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); }; diff --git a/app/addons/documents/index-results/components/results/IndexResults.js b/app/addons/documents/index-results/components/results/IndexResults.js index 6e3fc99cc..d601d4165 100644 --- a/app/addons/documents/index-results/components/results/IndexResults.js +++ b/app/addons/documents/index-results/components/results/IndexResults.js @@ -45,8 +45,8 @@ export default class IndexResults extends React.Component { } deleteSelectedDocs () { - const { bulkDeleteDocs, fetchParams, selectedDocs } = this.props; - bulkDeleteDocs(selectedDocs, fetchParams); + const { bulkDeleteDocs, fetchParams, selectedDocs, queryOptionsParams } = this.props; + bulkDeleteDocs(selectedDocs, fetchParams, queryOptionsParams); } isSelected (id) { @@ -64,8 +64,7 @@ export default class IndexResults extends React.Component { // dispatch an action to push this doc on to the array of selected docs const doc = { _id: _id, - _rev: _rev, - _deleted: true + _rev: _rev }; selectDoc(doc, selectedDocs); diff --git a/app/addons/documents/index-results/containers/IndexResultsContainer.js b/app/addons/documents/index-results/containers/IndexResultsContainer.js index 439e1d1e7..ef1112c54 100644 --- a/app/addons/documents/index-results/containers/IndexResultsContainer.js +++ b/app/addons/documents/index-results/containers/IndexResultsContainer.js @@ -66,8 +66,13 @@ const mapDispatchToProps = (dispatch, ownProps) => { selectDoc: (doc, selectedDocs) => { dispatch(selectDoc(doc, selectedDocs)); }, - bulkDeleteDocs: (docs, params) => { - dispatch(bulkDeleteDocs(ownProps.databaseName, docs, ownProps.designDocs, params)); + bulkDeleteDocs: (docs, fetchParams, queryOptionsParams) => { + dispatch(bulkDeleteDocs(ownProps.databaseName, + ownProps.fetchUrl, + docs, + ownProps.designDocs, + fetchParams, + queryOptionsParams)); }, changeLayout: (newLayout) => { dispatch(changeLayout(newLayout)); diff --git a/app/addons/documents/index-results/helpers/json-view.js b/app/addons/documents/index-results/helpers/json-view.js index 2b996a2e7..f5a81d486 100644 --- a/app/addons/documents/index-results/helpers/json-view.js +++ b/app/addons/documents/index-results/helpers/json-view.js @@ -17,7 +17,7 @@ export const getJsonViewData = (docs, { databaseName, typeOfIndex }) => { return { content: JSON.stringify(doc, null, ' '), id: doc.id, //|| doc.key.toString(), - _rev: doc._rev, + _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, diff --git a/app/addons/documents/index-results/helpers/shared-helpers.js b/app/addons/documents/index-results/helpers/shared-helpers.js index 27712d946..a67327f9a 100644 --- a/app/addons/documents/index-results/helpers/shared-helpers.js +++ b/app/addons/documents/index-results/helpers/shared-helpers.js @@ -53,7 +53,8 @@ const isJSONDocBulkDeletable = (doc, docType) => { if (docType === 'MangoIndex') { return doc.type !== 'special'; } - return (!!doc._id || !!doc.id) && (!!doc._rev || !! doc.value.rev); + const result = (doc._id || doc.id) && (doc._rev || (doc.value && doc.value.rev)); + return !!result; }; const hasBulkDeletableDoc = (docs, docType) => { diff --git a/app/addons/documents/index-results/helpers/table-view.js b/app/addons/documents/index-results/helpers/table-view.js index 059d78a9a..91e001a5b 100644 --- a/app/addons/documents/index-results/helpers/table-view.js +++ b/app/addons/documents/index-results/helpers/table-view.js @@ -18,7 +18,7 @@ import { getDocUrl } from "./shared-helpers"; -const getPseudoSchema = (docs) => { +export const getPseudoSchema = (docs) => { let cache = []; docs.forEach((doc) => { @@ -39,7 +39,7 @@ const getPseudoSchema = (docs) => { return cache; }; -const getPrioritizedFields = (docs, max) => { +export const getPrioritizedFields = (docs, max) => { let res = docs.reduce((acc, el) => { acc = acc.concat(Object.keys(el)); return acc; @@ -66,7 +66,7 @@ const getPrioritizedFields = (docs, max) => { }, []); }; -const sortByTwoFields = (elements) => { +export const sortByTwoFields = (elements) => { // given: // var a = [[2, "b"], [3, "z"], [1, "a"], [3, "a"]] // it sorts to: @@ -93,9 +93,9 @@ const sortByTwoFields = (elements) => { }); }; -const getNotSelectedFields = (selectedFields, allFields) => { - const without = _.without.bind(this, allFields); - return without.apply(this, selectedFields); +export const getNotSelectedFields = (selectedFields, allFields) => { + const without = _.without.bind(this, allFields); + return without.apply(this, selectedFields); }; const getFullTableViewData = (docs, options) => { diff --git a/app/addons/documents/routes-documents.js b/app/addons/documents/routes-documents.js index 4d1c2c248..1a8dae1fd 100644 --- a/app/addons/documents/routes-documents.js +++ b/app/addons/documents/routes-documents.js @@ -87,7 +87,7 @@ var DocumentsRouteObject = BaseRoute.extend({ urlParams = params.urlParams, docParams = params.docParams; - const url = `/${databaseName}/_all_docs`; + const url = `/${encodeURIComponent(databaseName)}/_all_docs`; // this is used for the header and sidebar this.database.buildAllDocs(docParams); diff --git a/app/addons/documents/tests/nightwatch/resultsToolbar.js b/app/addons/documents/tests/nightwatch/resultsToolbar.js index cb63a76ac..6a6c95b45 100644 --- a/app/addons/documents/tests/nightwatch/resultsToolbar.js +++ b/app/addons/documents/tests/nightwatch/resultsToolbar.js @@ -45,20 +45,17 @@ module.exports = { .createDocument(newDocumentName, newDatabaseName, docContent) .loginToGUI() .checkForDocumentCreated(newDocumentName) - - // displays table view if url manually updates with include_docs=true - .url(`${baseUrl}#/database/${newDatabaseName}/_all_docs?include_docs=true`) + .url(`${baseUrl}#/database/${newDatabaseName}/_all_docs`) .waitForElementPresent('.two-sides-toggle-button', waitTime, false) - .assert.containsText('.two-sides-toggle-button button.active', 'Table') + .assert.containsText('.two-sides-toggle-button button.active', 'Metadata') - // turn include_docs off through query options + // turn include_docs on through query options .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') + .assert.containsText('.two-sides-toggle-button button.active', 'Table') // switch to json view and then turn off include_docs .clickWhenVisible('.fonticon-json') diff --git a/app/addons/documents/tests/nightwatch/tableView.js b/app/addons/documents/tests/nightwatch/tableView.js index d57fd4dcd..4c6ce2a7d 100644 --- a/app/addons/documents/tests/nightwatch/tableView.js +++ b/app/addons/documents/tests/nightwatch/tableView.js @@ -27,8 +27,8 @@ module.exports = { .loginToGUI() .checkForDocumentCreated(newDocumentName1) .checkForDocumentCreated(newDocumentName2) - .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs?include_docs=true') - .assert.cssClassPresent('button.active i', 'fonticon-table') + .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs') + .clickWhenVisible('.fonticon-table') .waitForElementVisible('.tableview-checkbox-cell', waitTime, false) .getText('.table', function (result) { From 5f5d2b6abc64bc5f67a4f90af1e39a66aec2116c Mon Sep 17 00:00:00 2001 From: Ryan Millay Date: Mon, 5 Jun 2017 09:03:09 -0400 Subject: [PATCH 20/25] adding missing semi colon --- app/addons/documents/__tests__/shared-helpers.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/addons/documents/__tests__/shared-helpers.test.js b/app/addons/documents/__tests__/shared-helpers.test.js index b8eb398ee..a5e479b83 100644 --- a/app/addons/documents/__tests__/shared-helpers.test.js +++ b/app/addons/documents/__tests__/shared-helpers.test.js @@ -156,7 +156,7 @@ describe('Docs Shared Helpers', () => { diet: "omnivore" } ]; - let docType = 'MangoIndex' + let docType = 'MangoIndex'; it('returns true if any docs are bulk deletable', () => { expect(hasBulkDeletableDoc(docs, docType)).toBe(true); From feea7ea7a0f57ec9ab3cbcc6741b7dc93f430036 Mon Sep 17 00:00:00 2001 From: Ryan Millay Date: Fri, 16 Jun 2017 10:28:55 -0400 Subject: [PATCH 21/25] api, reducer, helper, pagination, and queryoptions unit tests complete --- .../documents/__tests__/additional-params.test.js | 58 ++++++ .../documents/__tests__/key-search-fields.test.js | 123 +++++++++++++ .../documents/__tests__/main-fields-view.test.js | 91 ++++++++++ .../documents/__tests__/pagination-footer.test.js | 199 +++++++++++++++++++++ .../documents/__tests__/paging-controls.test.js | 62 +++++++ .../documents/__tests__/perpage-selector.test.js | 27 +++ .../documents/__tests__/query-buttons.test.js | 27 +++ .../documents/__tests__/query-options.test.js | 119 ++++++++++++ app/addons/documents/__tests__/reducers.test.js | 10 ++ .../documents/__tests__/table-controls.test.js | 51 ++++++ app/addons/documents/__tests__/table-view.test.js | 100 +++++++++-- .../documents/index-results/helpers/table-view.js | 4 +- app/addons/documents/index-results/reducers.js | 2 +- 13 files changed, 855 insertions(+), 18 deletions(-) create mode 100644 app/addons/documents/__tests__/additional-params.test.js create mode 100644 app/addons/documents/__tests__/key-search-fields.test.js create mode 100644 app/addons/documents/__tests__/main-fields-view.test.js create mode 100644 app/addons/documents/__tests__/pagination-footer.test.js create mode 100644 app/addons/documents/__tests__/paging-controls.test.js create mode 100644 app/addons/documents/__tests__/perpage-selector.test.js create mode 100644 app/addons/documents/__tests__/query-buttons.test.js create mode 100644 app/addons/documents/__tests__/query-options.test.js create mode 100644 app/addons/documents/__tests__/table-controls.test.js 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__/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-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__/reducers.test.js b/app/addons/documents/__tests__/reducers.test.js index f950623f0..b0e7a4f5a 100644 --- a/app/addons/documents/__tests__/reducers.test.js +++ b/app/addons/documents/__tests__/reducers.test.js @@ -159,6 +159,16 @@ describe('Docs Reducers', () => { }); }); + 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); 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 index 1ed80b8f3..b15e3455f 100644 --- a/app/addons/documents/__tests__/table-view.test.js +++ b/app/addons/documents/__tests__/table-view.test.js @@ -14,7 +14,9 @@ import { getPseudoSchema, getPrioritizedFields, sortByTwoFields, - getNotSelectedFields + getNotSelectedFields, + getMetaDataTableView, + getFullTableViewData } from '../index-results/helpers/table-view'; describe('Docs Table View', () => { @@ -47,22 +49,24 @@ describe('Docs Table View', () => { } ]; + 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([ - '_id', - '_rev', - 'wiki_page', - 'min_weight', - 'max_weight', - 'min_length', - 'max_length', - 'latin_name', - 'class', - 'diet', - 'test', - 'foo' - ]); + expect(getPseudoSchema(docs)).toEqual(schema); }); }); @@ -106,4 +110,70 @@ describe('Docs Table View', () => { ]); }); }); + + 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/index-results/helpers/table-view.js b/app/addons/documents/index-results/helpers/table-view.js index 91e001a5b..2bce347ce 100644 --- a/app/addons/documents/index-results/helpers/table-view.js +++ b/app/addons/documents/index-results/helpers/table-view.js @@ -98,7 +98,7 @@ export const getNotSelectedFields = (selectedFields, allFields) => { return without.apply(this, selectedFields); }; -const getFullTableViewData = (docs, options) => { +export const getFullTableViewData = (docs, options) => { let notSelectedFieldsTableView = null, selectedFieldsTableView = options.selectedFieldsTableView, showAllFieldsTableView = options.showAllFieldsTableView, @@ -133,7 +133,7 @@ const getFullTableViewData = (docs, options) => { }; }; -const getMetaDataTableView = (docs) => { +export const getMetaDataTableView = (docs) => { const schema = getPseudoSchema(docs); return { schema, diff --git a/app/addons/documents/index-results/reducers.js b/app/addons/documents/index-results/reducers.js index 7203d9ee5..c0a209922 100644 --- a/app/addons/documents/index-results/reducers.js +++ b/app/addons/documents/index-results/reducers.js @@ -145,7 +145,7 @@ export default function resultsState (state = initialState, action) { // we don't want to muddy the waters with autogenerated mango docs -const removeGeneratedMangoDocs = (doc) => { +export const removeGeneratedMangoDocs = (doc) => { return doc.language !== 'query'; }; From 961191bd7941360755f83fdb6c9dbd7a7e353ade Mon Sep 17 00:00:00 2001 From: Ryan Millay Date: Mon, 19 Jun 2017 13:41:40 -0400 Subject: [PATCH 22/25] review fixes and another test --- app/addons/documents/__tests__/base-api.test.js | 2 +- app/addons/documents/__tests__/fetch-api.test.js | 2 +- .../documents/__tests__/index-results.test.js | 123 +++++++++++++++++++++ .../documents/__tests__/pagination-api.test.js | 2 +- .../documents/__tests__/queryoptions-api.test.js | 2 +- .../documents/components/header-docs-right.js | 16 ++- app/addons/documents/components/results-toolbar.js | 28 +++-- app/addons/documents/header/header.js | 18 +-- .../index-results/apis/{base-api.js => base.js} | 5 + .../index-results/apis/{fetch-api.js => fetch.js} | 2 +- .../apis/{pagination-api.js => pagination.js} | 2 +- .../apis/{queryoptions-api.js => queryoptions.js} | 2 +- .../components/queryoptions/AdditionalParams.js | 2 +- .../components/results/IndexResults.js | 5 +- .../containers/IndexResultsContainer.js | 6 +- .../containers/PaginationContainer.js | 2 +- .../containers/QueryOptionsContainer.js | 6 +- .../documents/index-results/helpers/json-view.js | 5 +- .../index-results/helpers/shared-helpers.js | 14 +-- 19 files changed, 199 insertions(+), 45 deletions(-) create mode 100644 app/addons/documents/__tests__/index-results.test.js rename app/addons/documents/index-results/apis/{base-api.js => base.js} (90%) rename app/addons/documents/index-results/apis/{fetch-api.js => fetch.js} (99%) rename app/addons/documents/index-results/apis/{pagination-api.js => pagination.js} (98%) rename app/addons/documents/index-results/apis/{queryoptions-api.js => queryoptions.js} (98%) diff --git a/app/addons/documents/__tests__/base-api.test.js b/app/addons/documents/__tests__/base-api.test.js index d3d09fdab..c966316d3 100644 --- a/app/addons/documents/__tests__/base-api.test.js +++ b/app/addons/documents/__tests__/base-api.test.js @@ -19,7 +19,7 @@ import { bulkCheckOrUncheck, changeLayout, changeTableHeaderAttribute -} from '../index-results/apis/base-api'; +} from '../index-results/apis/base'; import ActionTypes from '../index-results/actiontypes'; import Constants from '../constants'; diff --git a/app/addons/documents/__tests__/fetch-api.test.js b/app/addons/documents/__tests__/fetch-api.test.js index 4b6813dcf..34d936fb1 100644 --- a/app/addons/documents/__tests__/fetch-api.test.js +++ b/app/addons/documents/__tests__/fetch-api.test.js @@ -17,7 +17,7 @@ import { validateBulkDelete, postToBulkDocs, processBulkDeleteResponse -} from '../index-results/apis/fetch-api'; +} from '../index-results/apis/fetch'; import fetchMock from 'fetch-mock'; import queryString from 'query-string'; import sinon from 'sinon'; 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__/pagination-api.test.js b/app/addons/documents/__tests__/pagination-api.test.js index abc0d6955..75de7e63f 100644 --- a/app/addons/documents/__tests__/pagination-api.test.js +++ b/app/addons/documents/__tests__/pagination-api.test.js @@ -17,7 +17,7 @@ import { incrementSkipForPageNext, decrementSkipForPagePrevious, resetPagination -} from '../index-results/apis/pagination-api'; +} from '../index-results/apis/pagination'; import ActionTypes from '../index-results/actiontypes'; import FauxtonAPI from '../../../core/api'; diff --git a/app/addons/documents/__tests__/queryoptions-api.test.js b/app/addons/documents/__tests__/queryoptions-api.test.js index 6e4aefc6d..133feb252 100644 --- a/app/addons/documents/__tests__/queryoptions-api.test.js +++ b/app/addons/documents/__tests__/queryoptions-api.test.js @@ -10,7 +10,7 @@ // License for the specific language governing permissions and limitations under // the License. -import * as Api from '../index-results/apis/queryoptions-api'; +import * as Api from '../index-results/apis/queryoptions'; import ActionTypes from '../index-results/actiontypes'; describe('Docs Query Options API', () => { diff --git a/app/addons/documents/components/header-docs-right.js b/app/addons/documents/components/header-docs-right.js index 4a94ef3ee..4f03e507d 100644 --- a/app/addons/documents/components/header-docs-right.js +++ b/app/addons/documents/components/header-docs-right.js @@ -18,6 +18,19 @@ import Actions from './actions'; const { QueryOptionsController } = QueryOptions; +const getQueryOptionsComponent = (hideQueryOptions, isRedux, fetchUrl, ddocsOnly) => { + if (hideQueryOptions) { + return null; + } + + let queryOptionsComponent = ; + if (isRedux) { + queryOptionsComponent = ; + } + + return queryOptionsComponent; +}; + const RightAllDocsHeader = ({database, hideQueryOptions, isRedux, fetchUrl, ddocsOnly}) =>
@@ -26,8 +39,7 @@ const RightAllDocsHeader = ({database, hideQueryOptions, isRedux, fetchUrl, ddoc
- {!hideQueryOptions && isRedux ? : ''} - {!hideQueryOptions && !isRedux ? : ''} + {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 index 95ffacbea..e5c5a2a01 100644 --- a/app/addons/documents/components/results-toolbar.js +++ b/app/addons/documents/components/results-toolbar.js @@ -34,16 +34,28 @@ export class ResultsToolBar extends React.Component { 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 (
- {(isListDeletable && hasResults) || isLoading ? : null} - {hasResults || isLoading ? : null} + {bulkAction} + {bulkHeader}
Create Document diff --git a/app/addons/documents/header/header.js b/app/addons/documents/header/header.js index 543c83421..e263cdaa3 100644 --- a/app/addons/documents/header/header.js +++ b/app/addons/documents/header/header.js @@ -50,20 +50,20 @@ export default class BulkDocumentHeaderController extends React.Component { } render () { - //console.log(this.props); - const { changeLayout, selectedLayout } = 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; - const metadata = this.state.isMango ? null : - ; + let metadata = null; + if (!this.state.isMango) { + metadata = ; + } return (
diff --git a/app/addons/documents/index-results/apis/base-api.js b/app/addons/documents/index-results/apis/base.js similarity index 90% rename from app/addons/documents/index-results/apis/base-api.js rename to app/addons/documents/index-results/apis/base.js index 026a1643a..c0ce4c5d8 100644 --- a/app/addons/documents/index-results/apis/base-api.js +++ b/app/addons/documents/index-results/apis/base.js @@ -41,12 +41,17 @@ export const newSelectedDocs = (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); diff --git a/app/addons/documents/index-results/apis/fetch-api.js b/app/addons/documents/index-results/apis/fetch.js similarity index 99% rename from app/addons/documents/index-results/apis/fetch-api.js rename to app/addons/documents/index-results/apis/fetch.js index 8659a981b..1b580a390 100644 --- a/app/addons/documents/index-results/apis/fetch-api.js +++ b/app/addons/documents/index-results/apis/fetch.js @@ -15,7 +15,7 @@ import 'whatwg-fetch'; import FauxtonAPI from '../../../../core/api'; import queryString from 'query-string'; import SidebarActions from '../../sidebar/actions'; -import { nowLoading, newResultsAvailable, newSelectedDocs } from './base-api'; +import { nowLoading, newResultsAvailable, newSelectedDocs } from './base'; const maxDocLimit = 10000; diff --git a/app/addons/documents/index-results/apis/pagination-api.js b/app/addons/documents/index-results/apis/pagination.js similarity index 98% rename from app/addons/documents/index-results/apis/pagination-api.js rename to app/addons/documents/index-results/apis/pagination.js index 37069bc2b..366f9e726 100644 --- a/app/addons/documents/index-results/apis/pagination-api.js +++ b/app/addons/documents/index-results/apis/pagination.js @@ -11,7 +11,7 @@ // the License. import FauxtonAPI from '../../../../core/api'; -import { fetchAllDocs } from './fetch-api'; +import { fetchAllDocs } from './fetch'; import ActionTypes from '../actiontypes'; export const toggleShowAllColumns = () => { diff --git a/app/addons/documents/index-results/apis/queryoptions-api.js b/app/addons/documents/index-results/apis/queryoptions.js similarity index 98% rename from app/addons/documents/index-results/apis/queryoptions-api.js rename to app/addons/documents/index-results/apis/queryoptions.js index 392703a46..94e16205f 100644 --- a/app/addons/documents/index-results/apis/queryoptions-api.js +++ b/app/addons/documents/index-results/apis/queryoptions.js @@ -11,7 +11,7 @@ // the License. import ActionTypes from '../actiontypes'; -import { fetchAllDocs } from './fetch-api'; +import { fetchAllDocs } from './fetch'; const updateQueryOptions = (queryOptions) => { return { diff --git a/app/addons/documents/index-results/components/queryoptions/AdditionalParams.js b/app/addons/documents/index-results/components/queryoptions/AdditionalParams.js index 3be40d070..f3be5f0f9 100644 --- a/app/addons/documents/index-results/components/queryoptions/AdditionalParams.js +++ b/app/addons/documents/index-results/components/queryoptions/AdditionalParams.js @@ -68,7 +68,7 @@ export default class AdditionalParams extends React.Component {
diff --git a/app/addons/documents/index-results/components/results/IndexResults.js b/app/addons/documents/index-results/components/results/IndexResults.js index d601d4165..58f17eb7b 100644 --- a/app/addons/documents/index-results/components/results/IndexResults.js +++ b/app/addons/documents/index-results/components/results/IndexResults.js @@ -16,6 +16,9 @@ import ResultsScreen from './ResultsScreen'; export default class IndexResults extends React.Component { constructor (props) { super(props); + } + + componentDidMount () { const { fetchAllDocs, fetchParams, @@ -26,7 +29,7 @@ export default class IndexResults extends React.Component { fetchAllDocs(fetchParams, queryOptionsParams); } - componentWillUpdate(nextProps) { + componentWillUpdate (nextProps) { const { fetchAllDocs, fetchParams, diff --git a/app/addons/documents/index-results/containers/IndexResultsContainer.js b/app/addons/documents/index-results/containers/IndexResultsContainer.js index ef1112c54..d865401ff 100644 --- a/app/addons/documents/index-results/containers/IndexResultsContainer.js +++ b/app/addons/documents/index-results/containers/IndexResultsContainer.js @@ -12,15 +12,15 @@ import { connect } from 'react-redux'; import IndexResults from '../components/results/IndexResults'; -import { fetchAllDocs, bulkDeleteDocs } from '../apis/fetch-api'; -import { queryOptionsToggleIncludeDocs } from '../apis/queryoptions-api'; +import { fetchAllDocs, bulkDeleteDocs } from '../apis/fetch'; +import { queryOptionsToggleIncludeDocs } from '../apis/queryoptions'; import { selectDoc, changeLayout, bulkCheckOrUncheck, changeTableHeaderAttribute, resetState -} from '../apis/base-api'; +} from '../apis/base'; import { getDocs, getSelectedDocs, diff --git a/app/addons/documents/index-results/containers/PaginationContainer.js b/app/addons/documents/index-results/containers/PaginationContainer.js index f70970532..27a77573f 100644 --- a/app/addons/documents/index-results/containers/PaginationContainer.js +++ b/app/addons/documents/index-results/containers/PaginationContainer.js @@ -17,7 +17,7 @@ import { updatePerPageResults, paginateNext, paginatePrevious -} from '../apis/pagination-api'; +} from '../apis/pagination'; import { getDocs, getSelectedDocs, diff --git a/app/addons/documents/index-results/containers/QueryOptionsContainer.js b/app/addons/documents/index-results/containers/QueryOptionsContainer.js index 622cd9e82..eb08ae295 100644 --- a/app/addons/documents/index-results/containers/QueryOptionsContainer.js +++ b/app/addons/documents/index-results/containers/QueryOptionsContainer.js @@ -12,8 +12,8 @@ import { connect } from 'react-redux'; import QueryOptions from '../components/queryoptions/QueryOptions'; -import { changeLayout, resetState } from '../apis/base-api'; -import { resetPagination } from '../apis/pagination-api'; +import { changeLayout, resetState } from '../apis/base'; +import { resetPagination } from '../apis/pagination'; import { queryOptionsExecute, queryOptionsToggleReduce, @@ -28,7 +28,7 @@ import { queryOptionsToggleIncludeDocs, queryOptionsToggleVisibility, queryOptionsFilterOnlyDdocs -} from '../apis/queryoptions-api'; +} from '../apis/queryoptions'; import { getQueryOptionsPanel, getFetchParams, diff --git a/app/addons/documents/index-results/helpers/json-view.js b/app/addons/documents/index-results/helpers/json-view.js index f5a81d486..89a04dd77 100644 --- a/app/addons/documents/index-results/helpers/json-view.js +++ b/app/addons/documents/index-results/helpers/json-view.js @@ -13,6 +13,7 @@ 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, ' '), @@ -21,8 +22,8 @@ export const getJsonViewData = (docs, { databaseName, typeOfIndex }) => { header: doc.id, //|| doc.key.toString(), keylabel: 'id', //doc.isFromView() ? 'key' : 'id', url: doc.id ? getDocUrl('app', doc.id, databaseName) : null, - isDeletable: true, //TODO: determine logic here - isEditable: true //TODO: determine logic here + isDeletable: true, + isEditable: true }; }); diff --git a/app/addons/documents/index-results/helpers/shared-helpers.js b/app/addons/documents/index-results/helpers/shared-helpers.js index a67327f9a..86a158305 100644 --- a/app/addons/documents/index-results/helpers/shared-helpers.js +++ b/app/addons/documents/index-results/helpers/shared-helpers.js @@ -23,7 +23,7 @@ const getDocUrl = (context, id, databaseName) => { if (!safeId) { safeId = ''; } - const safeDatabaseName = app.utils.safeURLName(databaseName); + const safeDatabaseName = encodeURIComponent(databaseName); return FauxtonAPI.urls('document', context, safeDatabaseName, safeId, '?conflicts=true'); }; @@ -58,13 +58,11 @@ const isJSONDocBulkDeletable = (doc, docType) => { }; const hasBulkDeletableDoc = (docs, docType) => { - // use a for loop here as we can end it once we found the first id - for (let i = 0; i < docs.length; i++) { - if (isJSONDocBulkDeletable(docs[i], docType)) { - return true; - } - } - return false; + const doc = docs.find((doc) => { + return isJSONDocBulkDeletable(doc, docType); + }); + + return !!doc; }; export { From 871dbd0e24ae239029854b1837fe4abc750f621b Mon Sep 17 00:00:00 2001 From: Ryan Millay Date: Tue, 20 Jun 2017 08:45:49 -0400 Subject: [PATCH 23/25] nightwatch fixes --- app/addons/documents/header/header.js | 44 ++++++++++++++-------- app/addons/documents/queryoptions/queryoptions.js | 9 ++++- app/addons/documents/routes-index-editor.js | 1 + .../documents/tests/nightwatch/createsDocument.js | 2 +- .../tests/nightwatch/viewCreateBadView.js | 4 +- .../fauxton/tests/nightwatch/notificationCenter.js | 1 + 6 files changed, 42 insertions(+), 19 deletions(-) diff --git a/app/addons/documents/header/header.js b/app/addons/documents/header/header.js index e263cdaa3..046470975 100644 --- a/app/addons/documents/header/header.js +++ b/app/addons/documents/header/header.js @@ -50,13 +50,18 @@ export default class BulkDocumentHeaderController extends React.Component { } render () { - const { changeLayout, selectedLayout } = this.props; + 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 = null; - if (!this.state.isMango) { + let metadata, json, table; + if ((typeOfIndex && typeOfIndex === 'view') || !this.state.isMango) { metadata = ; + + json = ; + } + return (
- + {table} {metadata} - + {json}
); diff --git a/app/addons/documents/queryoptions/queryoptions.js b/app/addons/documents/queryoptions/queryoptions.js index fc75f70f3..34e3fe9a1 100644 --- a/app/addons/documents/queryoptions/queryoptions.js +++ b/app/addons/documents/queryoptions/queryoptions.js @@ -371,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/routes-index-editor.js b/app/addons/documents/routes-index-editor.js index 5fe95cdad..ea2fdf62e 100644 --- a/app/addons/documents/routes-index-editor.js +++ b/app/addons/documents/routes-index-editor.js @@ -80,6 +80,7 @@ const IndexEditorAndResults = BaseRoute.extend({ IndexResultsActions.newResultsList({ collection: this.indexedDocs, + typeOfIndex: 'view', bulkCollection: new Documents.BulkDeleteDocCollection([], { databaseId: this.database.safeID() }), }); diff --git a/app/addons/documents/tests/nightwatch/createsDocument.js b/app/addons/documents/tests/nightwatch/createsDocument.js index 995eb655b..7fae7889b 100644 --- a/app/addons/documents/tests/nightwatch/createsDocument.js +++ b/app/addons/documents/tests/nightwatch/createsDocument.js @@ -75,7 +75,7 @@ module.exports = { // confirm the header elements are showing up .waitForElementVisible('.faux-header__breadcrumbs', waitTime, true) - .waitForElementVisible('#api-navbar', waitTime, true) + .waitForElementVisible('.faux__jsondoc-wrapper', waitTime, true) .execute('\ var editor = ace.edit("doc-editor");\ 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/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) From 259d3e90f0466da7dc51a5804a1ce3438c1603b3 Mon Sep 17 00:00:00 2001 From: Ryan Millay Date: Tue, 20 Jun 2017 09:27:37 -0400 Subject: [PATCH 24/25] repairing table view bug for mango and views --- .../index-results/index-results.components.js | 23 ++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/app/addons/documents/index-results/index-results.components.js b/app/addons/documents/index-results/index-results.components.js index 538b777e7..26dd13593 100644 --- a/app/addons/documents/index-results/index-results.components.js +++ b/app/addons/documents/index-results/index-results.components.js @@ -173,6 +173,20 @@ const WrappedAutocomplete = ({selectedField, notSelectedFields, index, changeFie 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 (
@@ -180,14 +194,7 @@ const WrappedAutocomplete = ({selectedField, notSelectedFields, index, changeFie value={selectedField} options={options} clearable={false} - onChange={(el) => { - const newField = { - newSelectedRow: el.value, - index: index - }; - changeField(newField, selectedFields) || Actions.changeField(newField); - } - } /> + onChange={onChange} />
); From 2a82d1db3a50e0d9088ef6e356c083a4c335b840 Mon Sep 17 00:00:00 2001 From: Ryan Millay Date: Tue, 20 Jun 2017 12:37:14 -0400 Subject: [PATCH 25/25] adding localStorage for perPage --- .../index-results/helpers/shared-helpers.js | 14 +++++++++++++- app/addons/documents/index-results/reducers.js | 20 ++++++++++++-------- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/app/addons/documents/index-results/helpers/shared-helpers.js b/app/addons/documents/index-results/helpers/shared-helpers.js index 86a158305..b21142ddc 100644 --- a/app/addons/documents/index-results/helpers/shared-helpers.js +++ b/app/addons/documents/index-results/helpers/shared-helpers.js @@ -65,9 +65,21 @@ const hasBulkDeletableDoc = (docs, 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 + hasBulkDeletableDoc, + getDefaultPerPage }; diff --git a/app/addons/documents/index-results/reducers.js b/app/addons/documents/index-results/reducers.js index c0a209922..6c38011cc 100644 --- a/app/addons/documents/index-results/reducers.js +++ b/app/addons/documents/index-results/reducers.js @@ -10,11 +10,12 @@ // License for the specific language governing permissions and limitations under // the License. -import FauxtonAPI from '../../../core/api'; +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 @@ -29,13 +30,13 @@ const initialState = { textEmptyIndex: 'No Documents Found', typeOfIndex: 'view', fetchParams: { - limit: FauxtonAPI.constants.MISC.DEFAULT_PAGE_SIZE + 1, + 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: FauxtonAPI.constants.MISC.DEFAULT_PAGE_SIZE, + perPage: getDefaultPerPage(), canShowNext: false // flag indicating if we can show a next page }, queryOptionsPanel: { @@ -64,9 +65,12 @@ export default function resultsState (state = initialState, action) { case ActionTypes.INDEX_RESULTS_REDUX_RESET_STATE: return Object.assign({}, initialState, { fetchParams: { - limit: FauxtonAPI.constants.MISC.DEFAULT_PAGE_SIZE + 1, + limit: getDefaultPerPage() + 1, skip: 0 - } + }, + pagination: Object.assign({}, initialState.pagination, { + perPage: state.pagination.perPage + }) }); case ActionTypes.INDEX_RESULTS_REDUX_IS_LOADING: @@ -111,6 +115,7 @@ export default function resultsState (state = initialState, action) { }); 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 @@ -143,7 +148,6 @@ export default function resultsState (state = initialState, action) { } }; - // we don't want to muddy the waters with autogenerated mango docs export const removeGeneratedMangoDocs = (doc) => { return doc.language !== 'query'; @@ -294,9 +298,9 @@ 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 getFetchParams = state => state.fetchParams; export const getPageStart = state => state.pagination.pageStart; export const getPrioritizedEnabled = state => state.tableView.showAllFieldsTableView; -export const getPerPage = state => state.pagination.perPage; 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;