diff --git a/openbis_ng_ui/src/js/components/common/grid/Grid.jsx b/openbis_ng_ui/src/js/components/common/grid/Grid.jsx index 6c0f66ee7689524156595ddd7e752d906627af36..32299cf92ffabdb93602e0d674177f4ac62b405f 100644 --- a/openbis_ng_ui/src/js/components/common/grid/Grid.jsx +++ b/openbis_ng_ui/src/js/components/common/grid/Grid.jsx @@ -122,8 +122,7 @@ class Grid extends React.PureComponent { filterMode, filters, globalFilter, - sort, - sortDirection, + sortings, page, pageSize, columnsVisibility, @@ -168,8 +167,7 @@ class Grid extends React.PureComponent { <GridHeaders columns={visibleColumns} rows={rows} - sort={sort} - sortDirection={sortDirection} + sortings={sortings} onSortChange={this.controller.handleSortChange} onMultiselectAllRowsChange={ this.controller.handleMultiselectAllRowsChange diff --git a/openbis_ng_ui/src/js/components/common/grid/GridController.js b/openbis_ng_ui/src/js/components/common/grid/GridController.js index e4d4bcda8349a27e1616826b686be37df394c54f..1dce97aa1e5fe40c478141341e355db87732aee9 100644 --- a/openbis_ng_ui/src/js/components/common/grid/GridController.js +++ b/openbis_ng_ui/src/js/components/common/grid/GridController.js @@ -27,6 +27,17 @@ export default class GridController { } } + let sortings = [] + + if (props.sort) { + sortings.push({ + columnName: props.sort, + sortDirection: props.sortDirection + ? props.sortDirection + : GridSortingOptions.ASC + }) + } + context.initState({ loaded: false, loading: false, @@ -48,10 +59,7 @@ export default class GridController { allRows: [], selectedRow: null, multiselectedRows: {}, - sort: props.sort, - sortDirection: props.sortDirection - ? props.sortDirection - : GridSortingOptions.ASC, + sortings: sortings, totalCount: 0, exportOptions: { columns: GridExportOptions.VISIBLE_COLUMNS, @@ -118,8 +126,7 @@ export default class GridController { globalFilter: newState.globalFilter, page: newState.page, pageSize: newState.pageSize, - sort: newState.sort, - sortDirection: newState.sortDirection + sortings: newState.sortings }) if (_.isArray(loadedResult)) { result.rows = loadedResult @@ -158,8 +165,7 @@ export default class GridController { newState.sortedRows = this._sortRows( newState.filteredRows, newState.allColumns, - newState.sort, - newState.sortDirection + newState.sortings ) newState.totalCount = newState.filteredRows.length @@ -294,25 +300,27 @@ export default class GridController { } _loadColumn(column) { + const defaultMatches = function (value, filter) { + if (filter) { + return value !== null && value !== undefined + ? String(value) + .trim() + .toUpperCase() + .includes(filter.trim().toUpperCase()) + : false + } else { + return true + } + } + + const defaultCompare = compare + return { ...column, name: column.name, label: column.label, getValue: column.getValue, matches: (row, filter) => { - function defaultMatches(value, filter) { - if (filter) { - return value !== null && value !== undefined - ? String(value) - .trim() - .toUpperCase() - .includes(filter.trim().toUpperCase()) - : false - } else { - return true - } - } - const value = column.getValue({ row, column, operation: 'match' }) if (column.matchesValue) { @@ -327,8 +335,7 @@ export default class GridController { return defaultMatches(value, filter) } }, - compare: (row1, row2) => { - const defaultCompare = compare + compare: (row1, row2, sortDirection) => { const value1 = column.getValue({ row: row1, column, @@ -339,7 +346,6 @@ export default class GridController { column, operation: 'compare' }) - const { sortDirection } = this.context.getState() if (column.compareValue) { return column.compareValue({ @@ -550,13 +556,32 @@ export default class GridController { } } - _sortRows(rows, columns, sort, sortDirection) { - if (sort) { - const column = _.find(columns, ['name', sort]) - if (column) { + _sortRows(rows, columns, sortings) { + if (sortings && sortings.length > 0) { + const columnSortings = [] + + sortings.forEach(sorting => { + const column = _.find(columns, ['name', sorting.columnName]) + if (column) { + columnSortings.push({ + column, + sorting + }) + } + }) + + if (columnSortings.length > 0) { return rows.sort((t1, t2) => { - let sign = sortDirection === GridSortingOptions.ASC ? 1 : -1 - return sign * column.compare(t1, t2) + let result = 0 + let index = 0 + while (index < columnSortings.length && result === 0) { + const { column, sorting } = columnSortings[index] + const sign = + sorting.sortDirection === GridSortingOptions.ASC ? 1 : -1 + result = sign * column.compare(t1, t2, sorting.sortDirection) + index++ + } + return result }) } } @@ -811,25 +836,58 @@ export default class GridController { await this._saveSettings() } - async handleSortChange(column) { + async handleSortChange(column, append) { if (!column.sortable) { return } + function createInitialSorting(column) { + return { + columnName: column.name, + sortDirection: GridSortingOptions.ASC + } + } + + function createReversedSorting(column, sorting) { + return { + columnName: column.name, + sortDirection: + sorting.sortDirection === GridSortingOptions.ASC + ? GridSortingOptions.DESC + : GridSortingOptions.ASC + } + } + await this.context.setState(state => { - if (column.name === state.sort) { - return { - sortDirection: - state.sortDirection === GridSortingOptions.ASC - ? GridSortingOptions.DESC - : GridSortingOptions.ASC + const newSortings = [] + + const index = _.findIndex( + state.sortings, + sorting => sorting.columnName === column.name + ) + const sorting = state.sortings[index] + + if (append) { + if (index !== -1) { + newSortings.push(...state.sortings) + newSortings.splice(index, 1) + } else { + newSortings.push(...state.sortings) + newSortings.push(createInitialSorting(column)) } } else { - return { - sort: column.name, - sortDirection: GridSortingOptions.ASC + if (index !== -1) { + newSortings.push(...state.sortings) + newSortings[index] = createReversedSorting(column, sorting) + } else { + newSortings.push(createInitialSorting(column)) } } + + return { + page: 0, + sortings: newSortings + } }) await this.load() @@ -1006,8 +1064,7 @@ export default class GridController { globalFilter: state.globalFilter, page: 0, pageSize: 1000000, - sort: state.sort, - sortDirection: state.sortDirection + sortings: state.sortings }) data = loadedResult.rows } @@ -1064,14 +1121,9 @@ export default class GridController { return pageSize } - getSort() { - const { sort } = this.context.getState() - return sort - } - - getSortDirection() { - const { sortDirection } = this.context.getState() - return sortDirection + getSortings() { + const { sortings } = this.context.getState() + return sortings } getFilters() { diff --git a/openbis_ng_ui/src/js/components/common/grid/GridHeader.jsx b/openbis_ng_ui/src/js/components/common/grid/GridHeader.jsx index 72fec644bc579b84d0811a36416979d76cab7586..cac5f561e0b6c211541c05679521fa4b51601648 100644 --- a/openbis_ng_ui/src/js/components/common/grid/GridHeader.jsx +++ b/openbis_ng_ui/src/js/components/common/grid/GridHeader.jsx @@ -15,6 +15,13 @@ const styles = theme => ({ '&:last-child': { paddingRight: theme.spacing(2) } + }, + sortIndex: { + color: theme.typography.label.color, + position: 'absolute', + right: 0, + paddingTop: '10px', + fontSize: theme.typography.label.fontSize } }) @@ -24,20 +31,20 @@ class GridHeader extends React.PureComponent { this.handleClick = this.handleClick.bind(this) } - handleClick() { + handleClick(event) { const { column, onSortChange } = this.props if (onSortChange) { - onSortChange(column) + onSortChange(column, event.ctrlKey || event.metaKey) } } render() { logger.log(logger.DEBUG, 'GridHeader.render') - const { column, sort, sortDirection, classes } = this.props + const { column, sortCount, sortIndex, sortDirection, classes } = this.props if (column.sortable) { - const active = sort === column.name + const active = sortIndex !== null && sortDirection !== null return ( <TableCell classes={{ root: classes.cell }}> <TableSortLabel @@ -46,6 +53,9 @@ class GridHeader extends React.PureComponent { onClick={this.handleClick} > {column.label} + {sortCount > 1 && sortIndex !== null && ( + <span className={classes.sortIndex}>{sortIndex + 1}</span> + )} </TableSortLabel> </TableCell> ) diff --git a/openbis_ng_ui/src/js/components/common/grid/GridHeaders.jsx b/openbis_ng_ui/src/js/components/common/grid/GridHeaders.jsx index a9482cf541edb9a56e3c3c2a3e46af7c579f53cc..fef28aebf81950d34ff278f6cf76fe077685ab6b 100644 --- a/openbis_ng_ui/src/js/components/common/grid/GridHeaders.jsx +++ b/openbis_ng_ui/src/js/components/common/grid/GridHeaders.jsx @@ -51,14 +51,20 @@ class GridHeaders extends React.PureComponent { } renderHeaderCell(column) { - const { sort, sortDirection, onSortChange } = this.props + const { sortings, onSortChange } = this.props + + const index = _.findIndex( + sortings, + sorting => sorting.columnName === column.name + ) return ( <GridHeader key={column.name} column={column} - sort={sort} - sortDirection={sortDirection} + sortCount={sortings.length} + sortIndex={index !== -1 ? index : null} + sortDirection={index !== -1 ? sortings[index].sortDirection : null} onSortChange={onSortChange} /> ) diff --git a/openbis_ng_ui/src/js/components/tools/common/HistoryGrid.jsx b/openbis_ng_ui/src/js/components/tools/common/HistoryGrid.jsx index 5d7e7e6bb8be753ce86218e0ab7088fef2f9d19b..4fac0c10d7803148123bfb9f2eca22b439247027 100644 --- a/openbis_ng_ui/src/js/components/tools/common/HistoryGrid.jsx +++ b/openbis_ng_ui/src/js/components/tools/common/HistoryGrid.jsx @@ -31,10 +31,7 @@ class HistoryGrid extends React.PureComponent { } } - async loadHistory( - eventType, - { filters, page, pageSize, sort, sortDirection } - ) { + async loadHistory(eventType, { filters, page, pageSize, sortings }) { const criteria = new openbis.EventSearchCriteria() criteria.withEventType().thatEquals(eventType) @@ -68,9 +65,9 @@ class HistoryGrid extends React.PureComponent { fo.from(page * pageSize) fo.count(pageSize) - if (sort && sortDirection) { - fo.sortBy()[sort]()[sortDirection]() - } + sortings.forEach(sorting => { + fo.sortBy()[sorting.columnName]()[sorting.sortDirection]() + }) const result = await openbis.searchEvents(criteria, fo) @@ -218,7 +215,6 @@ class HistoryGrid extends React.PureComponent { name: 'registrationDate', label: messages.get(messages.DATE), sortable: true, - sort: 'desc', getValue: ({ row }) => date.format(row.registrationDate.value), renderFilter: ({ value, onChange }) => { return <DateRangeField value={value} onChange={onChange} /> @@ -226,6 +222,8 @@ class HistoryGrid extends React.PureComponent { } ]} loadRows={this.load} + sort='registrationDate' + sortDirection='desc' selectable={true} /> ) diff --git a/openbis_ng_ui/srcTest/js/components/common/grid/wrapper/GridFilterWrapper.js b/openbis_ng_ui/srcTest/js/components/common/grid/wrapper/GridFilterWrapper.js index 3ff0b455c1ed421e08389464f24b834c4530e047..aaf6946a605227e93ea7503285fef0035fea29c5 100644 --- a/openbis_ng_ui/srcTest/js/components/common/grid/wrapper/GridFilterWrapper.js +++ b/openbis_ng_ui/srcTest/js/components/common/grid/wrapper/GridFilterWrapper.js @@ -1,6 +1,6 @@ import BaseWrapper from '@srcTest/js/components/common/wrapper/BaseWrapper.js' -export default class GridColumnFilterWrapper extends BaseWrapper { +export default class GridFilterWrapper extends BaseWrapper { getValue() { return this.getStringValue(this.wrapper.prop('filter')) } diff --git a/openbis_ng_ui/srcTest/js/components/common/grid/wrapper/GridHeaderWrapper.js b/openbis_ng_ui/srcTest/js/components/common/grid/wrapper/GridHeaderWrapper.js index 1b67986a30fbd7c8a59656a1e8abab50643a7276..863e5ac0bb63f25a31383ba5a021e610ff1ca641 100644 --- a/openbis_ng_ui/srcTest/js/components/common/grid/wrapper/GridHeaderWrapper.js +++ b/openbis_ng_ui/srcTest/js/components/common/grid/wrapper/GridHeaderWrapper.js @@ -1,12 +1,12 @@ import BaseWrapper from '@srcTest/js/components/common/wrapper/BaseWrapper.js' -export default class GridColumnLabelWrapper extends BaseWrapper { +export default class GridHeaderWrapper extends BaseWrapper { getValue() { return this.wrapper.text() } click() { - this.wrapper.instance().handleClick() + this.wrapper.instance().handleClick({}) } toJSON() {