diff --git a/awx/ui_next/src/components/Search/Search.jsx b/awx/ui_next/src/components/Search/Search.jsx
index 6d8d599322..25b450053c 100644
--- a/awx/ui_next/src/components/Search/Search.jsx
+++ b/awx/ui_next/src/components/Search/Search.jsx
@@ -150,6 +150,16 @@ class Search extends React.Component {
return paramsArr.filter(key => defaultParamsKeys.indexOf(key) === -1);
};
+ const getLabelFromValue = (value, colKey) => {
+ const currentSearchColumn = columns.find(({ key }) => key === colKey);
+ if (currentSearchColumn?.options?.length) {
+ return currentSearchColumn.options.find(
+ ([optVal]) => optVal === value
+ )[1];
+ }
+ return value.toString();
+ };
+
const getChipsByKey = () => {
const queryParams = parseQueryString(qsConfig, location.search);
@@ -175,10 +185,16 @@ class Search extends React.Component {
if (Array.isArray(queryParams[key])) {
queryParams[key].forEach(val =>
- queryParamsByKey[columnKey].chips.push(val.toString())
+ queryParamsByKey[columnKey].chips.push({
+ key: `${key}:${val}`,
+ node: {getLabelFromValue(val, columnKey)},
+ })
);
} else {
- queryParamsByKey[columnKey].chips.push(queryParams[key].toString());
+ queryParamsByKey[columnKey].chips.push({
+ key: `${key}:${queryParams[key]}`,
+ node: {getLabelFromValue(queryParams[key], columnKey)},
+ });
}
});
@@ -215,8 +231,9 @@ class Search extends React.Component {
({ key, name, options, isBoolean, booleanLabels = {} }) => (
{
- onRemove(chipsByKey[key].key, val);
+ deleteChip={(unusedKey, chip) => {
+ const [columnKey, ...value] = chip.key.split(':');
+ onRemove(columnKey, value.join(':'));
}}
categoryName={chipsByKey[key] ? chipsByKey[key].label : key}
key={key}
@@ -231,7 +248,10 @@ class Search extends React.Component {
onSelect={(event, selection) =>
this.handleFilterDropdownSelect(key, event, selection)
}
- selections={chipsByKey[key].chips}
+ selections={chipsByKey[key].chips.map(chip => {
+ const [, ...value] = chip.key.split(':');
+ return value.join(':');
+ })}
isExpanded={isFilterDropdownOpen}
placeholderText={`Filter By ${name}`}
>
diff --git a/awx/ui_next/src/components/Search/Search.test.jsx b/awx/ui_next/src/components/Search/Search.test.jsx
index 7ff46b9108..bf4567320b 100644
--- a/awx/ui_next/src/components/Search/Search.test.jsx
+++ b/awx/ui_next/src/components/Search/Search.test.jsx
@@ -3,7 +3,9 @@ import {
DataToolbar,
DataToolbarContent,
} from '@patternfly/react-core/dist/umd/experimental';
+import { createMemoryHistory } from 'history';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
+import { act } from 'react-dom/test-utils';
import Search from './Search';
describe('', () => {
@@ -141,4 +143,129 @@ describe('', () => {
expect(onSearch).toHaveBeenCalledTimes(1);
expect(onSearch).toBeCalledWith('name__icontains', 'test-321');
});
+
+ test('filter keys are properly labeled', () => {
+ const columns = [
+ { name: 'Name', key: 'name', isDefault: true },
+ { name: 'Type', key: 'type', options: [['foo', 'Foo Bar!']] },
+ { name: 'Description', key: 'description' },
+ ];
+ const query =
+ '?organization.or__type=foo&organization.name=bar&item.page_size=10';
+ const history = createMemoryHistory({
+ initialEntries: [`/organizations/${query}`],
+ });
+ const wrapper = mountWithContexts(
+ {}}
+ collapseListedFiltersBreakpoint="md"
+ >
+
+
+
+ ,
+ { context: { router: { history } } }
+ );
+ const typeFilterWrapper = wrapper.find(
+ 'DataToolbarFilter[categoryName="Type"]'
+ );
+ expect(typeFilterWrapper.prop('chips')[0].key).toEqual('or__type:foo');
+ const nameFilterWrapper = wrapper.find(
+ 'DataToolbarFilter[categoryName="Name"]'
+ );
+ expect(nameFilterWrapper.prop('chips')[0].key).toEqual('name:bar');
+ });
+
+ test('should test handle remove of option-based key', async () => {
+ const qsConfigNew = {
+ namespace: 'item',
+ defaultParams: { page: 1, page_size: 5, order_by: '-type' },
+ integerFields: [],
+ };
+ const columns = [
+ {
+ name: 'type',
+ key: 'type',
+ options: [['foo', 'Foo Bar!']],
+ isDefault: true,
+ },
+ ];
+ const query = '?item.or__type=foo&item.page_size=10';
+ const history = createMemoryHistory({
+ initialEntries: [`/organizations/1/teams${query}`],
+ });
+ const onRemove = jest.fn();
+ const wrapper = mountWithContexts(
+ {}}
+ collapseListedFiltersBreakpoint="md"
+ >
+
+
+
+ ,
+ { context: { router: { history } } }
+ );
+ expect(history.location.search).toEqual(query);
+ // click remove button on chip
+ await act(async () => {
+ wrapper
+ .find('.pf-c-chip button[aria-label="close"]')
+ .at(0)
+ .simulate('click');
+ });
+ expect(onRemove).toBeCalledWith('or__type', 'foo');
+ });
+
+ test('should test handle remove of option-based with empty string value', async () => {
+ const qsConfigNew = {
+ namespace: 'item',
+ defaultParams: { page: 1, page_size: 5, order_by: '-type' },
+ integerFields: [],
+ };
+ const columns = [
+ {
+ name: 'type',
+ key: 'type',
+ options: [['', 'manual']],
+ isDefault: true,
+ },
+ ];
+ const query = '?item.or__type=&item.page_size=10';
+ const history = createMemoryHistory({
+ initialEntries: [`/organizations/1/teams${query}`],
+ });
+ const onRemove = jest.fn();
+ const wrapper = mountWithContexts(
+ {}}
+ collapseListedFiltersBreakpoint="md"
+ >
+
+
+
+ ,
+ { context: { router: { history } } }
+ );
+ expect(history.location.search).toEqual(query);
+ // click remove button on chip
+ await act(async () => {
+ wrapper
+ .find('.pf-c-chip button[aria-label="close"]')
+ .at(0)
+ .simulate('click');
+ });
+ expect(onRemove).toBeCalledWith('or__type', '');
+ });
});
diff --git a/awx/ui_next/src/util/qs.js b/awx/ui_next/src/util/qs.js
index b97f3ce9ac..c02e3be075 100644
--- a/awx/ui_next/src/util/qs.js
+++ b/awx/ui_next/src/util/qs.js
@@ -208,7 +208,7 @@ function mergeParam(oldVal, newVal) {
if (!newVal && newVal !== '') {
return oldVal;
}
- if (!oldVal) {
+ if (!oldVal && oldVal !== '') {
return newVal;
}
let merged;