cspace-ui
Version:
CollectionSpace user interface for browsers
447 lines (434 loc) • 16 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _react = _interopRequireWildcard(require("react"));
var _propTypes = _interopRequireDefault(require("prop-types"));
var _immutable = _interopRequireDefault(require("immutable"));
var _reactIntl = require("react-intl");
var _reactRouterDom = require("react-router-dom");
var _get = _interopRequireDefault(require("lodash/get"));
var _cspaceLayout = require("cspace-layout");
var _dimensions = _interopRequireDefault(require("../../../styles/dimensions.css"));
var _SearchResultTable = _interopRequireDefault(require("../../../styles/cspace-ui/SearchResultTable.css"));
var _SearchResultEmpty = _interopRequireDefault(require("../../../styles/cspace-ui/SearchResultEmpty.css"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
const rowHeight = parseInt(_dimensions.default.inputHeight, 10);
const messages = (0, _reactIntl.defineMessages)({
searchPending: {
"id": "searchResultTable.searchPending",
"defaultMessage": "\u22EF"
},
rowLabel: {
"id": "searchResultTable.rowLabel",
"defaultMessage": "{primary} selected row {index} of {total}"
}
});
/**
* Determines if a column is sortable for a given search. A column is sortable if sortBy is truthy,
* and the search is not constrained by a related record, or if it is, the field to sort by is not
* complex. This is here to deal with CSPACE-5366 (searches with related record constraints are
* done using CMIS, which can't see into complex fields). If that bug is ever fixed, then it will
* suffice just to check sortBy.
*/
const isSortable = (column, searchDescriptor) => {
const {
sortBy
} = column;
return sortBy && (!searchDescriptor.getIn(['searchQuery', 'rel']) || sortBy.indexOf('/0/') === -1);
};
const rowRenderer = (params, location, ariaLabel) => {
// This is a fork of react-virtualized's default row renderer:
// https://github.com/bvaughn/react-virtualized/blob/master/source/Table/defaultRowRenderer.js
const {
className,
columns,
index,
key,
onRowClick,
// onRowDoubleClick,
// onRowMouseOut,
// onRowMouseOver,
// onRowRightClick,
rowData,
style
} = params;
const a11yProps = {};
if (onRowClick // ||
// onRowDoubleClick ||
// onRowMouseOut ||
// onRowMouseOver ||
// onRowRightClick
) {
a11yProps['aria-label'] = ariaLabel;
a11yProps.tabIndex = 0;
if (onRowClick) {
a11yProps.onClick = event => onRowClick({
event,
index,
rowData
});
}
// if (onRowDoubleClick) {
// a11yProps.onDoubleClick = event =>
// onRowDoubleClick({ event, index, rowData });
// }
// if (onRowMouseOut) {
// a11yProps.onMouseOut = event => onRowMouseOut({ event, index, rowData });
// }
// if (onRowMouseOver) {
// a11yProps.onMouseOver = event => onRowMouseOver({ event, index, rowData });
// }
// if (onRowRightClick) {
// a11yProps.onContextMenu = event =>
// onRowRightClick({ event, index, rowData });
// }
}
if (location) {
return /*#__PURE__*/_react.default.createElement(_reactRouterDom.Link, _extends({}, a11yProps, {
className: className,
key: key,
role: "row",
style: style,
to: location
}), columns);
}
return /*#__PURE__*/_react.default.createElement("div", _extends({}, a11yProps, {
className: className,
"data-index": index,
key: key,
role: "row",
style: style
}), columns);
};
const propTypes = {
columnSetName: _propTypes.default.string,
config: _propTypes.default.shape({
listTypes: _propTypes.default.object,
recordTypes: _propTypes.default.object,
subresources: _propTypes.default.object
}).isRequired,
formatCellData: _propTypes.default.func,
formatColumnLabel: _propTypes.default.func,
intl: _reactIntl.intlShape,
isSearchPending: _propTypes.default.bool,
linkItems: _propTypes.default.bool,
// eslint-disable-next-line react/forbid-prop-types
linkState: _propTypes.default.object,
listType: _propTypes.default.string,
perms: _propTypes.default.instanceOf(_immutable.default.Map),
searchDescriptor: _propTypes.default.instanceOf(_immutable.default.Map),
searchError: _propTypes.default.instanceOf(_immutable.default.Map),
searchResult: _propTypes.default.instanceOf(_immutable.default.Map),
showCheckboxColumn: _propTypes.default.bool,
renderCheckbox: _propTypes.default.func,
renderHeader: _propTypes.default.func,
renderFooter: _propTypes.default.func,
renderSelectBar: _propTypes.default.func,
getItemLocation: _propTypes.default.func,
onItemClick: _propTypes.default.func,
onSortChange: _propTypes.default.func
};
const defaultProps = {
columnSetName: 'default',
formatCellData: (column, data) => data,
formatColumnLabel: column => (0, _get.default)(column, ['messages', 'label', 'defaultMessage']),
linkItems: true,
listType: 'common',
renderHeader: () => null,
renderFooter: () => null,
renderSelectBar: () => null
};
class SearchResultTable extends _react.Component {
constructor() {
super();
this.getColumnConfig = this.getColumnConfig.bind(this);
this.getItemLocation = this.getItemLocation.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleRowClick = this.handleRowClick.bind(this);
this.renderNoItems = this.renderNoItems.bind(this);
this.renderRow = this.renderRow.bind(this);
this.renderRowLabel = this.renderRowLabel.bind(this);
this.sort = this.sort.bind(this);
}
handleKeyDown(event) {
if (event.key === 'Enter') {
const index = (0, _get.default)(event, ['target', 'dataset', 'index']);
if (typeof index !== 'undefined') {
this.handleRowClick(event.target.dataset.index);
}
}
}
handleRowClick(index) {
const {
config,
listType,
searchResult,
onItemClick
} = this.props;
if (onItemClick) {
const listTypeConfig = config.listTypes[listType];
const {
listNodeName,
itemNodeName
} = listTypeConfig;
const items = searchResult.getIn([listNodeName, itemNodeName]);
const item = _immutable.default.List.isList(items) ? items.get(index) : items;
onItemClick(item, index);
}
}
getColumnConfig() {
const {
columnSetName,
config,
searchDescriptor
} = this.props;
const recordType = searchDescriptor.get('recordType');
const subresource = searchDescriptor.get('subresource');
const columnConfigurer = subresource ? config.subresources[subresource] : config.recordTypes[recordType];
let columnConfig = (0, _get.default)(columnConfigurer, ['columns', columnSetName]);
if (!columnConfig && columnSetName !== defaultProps.columnSetName) {
// Fall back to the default column set if the named one doesn't exist.
columnConfig = (0, _get.default)(columnConfigurer, ['columns', defaultProps.columnSetName]);
}
if (!columnConfig) {
columnConfig = [];
}
return columnConfig;
}
getItemLocation(item) {
const {
config,
linkState,
listType,
perms,
searchDescriptor
} = this.props;
const getItemLocationPath = (0, _get.default)(config, ['listTypes', listType, 'getItemLocationPath']);
if (!getItemLocationPath) {
return undefined;
}
const itemContext = {
config,
perms,
searchDescriptor
};
const itemLocationPath = getItemLocationPath(item, itemContext);
if (!itemLocationPath) {
return undefined;
}
// Create a location with the item location path, along with enough state to reproduce this
// search. The search descriptor is converted to an object in order to reliably store it in
// location state. Also merge in any object that was passed in via the linkState prop.
const state = {
searchDescriptor: searchDescriptor.toJS(),
// The search traverser on records will always link to the search result page, so use
// its search name.
searchName: 'searchResultPage',
...linkState
};
return {
state,
pathname: itemLocationPath
};
}
sort({
sortBy,
sortDirection
}) {
const {
onSortChange
} = this.props;
if (onSortChange) {
onSortChange(sortBy + (sortDirection === _cspaceLayout.Table.SortDirection.DESC ? ' desc' : ''));
}
}
renderNoItems() {
const {
isSearchPending
} = this.props;
const message = isSearchPending ? /*#__PURE__*/_react.default.createElement(_reactIntl.FormattedMessage, messages.searchPending) : null;
return /*#__PURE__*/_react.default.createElement("div", {
className: _SearchResultEmpty.default.common
}, message);
}
renderRowLabel(params, totalItems) {
const {
intl
} = this.props;
const {
index,
rowData
} = params;
const columnConfig = this.getColumnConfig();
const primaryCol = Object.keys(columnConfig).filter(col => col !== 'workflowState').at(0);
const primaryData = rowData.get(primaryCol);
const label = primaryData ? intl.formatMessage(messages.rowLabel, {
primary: primaryData,
index: index + 1,
total: totalItems
}) : 'row';
return label;
}
renderRow(params, totalItems) {
const {
getItemLocation,
linkItems
} = this.props;
const {
rowData
} = params;
let location;
if (linkItems) {
const locationGetter = getItemLocation || this.getItemLocation;
location = locationGetter(rowData);
}
const ariaLabel = this.renderRowLabel(params, totalItems);
return rowRenderer(params, location, ariaLabel);
}
renderTable() {
const {
config,
formatCellData,
formatColumnLabel,
listType,
searchDescriptor,
searchResult,
showCheckboxColumn,
renderCheckbox
} = this.props;
if (searchResult) {
const searchQuery = searchDescriptor.get('searchQuery');
const listTypeConfig = config.listTypes[listType];
const {
listNodeName,
itemNodeName
} = listTypeConfig;
let sortColumnName = null;
let sortDir = null;
const sortSpec = searchQuery.get('sort');
if (sortSpec) {
[sortColumnName, sortDir] = sortSpec.split(' ');
}
const list = searchResult.get(listNodeName);
const pageSize = parseInt(list.get('pageSize'), 10);
const totalItems = parseInt(list.get('totalItems'), 10);
const itemsInPage = parseInt(list.get('itemsInPage'), 10);
let items = list.get(itemNodeName);
if (!items) {
items = _immutable.default.List();
}
if (!_immutable.default.List.isList(items)) {
// If there's only one result, it won't be returned as a list.
items = _immutable.default.List.of(items);
}
const columnConfig = this.getColumnConfig();
const columns = Object.keys(columnConfig).filter(name => !columnConfig[name].disabled).sort((nameA, nameB) => {
const orderA = columnConfig[nameA].order;
const orderB = columnConfig[nameB].order;
return orderA - orderB;
}).map(name => {
const column = columnConfig[name];
return {
cellDataGetter: ({
dataKey,
rowData
}) => {
let data = null;
if (rowData) {
const keys = dataKey.split('|');
for (let i = 0; i < keys.length; i += 1) {
const candidateValue = rowData.get(keys[i]);
if (candidateValue) {
data = candidateValue;
break;
}
}
}
return formatCellData(column, data, rowData);
},
disableSort: !isSortable(column, searchDescriptor),
flexGrow: column.flexGrow,
flexShrink: column.flexShrink,
label: formatColumnLabel(column),
dataKey: column.dataKey || name,
width: column.width
};
});
let heightBasis;
if (Number.isNaN(totalItems)) {
// We don't yet know how many items are found by the search. Set the height to one item, so
// an ellipsis (or other calculating indicator) can be shown.
heightBasis = 1;
} else {
// If all of the search results fit on one page, shrink the table to fit the number of
// results. Otherwise, size the table to fit the desired page size, even if there aren't
// that many results on this page. This keeps the pager from jumping up on the last page
// and while page/sorting changes are in progress.
heightBasis = totalItems <= pageSize && !Number.isNaN(itemsInPage) ? itemsInPage : pageSize;
if (heightBasis === 0) {
// If there are no items, set the height to one, because it looks weird when the footer
// is mashed up against the header. This also leaves room to display a "no records found"
// message if desired.
heightBasis = 1;
}
}
const height = heightBasis * rowHeight + rowHeight;
const renderRowWithTotal = params => this.renderRow(params, totalItems);
return /*#__PURE__*/_react.default.createElement("div", {
style: {
height
}
}, /*#__PURE__*/_react.default.createElement(_cspaceLayout.Table, {
columns: columns,
rowCount: items.size,
rowGetter: ({
index
}) => items.get(index),
showCheckboxColumn: showCheckboxColumn,
onRowClick: this.handleRowClick,
renderCheckbox: renderCheckbox,
sort: this.sort,
sortBy: sortColumnName,
sortDirection: sortDir === 'desc' ? _cspaceLayout.Table.SortDirection.DESC : _cspaceLayout.Table.SortDirection.ASC,
noRowsRenderer: this.renderNoItems,
rowRenderer: renderRowWithTotal
}));
}
return null;
}
render() {
const {
isSearchPending,
searchError,
searchResult,
renderHeader,
renderSelectBar,
renderFooter
} = this.props;
return (
/*#__PURE__*/
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
_react.default.createElement("div", {
className: _SearchResultTable.default.common,
role: "presentation",
onKeyDown: this.handleKeyDown
}, renderHeader({
isSearchPending,
searchError,
searchResult
}), renderSelectBar(), this.renderTable(), renderFooter({
isSearchPending,
searchError,
searchResult
}))
);
}
}
exports.default = SearchResultTable;
SearchResultTable.propTypes = propTypes;
SearchResultTable.defaultProps = defaultProps;