UNPKG

cspace-ui

Version:
447 lines (389 loc) 13.5 kB
import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { batch, useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; import Immutable from 'immutable'; import qs from 'qs'; import get from 'lodash/get'; import { defineMessages, injectIntl, intlShape } from 'react-intl'; import classNames from 'classnames'; import { SEARCH_RESULT_GRID_VIEW, SEARCH_RESULT_LIST_VIEW, SEARCH_RESULT_PAGE_SEARCH_NAME, SEARCH_RESULT_TABLE_VIEW, } from '../../../constants/searchNames'; import SearchResultTitleBar from '../../search/SearchResultTitleBar'; import SearchResultFooter from '../../search/SearchResultFooter'; import SearchResultTable from '../../search/table/SearchTable'; import SearchResultGrid from '../../search/grid/SearchResultGrid'; import SearchDetailList from '../../search/list/SearchList'; import SearchResultSidebar from '../../search/SearchResultSidebar'; import SearchResultSummary from '../../search/SearchResultSummary'; import SortBy from '../../search/SortBy'; import { ToggleButton, ToggleButtonContainer } from '../../search/header/ToggleButtons'; import { useConfig } from '../../config/ConfigProvider'; import styles from '../../../../styles/cspace-ui/SearchResults.css'; import buttonBarStyles from '../../../../styles/cspace-ui/ButtonBar.css'; import { setSearchPageRecordType, setSearchPageVocabulary, setSearchResultPagePageSize, setSearchResultPageView, } from '../../../actions/prefs'; import { search, setAllResultItemsSelected, } from '../../../actions/search'; import { getSearchResult, isSearchResultSidebarOpen, getSearchSelectedItems, getUserPerms, getSearchResultPageView, } from '../../../reducers'; import SelectBar from '../../search/SelectBar'; import RelateResults from '../../search/RelateResults'; import ExportResults from '../../search/ExportResults'; import { getListTypeFromResult, createPageSizeChangeHandler, extractAdvancedSearchGroupedTerms, createSortByHandler, createSortDirHandler, normalizeSearchQueryParams, } from '../../../helpers/searchHelpers'; import { setSearchPageAdvanced, setSearchPageAdvancedLimitBy, setSearchPageAdvancedSearchTerms, setSearchPageKeyword, } from '../../../actions/searchPage'; const selectBarPropTypes = { toggleBar: PropTypes.object, searchResult: PropTypes.instanceOf(Immutable.Map), config: PropTypes.object, searchDescriptor: PropTypes.instanceOf(Immutable.Map), }; export function SelectExportRelateToggleBar({ toggleBar, searchResult, config, searchDescriptor, }) { if (!searchResult) { return null; } const selectedItems = useSelector((state) => getSearchSelectedItems(state, SEARCH_RESULT_PAGE_SEARCH_NAME)); const perms = useSelector((state) => getUserPerms(state)); const dispatch = useDispatch(); // button bar (relate/export) const exportButton = ( <ExportResults config={config} selectedItems={selectedItems} searchDescriptor={searchDescriptor} /> ); const relateButton = ( <RelateResults config={config} selectedItems={selectedItems} searchDescriptor={searchDescriptor} perms={perms} disabled={false} key="relate" /> ); const buttonBar = ( <div className={buttonBarStyles.common}> {relateButton} {exportButton} </div> ); const listType = getListTypeFromResult(config, searchResult); return ( <SelectBar config={config} listType={listType} searchDescriptor={searchDescriptor} searchName={SEARCH_RESULT_PAGE_SEARCH_NAME} searchResult={searchResult} selectedItems={selectedItems} setAllItemsSelected={ (...args) => dispatch(setAllResultItemsSelected(...args)) } > {buttonBar} {toggleBar} </SelectBar> ); } // memoize? const getSearchDescriptor = (query, props) => { const { match, } = props; const { params, } = match; const { view, ...queryWithoutView } = query; const searchQuery = { ...queryWithoutView, p: parseInt(query.p, 10) - 1, size: parseInt(query.size, 10), }; const advancedSearchCondition = query.as; if (advancedSearchCondition) { searchQuery.as = JSON.parse(advancedSearchCondition); } const searchDescriptor = { searchQuery, }; ['recordType', 'vocabulary', 'csid', 'subresource'].forEach((param) => { const value = params[param]; if (typeof value !== 'undefined') { searchDescriptor[param] = value; } }); return Immutable.fromJS(searchDescriptor); }; function setPreferredPageSize(props, dispatch) { const { location, } = props; const { search: searchFromLoc, } = location; const query = qs.parse(searchFromLoc.substring(1)); dispatch(setSearchResultPagePageSize(parseInt(query.size, 10))); } function normalizeQuery(props, config) { const { location, preferredPageSize, } = props; const { search: searchFromLoc, } = location; const query = qs.parse(searchFromLoc.substring(1)); const { normalizedQuery } = normalizeSearchQueryParams( query, preferredPageSize, config.defaultSearchPageSize, ); return normalizedQuery; } /** * Gets the current view which we want to display. We prioritize * query parameters first, then the last view a user was on (in redux), * and finally fall back to the table view. */ const currentView = (props) => { const { history, location, } = props; const { search: searchFromLoc, } = location; const preferred = useSelector((state) => getSearchResultPageView(state)); let current; const query = qs.parse(searchFromLoc.substring(1)); if (query) { current = query.view; const hasCurrent = current && ( current === SEARCH_RESULT_GRID_VIEW || current === SEARCH_RESULT_LIST_VIEW || current === SEARCH_RESULT_TABLE_VIEW ); // update the query parameter to include our view if (preferred && !hasCurrent) { query.view = preferred; history.push({ pathname: location.pathname, search: `?${qs.stringify(query)}`, state: location.state, }); } } return current || preferred || SEARCH_RESULT_TABLE_VIEW; }; const messages = defineMessages({ table: { id: 'search.result.view.table', description: 'The table button label', defaultMessage: 'Switch to table view', }, detailList: { id: 'search.result.view.detailList', description: 'The detailList button label', defaultMessage: 'Switch to detail list view', }, grid: { id: 'search.result.view.grid', description: 'The grid button label', defaultMessage: 'Switch to grid view', }, }); /** * The page for displaying Search Results. Before rendering it first executes the search based on * the query parameters provided by encapsulating them in a search descriptor and calling the search * action in redux. * * Currently this is more for the new search views on CollectionObjects which includes a grid and * detail based view compared to the older table based view. Ideally this be the only component for * displaying search results but we first would need to make sure we only display the views which * are supported for a given procedure or authority. * * @param {*} props * @returns the SearchResults page component */ function SearchResults(props) { const config = useConfig(); const dispatch = useDispatch(); const history = useHistory(); const { intl, location } = props; const normalizedQuery = normalizeQuery(props, config); const searchDescriptor = getSearchDescriptor(normalizedQuery, props); const searchResults = useSelector((state) => getSearchResult(state, SEARCH_RESULT_PAGE_SEARCH_NAME, searchDescriptor)); const isSidebarOpen = useSelector((state) => isSearchResultSidebarOpen(state)); const display = currentView(props); useEffect(() => { setPreferredPageSize(props, dispatch); dispatch(search(config, SEARCH_RESULT_PAGE_SEARCH_NAME, searchDescriptor)); }, [searchDescriptor.toString()]); const handlePageSizeChange = createPageSizeChangeHandler({ history, location, dispatch, setPreferredPageSize: setSearchResultPagePageSize, }); const handleEditSearchLinkClick = () => { // Transfer the search descriptor from this search to the search page. If this search // originated from the search page, the original descriptor will be in the location state. // Otherwise, build it from the URL params. If present, the search descriptor from the // originating search page will be more complete than one constructed from the URL; for // example, it will contain fields that are blank, which will have been removed from the // URL, to reduce the size. const origin = get(location.state, 'originSearchPage'); const conditionalSearchDescriptor = origin ? Immutable.fromJS(origin.searchDescriptor) : getSearchDescriptor(normalizedQuery, props); const searchQuery = conditionalSearchDescriptor.get('searchQuery'); batch(() => { dispatch(setSearchPageRecordType(conditionalSearchDescriptor.get('recordType'))); dispatch(setSearchPageVocabulary(conditionalSearchDescriptor.get('vocabulary'))); dispatch(setSearchPageKeyword(searchQuery.get('kw'))); dispatch(setSearchPageAdvanced(searchQuery.get('as'))); dispatch(setSearchPageAdvancedLimitBy(extractAdvancedSearchGroupedTerms(searchQuery.get('as')).limitBy)); dispatch(setSearchPageAdvancedSearchTerms(extractAdvancedSearchGroupedTerms(searchQuery.get('as')).searchTerms)); }); }; const handleSortChange = createSortByHandler({ history, location }); const handleSortDirChange = createSortDirHandler({ history, location }); const renderSortBy = () => ( <SortBy onSortChange={handleSortChange} onSortDirChange={handleSortDirChange} sort={normalizedQuery?.sort} recordType={searchDescriptor.get('recordType')} /> ); const gridLabel = intl.formatMessage(messages.grid); const tableLabel = intl.formatMessage(messages.table); const detailListLabel = intl.formatMessage(messages.detailList); const toggles = [ { key: SEARCH_RESULT_TABLE_VIEW, label: tableLabel, icon: 'format_list_bulleted' }, { key: SEARCH_RESULT_GRID_VIEW, label: gridLabel, icon: 'grid_view' }, { key: SEARCH_RESULT_LIST_VIEW, label: detailListLabel, icon: 'vertical_split', class: styles.detailList, }, ]; const updatePageView = (key) => { if (history && location) { const { search: searchFromLoc } = location; const query = qs.parse(searchFromLoc.substring(1)); query.view = key; const queryString = qs.stringify(query); history.push({ pathname: location.pathname, search: `?${queryString}`, state: location.state, }); } dispatch(setSearchResultPageView(key)); }; const displayToggles = ( <ToggleButtonContainer items={toggles} renderButton={(item) => ( <ToggleButton icon={item.icon} key={item.key} name={item.key} title={item.label} className={classNames(styles.toggle, item.class)} onClick={() => updatePageView(item.key)} /> )} /> ); let searchDisplay; if (display === SEARCH_RESULT_GRID_VIEW) { searchDisplay = <SearchResultGrid searchDescriptor={searchDescriptor} />; } else if (display === SEARCH_RESULT_LIST_VIEW) { searchDisplay = <SearchDetailList searchDescriptor={searchDescriptor} />; } else { searchDisplay = <SearchResultTable searchDescriptor={searchDescriptor} />; } const handleBatchInvokeComplete = () => { dispatch(search(config, SEARCH_RESULT_PAGE_SEARCH_NAME, searchDescriptor)); }; const sidebar = ( <SearchResultSidebar config={config} history={history} isOpen={isSidebarOpen} onInvokeComplete={handleBatchInvokeComplete} recordType={searchDescriptor.get('recordType')} /> ); return ( <main className={styles.common}> <SearchResultTitleBar config={config} searchDescriptor={searchDescriptor} searchName={SEARCH_RESULT_PAGE_SEARCH_NAME} updateDocumentTitle /> <div className={isSidebarOpen ? styles.body : styles.full}> {/* SearchResultHeader? */} <div className={styles.results}> <header> <SearchResultSummary config={config} searchName={SEARCH_RESULT_PAGE_SEARCH_NAME} searchDescriptor={searchDescriptor} onPageSizeChange={handlePageSizeChange} onEditSearchLinkClick={handleEditSearchLinkClick} renderSortBy={() => renderSortBy()} /> <SelectExportRelateToggleBar toggleBar={displayToggles} searchResult={searchResults} config={config} searchDescriptor={searchDescriptor} /> </header> {searchDisplay} <SearchResultFooter searchDescriptor={searchDescriptor} /> </div> {sidebar} </div> </main> ); } const searchResultsPropTypes = { intl: intlShape, location: PropTypes.object.isRequired, }; SearchResults.propTypes = searchResultsPropTypes; SelectExportRelateToggleBar.propTypes = selectBarPropTypes; export default injectIntl(SearchResults);