diff --git a/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx b/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx index 31a1184cc9..331fcacafc 100644 --- a/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx +++ b/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx @@ -47,6 +47,7 @@ class DataListToolbar extends React.Component { isCompact, onSort, onSearch, + onReplaceSearch, onRemove, onCompact, onExpand, @@ -78,6 +79,7 @@ class DataListToolbar extends React.Component { qsConfig={qsConfig} columns={searchColumns} onSearch={onSearch} + onReplaceSearch={onReplaceSearch} onRemove={onRemove} /> @@ -124,6 +126,7 @@ DataListToolbar.propTypes = { onCompact: PropTypes.func, onExpand: PropTypes.func, onSearch: PropTypes.func, + onReplaceSearch: PropTypes.func, onSelectAll: PropTypes.func, onSort: PropTypes.func, additionalControls: PropTypes.arrayOf(PropTypes.node), @@ -136,6 +139,7 @@ DataListToolbar.defaultProps = { onCompact: null, onExpand: null, onSearch: null, + onReplaceSearch: null, onSelectAll: null, onSort: null, additionalControls: [], diff --git a/awx/ui_next/src/components/FilterTags/FilterTags.jsx b/awx/ui_next/src/components/FilterTags/FilterTags.jsx deleted file mode 100644 index b088d10b8d..0000000000 --- a/awx/ui_next/src/components/FilterTags/FilterTags.jsx +++ /dev/null @@ -1,88 +0,0 @@ -import React, { Fragment } from 'react'; -import { withRouter } from 'react-router-dom'; -import { withI18n } from '@lingui/react'; -import { t } from '@lingui/macro'; -import styled from 'styled-components'; -import { DataToolbarGroup, DataToolbarItem } from '@patternfly/react-core/dist/esm/experimental'; -import { parseQueryString } from '@util/qs'; -import { Button, Chip, ChipGroup, ChipGroupToolbarItem } from '@patternfly/react-core'; -import VerticalSeparator from '@components/VerticalSeparator'; - -const ResultCount = styled.span` - font-weight: bold; -`; - -const FilterLabel = styled.span` - padding-right: 20px; -`; - -// remove non-default query params so they don't show up as filter tags -const filterDefaultParams = (paramsArr, config) => { - const defaultParamsKeys = Object.keys(config.defaultParams); - return paramsArr.filter(key => defaultParamsKeys.indexOf(key) === -1); -}; - -const FilterTags = ({ - i18n, - itemCount, - qsConfig, - location, - onRemove, - onRemoveAll, -}) => { - const queryParams = parseQueryString(qsConfig, location.search); - const queryParamsByKey = {}; - const nonDefaultParams = filterDefaultParams( - Object.keys(queryParams), - qsConfig - ); - nonDefaultParams.forEach(key => { - const label = key - .replace('__icontains', '') - .split('_') - .map(word => `${word.charAt(0).toUpperCase()}${word.slice(1)}`) - .join(' '); - queryParamsByKey[key] = { label, tags: [] }; - - if (Array.isArray(queryParams[key])) { - queryParams[key].forEach(val => - queryParamsByKey[key].tags.push(val) - ); - } else { - queryParamsByKey[key].tags.push(queryParams[key]); - } - }); - - return ( - Object.keys(queryParamsByKey).length > 0 && ( - - - {i18n._(t`${itemCount} results`)} - - - {i18n._(t`Active Filters:`)} - - {Object.keys(queryParamsByKey).map(key => ( - - - {queryParamsByKey[key].tags.map(chip => ( - onRemove(key, chip)}> - {chip} - - ))} - - - ))} - - - - - - - ) - ); -}; - -export default withI18n()(withRouter(FilterTags)); diff --git a/awx/ui_next/src/components/FilterTags/FilterTags.test.jsx b/awx/ui_next/src/components/FilterTags/FilterTags.test.jsx deleted file mode 100644 index 8bfd6642d7..0000000000 --- a/awx/ui_next/src/components/FilterTags/FilterTags.test.jsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; -import { createMemoryHistory } from 'history'; -import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; -import FilterTags from './FilterTags'; - -describe('', () => { - const qsConfig = { - namespace: 'item', - defaultParams: { page: 1, page_size: 5, order_by: 'name' }, - integerFields: [], - }; - const onRemoveFn = jest.fn(); - const onRemoveAllFn = jest.fn(); - - test('initially renders without crashing', () => { - const wrapper = mountWithContexts( - - ); - expect(wrapper.length).toBe(1); - wrapper.unmount(); - }); - - test('renders non-default param tags based on location history', () => { - const history = createMemoryHistory({ - initialEntries: [ - '/foo?item.page=1&item.page_size=2&item.name__icontains=bar&item.job_type__icontains=project', - ], - }); - const wrapper = mountWithContexts( - , - { - context: { router: { history, route: { location: history.location } } }, - } - ); - const chips = wrapper.find('.pf-c-chip.searchTagChip'); - expect(chips.length).toBe(2); - const chipLabels = wrapper.find('.pf-c-chip__text b'); - expect(chipLabels.length).toBe(2); - expect(chipLabels.at(0).text()).toEqual('Name:'); - expect(chipLabels.at(1).text()).toEqual('Job Type:'); - wrapper.unmount(); - }); -}); diff --git a/awx/ui_next/src/components/FilterTags/index.js b/awx/ui_next/src/components/FilterTags/index.js deleted file mode 100644 index 19a1fa21b9..0000000000 --- a/awx/ui_next/src/components/FilterTags/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './FilterTags'; diff --git a/awx/ui_next/src/components/ListHeader/ListHeader.jsx b/awx/ui_next/src/components/ListHeader/ListHeader.jsx index e88505cb5f..54ca0ddca5 100644 --- a/awx/ui_next/src/components/ListHeader/ListHeader.jsx +++ b/awx/ui_next/src/components/ListHeader/ListHeader.jsx @@ -30,6 +30,7 @@ class ListHeader extends React.Component { super(props); this.handleSearch = this.handleSearch.bind(this); + this.handleReplaceSearch = this.handleReplaceSearch.bind(this); this.handleSort = this.handleSort.bind(this); this.handleRemove = this.handleRemove.bind(this); this.handleRemoveAll = this.handleRemoveAll.bind(this); @@ -41,9 +42,18 @@ class ListHeader extends React.Component { this.pushHistoryState(mergeParams(oldParams, { [key]: value })); } - handleRemove(key, value) { + handleReplaceSearch(key, value) { const { location, qsConfig } = this.props; const oldParams = parseQueryString(qsConfig, location.search); + this.pushHistoryState(replaceParams(oldParams, { [key]: value })); + } + + handleRemove(key, value) { + const { location, qsConfig } = this.props; + let oldParams = parseQueryString(qsConfig, location.search); + if (parseInt(value, 10)) { + oldParams = removeParams(qsConfig, oldParams, { [key]: parseInt(value, 10) }); + } this.pushHistoryState(removeParams(qsConfig, oldParams, { [key]: value })); } @@ -104,6 +114,7 @@ class ListHeader extends React.Component { searchColumns, sortColumns, onSearch: this.handleSearch, + onReplaceSearch: this.handleReplaceSearch, onSort: this.handleSort, onRemove: this.handleRemove, qsConfig, diff --git a/awx/ui_next/src/components/Lookup/shared/OptionsList.jsx b/awx/ui_next/src/components/Lookup/shared/OptionsList.jsx index ae3451f9ff..fa9eb8f67c 100644 --- a/awx/ui_next/src/components/Lookup/shared/OptionsList.jsx +++ b/awx/ui_next/src/components/Lookup/shared/OptionsList.jsx @@ -80,8 +80,8 @@ OptionsList.propTypes = { value: arrayOf(Item).isRequired, options: arrayOf(Item).isRequired, optionCount: number.isRequired, - searchColumns: SearchColumns.isRequired, - sortColumns: SortColumns.isRequired, + searchColumns: SearchColumns, + sortColumns: SortColumns, multiple: bool, qsConfig: QSConfig.isRequired, selectItem: func.isRequired, @@ -91,6 +91,8 @@ OptionsList.propTypes = { OptionsList.defaultProps = { multiple: false, renderItemChip: null, + searchColumns: [], + sortColumns: [] }; export default withI18n()(OptionsList); diff --git a/awx/ui_next/src/components/Search/Search.jsx b/awx/ui_next/src/components/Search/Search.jsx index 528b428cc2..a2a5a6a906 100644 --- a/awx/ui_next/src/components/Search/Search.jsx +++ b/awx/ui_next/src/components/Search/Search.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; @@ -11,6 +11,9 @@ import { DropdownToggle, DropdownItem, InputGroup, + Select, + SelectOption, + SelectVariant, TextInput, } from '@patternfly/react-core'; import { @@ -41,6 +44,7 @@ class Search extends React.Component { isSearchDropdownOpen: false, searchKey: columns.find(col => col.isDefault).key, searchValue: '', + isFilterDropdownOpen: false }; this.handleSearchInputChange = this.handleSearchInputChange.bind(this); @@ -48,6 +52,9 @@ class Search extends React.Component { this.handleDropdownSelect = this.handleDropdownSelect.bind(this); this.handleSearch = this.handleSearch.bind(this); this.handleTextKeyDown = this.handleTextKeyDown.bind(this); + this.handleFilterDropdownToggle = this.handleFilterDropdownToggle.bind(this); + this.handleFilterDropdownSelect = this.handleFilterDropdownSelect.bind(this); + this.handleFilterBooleanSelect = this.handleFilterBooleanSelect.bind(this); } handleDropdownToggle(isSearchDropdownOpen) { @@ -73,8 +80,6 @@ class Search extends React.Component { qsConfig.integerFields.filter(field => field === searchKey).length || qsConfig.dateFields.filter(field => field === searchKey).length; - // TODO: this will probably become more sophisticated, where date - // fields and string fields are passed to a formatter const actualSearchKey = isNonStringField ? searchKey : `${searchKey}__icontains`; @@ -94,10 +99,29 @@ class Search extends React.Component { } } + handleFilterDropdownToggle(isFilterDropdownOpen) { + this.setState({ isFilterDropdownOpen }); + } + + handleFilterDropdownSelect(key, event, actualValue) { + const { onSearch, onRemove } = this.props; + + if (event.target.checked) { + onSearch(`or__${key}`, actualValue); + } else { + onRemove(`or__${key}`, actualValue); + } + } + + handleFilterBooleanSelect(key, selection) { + const { onReplaceSearch } = this.props; + onReplaceSearch(key, selection); + } + render() { const { up } = DropdownPosition; const { columns, i18n, onRemove, qsConfig, location } = this.props; - const { isSearchDropdownOpen, searchKey, searchValue } = this.state; + const { isSearchDropdownOpen, searchKey, searchValue, isFilterDropdownOpen } = this.state; const { name: searchColumnName } = columns.find( ({ key }) => key === searchKey ); @@ -129,21 +153,20 @@ class Search extends React.Component { nonDefaultParams.forEach(key => { const columnKey = key - .replace('__icontains', ''); - const label = key .replace('__icontains', '') - .split('_') - .map(word => `${word.charAt(0).toUpperCase()}${word.slice(1)}`) - .join(' '); + .replace('or__', ''); + const label = columns + .filter(({key: keyToCheck}) => columnKey === keyToCheck).length ? columns + .filter(({key: keyToCheck}) => columnKey === keyToCheck)[0].name : columnKey; queryParamsByKey[columnKey] = { key, label, chips: [] }; if (Array.isArray(queryParams[key])) { queryParams[key].forEach(val => - queryParamsByKey[columnKey].chips.push(val) + queryParamsByKey[columnKey].chips.push(val.toString()) ); } else { - queryParamsByKey[columnKey].chips.push(queryParams[key]); + queryParamsByKey[columnKey].chips.push(queryParams[key].toString()); } }); @@ -174,16 +197,52 @@ class Search extends React.Component { style={{ width: '100%' }} />) : ({searchColumnName})} - {columns.map(({key}) => ( ( { onRemove(chipsByKey[key].key, val) }} categoryName={chipsByKey[key] ? chipsByKey[key].label : key} key={key} showToolbarItem={searchKey === key} > - + {options && () || isBoolean && ( + + ) || ( + {/* TODO: add support for dates: + qsConfig.dateFields.filter(field => field === key).length && "date" */} field === key).length && "number" || "search"} aria-label={i18n._(t`Search text input`)} value={searchValue} onChange={this.handleSearchInputChange} @@ -196,7 +255,7 @@ class Search extends React.Component { > - + )} ))} );