cspace-ui
Version:
CollectionSpace user interface for browsers
643 lines (531 loc) • 17.3 kB
JSX
/* global window */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import get from 'lodash/get';
import Immutable from 'immutable';
import qs from 'qs';
import CheckboxInput from 'cspace-input/lib/components/CheckboxInput';
import ErrorPage from './ErrorPage';
import Pager from '../search/Pager';
import SearchResultSidebar from '../search/SearchResultSidebar';
import SearchResultSummary from '../search/SearchResultSummary';
import SearchResultTitleBar from '../search/SearchResultTitleBar';
import SelectBar from '../search/SelectBar';
import WatchedSearchResultTableContainer from '../../containers/search/WatchedSearchResultTableContainer';
import {
deriveSearchType,
getListTypeFromResult,
createPageSizeChangeHandler,
createPageChangeHandler,
extractAdvancedSearchGroupedTerms,
createSortByHandler,
createSortDirHandler,
normalizeSearchQueryParams,
} from '../../helpers/searchHelpers';
import { SEARCH_RESULT_PAGE_SEARCH_NAME } from '../../constants/searchNames';
import {
validateLocation,
} from '../../helpers/configHelpers';
import styles from '../../../styles/cspace-ui/SearchResultPage.css';
import pageBodyStyles from '../../../styles/cspace-ui/PageBody.css';
import ExportResults from '../search/ExportResults';
import RelateResults from '../search/RelateResults';
import SortBy from '../search/SortBy';
// const stopPropagation = (event) => {
// event.stopPropagation();
// };
const propTypes = {
isSidebarOpen: PropTypes.bool,
history: PropTypes.shape({
push: PropTypes.func,
replace: PropTypes.func,
}),
location: PropTypes.shape({
pathname: PropTypes.string,
search: PropTypes.string,
state: PropTypes.object,
}),
match: PropTypes.shape({
params: PropTypes.object,
}),
perms: PropTypes.instanceOf(Immutable.Map),
preferredPageSize: PropTypes.number,
search: PropTypes.func,
selectedItems: PropTypes.instanceOf(Immutable.Map),
setPreferredPageSize: PropTypes.func,
setSearchPageAdvanced: PropTypes.func,
setSearchPageAdvancedSearchTerms: PropTypes.func,
setSearchPageAdvancedLimitBy: PropTypes.func,
setSearchPageKeyword: PropTypes.func,
setSearchPageRecordType: PropTypes.func,
setSearchPageVocabulary: PropTypes.func,
setAllItemsSelected: PropTypes.func,
onItemSelectChange: PropTypes.func,
};
const defaultProps = {
isSidebarOpen: true,
};
const contextTypes = {
config: PropTypes.shape({
recordTypes: PropTypes.object,
}).isRequired,
};
export default class SearchResultPage extends Component {
constructor() {
super();
this.handleCheckboxClick = this.handleCheckboxClick.bind(this);
this.handleCheckboxCommit = this.handleCheckboxCommit.bind(this);
this.handleEditSearchLinkClick = this.handleEditSearchLinkClick.bind(this);
this.handleModalCancelButtonClick = this.handleModalCancelButtonClick.bind(this);
this.handleModalCloseButtonClick = this.handleModalCloseButtonClick.bind(this);
this.handlePageChange = this.handlePageChange.bind(this);
this.handlePageSizeChange = this.handlePageSizeChange.bind(this);
this.handleSortChange = this.handleSortChange.bind(this);
this.renderCheckbox = this.renderCheckbox.bind(this);
this.renderFooter = this.renderFooter.bind(this);
this.renderHeader = this.renderHeader.bind(this);
this.search = this.search.bind(this);
}
componentDidMount() {
if (!this.normalizeQuery()) {
const {
location,
setPreferredPageSize,
} = this.props;
if (setPreferredPageSize) {
const {
search,
} = location;
const query = qs.parse(search.substring(1));
setPreferredPageSize(parseInt(query.size, 10));
}
this.search();
}
}
componentDidUpdate(prevProps) {
const {
location,
match,
perms,
} = this.props;
const {
location: prevLocation,
match: prevMatch,
perms: prevPerms,
} = prevProps;
const { params } = match;
const { params: prevParams } = prevMatch;
if (
perms !== prevPerms
|| params.recordType !== prevParams.recordType
|| params.vocabulary !== prevParams.vocabulary
|| params.csid !== prevParams.csid
|| params.subresource !== prevParams.subresource
|| location.search !== prevLocation.search
) {
if (!this.normalizeQuery()) {
const {
setPreferredPageSize,
} = this.props;
if (setPreferredPageSize) {
const {
search,
} = location;
const query = qs.parse(search.substring(1));
setPreferredPageSize(parseInt(query.size, 10));
}
this.search();
}
}
}
handleCheckboxClick(event) {
// DRYD-252: Elaborate workaround for Firefox. When a checkbox is a child of an a, clicking on
// the checkbox navigates to the link. So we have to handle the checkbox click, and prevent the
// default. This prevents the navigation, but also prevents the checkbox state from changing.
// So we also have to manually commit the change. The Firefox bug has been open for 17 years
// now. https://bugzilla.mozilla.org/show_bug.cgi?id=62151
event.preventDefault();
event.stopPropagation();
const checkbox = event.currentTarget.querySelector('input[type="checkbox"]');
const index = checkbox.dataset.name;
window.setTimeout(() => {
this.handleCheckboxCommit([index], !checkbox.checked);
}, 0);
}
handleCheckboxCommit(path, value) {
const index = parseInt(path[0], 10);
const selected = value;
const {
onItemSelectChange,
} = this.props;
const {
config,
} = this.context;
if (onItemSelectChange) {
const searchDescriptor = this.getSearchDescriptor();
const {
listType,
} = deriveSearchType(config, SEARCH_RESULT_PAGE_SEARCH_NAME, searchDescriptor);
onItemSelectChange(
config, SEARCH_RESULT_PAGE_SEARCH_NAME, searchDescriptor, listType, index, selected,
);
}
}
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 {
location,
setSearchPageAdvanced,
setSearchPageAdvancedLimitBy,
setSearchPageAdvancedSearchTerms,
setSearchPageKeyword,
setSearchPageRecordType,
setSearchPageVocabulary,
} = this.props;
const origin = get(location.state, 'originSearchPage');
const searchDescriptor = origin
? Immutable.fromJS(origin.searchDescriptor)
: this.getSearchDescriptor();
const searchQuery = searchDescriptor.get('searchQuery');
if (setSearchPageRecordType) {
setSearchPageRecordType(searchDescriptor.get('recordType'));
}
if (setSearchPageVocabulary) {
setSearchPageVocabulary(searchDescriptor.get('vocabulary'));
}
if (setSearchPageKeyword) {
setSearchPageKeyword(searchQuery.get('kw'));
}
if (setSearchPageAdvanced) {
setSearchPageAdvanced(searchQuery.get('as'));
}
if (setSearchPageAdvancedSearchTerms) {
setSearchPageAdvancedSearchTerms(extractAdvancedSearchGroupedTerms(searchQuery.get('as')).searchTerms);
}
if (setSearchPageAdvancedLimitBy) {
setSearchPageAdvancedLimitBy(extractAdvancedSearchGroupedTerms(searchQuery.get('as')).limitBy);
}
}
handleModalCancelButtonClick() {
this.closeModal();
}
handleModalCloseButtonClick() {
this.closeModal();
}
handlePageChange(pageNum) {
const {
history,
location,
} = this.props;
const handler = createPageChangeHandler({
history,
location,
});
return handler(pageNum);
}
handlePageSizeChange(pageSize) {
const {
history,
location,
setPreferredPageSize,
} = this.props;
// Call setPreferredPageSize directly (not through dispatch)
if (setPreferredPageSize) {
setPreferredPageSize(pageSize);
}
// Use helper for URL navigation
const handler = createPageSizeChangeHandler({
history,
location,
});
return handler(pageSize);
}
handleSortChange(sort) {
const {
history,
location,
} = this.props;
if (history) {
const {
search,
} = location;
const query = qs.parse(search.substring(1));
query.sort = sort;
const queryString = qs.stringify(query);
history.push({
pathname: location.pathname,
search: `?${queryString}`,
state: location.state,
});
}
}
getSearchDescriptor() {
// FIXME: Refactor this into a wrapper component that calculates the search descriptor from
// location and params, and passes it into a child. This will eliminate the multiple calls to
// this method from the various render methods in this class.
const {
location,
match,
} = this.props;
const {
params,
} = match;
const {
search,
} = location;
const query = qs.parse(search.substring(1));
const searchQuery = {
...query,
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);
}
normalizeQuery() {
const {
config,
} = this.context;
const {
history,
location,
preferredPageSize,
} = this.props;
const {
search,
} = location;
const query = qs.parse(search.substring(1));
const { normalizedQuery, changed } = normalizeSearchQueryParams(
query,
preferredPageSize,
config.defaultSearchPageSize,
);
if (history && changed) {
const queryString = qs.stringify(normalizedQuery);
history.replace({
pathname: location.pathname,
search: `?${queryString}`,
state: location.state,
});
return true;
}
return false;
}
search() {
const {
search,
} = this.props;
const {
config,
} = this.context;
const searchDescriptor = this.getSearchDescriptor();
const searchQuery = searchDescriptor.get('searchQuery');
// Don't send the query if the provided page size and/or number are invalid.
// The search will be repeated with a valid descriptor once normalizeQuery
// has set them to the defaults.
if (!Number.isNaN(searchQuery.get('p')) && !Number.isNaN(searchQuery.get('size')) && search) {
search(config, SEARCH_RESULT_PAGE_SEARCH_NAME, searchDescriptor);
}
}
renderCheckbox({ rowData, rowIndex }) {
const {
selectedItems,
} = this.props;
const itemCsid = rowData.get('csid');
const selected = selectedItems ? selectedItems.has(itemCsid) : false;
return (
<CheckboxInput
embedded
name={`${rowIndex}`}
value={selected}
// DRYD-252: Elaborate workaround for Firefox, part II. Use this onClick instead of the
// onCommit and onClick below.
onClick={this.handleCheckboxClick}
// onCommit={this.handleCheckboxCommit}
// Prevent clicking on the checkbox from selecting the record.
// onClick={stopPropagation}
/>
);
}
renderHeader({ searchError, searchResult }) {
const {
history,
location,
selectedItems,
setAllItemsSelected,
perms,
} = this.props;
const {
config,
} = this.context;
const searchDescriptor = this.getSearchDescriptor();
const { listType } = deriveSearchType(config, SEARCH_RESULT_PAGE_SEARCH_NAME, searchDescriptor);
let selectBar;
if (!searchError) {
const relateButton = (
<RelateResults
config={config}
selectedItems={selectedItems}
searchDescriptor={searchDescriptor}
perms={perms}
disabled={false}
key="relate"
/>
);
const exportButton = (
<ExportResults
config={config}
selectedItems={selectedItems}
searchDescriptor={searchDescriptor}
/>
);
selectBar = (
<SelectBar
buttons={[relateButton, exportButton]}
config={config}
listType={listType}
searchDescriptor={searchDescriptor}
searchName={SEARCH_RESULT_PAGE_SEARCH_NAME}
searchResult={searchResult}
selectedItems={selectedItems}
setAllItemsSelected={setAllItemsSelected}
/>
);
}
const {
search,
} = location;
const query = qs.parse(search.substring(1));
const sortChangeHandler = createSortByHandler({ history, location });
const sortDirChangeHandler = createSortDirHandler({ history, location });
const renderSortBy = () => (
<SortBy
onSortChange={sortChangeHandler}
onSortDirChange={sortDirChangeHandler}
sort={query?.sort}
recordType={searchDescriptor.get('recordType')}
/>
);
return (
<header>
<SearchResultSummary
config={config}
listType={listType}
searchName={SEARCH_RESULT_PAGE_SEARCH_NAME}
searchDescriptor={searchDescriptor}
onEditSearchLinkClick={this.handleEditSearchLinkClick}
onPageSizeChange={this.handlePageSizeChange}
renderSortBy={() => renderSortBy()}
/>
{selectBar}
</header>
);
}
renderFooter({ searchResult }) {
if (searchResult) {
const {
config,
} = this.context;
const listType = getListTypeFromResult(config, searchResult);
const listTypeConfig = config.listTypes[listType];
const { listNodeName } = listTypeConfig;
const list = searchResult.get(listNodeName);
const totalItems = parseInt(list.get('totalItems'), 10);
const pageNum = parseInt(list.get('pageNum'), 10);
const pageSize = parseInt(list.get('pageSize'), 10);
const lastPage = Math.max(
0,
Number.isNaN(totalItems) ? 0 : Math.ceil(totalItems / pageSize) - 1,
);
return (
<footer>
<Pager
currentPage={pageNum}
lastPage={lastPage}
pageSize={pageSize}
onPageChange={this.handlePageChange}
onPageSizeChange={this.handlePageSizeChange}
/>
</footer>
);
}
return null;
}
render() {
const {
location,
history,
isSidebarOpen,
} = this.props;
const {
config,
} = this.context;
const searchDescriptor = this.getSearchDescriptor();
const { listType } = deriveSearchType(config, SEARCH_RESULT_PAGE_SEARCH_NAME, searchDescriptor);
const recordType = searchDescriptor.get('recordType');
const vocabulary = searchDescriptor.get('vocabulary');
const csid = searchDescriptor.get('csid');
const subresource = searchDescriptor.get('subresource');
const validation = validateLocation(config, {
recordType, vocabulary, csid, subresource,
});
if (validation.error) {
return (
<ErrorPage error={validation.error} />
);
}
return (
<div className={styles.common}>
<SearchResultTitleBar
config={config}
searchDescriptor={searchDescriptor}
searchName={SEARCH_RESULT_PAGE_SEARCH_NAME}
updateDocumentTitle
/>
<div className={isSidebarOpen ? pageBodyStyles.common : pageBodyStyles.full}>
<WatchedSearchResultTableContainer
config={config}
history={history}
linkState={{ originSearchPage: get(location, ['state', 'originSearchPage']) }}
listType={listType}
searchName={SEARCH_RESULT_PAGE_SEARCH_NAME}
searchDescriptor={searchDescriptor}
recordType={recordType}
showCheckboxColumn
renderCheckbox={this.renderCheckbox}
renderHeader={this.renderHeader}
renderFooter={this.renderFooter}
onSortChange={this.handleSortChange}
search={this.search}
/>
<SearchResultSidebar
config={config}
history={history}
isOpen={isSidebarOpen}
recordType={recordType}
/>
</div>
</div>
);
}
}
SearchResultPage.propTypes = propTypes;
SearchResultPage.defaultProps = defaultProps;
SearchResultPage.contextTypes = contextTypes;