UNPKG

cspace-ui

Version:
447 lines (434 loc) 16 kB
"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;