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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,12 +18,6 @@ const QS_CONFIG = getQSConfig('notification', {
order_by: 'name', 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 { class NotificationList extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
@@ -204,7 +198,35 @@ class NotificationList extends Component {
itemCount={itemCount} itemCount={itemCount}
pluralizedItemName={i18n._(t`Notifications`)} pluralizedItemName={i18n._(t`Notifications`)}
qsConfig={QS_CONFIG} 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 => ( renderItem={notification => (
<NotificationListItem <NotificationListItem
key={notification.id} key={notification.id}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -119,24 +119,33 @@ function OrganizationsList({ i18n }) {
pluralizedItemName="Organizations" pluralizedItemName="Organizations"
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
onRowClick={handleSelect} 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`), name: i18n._(t`Name`),
key: 'name', key: 'name',
isSortable: true,
isSearchable: true,
}, },
{ {
name: i18n._(t`Modified`), name: i18n._(t`Modified`),
key: 'modified', key: 'modified',
isSortable: true,
isNumeric: true,
}, },
{ {
name: i18n._(t`Created`), name: i18n._(t`Created`),
key: 'created', key: 'created',
isSortable: true,
isNumeric: true,
}, },
]} ]}
renderToolbar={props => ( renderToolbar={props => (

View File

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

View File

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

View File

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

View File

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

View File

@@ -249,3 +249,18 @@ export const Group = shape({
inventory: number, inventory: number,
variables: string, 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) { if (!namespace) {
throw new Error('a QS namespace is required'); 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 { return {
namespace, namespace,
defaultParams, 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', () => { test('should throw if no namespace given', () => {
expect(() => getQSConfig()).toThrow(); expect(() => getQSConfig()).toThrow();
}); });
@@ -132,7 +144,7 @@ describe('qs (qs.js)', () => {
}; };
expect(getQSConfig('inventory', defaults)).toEqual({ expect(getQSConfig('inventory', defaults)).toEqual({
namespace: 'inventory', namespace: 'inventory',
defaultParams: { page: 1, page_size: 15 }, defaultParams: { page: 1, page_size: 15, order_by: 'name' },
integerFields: ['page', 'page_size'], integerFields: ['page', 'page_size'],
dateFields: ['modified', 'created'], dateFields: ['modified', 'created'],
}); });