diff --git a/app/scenes/Search/Search.js b/app/scenes/Search/Search.js
index 9d63fde7b..8766e3789 100644
--- a/app/scenes/Search/Search.js
+++ b/app/scenes/Search/Search.js
@@ -2,10 +2,13 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import keydown from 'react-keydown';
+import Waypoint from 'react-waypoint';
import { observable, action } from 'mobx';
import { observer, inject } from 'mobx-react';
import _ from 'lodash';
-import DocumentsStore from 'stores/DocumentsStore';
+import DocumentsStore, {
+ DEFAULT_PAGINATION_LIMIT,
+} from 'stores/DocumentsStore';
import { withRouter } from 'react-router-dom';
import { searchUrl } from 'utils/routeHelpers';
@@ -44,6 +47,7 @@ const ResultsWrapper = styled(Flex)`
`;
const ResultList = styled(Flex)`
+ margin-bottom: 150px;
opacity: ${props => (props.visible ? '1' : '0')};
transition: all 400ms cubic-bezier(0.65, 0.05, 0.36, 1);
`;
@@ -61,6 +65,8 @@ class Search extends Component {
@observable resultIds: string[] = []; // Document IDs
@observable query: string = '';
+ @observable offset: number = 0;
+ @observable allowLoadMore: boolean = true;
@observable isFetching = false;
componentDidMount() {
@@ -98,10 +104,27 @@ class Search extends Component {
handleQueryChange = () => {
const query = this.props.match.params.query;
this.query = query ? decodeURIComponent(query) : '';
+ this.allowLoadMore = true;
+
+ // To prevent "no results" showing before debounce kicks in
+ if (this.query) this.isFetching = true;
+
this.fetchResultsDebounced();
};
- fetchResultsDebounced = _.debounce(this.fetchResults, 250);
+ fetchResultsDebounced = _.debounce(this.fetchResults, 350, {
+ leading: false,
+ trailing: true,
+ });
+
+ @action
+ loadMoreResults = async () => {
+ // Don't paginate if there aren't more results or we're in the middle of fetching
+ if (!this.allowLoadMore || this.isFetching) return;
+
+ // Fetch more results
+ await this.fetchResults();
+ };
@action
fetchResults = async () => {
@@ -109,7 +132,19 @@ class Search extends Component {
if (this.query) {
try {
- this.resultIds = await this.props.documents.search(this.query);
+ const newResults = await this.props.documents.search(this.query, {
+ offset: this.offset,
+ limit: DEFAULT_PAGINATION_LIMIT,
+ });
+ this.resultIds = this.resultIds.concat(newResults);
+ if (
+ newResults.length === 0 ||
+ newResults.length < DEFAULT_PAGINATION_LIMIT
+ ) {
+ this.allowLoadMore = false;
+ } else {
+ this.offset += DEFAULT_PAGINATION_LIMIT;
+ }
} catch (e) {
console.error('Something went wrong');
}
@@ -157,7 +192,7 @@ class Search extends Component {
value={this.query}
/>
{showEmpty && No matching documents.}
-
+
+ {this.allowLoadMore && (
+
+ )}
diff --git a/app/stores/DocumentsStore.js b/app/stores/DocumentsStore.js
index 8bdc77eac..f0191904d 100644
--- a/app/stores/DocumentsStore.js
+++ b/app/stores/DocumentsStore.js
@@ -17,8 +17,10 @@ import Document from 'models/Document';
import ErrorsStore from 'stores/ErrorsStore';
import CacheStore from 'stores/CacheStore';
import UiStore from 'stores/UiStore';
+import type { PaginationParams } from 'types';
const DOCUMENTS_CACHE_KEY = 'DOCUMENTS_CACHE_KEY';
+export const DEFAULT_PAGINATION_LIMIT = 25;
type Options = {
cache: CacheStore,
@@ -77,7 +79,10 @@ class DocumentsStore extends BaseStore {
/* Actions */
@action
- fetchAll = async (request: string = 'list', options: ?Object): Promise<*> => {
+ fetchAll = async (
+ request: string = 'list',
+ options: ?PaginationParams
+ ): Promise<*> => {
this.isFetching = true;
try {
@@ -99,12 +104,12 @@ class DocumentsStore extends BaseStore {
};
@action
- fetchRecentlyModified = async (options: ?Object): Promise<*> => {
+ fetchRecentlyModified = async (options: ?PaginationParams): Promise<*> => {
return await this.fetchAll('list', options);
};
@action
- fetchRecentlyViewed = async (options: ?Object): Promise<*> => {
+ fetchRecentlyViewed = async (options: ?PaginationParams): Promise<*> => {
const data = await this.fetchAll('viewed', options);
runInAction('DocumentsStore#fetchRecentlyViewed', () => {
@@ -119,8 +124,14 @@ class DocumentsStore extends BaseStore {
};
@action
- search = async (query: string): Promise<*> => {
- const res = await client.get('/documents.search', { query });
+ search = async (
+ query: string,
+ options: PaginationParams
+ ): Promise => {
+ const res = await client.get('/documents.search', {
+ ...options,
+ query,
+ });
invariant(res && res.data, 'res or res.data missing');
const { data } = res;
data.forEach(documentData => this.add(new Document(documentData)));
diff --git a/app/types/index.js b/app/types/index.js
index 63d861398..a03ef21eb 100644
--- a/app/types/index.js
+++ b/app/types/index.js
@@ -39,12 +39,21 @@ export type Document = {
views: number,
};
+// Pagination response in an API call
export type Pagination = {
limit: number,
nextPath: string,
offset: number,
};
+// Pagination request params
+export type PaginationParams = {
+ limit?: number,
+ offset?: number,
+ sort?: string,
+ direction?: 'ASC' | 'DESC',
+};
+
export type ApiKey = {
id: string,
name: ?string,
diff --git a/package.json b/package.json
index d80f7a16b..6d730d68f 100644
--- a/package.json
+++ b/package.json
@@ -153,6 +153,7 @@
"react-modal": "^3.1.2",
"react-portal": "^4.0.0",
"react-router-dom": "^4.2.0",
+ "react-waypoint": "^7.3.1",
"redis": "^2.6.2",
"redis-lock": "^0.1.0",
"rimraf": "^2.5.4",
diff --git a/server/api/documents.js b/server/api/documents.js
index 2aeae2787..1c29fead3 100644
--- a/server/api/documents.js
+++ b/server/api/documents.js
@@ -142,13 +142,17 @@ router.post('documents.revisions', auth(), pagination(), async ctx => {
};
});
-router.post('documents.search', auth(), async ctx => {
+router.post('documents.search', auth(), pagination(), async ctx => {
const { query } = ctx.body;
+ const { offset, limit } = ctx.state.pagination;
ctx.assertPresent(query, 'query is required');
const user = await ctx.state.user;
- const documents = await Document.searchForUser(user, query);
+ const documents = await Document.searchForUser(user, query, {
+ offset,
+ limit,
+ });
const data = await Promise.all(
documents.map(async document => await presentDocument(ctx, document))
diff --git a/yarn.lock b/yarn.lock
index 2d541e7d0..6c993f020 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1825,6 +1825,10 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
+consolidated-events@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/consolidated-events/-/consolidated-events-1.1.1.tgz#25395465b35e531395418b7bbecb5ecaf198d179"
+
constant-case@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-2.0.0.tgz#4175764d389d3fa9c8ecd29186ed6005243b6a46"
@@ -7207,7 +7211,7 @@ prop-types@>=15.5.6, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8
fbjs "^0.8.9"
loose-envify "^1.3.1"
-prop-types@^15.5.7, prop-types@^15.6.0:
+prop-types@^15.0.0, prop-types@^15.5.7, prop-types@^15.6.0:
version "15.6.0"
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856"
dependencies:
@@ -7498,6 +7502,13 @@ react-transform-hmr@^1.0.3:
global "^4.3.0"
react-proxy "^1.1.7"
+react-waypoint@^7.3.1:
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/react-waypoint/-/react-waypoint-7.3.1.tgz#abb165d9b6c9590f8d82ceafbe61c2c887262a37"
+ dependencies:
+ consolidated-events "^1.1.0"
+ prop-types "^15.0.0"
+
react@^15.5.4:
version "15.6.2"
resolved "https://registry.npmjs.org/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72"