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 {
>
-
+ )}
))}
);