Implements local search cache

Results no longer disappear when searching something previously searched
Navigating from a document back to results is now instant
Search item in left nav no longer unhighlights
This commit is contained in:
Tom Moor
2019-01-09 21:57:17 -08:00
parent e6fd7276fc
commit 4ba10fc5f7
3 changed files with 47 additions and 20 deletions

View File

@ -65,7 +65,12 @@ class MainSidebar extends React.Component<Props> {
exact={false} exact={false}
label="Home" label="Home"
/> />
<SidebarLink to="/search" icon={<SearchIcon />} label="Search" /> <SidebarLink
to="/search"
icon={<SearchIcon />}
label="Search"
exact={false}
/>
<SidebarLink <SidebarLink
to="/starred" to="/starred"
icon={<StarredIcon />} icon={<StarredIcon />}

View File

@ -10,7 +10,6 @@ import { withRouter } from 'react-router-dom';
import styled from 'styled-components'; import styled from 'styled-components';
import ArrowKeyNavigation from 'boundless-arrow-key-navigation'; import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
import type { SearchResult } from 'types';
import { DEFAULT_PAGINATION_LIMIT } from 'stores/BaseStore'; import { DEFAULT_PAGINATION_LIMIT } from 'stores/BaseStore';
import DocumentsStore from 'stores/DocumentsStore'; import DocumentsStore from 'stores/DocumentsStore';
import { searchUrl } from 'utils/routeHelpers'; import { searchUrl } from 'utils/routeHelpers';
@ -61,12 +60,11 @@ const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)`
class Search extends React.Component<Props> { class Search extends React.Component<Props> {
firstDocument: ?DocumentPreview; firstDocument: ?DocumentPreview;
@observable results: SearchResult[] = [];
@observable query: string = ''; @observable query: string = '';
@observable offset: number = 0; @observable offset: number = 0;
@observable allowLoadMore: boolean = true; @observable allowLoadMore: boolean = true;
@observable isFetching: boolean = false; @observable isFetching: boolean = false;
@observable pinToTop: boolean = false; @observable pinToTop: boolean = !!this.props.match.params.query;
componentDidMount() { componentDidMount() {
this.handleQueryChange(); this.handleQueryChange();
@ -103,12 +101,11 @@ class Search extends React.Component<Props> {
handleQueryChange = () => { handleQueryChange = () => {
const query = this.props.match.params.query; const query = this.props.match.params.query;
this.query = query ? query : ''; this.query = query ? query : '';
this.results = [];
this.offset = 0; this.offset = 0;
this.allowLoadMore = true; this.allowLoadMore = true;
// To prevent "no results" showing before debounce kicks in // To prevent "no results" showing before debounce kicks in
if (this.query) this.isFetching = true; this.isFetching = !!this.query;
this.fetchResultsDebounced(); this.fetchResultsDebounced();
}; };
@ -124,31 +121,27 @@ class Search extends React.Component<Props> {
@action @action
fetchResults = async () => { fetchResults = async () => {
if (this.query) {
this.isFetching = true; this.isFetching = true;
if (this.query) {
try { try {
const results = await this.props.documents.search(this.query, { const results = await this.props.documents.search(this.query, {
offset: this.offset, offset: this.offset,
limit: DEFAULT_PAGINATION_LIMIT, 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) { if (results.length === 0 || results.length < DEFAULT_PAGINATION_LIMIT) {
this.allowLoadMore = false; this.allowLoadMore = false;
} else { } else {
this.offset += DEFAULT_PAGINATION_LIMIT; this.offset += DEFAULT_PAGINATION_LIMIT;
} }
} catch (e) { } finally {
console.error('Something went wrong'); this.isFetching = false;
} }
} else { } else {
this.results = [];
this.pinToTop = false; this.pinToTop = false;
} }
this.isFetching = false;
}; };
fetchResultsDebounced = debounce(this.fetchResults, 350, { fetchResultsDebounced = debounce(this.fetchResults, 350, {
@ -173,8 +166,8 @@ class Search extends React.Component<Props> {
render() { render() {
const { documents, notFound } = this.props; const { documents, notFound } = this.props;
const showEmpty = const results = documents.searchResults(this.query);
!this.isFetching && this.query && this.results.length === 0; const showEmpty = !this.isFetching && this.query && results.length === 0;
return ( return (
<Container auto> <Container auto>
@ -198,7 +191,7 @@ class Search extends React.Component<Props> {
mode={ArrowKeyNavigation.mode.VERTICAL} mode={ArrowKeyNavigation.mode.VERTICAL}
defaultActiveChildIndex={0} defaultActiveChildIndex={0}
> >
{this.results.map((result, index) => { {results.map((result, index) => {
const document = documents.data.get(result.document.id); const document = documents.data.get(result.document.id);
if (!document) return null; if (!document) return null;

View File

@ -13,6 +13,7 @@ import type { FetchOptions, PaginationParams, SearchResult } from 'types';
export default class DocumentsStore extends BaseStore<Document> { export default class DocumentsStore extends BaseStore<Document> {
@observable recentlyViewedIds: string[] = []; @observable recentlyViewedIds: string[] = [];
@observable searchCache: Map<string, SearchResult[]> = new Map();
constructor(rootStore: RootStore) { constructor(rootStore: RootStore) {
super(rootStore, Document); super(rootStore, Document);
@ -86,6 +87,10 @@ export default class DocumentsStore extends BaseStore<Document> {
return naturalSort(this.publishedInCollection(collectionId), 'title'); return naturalSort(this.publishedInCollection(collectionId), 'title');
} }
searchResults(query: string): SearchResult[] {
return this.searchCache.get(query) || [];
}
@computed @computed
get starred(): Document[] { get starred(): Document[] {
return filter(this.orderedData, d => d.starred); return filter(this.orderedData, d => d.starred);
@ -202,15 +207,39 @@ export default class DocumentsStore extends BaseStore<Document> {
@action @action
search = async ( search = async (
query: string, query: string,
options: ?PaginationParams options: PaginationParams = {}
): Promise<SearchResult[]> => { ): Promise<SearchResult[]> => {
const res = await client.get('/documents.search', { const res = await client.get('/documents.search', {
...options, ...options,
query, query,
}); });
invariant(res && res.data, 'Search API response should be available'); invariant(res && res.data, 'Search response should be available');
const { data } = res; const { data } = res;
// add the document to the store
data.forEach(result => this.add(result.document)); 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; return data;
}; };