diff --git a/app/components/Sidebar/Main.js b/app/components/Sidebar/Main.js index f1dce812..ad72711e 100644 --- a/app/components/Sidebar/Main.js +++ b/app/components/Sidebar/Main.js @@ -65,7 +65,12 @@ class MainSidebar extends React.Component { exact={false} label="Home" /> - } label="Search" /> + } + label="Search" + exact={false} + /> } diff --git a/app/scenes/Search/Search.js b/app/scenes/Search/Search.js index a9abad3a..3c9317cf 100644 --- a/app/scenes/Search/Search.js +++ b/app/scenes/Search/Search.js @@ -10,7 +10,6 @@ import { withRouter } from 'react-router-dom'; import styled from 'styled-components'; import ArrowKeyNavigation from 'boundless-arrow-key-navigation'; -import type { SearchResult } from 'types'; import { DEFAULT_PAGINATION_LIMIT } from 'stores/BaseStore'; import DocumentsStore from 'stores/DocumentsStore'; import { searchUrl } from 'utils/routeHelpers'; @@ -61,12 +60,11 @@ const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)` class Search extends React.Component { firstDocument: ?DocumentPreview; - @observable results: SearchResult[] = []; @observable query: string = ''; @observable offset: number = 0; @observable allowLoadMore: boolean = true; @observable isFetching: boolean = false; - @observable pinToTop: boolean = false; + @observable pinToTop: boolean = !!this.props.match.params.query; componentDidMount() { this.handleQueryChange(); @@ -103,12 +101,11 @@ class Search extends React.Component { handleQueryChange = () => { const query = this.props.match.params.query; this.query = query ? query : ''; - this.results = []; this.offset = 0; this.allowLoadMore = true; // To prevent "no results" showing before debounce kicks in - if (this.query) this.isFetching = true; + this.isFetching = !!this.query; this.fetchResultsDebounced(); }; @@ -124,31 +121,27 @@ class Search extends React.Component { @action fetchResults = async () => { - this.isFetching = true; - if (this.query) { + this.isFetching = true; + try { const results = await this.props.documents.search(this.query, { offset: this.offset, limit: DEFAULT_PAGINATION_LIMIT, }); - this.results = this.results.concat(results); - if (this.results.length > 0) this.pinToTop = true; + if (results.length > 0) this.pinToTop = true; if (results.length === 0 || results.length < DEFAULT_PAGINATION_LIMIT) { this.allowLoadMore = false; } else { this.offset += DEFAULT_PAGINATION_LIMIT; } - } catch (e) { - console.error('Something went wrong'); + } finally { + this.isFetching = false; } } else { - this.results = []; this.pinToTop = false; } - - this.isFetching = false; }; fetchResultsDebounced = debounce(this.fetchResults, 350, { @@ -173,8 +166,8 @@ class Search extends React.Component { render() { const { documents, notFound } = this.props; - const showEmpty = - !this.isFetching && this.query && this.results.length === 0; + const results = documents.searchResults(this.query); + const showEmpty = !this.isFetching && this.query && results.length === 0; return ( @@ -198,7 +191,7 @@ class Search extends React.Component { mode={ArrowKeyNavigation.mode.VERTICAL} defaultActiveChildIndex={0} > - {this.results.map((result, index) => { + {results.map((result, index) => { const document = documents.data.get(result.document.id); if (!document) return null; diff --git a/app/stores/DocumentsStore.js b/app/stores/DocumentsStore.js index cecc5e23..dd74d4ff 100644 --- a/app/stores/DocumentsStore.js +++ b/app/stores/DocumentsStore.js @@ -13,6 +13,7 @@ import type { FetchOptions, PaginationParams, SearchResult } from 'types'; export default class DocumentsStore extends BaseStore { @observable recentlyViewedIds: string[] = []; + @observable searchCache: Map = new Map(); constructor(rootStore: RootStore) { super(rootStore, Document); @@ -86,6 +87,10 @@ export default class DocumentsStore extends BaseStore { return naturalSort(this.publishedInCollection(collectionId), 'title'); } + searchResults(query: string): SearchResult[] { + return this.searchCache.get(query) || []; + } + @computed get starred(): Document[] { return filter(this.orderedData, d => d.starred); @@ -202,15 +207,39 @@ export default class DocumentsStore extends BaseStore { @action search = async ( query: string, - options: ?PaginationParams + options: PaginationParams = {} ): Promise => { const res = await client.get('/documents.search', { ...options, query, }); - invariant(res && res.data, 'Search API response should be available'); + invariant(res && res.data, 'Search response should be available'); const { data } = res; + + // add the document to the store data.forEach(result => this.add(result.document)); + + // store a reference to the document model in the search cache instead + // of the original result from the API. + const results: SearchResult[] = compact( + data.map(result => { + const document = this.data.get(result.document.id); + if (!document) return null; + + return { + ranking: result.ranking, + context: result.context, + document, + }; + }) + ); + + let existing = this.searchCache.get(query) || []; + + // splice modifies any existing results, taking into account pagination + existing.splice(options.offset || 0, options.limit || 0, ...results); + + this.searchCache.set(query, existing); return data; };