From 8b9810e466cb849842b266742eec10c0ac10ef33 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 26 Nov 2019 17:23:53 -0500 Subject: [PATCH] update search and sort column configuration --- .../components/AddRole/AddResourceRole.jsx | 32 +++- .../components/AddRole/SelectResourceStep.jsx | 18 +- .../AddRole/SelectResourceStep.test.jsx | 31 ++-- .../DataListToolbar/DataListToolbar.jsx | 23 +-- .../DataListToolbar/DataListToolbar.test.jsx | 138 ++++++++++----- .../src/components/ListHeader/ListHeader.jsx | 33 +--- .../components/ListHeader/ListHeader.test.jsx | 31 ++-- .../Lookup/InstanceGroupsLookup.jsx | 12 +- .../src/components/Lookup/InventoryLookup.jsx | 15 +- .../components/Lookup/shared/OptionsList.jsx | 6 +- .../NotificationList/NotificationList.jsx | 36 +++- .../PaginatedDataList/PaginatedDataList.jsx | 37 ++-- .../ResourceAccessList/ResourceAccessList.jsx | 32 ++-- awx/ui_next/src/components/Search/Search.jsx | 15 +- .../src/components/Search/Search.test.jsx | 18 +- awx/ui_next/src/components/Sort/Sort.jsx | 76 +++++--- awx/ui_next/src/components/Sort/Sort.test.jsx | 167 +++++++++++++----- .../src/screens/Host/HostList/HostList.jsx | 23 ++- .../Inventory/InventoryList/InventoryList.jsx | 23 ++- .../src/screens/Job/JobList/JobList.jsx | 17 +- .../OrganizationList/OrganizationList.jsx | 23 ++- .../Project/ProjectList/ProjectList.jsx | 23 ++- .../src/screens/Team/TeamList/TeamList.jsx | 23 ++- .../Template/TemplateList/TemplateList.jsx | 23 ++- .../src/screens/User/UserList/UserList.jsx | 24 ++- awx/ui_next/src/types.js | 15 ++ awx/ui_next/src/util/qs.js | 4 + awx/ui_next/src/util/qs.test.js | 14 +- 28 files changed, 612 insertions(+), 320 deletions(-) diff --git a/awx/ui_next/src/components/AddRole/AddResourceRole.jsx b/awx/ui_next/src/components/AddRole/AddResourceRole.jsx index 596b3bbe97..f0542321b3 100644 --- a/awx/ui_next/src/components/AddRole/AddResourceRole.jsx +++ b/awx/ui_next/src/components/AddRole/AddResourceRole.jsx @@ -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 { {selectedResource === 'users' && ( 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 => ( 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 }; diff --git a/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx b/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx index 3d32abef06..6e37f1019d 100644 --- a/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx +++ b/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx @@ -6,8 +6,19 @@ import { sleep } from '../../../testUtils/testUtils'; import SelectResourceStep from './SelectResourceStep'; describe('', () => { - 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('', () => { test('initially renders without crashing', () => { shallow( {}} onSearch={() => {}} - sortedColumnKey="username" /> ); }); @@ -36,11 +47,11 @@ describe('', () => { }); mountWithContexts( {}} onSearch={handleSearch} - sortedColumnKey="username" /> ); expect(handleSearch).toHaveBeenCalledWith({ @@ -68,12 +79,12 @@ describe('', () => { }); const wrapper = await mountWithContexts( {}} onSearch={handleSearch} selectedResourceRows={selectedResourceRows} - sortedColumnKey="username" />, { context: { router: { history, route: { location: history.location } } }, @@ -102,12 +113,12 @@ describe('', () => { }; const wrapper = mountWithContexts( ({ data })} selectedResourceRows={[]} - sortedColumnKey="username" /> ); await sleep(0); diff --git a/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx b/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx index 305a0f0ae2..7248dc0c51 100644 --- a/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx +++ b/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx @@ -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 { @@ -134,10 +131,9 @@ class DataListToolbar extends React.Component { {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: [], }; diff --git a/awx/ui_next/src/components/DataListToolbar/DataListToolbar.test.jsx b/awx/ui_next/src/components/DataListToolbar/DataListToolbar.test.jsx index 376cbc8efa..8e5b705244 100644 --- a/awx/ui_next/src/components/DataListToolbar/DataListToolbar.test.jsx +++ b/awx/ui_next/src/components/DataListToolbar/DataListToolbar.test.jsx @@ -23,11 +23,13 @@ describe('', () => { 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('', () => { 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('', () => { 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( ); @@ -106,10 +116,9 @@ describe('', () => { searchDropdownItems.at(0).simulate('click', mockedSortEvent); toolbar = mountWithContexts( ); @@ -145,24 +154,57 @@ describe('', () => { }); 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( ); @@ -171,10 +213,9 @@ describe('', () => { toolbar = mountWithContexts( ); @@ -183,10 +224,9 @@ describe('', () => { toolbar = mountWithContexts( ); @@ -195,10 +235,9 @@ describe('', () => { toolbar = mountWithContexts( ); @@ -207,14 +246,18 @@ describe('', () => { }); 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( ', () => { }); 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( {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, }; diff --git a/awx/ui_next/src/components/ListHeader/ListHeader.test.jsx b/awx/ui_next/src/components/ListHeader/ListHeader.test.jsx index 9435f2e8b3..4480824a5a 100644 --- a/awx/ui_next/src/components/ListHeader/ListHeader.test.jsx +++ b/awx/ui_next/src/components/ListHeader/ListHeader.test.jsx @@ -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', () => { @@ -35,26 +38,20 @@ describe('ListHeader', () => { , { 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(''); }); diff --git a/awx/ui_next/src/components/Lookup/InstanceGroupsLookup.jsx b/awx/ui_next/src/components/Lookup/InstanceGroupsLookup.jsx index 20c2e0cf20..129eaf89d1 100644 --- a/awx/ui_next/src/components/Lookup/InstanceGroupsLookup.jsx +++ b/awx/ui_next/src/components/Lookup/InstanceGroupsLookup.jsx @@ -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" diff --git a/awx/ui_next/src/components/Lookup/InventoryLookup.jsx b/awx/ui_next/src/components/Lookup/InventoryLookup.jsx index 0286561a6a..67cd6bc965 100644 --- a/awx/ui_next/src/components/Lookup/InventoryLookup.jsx +++ b/awx/ui_next/src/components/Lookup/InventoryLookup.jsx @@ -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" diff --git a/awx/ui_next/src/components/Lookup/shared/OptionsList.jsx b/awx/ui_next/src/components/Lookup/shared/OptionsList.jsx index 0d8b92fed3..ae3451f9ff 100644 --- a/awx/ui_next/src/components/Lookup/shared/OptionsList.jsx +++ b/awx/ui_next/src/components/Lookup/shared/OptionsList.jsx @@ -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); diff --git a/awx/ui_next/src/components/NotificationList/NotificationList.jsx b/awx/ui_next/src/components/NotificationList/NotificationList.jsx index 8ae2c2c3f3..088f50d725 100644 --- a/awx/ui_next/src/components/NotificationList/NotificationList.jsx +++ b/awx/ui_next/src/components/NotificationList/NotificationList.jsx @@ -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 => ( {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 => , diff --git a/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx b/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx index 68ad3fe60d..66fba00327 100644 --- a/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx +++ b/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx @@ -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 => ( diff --git a/awx/ui_next/src/components/Search/Search.jsx b/awx/ui_next/src/components/Search/Search.jsx index a33e71e070..6dda72a866 100644 --- a/awx/ui_next/src/components/Search/Search.jsx +++ b/awx/ui_next/src/components/Search/Search.jsx @@ -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 }) => ( {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); diff --git a/awx/ui_next/src/components/Search/Search.test.jsx b/awx/ui_next/src/components/Search/Search.test.jsx index d0ee274107..cddce90761 100644 --- a/awx/ui_next/src/components/Search/Search.test.jsx +++ b/awx/ui_next/src/components/Search/Search.test.jsx @@ -8,7 +8,7 @@ describe('', () => { 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('', () => { 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 = mountWithContexts( @@ -47,13 +46,12 @@ describe('', () => { 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( @@ -65,19 +63,13 @@ describe('', () => { 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( diff --git a/awx/ui_next/src/components/Sort/Sort.jsx b/awx/ui_next/src/components/Sort/Sort.jsx index cc53ebdcce..5b9ba6f523 100644 --- a/awx/ui_next/src/components/Sort/Sort.jsx +++ b/awx/ui_next/src/components/Sort/Sort.jsx @@ -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 }) => ( {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)); diff --git a/awx/ui_next/src/components/Sort/Sort.test.jsx b/awx/ui_next/src/components/Sort/Sort.test.jsx index 0e0208cd16..10bd4a4bc4 100644 --- a/awx/ui_next/src/components/Sort/Sort.test.jsx +++ b/awx/ui_next/src/components/Sort/Sort.test.jsx @@ -12,8 +12,17 @@ describe('', () => { }); 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('', () => { const wrapper = mountWithContexts( @@ -36,20 +44,33 @@ describe('', () => { }); 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( ).find('Sort'); @@ -60,20 +81,33 @@ describe('', () => { }); 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( ).find('Sort'); @@ -84,20 +118,33 @@ describe('', () => { }); 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( ).find('Sort'); @@ -107,20 +154,33 @@ describe('', () => { }); 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( ).find('Sort'); @@ -135,18 +195,38 @@ describe('', () => { 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( @@ -157,8 +237,7 @@ describe('', () => { sort = mountWithContexts( @@ -169,8 +248,7 @@ describe('', () => { sort = mountWithContexts( @@ -181,8 +259,7 @@ describe('', () => { sort = mountWithContexts( diff --git a/awx/ui_next/src/screens/Host/HostList/HostList.jsx b/awx/ui_next/src/screens/Host/HostList/HostList.jsx index f907b78be9..c9b3958e3b 100644 --- a/awx/ui_next/src/screens/Host/HostList/HostList.jsx +++ b/awx/ui_next/src/screens/Host/HostList/HostList.jsx @@ -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 => ( diff --git a/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx b/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx index 5b43ce0e04..d0ffa22023 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx @@ -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 => ( diff --git a/awx/ui_next/src/screens/Job/JobList/JobList.jsx b/awx/ui_next/src/screens/Job/JobList/JobList.jsx index ad69ab8c18..0e732a4b61 100644 --- a/awx/ui_next/src/screens/Job/JobList/JobList.jsx +++ b/awx/ui_next/src/screens/Job/JobList/JobList.jsx @@ -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 => ( diff --git a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx index cba609f3aa..a5a4462be7 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx @@ -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 => ( diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx index fb6ed4357c..ab59019e18 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx @@ -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 => ( diff --git a/awx/ui_next/src/screens/Team/TeamList/TeamList.jsx b/awx/ui_next/src/screens/Team/TeamList/TeamList.jsx index 7d9ff71e26..5637962520 100644 --- a/awx/ui_next/src/screens/Team/TeamList/TeamList.jsx +++ b/awx/ui_next/src/screens/Team/TeamList/TeamList.jsx @@ -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 => ( diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx index 4516d44497..601d542ddb 100644 --- a/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx +++ b/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx @@ -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 => ( diff --git a/awx/ui_next/src/screens/User/UserList/UserList.jsx b/awx/ui_next/src/screens/User/UserList/UserList.jsx index 121d9ea6c3..a25d727b79 100644 --- a/awx/ui_next/src/screens/User/UserList/UserList.jsx +++ b/awx/ui_next/src/screens/User/UserList/UserList.jsx @@ -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 => ( diff --git a/awx/ui_next/src/types.js b/awx/ui_next/src/types.js index 0b6542f3ac..002f6f5a46 100644 --- a/awx/ui_next/src/types.js +++ b/awx/ui_next/src/types.js @@ -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, + }) +); diff --git a/awx/ui_next/src/util/qs.js b/awx/ui_next/src/util/qs.js index e7b38bad36..b029e62ecc 100644 --- a/awx/ui_next/src/util/qs.js +++ b/awx/ui_next/src/util/qs.js @@ -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, diff --git a/awx/ui_next/src/util/qs.test.js b/awx/ui_next/src/util/qs.test.js index c50ef8d217..d03d38baaa 100644 --- a/awx/ui_next/src/util/qs.test.js +++ b/awx/ui_next/src/util/qs.test.js @@ -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'], });