frontend/src/js/components/Search/Search.js (239 lines of code) (raw):
import React from 'react';
import PropTypes from 'prop-types';
import _isEqual from 'lodash/fp/isEqual';
import SearchBox from './SearchBox';
import SearchResults from '../SearchResults/SearchResults';
import SearchStatus from './SearchStatus';
import PageNavigator from '../UtilComponents/PageNavigator';
import {Checkbox} from '../UtilComponents/Checkbox';
import {KeyboardShortcut} from '../UtilComponents/KeyboardShortcut';
import Select from 'react-select';
import {searchResultsPropType} from '../../types/SearchResults';
import _get from 'lodash/get';
import _debounce from 'lodash/debounce';
import {suggestedFieldsPropType} from '../../types/SuggestedFields';
import {keyboardShortcuts} from '../../util/keyboardShortcuts';
import SearchVisualizations from './SearchVisualizations';
import {calculateSearchTitle} from '../UtilComponents/documentTitle';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {updateSearchText} from '../../actions/urlParams/updateSearchQuery';
import {performSearch} from '../../actions/search/performSearch';
import {clearSearch} from '../../actions/search/clearSearch';
import {updateSearchQueryFilters} from '../../actions/urlParams/updateSearchQuery';
import {updatePage} from '../../actions/urlParams/updateSearchQuery';
import {updatePageSize} from '../../actions/urlParams/updateSearchQuery';
import {updateSortBy} from '../../actions/urlParams/updateSearchQuery';
import {getSuggestedFields} from '../../actions/search/getSuggestedFields';
import {resetResource} from '../../actions/resources/getResource';
import {updatePreference} from '../../actions/preferences';
class Search extends React.Component {
static propTypes = {
urlParams: PropTypes.shape({
q: PropTypes.string,
page: PropTypes.any,
pageSize: PropTypes.any,
sortBy: PropTypes.string,
filters: PropTypes.any
}),
lastUri: PropTypes.string,
updateSearchText: PropTypes.func.isRequired,
updatePage: PropTypes.func.isRequired,
updatePageSize: PropTypes.func.isRequired,
updateSortBy: PropTypes.func.isRequired,
performSearch: PropTypes.func.isRequired,
clearSearch: PropTypes.func.isRequired,
updateSearchQueryFilters: PropTypes.func.isRequired,
resetResource: PropTypes.func.isRequired,
getSuggestedFields: PropTypes.func.isRequired,
updatePreference: PropTypes.func.isRequired,
preferences: PropTypes.object,
search: PropTypes.shape({
isSearchInProgress: PropTypes.bool.isRequired,
currentQuery: PropTypes.object,
currentResults: searchResultsPropType,
suggestedFields: PropTypes.arrayOf(suggestedFieldsPropType),
searchFailed: PropTypes.bool
}).isRequired,
}
state = {
visibleText: ''
}
selectSearchBox = (e) => {
e.preventDefault();
this.searchBox.focus();
}
clearSearch = (e) => {
e.preventDefault();
this.props.updateVisibleText('');
this.props.clearSearch();
this.props.updateSearchQueryFilters({});
this.setState({visibleText: ''});
this.searchBox.select();
}
debouncedUpdate = _debounce((text) => {
if (text !== this.props.urlParams.q) {
this.props.updatePage('1');
}
this.props.updateSearchText(text);
this.triggerSearch(this.props.urlParams);
}, 500)
updateVisibleText = (text) => {
this.setState({
visibleText: text
});
}
triggerSearch(query) {
if (query.q) {
this.props.resetResource();
this.props.performSearch(query);
}
}
updateSearchText = () => {
this.debouncedUpdate(this.state.visibleText);
}
componentDidMount() {
this.props.getSuggestedFields();
const search = this.props.urlParams.q || '';
this.setState({
visibleText: search
});
const currentQuery = _get(this.props.search, 'currentQuery.q');
// If we're mounting with a different query or without any results, trigger a search
if (search !== currentQuery || !_get(this.props.search, 'currentResults.results.length')) {
this.triggerSearch(this.props.urlParams);
}
document.title = calculateSearchTitle(this.props.search.currentQuery);
}
UNSAFE_componentWillReceiveProps(props) {
const before = {
filters: props.urlParams.filters,
page: props.urlParams.page,
pageSize: props.urlParams.pageSize,
sortBy: props.urlParams.sortBy
};
const after = {
filters: this.props.urlParams.filters,
page: this.props.urlParams.page,
pageSize: this.props.urlParams.pageSize,
sortBy: this.props.urlParams.sortBy
};
if (!_isEqual(before, after)){
this.triggerSearch(props.urlParams);
}
}
componentDidUpdate() {
document.title = calculateSearchTitle(this.props.search.currentQuery);
}
componentWillUnmount() {
document.title = "Giant";
}
pageSelectCallback = (page) => {
this.props.updatePage(page.toString());
}
toggleCompactSearchResults = () => {
this.props.updatePreference('compactSearchResults', !this.props.preferences.compactSearchResults);
}
toggleHistogram = () => {
this.props.updatePreference('searchResultHistogram', !this.props.preferences.searchResultHistogram);
}
renderControls() {
// TODO replace with user preferences for page size
const pageSize = this.props.urlParams.pageSize || '100';
// TODO replace with user preferences for sort order
const sortBy = this.props.urlParams.sortBy || 'relevance';
return (
<div className='search__controls'>
<Checkbox
selected={this.props.preferences.searchResultHistogram}
onClick={this.toggleHistogram}>
Show Date Created Graph
</Checkbox>
<Checkbox
selected={this.props.preferences.compactSearchResults}
onClick={this.toggleCompactSearchResults}>
Compact
</Checkbox>
<Select
className='search__control'
value={sortBy}
options={[
{ value: 'relevance', label: 'Sort by relevance'},
{ value: 'size-asc', label: 'Sort by size (smallest first)'},
{ value: 'size-desc', label: 'Sort by size (largest first)'},
{ value: 'date-created-asc', label: 'Sort by date created (oldest first)'},
{ value: 'date-created-desc', label: 'Sort by date created (newest first)'}
]}
onChange={v => {
this.props.updatePage('1');
this.props.updateSortBy(v.value);
}}
clearable={false}
/>
<Select
className='search__control'
value={pageSize}
options={[
{ value: '25', label: '25 results per page'},
{ value: '50', label: '50 results per page'},
{ value: '100', label: '100 results per page'}
]}
onChange={v => {
this.props.updatePage('1');
this.props.updatePageSize(v.value);
}}
clearable={false}
/>
</div>
);
}
renderPageNav() {
if (this.props.search.currentResults) {
if (this.props.search.currentResults.hits > this.props.search.currentResults.pageSize) {
return (
<PageNavigator
pageSelectCallback={this.pageSelectCallback}
currentPage={this.props.search.currentResults.page}
pageSpan={5}
lastPage={Math.ceil(this.props.search.currentResults.hits / this.props.search.currentResults.pageSize)}/>
);
}
}
return false;
}
render() {
return(
<div className='app__main-content search'>
<KeyboardShortcut shortcut={keyboardShortcuts.focusSearchBox} func={this.selectSearchBox} />
<SearchBox
ref={input => this.searchBox = input}
updateVisibleText={this.updateVisibleText}
resetQuery={this.clearSearch}
addQuery={this.addQuery}
q={this.state.visibleText}
isSearchInProgress={this.props.search.isSearchInProgress}
suggestedFields={this.props.search.suggestedFields}
updateSearchText={this.updateSearchText}
/>
<div className='search__underbar'>
<SearchStatus
results={this.props.search.currentResults}
currentQuery={this.props.search.currentQuery}
searchFailed={this.props.search.searchFailed}
/>
<div>
{this.renderControls()}
</div>
</div>
{this.props.preferences.searchResultHistogram ?
<SearchVisualizations
q={this.props.urlParams.q}
results={this.props.search.currentResults}
updateSearchText={this.updateSearchText}
/>
: false}
<SearchResults
compact={!!this.props.preferences.compactSearchResults}
lastUri={this.props.lastUri}
isSearchInProgress={this.props.search.isSearchInProgress}
searchResults={this.props.search.currentResults}
/>
{this.renderPageNav()}
</div>
);
}
}
function mapStateToProps(state) {
return {
urlParams: state.urlParams,
search: state.search,
lastUri: state.resource ? state.resource.uri : undefined,
preferences: state.app.preferences
};
}
function mapDispatchToProps(dispatch) {
return {
getSuggestedFields: bindActionCreators(getSuggestedFields, dispatch),
updateSearchText: bindActionCreators(updateSearchText, dispatch),
updatePage: bindActionCreators(updatePage, dispatch),
updatePageSize: bindActionCreators(updatePageSize, dispatch),
updateSortBy: bindActionCreators(updateSortBy, dispatch),
performSearch: bindActionCreators(performSearch, dispatch),
clearSearch: bindActionCreators(clearSearch, dispatch),
updateSearchQueryFilters: bindActionCreators(updateSearchQueryFilters, dispatch),
resetResource: bindActionCreators(resetResource, dispatch),
updatePreference: bindActionCreators(updatePreference, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Search);