From 6f80e5b67b115e71ac01f453965510b9ababf646 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 24 Apr 2020 12:24:50 -0400 Subject: [PATCH] use labels in chips for select based filters --- awx/ui_next/src/components/Search/Search.jsx | 30 ++++- .../src/components/Search/Search.test.jsx | 127 ++++++++++++++++++ awx/ui_next/src/util/qs.js | 2 +- 3 files changed, 153 insertions(+), 6 deletions(-) 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;