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:
@ -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 />}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user