update search and sort column configuration

This commit is contained in:
John Mitchell 2019-11-26 17:23:53 -05:00
parent 16f9411914
commit 8b9810e466
28 changed files with 612 additions and 320 deletions

View File

@ -142,21 +142,33 @@ class AddResourceRole extends React.Component {
} = this.state;
const { onClose, roles, i18n } = this.props;
const userColumns = [
const userSearchColumns = [
{
name: i18n._(t`Username`),
key: 'username',
isSortable: true,
isSearchable: true,
isDefault: true
},
];
const teamColumns = [
const userSortColumns = [
{
name: i18n._(t`Name`),
name: i18n._(t`Username`),
key: 'username',
},
];
const teamSearchColumns = [
{
name: i18n._(t`name`),
key: 'name',
isDefault: true
},
];
const teamSortColumns = [
{
name: i18n._(t`name`),
key: 'name',
isSortable: true,
isSearchable: true,
},
];
@ -207,7 +219,8 @@ class AddResourceRole extends React.Component {
<Fragment>
{selectedResource === 'users' && (
<SelectResourceStep
columns={userColumns}
searchColumns={userSearchColumns}
sortColumns={userSortColumns}
displayKey="username"
onRowClick={this.handleResourceCheckboxClick}
onSearch={readUsers}
@ -218,7 +231,8 @@ class AddResourceRole extends React.Component {
)}
{selectedResource === 'teams' && (
<SelectResourceStep
columns={teamColumns}
searchColumns={teamSearchColumns}
sortColumns={teamSortColumns}
onRowClick={this.handleResourceCheckboxClick}
onSearch={readTeams}
selectedLabel={i18n._(t`Selected`)}

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { SearchColumns, SortColumns } from '@types';
import PaginatedDataList from '../PaginatedDataList';
import DataListToolbar from '../DataListToolbar';
import CheckboxListItem from '../CheckboxListItem';
@ -23,7 +24,7 @@ class SelectResourceStep extends React.Component {
this.qsConfig = getQSConfig('resource', {
page: 1,
page_size: 5,
order_by: props.sortedColumnKey,
order_by: `${props.sortColumns.filter(col => col.key === 'name').length ? 'name' : 'username'}`
});
}
@ -50,6 +51,8 @@ class SelectResourceStep extends React.Component {
const { data } = await onSearch(queryParams);
const { count, results } = data;
debugger;
this.setState({
resources: results,
count,
@ -69,7 +72,8 @@ class SelectResourceStep extends React.Component {
const { isInitialized, isLoading, count, error, resources } = this.state;
const {
columns,
searchColumns,
sortColumns,
displayKey,
onRowClick,
selectedLabel,
@ -99,8 +103,9 @@ class SelectResourceStep extends React.Component {
items={resources}
itemCount={count}
qsConfig={this.qsConfig}
toolbarColumns={columns}
onRowClick={onRowClick}
toolbarSearchColumns={searchColumns}
toolbarSortColumns={sortColumns}
renderItem={item => (
<CheckboxListItem
isSelected={selectedResourceRows.some(i => i.id === item.id)}
@ -123,21 +128,22 @@ class SelectResourceStep extends React.Component {
}
SelectResourceStep.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
searchColumns: SearchColumns,
sortColumns: SortColumns,
displayKey: PropTypes.string,
onRowClick: PropTypes.func,
onSearch: PropTypes.func.isRequired,
selectedLabel: PropTypes.string,
selectedResourceRows: PropTypes.arrayOf(PropTypes.object),
sortedColumnKey: PropTypes.string,
};
SelectResourceStep.defaultProps = {
searchColumns: null,
sortColumns: null,
displayKey: 'name',
onRowClick: () => {},
selectedLabel: null,
selectedResourceRows: [],
sortedColumnKey: 'name',
};
export { SelectResourceStep as _SelectResourceStep };

View File

@ -6,8 +6,19 @@ import { sleep } from '../../../testUtils/testUtils';
import SelectResourceStep from './SelectResourceStep';
describe('<SelectResourceStep />', () => {
const columns = [
{ name: 'Username', key: 'username', isSortable: true, isSearchable: true },
const searchColumns = [
{
name: 'Username',
key: 'username',
isDefault: true
},
];
const sortColumns = [
{
name: 'Username',
key: 'username'
},
];
afterEach(() => {
jest.restoreAllMocks();
@ -15,11 +26,11 @@ describe('<SelectResourceStep />', () => {
test('initially renders without crashing', () => {
shallow(
<SelectResourceStep
columns={columns}
searchColumns={searchColumns}
sortColumns={sortColumns}
displayKey="username"
onRowClick={() => {}}
onSearch={() => {}}
sortedColumnKey="username"
/>
);
});
@ -36,11 +47,11 @@ describe('<SelectResourceStep />', () => {
});
mountWithContexts(
<SelectResourceStep
columns={columns}
searchColumns={searchColumns}
sortColumns={sortColumns}
displayKey="username"
onRowClick={() => {}}
onSearch={handleSearch}
sortedColumnKey="username"
/>
);
expect(handleSearch).toHaveBeenCalledWith({
@ -68,12 +79,12 @@ describe('<SelectResourceStep />', () => {
});
const wrapper = await mountWithContexts(
<SelectResourceStep
columns={columns}
searchColumns={searchColumns}
sortColumns={sortColumns}
displayKey="username"
onRowClick={() => {}}
onSearch={handleSearch}
selectedResourceRows={selectedResourceRows}
sortedColumnKey="username"
/>,
{
context: { router: { history, route: { location: history.location } } },
@ -102,12 +113,12 @@ describe('<SelectResourceStep />', () => {
};
const wrapper = mountWithContexts(
<SelectResourceStep
columns={columns}
searchColumns={searchColumns}
sortColumns={sortColumns}
displayKey="username"
onRowClick={handleRowClick}
onSearch={() => ({ data })}
selectedResourceRows={[]}
sortedColumnKey="username"
/>
);
await sleep(0);

View File

@ -8,14 +8,13 @@ import {
ToolbarGroup as PFToolbarGroup,
ToolbarItem,
} from '@patternfly/react-core';
import styled from 'styled-components';
import ExpandCollapse from '../ExpandCollapse';
import Search from '../Search';
import Sort from '../Sort';
import VerticalSeparator from '../VerticalSeparator';
import { QSConfig } from '@types';
import { SearchColumns, SortColumns, QSConfig } from '@types';
const AWXToolbar = styled.div`
--awx-toolbar--BackgroundColor: var(--pf-global--BackgroundColor--light-100);
@ -86,7 +85,8 @@ const AdditionalControlsWrapper = styled.div`
class DataListToolbar extends React.Component {
render() {
const {
columns,
searchColumns,
sortColumns,
showSelectAll,
isAllSelected,
isCompact,
@ -96,8 +96,6 @@ class DataListToolbar extends React.Component {
onCompact,
onExpand,
onSelectAll,
sortOrder,
sortedColumnKey,
additionalControls,
i18n,
qsConfig,
@ -124,9 +122,8 @@ class DataListToolbar extends React.Component {
<ToolbarItem css="flex-grow: 1;">
<Search
qsConfig={qsConfig}
columns={columns}
columns={searchColumns}
onSearch={onSearch}
sortedColumnKey={sortedColumnKey}
/>
</ToolbarItem>
<VerticalSeparator />
@ -134,10 +131,9 @@ class DataListToolbar extends React.Component {
<ColumnRight fillWidth={fillWidth}>
<ToolbarItem>
<Sort
columns={columns}
qsConfig={qsConfig}
columns={sortColumns}
onSort={onSort}
sortOrder={sortOrder}
sortedColumnKey={sortedColumnKey}
/>
</ToolbarItem>
{showExpandCollapse && (
@ -165,7 +161,8 @@ class DataListToolbar extends React.Component {
DataListToolbar.propTypes = {
qsConfig: QSConfig.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
searchColumns: SearchColumns.isRequired,
sortColumns: SortColumns.isRequired,
showSelectAll: PropTypes.bool,
isAllSelected: PropTypes.bool,
isCompact: PropTypes.bool,
@ -175,8 +172,6 @@ DataListToolbar.propTypes = {
onSearch: PropTypes.func,
onSelectAll: PropTypes.func,
onSort: PropTypes.func,
sortOrder: PropTypes.string,
sortedColumnKey: PropTypes.string,
additionalControls: PropTypes.arrayOf(PropTypes.node),
};
@ -190,8 +185,6 @@ DataListToolbar.defaultProps = {
onSearch: null,
onSelectAll: null,
onSort: null,
sortOrder: 'ascending',
sortedColumnKey: 'name',
additionalControls: [],
};

View File

@ -23,11 +23,13 @@ describe('<DataListToolbar />', () => {
const onSort = jest.fn();
const onSelectAll = jest.fn();
test('it triggers the expected callbacks', () => {
const columns = [
{ name: 'Name', key: 'name', isSortable: true, isSearchable: true },
test('it triggers the expected callbacks', () => {
const searchColumns = [
{ name: 'Name', key: 'name', isDefault: true }
];
const sortColumns = [
{ name: 'Name', key: 'name' }
];
const search = 'button[aria-label="Search submit button"]';
const searchTextInput = 'input[aria-label="Search text input"]';
const selectAll = 'input[aria-label="Select all"]';
@ -38,9 +40,8 @@ describe('<DataListToolbar />', () => {
qsConfig={QS_CONFIG}
isAllSelected={false}
showExpandCollapse
sortedColumnKey="name"
sortOrder="ascending"
columns={columns}
searchColumns={searchColumns}
sortColumns={sortColumns}
onSearch={onSearch}
onSort={onSort}
onSelectAll={onSelectAll}
@ -74,19 +75,28 @@ describe('<DataListToolbar />', () => {
const searchDropdownMenuItems =
'DropdownMenu > ul[aria-labelledby="awx-search"]';
const multipleColumns = [
{ name: 'Foo', key: 'foo', isSortable: true, isSearchable: true },
{ name: 'Bar', key: 'bar', isSortable: true, isSearchable: true },
{ name: 'Bakery', key: 'bakery', isSortable: true },
{ name: 'Baz', key: 'baz' },
const NEW_QS_CONFIG = {
namespace: 'organization',
dateFields: ['modified', 'created'],
defaultParams: { page: 1, page_size: 5, order_by: 'foo' },
integerFields: ['page', 'page_size'],
};
const searchColumns = [
{ name: 'Foo', key: 'foo', isDefault: true },
{ name: 'Bar', key: 'bar' }
];
const sortColumns = [
{ name: 'Foo', key: 'foo' },
{ name: 'Bar', key: 'bar' },
{ name: 'Bakery', key: 'Bakery' }
];
toolbar = mountWithContexts(
<DataListToolbar
qsConfig={QS_CONFIG}
sortedColumnKey="foo"
sortOrder="ascending"
columns={multipleColumns}
qsConfig={NEW_QS_CONFIG}
searchColumns={searchColumns}
sortColumns={sortColumns}
onSort={onSort}
/>
);
@ -106,10 +116,9 @@ describe('<DataListToolbar />', () => {
searchDropdownItems.at(0).simulate('click', mockedSortEvent);
toolbar = mountWithContexts(
<DataListToolbar
qsConfig={QS_CONFIG}
sortedColumnKey="foo"
sortOrder="descending"
columns={multipleColumns}
qsConfig={NEW_QS_CONFIG}
searchColumns={searchColumns}
sortColumns={sortColumns}
onSort={onSort}
/>
);
@ -145,24 +154,57 @@ describe('<DataListToolbar />', () => {
});
test('it displays correct sort icon', () => {
const NUM_QS_CONFIG = {
namespace: 'organization',
dateFields: ['modified', 'created'],
defaultParams: { page: 1, page_size: 5, order_by: 'id' },
integerFields: ['page', 'page_size', 'id'],
};
const NUM_DESC_QS_CONFIG = {
namespace: 'organization',
dateFields: ['modified', 'created'],
defaultParams: { page: 1, page_size: 5, order_by: '-id' },
integerFields: ['page', 'page_size', 'id'],
};
const ALPH_QS_CONFIG = {
namespace: 'organization',
dateFields: ['modified', 'created'],
defaultParams: { page: 1, page_size: 5, order_by: 'name' },
integerFields: ['page', 'page_size', 'id'],
};
const ALPH_DESC_QS_CONFIG = {
namespace: 'organization',
dateFields: ['modified', 'created'],
defaultParams: { page: 1, page_size: 5, order_by: '-name' },
integerFields: ['page', 'page_size', 'id'],
};
const downNumericIconSelector = 'SortNumericDownIcon';
const upNumericIconSelector = 'SortNumericUpIcon';
const downAlphaIconSelector = 'SortAlphaDownIcon';
const upAlphaIconSelector = 'SortAlphaUpIcon';
const numericColumns = [
{ name: 'ID', key: 'id', isSortable: true, isNumeric: true },
{ name: 'ID', key: 'id', isDefault: true },
];
const alphaColumns = [
{ name: 'Name', key: 'name', isSortable: true, isNumeric: false },
{ name: 'Name', key: 'name', isDefault: true },
];
const searchColumns = [
{ name: 'Name', key: 'name', isDefault: true },
{ name: 'ID', key: 'id' }
];
toolbar = mountWithContexts(
<DataListToolbar
qsConfig={QS_CONFIG}
sortedColumnKey="id"
sortOrder="descending"
columns={numericColumns}
qsConfig={NUM_DESC_QS_CONFIG}
searchColumns={searchColumns}
sortColumns={numericColumns}
/>
);
@ -171,10 +213,9 @@ describe('<DataListToolbar />', () => {
toolbar = mountWithContexts(
<DataListToolbar
qsConfig={QS_CONFIG}
sortedColumnKey="id"
sortOrder="ascending"
columns={numericColumns}
qsConfig={NUM_QS_CONFIG}
searchColumns={searchColumns}
sortColumns={numericColumns}
/>
);
@ -183,10 +224,9 @@ describe('<DataListToolbar />', () => {
toolbar = mountWithContexts(
<DataListToolbar
qsConfig={QS_CONFIG}
sortedColumnKey="name"
sortOrder="descending"
columns={alphaColumns}
qsConfig={ALPH_DESC_QS_CONFIG}
searchColumns={searchColumns}
sortColumns={alphaColumns}
/>
);
@ -195,10 +235,9 @@ describe('<DataListToolbar />', () => {
toolbar = mountWithContexts(
<DataListToolbar
qsConfig={QS_CONFIG}
sortedColumnKey="name"
sortOrder="ascending"
columns={alphaColumns}
qsConfig={ALPH_QS_CONFIG}
searchColumns={searchColumns}
sortColumns={alphaColumns}
/>
);
@ -207,14 +246,18 @@ describe('<DataListToolbar />', () => {
});
test('should render additionalControls', () => {
const columns = [
{ name: 'Name', key: 'name', isSortable: true, isSearchable: true },
const searchColumns = [
{ name: 'Name', key: 'name', isDefault: true }
];
const sortColumns = [
{ name: 'Name', key: 'name', isDefault: true }
];
toolbar = mountWithContexts(
<DataListToolbar
qsConfig={QS_CONFIG}
columns={columns}
searchColumns={searchColumns}
sortColumns={sortColumns}
onSearch={onSearch}
onSort={onSort}
onSelectAll={onSelectAll}
@ -232,18 +275,19 @@ describe('<DataListToolbar />', () => {
});
test('it triggers the expected callbacks', () => {
const columns = [
{ name: 'Name', key: 'name', isSortable: true, isSearchable: true },
const searchColumns = [
{ name: 'Name', key: 'name', isDefault: true }
];
const sortColumns = [
{ name: 'Name', key: 'name' }
];
toolbar = mountWithContexts(
<DataListToolbar
qsConfig={QS_CONFIG}
isAllSelected
showExpandCollapse
sortedColumnKey="name"
sortOrder="ascending"
columns={columns}
searchColumns={searchColumns}
sortColumns={sortColumns}
onSearch={onSearch}
onSort={onSort}
onSelectAll={onSelectAll}

View File

@ -1,8 +1,7 @@
import React, { Fragment } from 'react';
import PropTypes, { arrayOf, shape, string, bool } from 'prop-types';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import styled from 'styled-components';
import DataListToolbar from '@components/DataListToolbar';
import FilterTags from '@components/FilterTags';
@ -13,7 +12,7 @@ import {
replaceParams,
removeParams,
} from '@util/qs';
import { QSConfig } from '@types';
import { QSConfig, SearchColumns, SortColumns } from '@types';
const EmptyStateControlsWrapper = styled.div`
display: flex;
@ -36,15 +35,6 @@ class ListHeader extends React.Component {
this.handleRemoveAll = this.handleRemoveAll.bind(this);
}
getSortOrder() {
const { qsConfig, location } = this.props;
const queryParams = parseQueryString(qsConfig, location.search);
if (queryParams.order_by && queryParams.order_by.startsWith('-')) {
return [queryParams.order_by.substr(1), 'descending'];
}
return [queryParams.order_by, 'ascending'];
}
handleSearch(key, value) {
const { location, qsConfig } = this.props;
const oldParams = parseQueryString(qsConfig, location.search);
@ -83,12 +73,12 @@ class ListHeader extends React.Component {
const {
emptyStateControls,
itemCount,
columns,
searchColumns,
sortColumns,
renderToolbar,
qsConfig,
location,
} = this.props;
const [orderBy, sortOrder] = this.getSortOrder();
const params = parseQueryString(qsConfig, location.search);
const isEmpty = itemCount === 0 && Object.keys(params).length === 0;
return (
@ -108,9 +98,8 @@ class ListHeader extends React.Component {
) : (
<Fragment>
{renderToolbar({
sortedColumnKey: orderBy,
sortOrder,
columns,
searchColumns,
sortColumns,
onSearch: this.handleSearch,
onSort: this.handleSort,
qsConfig,
@ -131,14 +120,8 @@ class ListHeader extends React.Component {
ListHeader.propTypes = {
itemCount: PropTypes.number.isRequired,
qsConfig: QSConfig.isRequired,
columns: arrayOf(
shape({
name: string.isRequired,
key: string.isRequired,
isSortable: bool,
isSearchable: bool,
})
).isRequired,
searchColumns: SearchColumns.isRequired,
sortColumns: SortColumns.isRequired,
renderToolbar: PropTypes.func,
};

View File

@ -7,7 +7,7 @@ import ListHeader from './ListHeader';
describe('ListHeader', () => {
const qsConfig = {
namespace: 'item',
defaultParams: { page: 1, page_size: 5, order_by: 'name' },
defaultParams: { page: 1, page_size: 5, order_by: 'foo' },
integerFields: [],
};
const renderToolbarFn = jest.fn();
@ -17,8 +17,11 @@ describe('ListHeader', () => {
<ListHeader
itemCount={50}
qsConfig={qsConfig}
columns={[
{ name: 'foo', key: 'foo', isSearchable: true, isSortable: true },
searchColumns={[
{ name: 'foo', key: 'foo', isDefault: true},
]}
sortColumns={[
{ name: 'foo', key: 'foo', isDefault: true},
]}
renderToolbar={renderToolbarFn}
/>
@ -35,26 +38,20 @@ describe('ListHeader', () => {
<ListHeader
itemCount={7}
qsConfig={qsConfig}
columns={[
{ name: 'name', key: 'name', isSearchable: true, isSortable: true },
searchColumns={[
{ name: 'foo', key: 'foo', isDefault: true},
]}
sortColumns={[
{ name: 'foo', key: 'foo', isDefault: true},
]}
/>,
{ context: { router: { history } } }
);
const toolbar = wrapper.find('DataListToolbar');
expect(toolbar.prop('sortedColumnKey')).toEqual('name');
expect(toolbar.prop('sortOrder')).toEqual('ascending');
toolbar.prop('onSort')('name', 'descending');
expect(history.location.search).toEqual('?item.order_by=-name');
await sleep(0);
wrapper.update();
expect(toolbar.prop('sortedColumnKey')).toEqual('name');
// TODO: this assertion required updating queryParams prop. Consider
// fixing after #147 is done:
// expect(toolbar.prop('sortOrder')).toEqual('descending');
toolbar.prop('onSort')('name', 'ascending');
toolbar.prop('onSort')('foo', 'descending');
expect(history.location.search).toEqual('?item.order_by=-foo');
toolbar.prop('onSort')('foo', 'ascending');
// since order_by = name is the default, that should be strip out of the search
expect(history.location.search).toEqual('');
});

View File

@ -64,26 +64,24 @@ function InstanceGroupsLookup(props) {
value={state.selectedItems}
options={instanceGroups}
optionCount={count}
columns={[
searchColumns={[
{
name: i18n._(t`Name`),
key: 'name',
isSortable: true,
isSearchable: true,
},
{
name: i18n._(t`Modified`),
key: 'modified',
isSortable: false,
isNumeric: true,
},
{
name: i18n._(t`Created`),
key: 'created',
isSortable: false,
isNumeric: true,
},
]}
sortColumns={[{
name: i18n._(t`Name`),
key: 'name'
}]}
multiple={state.multiple}
header={i18n._(t`Instance Groups`)}
name="instanceGroups"

View File

@ -68,21 +68,24 @@ function InventoryLookup({
value={state.selectedItems}
options={inventories}
optionCount={count}
columns={[
{ name: i18n._(t`Name`), key: 'name', isSortable: true },
searchColumns={[
{
name: i18n._(t`Name`),
key: 'name',
},
{
name: i18n._(t`Modified`),
key: 'modified',
isSortable: false,
isNumeric: true,
},
{
name: i18n._(t`Created`),
key: 'created',
isSortable: false,
isNumeric: true,
},
]}
sortColumns={[{
name: i18n._(t`Name`),
key: 'name'
}]}
multiple={state.multiple}
header={i18n._(t`Inventory`)}
name="inventory"

View File

@ -14,7 +14,7 @@ import SelectedList from '../../SelectedList';
import PaginatedDataList from '../../PaginatedDataList';
import CheckboxListItem from '../../CheckboxListItem';
import DataListToolbar from '../../DataListToolbar';
import { QSConfig } from '@types';
import { QSConfig, SearchColumns, SortColumns } from '@types';
function OptionsList({
value,
@ -80,7 +80,8 @@ OptionsList.propTypes = {
value: arrayOf(Item).isRequired,
options: arrayOf(Item).isRequired,
optionCount: number.isRequired,
columns: arrayOf(shape({})),
searchColumns: SearchColumns.isRequired,
sortColumns: SortColumns.isRequired,
multiple: bool,
qsConfig: QSConfig.isRequired,
selectItem: func.isRequired,
@ -90,7 +91,6 @@ OptionsList.propTypes = {
OptionsList.defaultProps = {
multiple: false,
renderItemChip: null,
columns: [],
};
export default withI18n()(OptionsList);

View File

@ -18,12 +18,6 @@ const QS_CONFIG = getQSConfig('notification', {
order_by: 'name',
});
const COLUMNS = [
{ key: 'name', name: 'Name', isSortable: true, isSearchable: true },
{ key: 'modified', name: 'Modified', isSortable: true, isNumeric: true },
{ key: 'created', name: 'Created', isSortable: true, isNumeric: true },
];
class NotificationList extends Component {
constructor(props) {
super(props);
@ -204,7 +198,35 @@ class NotificationList extends Component {
itemCount={itemCount}
pluralizedItemName={i18n._(t`Notifications`)}
qsConfig={QS_CONFIG}
toolbarColumns={COLUMNS}
toolbarSearchColumns={[
{
name: i18n._(t`Name`),
key: 'name',
isDefault: true
},
{
name: i18n._(t`Modified`),
key: 'modified',
},
{
name: i18n._(t`Created`),
key: 'created',
},
]}
toolbarSortColumns={[
{
name: i18n._(t`Name`),
key: 'name',
},
{
name: i18n._(t`Modified`),
key: 'modified',
},
{
name: i18n._(t`Created`),
key: 'created',
},
]}
renderItem={notification => (
<NotificationListItem
key={notification.id}

View File

@ -1,5 +1,5 @@
import React, { Fragment } from 'react';
import PropTypes, { arrayOf, shape, string, bool } from 'prop-types';
import PropTypes from 'prop-types';
import { DataList } from '@patternfly/react-core';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
@ -18,7 +18,7 @@ import {
replaceParams,
} from '@util/qs';
import { QSConfig } from '@types';
import { QSConfig, SearchColumns, SortColumns } from '@types';
import PaginatedDataListItem from './PaginatedDataListItem';
@ -66,21 +66,29 @@ class PaginatedDataList extends React.Component {
itemCount,
qsConfig,
renderItem,
toolbarColumns,
toolbarSearchColumns,
toolbarSortColumns,
pluralizedItemName,
showPageSizeOptions,
location,
i18n,
renderToolbar,
} = this.props;
const columns = toolbarColumns.length
? toolbarColumns
const searchColumns = toolbarSearchColumns.length
? toolbarSearchColumns
: [
{
name: i18n._(t`Name`),
key: 'name',
isDefault: true
},
];
const sortColumns = toolbarSortColumns.length
? toolbarSortColumns
: [
{
name: i18n._(t`Name`),
key: 'name',
isSortable: true,
isSearchable: true,
},
];
const queryParams = parseQueryString(qsConfig, location.search);
@ -117,7 +125,8 @@ class PaginatedDataList extends React.Component {
itemCount={itemCount}
renderToolbar={renderToolbar}
emptyStateControls={emptyStateControls}
columns={columns}
searchColumns={searchColumns}
sortColumns={sortColumns}
qsConfig={qsConfig}
/>
{Content}
@ -158,13 +167,8 @@ PaginatedDataList.propTypes = {
pluralizedItemName: PropTypes.string,
qsConfig: QSConfig.isRequired,
renderItem: PropTypes.func,
toolbarColumns: arrayOf(
shape({
name: string.isRequired,
key: string.isRequired,
isSortable: bool,
})
),
toolbarSearchColumns: SearchColumns,
toolbarSortColumns: SortColumns,
showPageSizeOptions: PropTypes.bool,
renderToolbar: PropTypes.func,
hasContentLoading: PropTypes.bool,
@ -175,7 +179,8 @@ PaginatedDataList.propTypes = {
PaginatedDataList.defaultProps = {
hasContentLoading: false,
contentError: null,
toolbarColumns: [],
toolbarSearchColumns: [],
toolbarSortColumns: [],
pluralizedItemName: 'Items',
showPageSizeOptions: true,
renderItem: item => <PaginatedDataListItem key={item.id} item={item} />,

View File

@ -162,24 +162,34 @@ class ResourceAccessList extends React.Component {
itemCount={itemCount}
pluralizedItemName="Roles"
qsConfig={QS_CONFIG}
toolbarColumns={[
{
name: i18n._(t`First Name`),
key: 'first_name',
isSortable: true,
isSearchable: true,
},
toolbarSearchColumns={[
{
name: i18n._(t`Username`),
key: 'username',
isSortable: true,
isSearchable: true,
isDefault: true
},
{
name: i18n._(t`First Name`),
key: 'first_name',
},
{
name: i18n._(t`Last Name`),
key: 'last_name',
},
]}
toolbarSortColumns={[
{
name: i18n._(t`Username`),
key: 'username',
isDefault: true
},
{
name: i18n._(t`First Name`),
key: 'first_name',
},
{
name: i18n._(t`Last Name`),
key: 'last_name',
isSortable: true,
isSearchable: true,
},
]}
renderToolbar={props => (

View File

@ -14,7 +14,7 @@ import {
} from '@patternfly/react-core';
import { SearchIcon } from '@patternfly/react-icons';
import { QSConfig } from '@types';
import { QSConfig, SearchColumns } from '@types';
import styled from 'styled-components';
@ -82,10 +82,11 @@ class Search extends React.Component {
constructor(props) {
super(props);
const { sortedColumnKey } = this.props;
const { columns } = this.props;
this.state = {
isSearchDropdownOpen: false,
searchKey: sortedColumnKey,
searchKey: columns.find(col => col.isDefault).key,
searchValue: '',
};
@ -142,7 +143,7 @@ class Search extends React.Component {
);
const searchDropdownItems = columns
.filter(({ key, isSearchable }) => isSearchable && key !== searchKey)
.filter(({ key }) => key !== searchKey)
.map(({ key, name }) => (
<DropdownItem key={key} component="button">
{name}
@ -214,14 +215,12 @@ class Search extends React.Component {
Search.propTypes = {
qsConfig: QSConfig.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSearch: PropTypes.func,
sortedColumnKey: PropTypes.string,
columns: SearchColumns.isRequired,
onSearch: PropTypes.func
};
Search.defaultProps = {
onSearch: null,
sortedColumnKey: 'name',
};
export default withI18n()(Search);

View File

@ -8,7 +8,7 @@ describe('<Search />', () => {
const QS_CONFIG = {
namespace: 'organization',
dateFields: ['modified', 'created'],
defaultParams: { page: 1, page_size: 5, order_by: 'name' },
defaulkntParams: { page: 1, page_size: 5, order_by: 'name' },
integerFields: ['page', 'page_size'],
};
@ -20,7 +20,7 @@ describe('<Search />', () => {
test('it triggers the expected callbacks', () => {
const columns = [
{ name: 'Name', key: 'name', isSortable: true, isSearchable: true },
{ name: 'Name', key: 'name', isDefault: true }
];
const searchBtn = 'button[aria-label="Search submit button"]';
@ -31,7 +31,6 @@ describe('<Search />', () => {
search = mountWithContexts(
<Search
qsConfig={QS_CONFIG}
sortedColumnKey="name"
columns={columns}
onSearch={onSearch}
/>
@ -47,13 +46,12 @@ describe('<Search />', () => {
test('handleDropdownToggle properly updates state', async () => {
const columns = [
{ name: 'Name', key: 'name', isSortable: true, isSearchable: true },
{ name: 'Name', key: 'name', isDefault: true }
];
const onSearch = jest.fn();
const wrapper = mountWithContexts(
<Search
qsConfig={QS_CONFIG}
sortedColumnKey="name"
columns={columns}
onSearch={onSearch}
/>
@ -65,19 +63,13 @@ describe('<Search />', () => {
test('handleDropdownSelect properly updates state', async () => {
const columns = [
{ name: 'Name', key: 'name', isSortable: true, isSearchable: true },
{
name: 'Description',
key: 'description',
isSortable: true,
isSearchable: true,
},
{ name: 'Name', key: 'name', isDefault: true },
{ name: 'Description', key: 'description' },
];
const onSearch = jest.fn();
const wrapper = mountWithContexts(
<Search
qsConfig={QS_CONFIG}
sortedColumnKey="name"
columns={columns}
onSearch={onSearch}
/>

View File

@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react';
import { withRouter } from 'react-router-dom';
import { t } from '@lingui/macro';
import {
Button,
@ -19,6 +20,11 @@ import {
import styled from 'styled-components';
import {
parseQueryString
} from '@util/qs';
import { SortColumns, QSConfig } from '@types';
const Dropdown = styled(PFDropdown)`
&&& {
> button {
@ -68,8 +74,31 @@ class Sort extends React.Component {
constructor(props) {
super(props);
let sortKey;
let sortOrder;
let isNumeric;
const { qsConfig, location } = this.props;
const queryParams = parseQueryString(qsConfig, location.search);
if (queryParams.order_by && queryParams.order_by.startsWith('-')) {
sortKey = queryParams.order_by.substr(1);
sortOrder = 'descending';
} else if (queryParams.order_by) {
sortKey = queryParams.order_by;
sortOrder = 'ascending';
}
if (qsConfig.integerFields.filter(field => field === sortKey).length) {
isNumeric = true;
} else {
isNumeric = false;
}
this.state = {
isSortDropdownOpen: false,
sortKey,
sortOrder,
isNumeric
};
this.handleDropdownToggle = this.handleDropdownToggle.bind(this);
@ -82,34 +111,44 @@ class Sort extends React.Component {
}
handleDropdownSelect({ target }) {
const { columns, onSort, sortOrder } = this.props;
const { columns, onSort, qsConfig } = this.props;
const { sortOrder } = this.state;
const { innerText } = target;
const [{ key: searchKey }] = columns.filter(
const [{ key: sortKey }] = columns.filter(
({ name }) => name === innerText
);
this.setState({ isSortDropdownOpen: false });
onSort(searchKey, sortOrder);
let isNumeric;
if (qsConfig.integerFields.filter(field => field === sortKey).length) {
isNumeric = true;
} else {
isNumeric = false;
}
this.setState({ isSortDropdownOpen: false, sortKey, isNumeric });
onSort(sortKey, sortOrder);
}
handleSort() {
const { onSort, sortedColumnKey, sortOrder } = this.props;
const { onSort } = this.props;
const { sortKey, sortOrder } = this.state;
const newSortOrder = sortOrder === 'ascending' ? 'descending' : 'ascending';
onSort(sortedColumnKey, newSortOrder);
this.setState({ sortOrder: newSortOrder });
onSort(sortKey, newSortOrder);
}
render() {
const { up } = DropdownPosition;
const { columns, sortedColumnKey, sortOrder, i18n } = this.props;
const { isSortDropdownOpen } = this.state;
const [{ name: sortedColumnName, isNumeric }] = columns.filter(
({ key }) => key === sortedColumnKey
const { columns, i18n } = this.props;
const { isSortDropdownOpen, sortKey, sortOrder, isNumeric } = this.state;
const [{ name: sortedColumnName }] = columns.filter(
({ key }) => key === sortKey
);
const sortDropdownItems = columns
.filter(({ key, isSortable }) => isSortable && key !== sortedColumnKey)
.filter(({ key }) => key !== sortKey)
.map(({ key, name }) => (
<DropdownItem key={key} component="button">
{name}
@ -168,16 +207,13 @@ class Sort extends React.Component {
}
Sort.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSort: PropTypes.func,
sortOrder: PropTypes.string,
sortedColumnKey: PropTypes.string,
qsConfig: QSConfig.isRequired,
columns: SortColumns.isRequired,
onSort: PropTypes.func
};
Sort.defaultProps = {
onSort: null,
sortOrder: 'ascending',
sortedColumnKey: 'name',
onSort: null
};
export default withI18n()(Sort);
export default withI18n()(withRouter(Sort));

View File

@ -12,8 +12,17 @@ describe('<Sort />', () => {
});
test('it triggers the expected callbacks', () => {
const qsConfig = {
namespace: 'item',
defaultParams: { page: 1, page_size: 5, order_by: 'name' },
integerFields: ['page', 'page_size'],
};
const columns = [
{ name: 'Name', key: 'name', isSortable: true, isSearchable: true },
{
name: 'Name',
key: 'name',
},
];
const sortBtn = 'button[aria-label="Sort"]';
@ -22,8 +31,7 @@ describe('<Sort />', () => {
const wrapper = mountWithContexts(
<Sort
sortedColumnKey="name"
sortOrder="ascending"
qsConfig={qsConfig}
columns={columns}
onSort={onSort}
/>
@ -36,20 +44,33 @@ describe('<Sort />', () => {
});
test('onSort properly passes back descending when ascending was passed as prop', () => {
const multipleColumns = [
{ name: 'Foo', key: 'foo', isSortable: true },
{ name: 'Bar', key: 'bar', isSortable: true },
{ name: 'Bakery', key: 'bakery', isSortable: true },
{ name: 'Baz', key: 'baz' },
const qsConfig = {
namespace: 'item',
defaultParams: { page: 1, page_size: 5, order_by: 'foo' },
integerFields: ['page', 'page_size'],
};
const columns = [
{
name: 'Foo',
key: 'foo',
},
{
name: 'Bar',
key: 'bar',
},
{
name: 'Bakery',
key: 'bakery',
}
];
const onSort = jest.fn();
const wrapper = mountWithContexts(
<Sort
sortedColumnKey="foo"
sortOrder="ascending"
columns={multipleColumns}
qsConfig={qsConfig}
columns={columns}
onSort={onSort}
/>
).find('Sort');
@ -60,20 +81,33 @@ describe('<Sort />', () => {
});
test('onSort properly passes back ascending when descending was passed as prop', () => {
const multipleColumns = [
{ name: 'Foo', key: 'foo', isSortable: true },
{ name: 'Bar', key: 'bar', isSortable: true },
{ name: 'Bakery', key: 'bakery', isSortable: true },
{ name: 'Baz', key: 'baz' },
const qsConfig = {
namespace: 'item',
defaultParams: { page: 1, page_size: 5, order_by: '-foo' },
integerFields: ['page', 'page_size'],
};
const columns = [
{
name: 'Foo',
key: 'foo',
},
{
name: 'Bar',
key: 'bar',
},
{
name: 'Bakery',
key: 'bakery',
}
];
const onSort = jest.fn();
const wrapper = mountWithContexts(
<Sort
sortedColumnKey="foo"
sortOrder="descending"
columns={multipleColumns}
qsConfig={qsConfig}
columns={columns}
onSort={onSort}
/>
).find('Sort');
@ -84,20 +118,33 @@ describe('<Sort />', () => {
});
test('Changing dropdown correctly passes back new sort key', () => {
const multipleColumns = [
{ name: 'Foo', key: 'foo', isSortable: true },
{ name: 'Bar', key: 'bar', isSortable: true },
{ name: 'Bakery', key: 'bakery', isSortable: true },
{ name: 'Baz', key: 'baz' },
const qsConfig = {
namespace: 'item',
defaultParams: { page: 1, page_size: 5, order_by: 'foo' },
integerFields: ['page', 'page_size'],
};
const columns = [
{
name: 'Foo',
key: 'foo',
},
{
name: 'Bar',
key: 'bar',
},
{
name: 'Bakery',
key: 'bakery',
}
];
const onSort = jest.fn();
const wrapper = mountWithContexts(
<Sort
sortedColumnKey="foo"
sortOrder="ascending"
columns={multipleColumns}
qsConfig={qsConfig}
columns={columns}
onSort={onSort}
/>
).find('Sort');
@ -107,20 +154,33 @@ describe('<Sort />', () => {
});
test('Opening dropdown correctly updates state', () => {
const multipleColumns = [
{ name: 'Foo', key: 'foo', isSortable: true },
{ name: 'Bar', key: 'bar', isSortable: true },
{ name: 'Bakery', key: 'bakery', isSortable: true },
{ name: 'Baz', key: 'baz' },
const qsConfig = {
namespace: 'item',
defaultParams: { page: 1, page_size: 5, order_by: 'foo' },
integerFields: ['page', 'page_size'],
};
const columns = [
{
name: 'Foo',
key: 'foo',
},
{
name: 'Bar',
key: 'bar',
},
{
name: 'Bakery',
key: 'bakery',
}
];
const onSort = jest.fn();
const wrapper = mountWithContexts(
<Sort
sortedColumnKey="foo"
sortOrder="ascending"
columns={multipleColumns}
qsConfig={qsConfig}
columns={columns}
onSort={onSort}
/>
).find('Sort');
@ -135,18 +195,38 @@ describe('<Sort />', () => {
const downAlphaIconSelector = 'SortAlphaDownIcon';
const upAlphaIconSelector = 'SortAlphaUpIcon';
const qsConfigNumDown = {
namespace: 'item',
defaultParams: { page: 1, page_size: 5, order_by: '-id' },
integerFields: ['page', 'page_size', 'id'],
};
const qsConfigNumUp = {
namespace: 'item',
defaultParams: { page: 1, page_size: 5, order_by: 'id' },
integerFields: ['page', 'page_size', 'id'],
};
const qsConfigAlphaDown = {
namespace: 'item',
defaultParams: { page: 1, page_size: 5, order_by: '-name' },
integerFields: ['page', 'page_size'],
};
const qsConfigAlphaUp = {
namespace: 'item',
defaultParams: { page: 1, page_size: 5, order_by: 'name' },
integerFields: ['page', 'page_size'],
};
const numericColumns = [
{ name: 'ID', key: 'id', isSortable: true, isNumeric: true },
{ name: 'ID', key: 'id' },
];
const alphaColumns = [
{ name: 'Name', key: 'name', isSortable: true, isNumeric: false },
{ name: 'Name', key: 'name' },
];
const onSort = jest.fn();
sort = mountWithContexts(
<Sort
sortedColumnKey="id"
sortOrder="descending"
qsConfig={qsConfigNumDown}
columns={numericColumns}
onSort={onSort}
/>
@ -157,8 +237,7 @@ describe('<Sort />', () => {
sort = mountWithContexts(
<Sort
sortedColumnKey="id"
sortOrder="ascending"
qsConfig={qsConfigNumUp}
columns={numericColumns}
onSort={onSort}
/>
@ -169,8 +248,7 @@ describe('<Sort />', () => {
sort = mountWithContexts(
<Sort
sortedColumnKey="name"
sortOrder="descending"
qsConfig={qsConfigAlphaDown}
columns={alphaColumns}
onSort={onSort}
/>
@ -181,8 +259,7 @@ describe('<Sort />', () => {
sort = mountWithContexts(
<Sort
sortedColumnKey="name"
sortOrder="ascending"
qsConfig={qsConfigAlphaUp}
columns={alphaColumns}
onSort={onSort}
/>

View File

@ -190,24 +190,33 @@ class HostsList extends Component {
pluralizedItemName={i18n._(t`Hosts`)}
qsConfig={QS_CONFIG}
onRowClick={this.handleSelect}
toolbarColumns={[
toolbarSearchColumns={[
{
name: i18n._(t`Name`),
key: 'name',
isDefault: true
},
{
name: i18n._(t`Modified`),
key: 'modified',
},
{
name: i18n._(t`Created`),
key: 'created',
},
]}
toolbarSortColumns={[
{
name: i18n._(t`Name`),
key: 'name',
isSortable: true,
isSearchable: true,
},
{
name: i18n._(t`Modified`),
key: 'modified',
isSortable: true,
isNumeric: true,
},
{
name: i18n._(t`Created`),
key: 'created',
isSortable: true,
isNumeric: true,
},
]}
renderToolbar={props => (

View File

@ -174,24 +174,33 @@ class InventoriesList extends Component {
pluralizedItemName={i18n._(t`Inventories`)}
qsConfig={QS_CONFIG}
onRowClick={this.handleSelect}
toolbarColumns={[
toolbarSearchColumns={[
{
name: i18n._(t`Name`),
key: 'name',
isDefault: true
},
{
name: i18n._(t`Modified`),
key: 'modified',
},
{
name: i18n._(t`Created`),
key: 'created',
},
]}
toolbarSortColumns={[
{
name: i18n._(t`Name`),
key: 'name',
isSortable: true,
isSearchable: true,
},
{
name: i18n._(t`Modified`),
key: 'modified',
isSortable: true,
isNumeric: true,
},
{
name: i18n._(t`Created`),
key: 'created',
isSortable: true,
isNumeric: true,
},
]}
renderToolbar={props => (

View File

@ -163,18 +163,25 @@ class JobList extends Component {
pluralizedItemName="Jobs"
qsConfig={QS_CONFIG}
onRowClick={this.handleSelect}
toolbarColumns={[
toolbarSearchColumns={[
{
name: i18n._(t`Name`),
key: 'name',
isDefault: true
},
{
name: i18n._(t`Finished`),
key: 'finished',
},
]}
toolbarSortColumns={[
{
name: i18n._(t`Name`),
key: 'name',
isSortable: true,
isSearchable: true,
},
{
name: i18n._(t`Finished`),
key: 'finished',
isSortable: true,
isNumeric: true,
},
]}
renderToolbar={props => (

View File

@ -119,24 +119,33 @@ function OrganizationsList({ i18n }) {
pluralizedItemName="Organizations"
qsConfig={QS_CONFIG}
onRowClick={handleSelect}
toolbarColumns={[
toolbarSearchColumns={[
{
name: i18n._(t`Name`),
key: 'name',
isDefault: true
},
{
name: i18n._(t`Modified`),
key: 'modified',
},
{
name: i18n._(t`Created`),
key: 'created',
},
]}
toolbarSortColumns={[
{
name: i18n._(t`Name`),
key: 'name',
isSortable: true,
isSearchable: true,
},
{
name: i18n._(t`Modified`),
key: 'modified',
isSortable: true,
isNumeric: true,
},
{
name: i18n._(t`Created`),
key: 'created',
isSortable: true,
isNumeric: true,
},
]}
renderToolbar={props => (

View File

@ -156,24 +156,33 @@ class ProjectsList extends Component {
pluralizedItemName={i18n._(t`Projects`)}
qsConfig={QS_CONFIG}
onRowClick={this.handleSelect}
toolbarColumns={[
toolbarSearchColumns={[
{
name: i18n._(t`Name`),
key: 'name',
isDefault: true
},
{
name: i18n._(t`Modified`),
key: 'modified',
},
{
name: i18n._(t`Created`),
key: 'created',
},
]}
toolbarSortColumns={[
{
name: i18n._(t`Name`),
key: 'name',
isSortable: true,
isSearchable: true,
},
{
name: i18n._(t`Modified`),
key: 'modified',
isSortable: true,
isNumeric: true,
},
{
name: i18n._(t`Created`),
key: 'created',
isSortable: true,
isNumeric: true,
},
]}
renderToolbar={props => (

View File

@ -154,24 +154,33 @@ class TeamsList extends Component {
pluralizedItemName={i18n._(t`Teams`)}
qsConfig={QS_CONFIG}
onRowClick={this.handleSelect}
toolbarColumns={[
toolbarSearchColumns={[
{
name: i18n._(t`Name`),
key: 'name',
isDefault: true
},
{
name: i18n._(t`Modified`),
key: 'modified',
},
{
name: i18n._(t`Created`),
key: 'created',
},
]}
toolbarSortColumns={[
{
name: i18n._(t`Name`),
key: 'name',
isSortable: true,
isSearchable: true,
},
{
name: i18n._(t`Modified`),
key: 'modified',
isSortable: true,
isNumeric: true,
},
{
name: i18n._(t`Created`),
key: 'created',
isSortable: true,
isNumeric: true,
},
]}
renderToolbar={props => (

View File

@ -214,24 +214,33 @@ class TemplatesList extends Component {
pluralizedItemName={i18n._(t`Templates`)}
qsConfig={QS_CONFIG}
onRowClick={this.handleSelect}
toolbarColumns={[
toolbarSearchColumns={[
{
name: i18n._(t`Name`),
key: 'name',
isDefault: true
},
{
name: i18n._(t`Modified`),
key: 'modified',
},
{
name: i18n._(t`Created`),
key: 'created',
},
]}
toolbarSortColumns={[
{
name: i18n._(t`Name`),
key: 'name',
isSortable: true,
isSearchable: true,
},
{
name: i18n._(t`Modified`),
key: 'modified',
isSortable: true,
isNumeric: true,
},
{
name: i18n._(t`Created`),
key: 'created',
isSortable: true,
isNumeric: true,
},
]}
renderToolbar={props => (

View File

@ -154,24 +154,34 @@ class UsersList extends Component {
pluralizedItemName="Users"
qsConfig={QS_CONFIG}
onRowClick={this.handleSelect}
toolbarColumns={[
toolbarSearchColumns={[
{
name: i18n._(t`Username`),
key: 'username',
isSortable: true,
isSearchable: true,
isDefault: true
},
{
name: i18n._(t`First Name`),
key: 'first_name',
},
{
name: i18n._(t`Last Name`),
key: 'last_name',
},
]}
toolbarSortColumns={[
{
name: i18n._(t`Username`),
key: 'username',
isDefault: true
},
{
name: i18n._(t`First Name`),
key: 'first_name',
isSortable: true,
isSearchable: true,
},
{
name: i18n._(t`Last Name`),
key: 'last_name',
isSortable: true,
isSearchable: true,
},
]}
renderToolbar={props => (

View File

@ -249,3 +249,18 @@ export const Group = shape({
inventory: number,
variables: string,
});
export const SearchColumns = arrayOf(
shape({
name: string.isRequired,
key: string.isRequired,
isDefault: bool,
})
);
export const SortColumns = arrayOf(
shape({
name: string.isRequired,
key: string.isRequired,
})
);

View File

@ -14,6 +14,10 @@ export function getQSConfig(
if (!namespace) {
throw new Error('a QS namespace is required');
}
// if order_by isn't passed, default to name
if (!Object.keys(defaultParams).filter(key => key === 'order_by').length) {
defaultParams.order_by = 'name';
}
return {
namespace,
defaultParams,

View File

@ -121,6 +121,18 @@ describe('qs (qs.js)', () => {
});
});
test('should set order_by in defaultParams if it is not passed', () => {
expect(getQSConfig('organization', {
page: 1,
page_size: 5,
})).toEqual({
namespace: 'organization',
defaultParams: { page: 1, page_size: 5, order_by: 'name' },
integerFields: ['page', 'page_size'],
dateFields: ['modified', 'created'],
});
});
test('should throw if no namespace given', () => {
expect(() => getQSConfig()).toThrow();
});
@ -132,7 +144,7 @@ describe('qs (qs.js)', () => {
};
expect(getQSConfig('inventory', defaults)).toEqual({
namespace: 'inventory',
defaultParams: { page: 1, page_size: 15 },
defaultParams: { page: 1, page_size: 15, order_by: 'name' },
integerFields: ['page', 'page_size'],
dateFields: ['modified', 'created'],
});