diff --git a/lib/facet-helpers.js b/lib/facet-helpers.js index abc01c0..b5ba21a 100644 --- a/lib/facet-helpers.js +++ b/lib/facet-helpers.js @@ -12,7 +12,9 @@ export function createFacetNameMap (arr, field) { return out } + export function getBreadcrumbList (pool, selected) { + const hasOwnProperty = Object.prototype.hasOwnProperty let out = [] if (!selected) @@ -23,31 +25,41 @@ export function getBreadcrumbList (pool, selected) { if (!selKeys.length) return out + // loop through the pool of potential facets + // (these are the responses returned w/ a search query) for (let p = 0; p < pool.length; p++) { - if (!selKeys.length) - break - - let group = pool[p] + const group = pool[p] + // loop through the keys of the selected facets + // and find our matching group (which should exist + // because it's part of our search) for (let s = 0; s < selKeys.length; s++) { - let name = selKeys[s] + const name = selKeys[s] if (group.name === name) { + + // we're going to append our collection (`out`) with data + // for each of the selected facets. this will allow us to + // display a breadcrumb like 'Facet Group > Facet Value' + // using label values (for cleaner values) + retain the + // values used by the back-end out = out.concat(selected[name].map(sk => { + const selVal = hasOwnProperty.call(sk, 'value') ? sk.value : sk + const selLabel = hasOwnProperty.call(sk, 'label') ? sk.label : sk return { group: { - name: pool[p].name, - label: pool[p].label + name: group.name, + label: group.label }, facet: { - value: sk.value, - label: sk.label, + value: selVal, + label: selLabel, } } })) - selKeys.splice(s, 1) - break + // selKeys.splice(s, 1) + // break } } } diff --git a/src/actions/search.js b/src/actions/search.js index 933dcf7..f9856b2 100644 --- a/src/actions/search.js +++ b/src/actions/search.js @@ -15,14 +15,6 @@ import { SEARCHING, } from '../constants' -const REQUIRED_OPTS = { - search_field: 'search', -} - -const DEFAULT_OPTS = { - per_page: 10, -} - const hasOwnProperty = Object.prototype.hasOwnProperty function conductSearch (dispatch, query, facets, options, queryString) { @@ -57,7 +49,7 @@ function conductSearch (dispatch, query, facets, options, queryString) { export const searchCatalog = (query, facets, opts) => dispatch => { // save ourselves the hassle of keeping track of these defaults - const options = assign({}, REQUIRED_OPTS, opts) + const options = assign({}, opts) if (!facets) facets = {} @@ -96,7 +88,7 @@ export const setSearchOption = (field, value) => (dispatch, getState) => { const query = search.query || '' const facets = assign({}, search.facets) - const options = assign({}, DEFAULT_OPTS, REQUIRED_OPTS, search.options) + const options = assign({}, search.options) // we'll pass null to remove the option if (value === null) { @@ -114,11 +106,11 @@ export const toggleSearchFacet = (field, facet, checked) => (dispatch, getState) // recycling the previous search info const query = search.query || '' - const options = assign({}, DEFAULT_OPTS, REQUIRED_OPTS, search.options) + const options = assign({}, search.options) const facets = assign({}, search.facets) let dirty = false - let idx + let idx = -1 if (facets[field]) { idx = findIndex(facets[field], f => { @@ -129,8 +121,6 @@ export const toggleSearchFacet = (field, facet, checked) => (dispatch, getState) }) } - else idx = -1 - // add to selected-facets if (checked) { if (idx === -1) { diff --git a/src/components/catalog/SearchBreadcrumbTrail.jsx b/src/components/catalog/SearchBreadcrumbTrail.jsx index 4fb2a09..7db9026 100644 --- a/src/components/catalog/SearchBreadcrumbTrail.jsx +++ b/src/components/catalog/SearchBreadcrumbTrail.jsx @@ -6,24 +6,23 @@ const T = React.PropTypes const SearchBreadcrumbTrail = React.createClass({ propTypes: { onRemoveBreadcrumb: T.func.isRequired, - - facets: T.object, + + breadcrumbs: T.array, query: T.string, }, - renderGroupBreadcrumbs: function (key) { - const group = this.props.facets[key] - - return group.map((facet, index) => { - const props = { - key: key + index + facet.value, - group: key, - value: facet.label, - onRemove: this.props.onRemoveBreadcrumb.bind(null, key, facet), - } - - return React.createElement(SearchBreadcrumb, props) - }) + renderGroupBreadcrumbs: function (breadcrumb, index) { + const props = { + key: `bc${index}`, + group: breadcrumb.group.label, + value: breadcrumb.facet.label, + onRemove: this.props.onRemoveBreadcrumb.bind(null, + breadcrumb.group.name, + breadcrumb.facet, + ), + } + + return }, renderQuery: function () { @@ -40,19 +39,13 @@ const SearchBreadcrumbTrail = React.createClass({ }, render: function () { - if (!this.props.facets) - return null - - const keys = Object.keys(this.props.facets) - - if (!keys.length) - return null + const bc = this.props.breadcrumbs return (
{this.renderQuery()} - {keys.map(this.renderGroupBreadcrumbs)} + {!!bc.length && bc.map(this.renderGroupBreadcrumbs)}
) } diff --git a/src/pages/SearchResults.jsx b/src/pages/SearchResults.jsx index 7ea7cd4..db58126 100644 --- a/src/pages/SearchResults.jsx +++ b/src/pages/SearchResults.jsx @@ -8,7 +8,6 @@ import FacetList from '../components/catalog/FacetList.jsx' import FacetListWithViewMore from '../components/catalog/FacetListWithViewMore.jsx' import FacetRangeLimitDate from '../components/catalog/FacetRangeLimitDate.jsx' -import SearchBreadcrumb from '../components/catalog/SearchBreadcrumb.jsx' import SearchBreadcrumbTrail from '../components/catalog/SearchBreadcrumbTrail.jsx' import SearchResultsHeader from '../components/catalog/SearchResultsHeader.jsx' @@ -19,11 +18,43 @@ import ResultsGalleryItem from '../components/catalog/ResultsGalleryItem.jsx' import { getBreadcrumbList } from '../../lib/facet-helpers' const SearchResults = React.createClass({ + // TODO: clean this up a bit? this is a hold-over from when this component + // was handling the starting search form as well as the results componentWillMount: function () { const qs = this.props.location.search - if (qs) - this.props.searchCatalogByQueryString(qs).then(this.handleSearchResponse) + if (qs) { + this.props.searchCatalogByQueryString(qs) + } + }, + + componentWillReceiveProps: function (nextProps) { + // compare the queryString in the browser to the previously-searched + // one. if it differs, submit the new search. this allows the search + // to be updated when the user uses the back/forward buttons in the + // browser in addition to selecting facets/options + const queryString = window.location.search + const previousQueryString = this.props.search.queryString + + // checking that `previousQueryString` is defined prevents this + // from being run on mount (when `queryString` will always not + // equal `undefined`). + if (previousQueryString && queryString !== previousQueryString) + return this.props.searchCatalogByQueryString(queryString) + + // we're using `props.search.timestamp` as a unique identifier + // to signify that the new search results being passed as props + // differ than the ones previous. this could also be done with + // a shallow compare of the `search` object but since what would + // be changing is at a deeper level (the `search.facets` and + // `search.options` objects in particular), this could be costly + const timestamp = this.props.search.timestamp + const next = nextProps.search.timestamp + + if (!next || timestamp === next) + return + + this.handleSearchResponse(nextProps.search) }, getInitialState: function () { @@ -37,7 +68,6 @@ const SearchResults = React.createClass({ const options = this.props.search.options this.props.searchCatalog(query, {}, this.props.search.options) - .then(this.handleSearchResponse) }, determineResultsComponent: function (which) { @@ -66,28 +96,17 @@ const SearchResults = React.createClass({ } }, - getFacetGroupInfo: function (pool, name) { - for (let i = 0; i < pool.length; i++) - if (pool[i].name === name) - return { - name: pool[i].name, - label: pool[i].label - } - - return null - }, - handleNextPage: function () { const pages = this.state.pages if (!pages.next_page) return - this.props.setSearchOption('page', pages.next_page).then(this.handleSearchResponse) + this.props.setSearchOption('page', pages.next_page) }, handlePerPageChange: function (val) { - this.props.setSearchOption('per_page', val).then(this.handleSearchResponse) + this.props.setSearchOption('per_page', val) }, handlePreviousPage: function () { @@ -98,24 +117,25 @@ const SearchResults = React.createClass({ const prev = pages.prev_page === 1 ? null : pages.prev_page - this.props.setSearchOption('page', prev).then(this.handleSearchResponse) + this.props.setSearchOption('page', prev) }, handleSearchResponse: function (res) { if (!res) { - console.log('no res!') + console.warn('no data passed to `SearchResults#handleSearchResponse') return } - const facets = res.response.facets + const facets = res.results.facets const breadcrumbs = getBreadcrumbList(facets, this.props.search.facets) this.setState({ - results: res.response.docs, - options: this.props.search.options, - pages: res.response.pages, - facets, breadcrumbs, + facets, + options: this.props.search.options, + pages: res.results.pages, + results: res.results.docs, + timestamp: res.timestamp, }) }, @@ -123,7 +143,6 @@ const SearchResults = React.createClass({ const { facets, options } = this.props.search this.props.searchCatalog(query, facets, options) - .then(this.handleSearchResponse) }, maybeRenderLoadingModal: function () { @@ -161,50 +180,26 @@ const SearchResults = React.createClass({ _onToggleFacet: function (which, key, facet) { return this.props.toggleSearchFacet(key, facet, which) - .then(this.handleSearchResponse) - .catch(console.warn) }, renderBreadcrumbs: function () { - const bc = this.state.breadcrumbs - - if (!bc) - return + if (!this.state.breadcrumbs) + return null - const query = this.props.search.query - - const querybc = !query ? null : ( - - ) - - const crumbs = bc.map((crumb, index) => { - const {label, name} = crumb.group - const facet = crumb.facet + const onRemoveBreadcrumb = (key, value) => { + if (key === 'q') + return this.handleSubmitSearchQuery('') - const props = { - key: 'bc' + index + facet.value, - group: label, - value: facet.label, - onRemove: this.onRemoveFacet.bind(null, name, facet) - } - - return React.createElement(SearchBreadcrumb, props) - }) + return this.onRemoveFacet(key, value) + } - const style = { - marginBottom: '10px', - marginTop: '-5px', + const props = { + breadcrumbs: this.state.breadcrumbs, + onRemoveBreadcrumb, + query: this.props.search.query, } - return ( -
- {[].concat(querybc, crumbs)} -
- ) + return React.createElement(SearchBreadcrumbTrail, props) }, renderFacetSidebar: function () { @@ -284,13 +279,15 @@ const SearchResults = React.createClass({ }, renderResults: function () { - if (!this.state.results) + const results = this.state.results + + if (typeof results === 'undefined') return const which = this.state.resultsView const props = { - data: this.state.results, + data: results, displayComponent: this.determineResultsComponent(which), offset: this.state.pages.offset_value, containerProps: { @@ -311,10 +308,6 @@ const SearchResults = React.createClass({ } const styles = { - container: { - // backgroundColor: '#fafafa', - }, - sidebar: { container: { display: 'inline-block', @@ -333,7 +326,7 @@ const SearchResults = React.createClass({ } return ( -
+
{this.maybeRenderLoadingModal()}
diff --git a/src/reducers/search.js b/src/reducers/search.js index 95da842..5caa204 100644 --- a/src/reducers/search.js +++ b/src/reducers/search.js @@ -7,22 +7,29 @@ * note: all of the heavy lifting is being done within the action/search creator * * { - * // flagged when SEARCHING action is received - * isSearching: bool - * - * // contains the actual search query - * query: string - * * // facets grouped by field * // { 'subject': [{value: 'art' ...}, {value: 'anthropology' ...} ]} * facets: object * + * // flagged when SEARCHING action is received + * isSearching: bool + * // search options * // { 'per_page': 25 } * options: object * + * // contains the search query + * query: string + * * // the actual formatted querystring (used for pushState) * queryString: string + * + * // raw Blacklight results (specificially, the `response` object) + * results: object + * + * // Date.now() used to determine whether or not to update state on the + * // SearchResults page + * timestamp: number * } */ @@ -81,45 +88,40 @@ function receiveError (/* state, action */) { // the server. function receiveResults (state, action) { + const results = action.results.response + const fullSet = results.facets + const selectedFacets = state.facets || {} const facets = {} - const selectedFacets = state.facets - - // if we previously don't have a `facets` to check against, - // use an empty object to prevent from throwing - const keys = Object.keys(selectedFacets || {}) - // bail early if no facet keys - if (!keys.length) { - return assign({}, state, { - isSearching: false, - }) - } + const keys = Object.keys(selectedFacets) - const fullSet = action.results.response.facets + if (keys.length) { + keys.forEach(key => { + const facet = selectedFacets[key] - keys.forEach(key => { - const facet = selectedFacets[key] + facets[key] = facet.map(facetValue => { + // in most cases (read: not arriving from a link) the facets + // will be objects, so we'll just return them and deal with + // the minimal extra work + if (typeof facetValue === 'object' && facetValue !== null) + return facetValue - facets[key] = facet.map(facetValue => { - // in most cases (read: not arriving from a link) the facets - // will be objects, so we'll just return them and deal with - // the minimal extra work - if (typeof facetValue === 'object' && facetValue !== null) - return facetValue + // otherwise, loop through all of the facet-groups to find + // the appropriate one, and then loop through its items + // to locate the facet object + const group = arrayFind(fullSet, g => g.name === key) + return arrayFind(group.items, facetItem => facetItem.value === facetValue) - // otherwise, loop through all of the facet-groups to find - // the appropriate one, and then loop through its items - // to locate the facet object - const group = arrayFind(fullSet, g => g.name === key) - return arrayFind(group.items, facetItem => facetItem.value === facetValue) - - // filter out any empty values that may have been returned - // as `null` - }).filter(Boolean) - }) + // filter out any empty values that may have been returned + // as `null` + }).filter(Boolean) + }) + } return assign({}, state, { isSearching: false, facets, + results, + timestamp: Date.now(), }) }