From 5df2983ef610be8ab34bbd7a6681b1dbbd986660 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Sun, 3 Dec 2017 16:50:50 -0800 Subject: [PATCH] Search improvements --- app/scenes/Search/Search.js | 46 ++++++++++++++++++++++++++++++++---- app/stores/DocumentsStore.js | 21 ++++++++++++---- app/types/index.js | 9 +++++++ package.json | 1 + server/api/documents.js | 8 +++++-- yarn.lock | 13 +++++++++- 6 files changed, 86 insertions(+), 12 deletions(-) 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"