diff --git a/awx/ui_next/src/components/Search/Search.jsx b/awx/ui_next/src/components/Search/Search.jsx
index 0fe29d550ca3..14a32d148700 100644
--- a/awx/ui_next/src/components/Search/Search.jsx
+++ b/awx/ui_next/src/components/Search/Search.jsx
@@ -1,5 +1,5 @@
import 'styled-components/macro';
-import React, { Fragment } from 'react';
+import React, { Fragment, useState } from 'react';
import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
@@ -7,10 +7,6 @@ import { withRouter } from 'react-router-dom';
import {
Button,
ButtonVariant,
- Dropdown,
- DropdownPosition,
- DropdownToggle,
- DropdownItem,
InputGroup,
Select,
SelectOption,
@@ -34,318 +30,256 @@ const NoOptionDropdown = styled.div`
border-bottom-color: var(--pf-global--BorderColor--200);
`;
-class Search extends React.Component {
- constructor(props) {
- super(props);
+function Search({
+ columns,
+ i18n,
+ onSearch,
+ onReplaceSearch,
+ onRemove,
+ qsConfig,
+ location,
+ searchableKeys,
+ relatedSearchableKeys,
+}) {
+ const [isSearchDropdownOpen, setIsSearchDropdownOpen] = useState(false);
+ const [searchKey, setSearchKey] = useState(
+ columns.find(col => col.isDefault).key
+ );
+ const [searchValue, setSearchValue] = useState('');
+ const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false);
- const { columns } = this.props;
-
- this.state = {
- isSearchDropdownOpen: false,
- searchKey: columns.find(col => col.isDefault).key,
- searchValue: '',
- isFilterDropdownOpen: false,
- };
-
- this.handleSearchInputChange = this.handleSearchInputChange.bind(this);
- this.handleDropdownToggle = this.handleDropdownToggle.bind(this);
- this.handleDropdownSelect = this.handleDropdownSelect.bind(this);
- this.handleSearch = this.handleSearch.bind(this);
- this.handleTextKeyDown = this.handleTextKeyDown.bind(this);
- this.handleFilterDropdownToggle = this.handleFilterDropdownToggle.bind(
- this
+ const handleDropdownSelect = ({ target }) => {
+ const { key: searchKey } = columns.find(
+ ({ name }) => name === target.innerText
);
- this.handleFilterDropdownSelect = this.handleFilterDropdownSelect.bind(
- this
- );
- this.handleFilterBooleanSelect = this.handleFilterBooleanSelect.bind(this);
- }
-
- handleDropdownToggle(isSearchDropdownOpen) {
- this.setState({ isSearchDropdownOpen });
- }
- handleDropdownSelect({ target }) {
- const { columns } = this.props;
- const { innerText } = target;
+ setIsFilterDropdownOpen(false);
+ setSearchKey(searchKey);
+ };
- const { key: searchKey } = columns.find(({ name }) => name === innerText);
- this.setState({ isSearchDropdownOpen: false, searchKey });
- }
-
- handleSearch(e) {
+ const handleSearch = e => {
// keeps page from fully reloading
e.preventDefault();
- const { searchKey, searchValue } = this.state;
- const { onSearch } = this.props;
-
if (searchValue) {
onSearch(searchKey, searchValue);
- this.setState({ searchValue: '' });
+ setSearchValue('');
}
- }
-
- handleSearchInputChange(searchValue) {
- this.setState({ searchValue });
- }
+ };
- handleTextKeyDown(e) {
+ const handleTextKeyDown = e => {
if (e.key && e.key === 'Enter') {
- this.handleSearch(e);
+ handleSearch(e);
}
- }
-
- handleFilterDropdownToggle(isFilterDropdownOpen) {
- this.setState({ isFilterDropdownOpen });
- }
-
- handleFilterDropdownSelect(key, event, actualValue) {
- const { onSearch, onRemove } = this.props;
+ };
+ const handleFilterDropdownSelect = (key, event, actualValue) => {
if (event.target.checked) {
onSearch(key, actualValue);
} else {
onRemove(key, actualValue);
}
- }
-
- handleFilterBooleanSelect(key, selection) {
- const { onReplaceSearch } = this.props;
- onReplaceSearch(key, selection);
- }
+ };
- render() {
- const { up } = DropdownPosition;
- const {
- columns,
- i18n,
- onSearch,
- onRemove,
- qsConfig,
- location,
- searchableKeys,
- relatedSearchableKeys,
- } = this.props;
- const {
- isSearchDropdownOpen,
- searchKey,
- searchValue,
- isFilterDropdownOpen,
- } = this.state;
- const { name: searchColumnName } = columns.find(
- ({ key }) => key === searchKey
- );
+ const filterDefaultParams = (paramsArr, config) => {
+ const defaultParamsKeys = Object.keys(config.defaultParams || {});
+ return paramsArr.filter(key => defaultParamsKeys.indexOf(key) === -1);
+ };
- const searchDropdownItems = columns
- .filter(({ key }) => key !== searchKey)
- .map(({ key, name }) => (
-
- {name}
-
- ));
+ 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 filterDefaultParams = (paramsArr, config) => {
- const defaultParamsKeys = Object.keys(config.defaultParams || {});
- return paramsArr.filter(key => defaultParamsKeys.indexOf(key) === -1);
- };
+ const getChipsByKey = () => {
+ const queryParams = parseQueryString(qsConfig, location.search);
- 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 queryParamsByKey = {};
+ columns.forEach(({ name, key }) => {
+ queryParamsByKey[key] = { key, label: name, chips: [] };
+ });
+ const nonDefaultParams = filterDefaultParams(
+ Object.keys(queryParams || {}),
+ qsConfig
+ );
- const getChipsByKey = () => {
- const queryParams = parseQueryString(qsConfig, location.search);
+ nonDefaultParams.forEach(key => {
+ const columnKey = key;
+ const label = columns.filter(
+ ({ key: keyToCheck }) => columnKey === keyToCheck
+ ).length
+ ? `${
+ columns.find(({ key: keyToCheck }) => columnKey === keyToCheck).name
+ } (${key})`
+ : columnKey;
- const queryParamsByKey = {};
- columns.forEach(({ name, key }) => {
- queryParamsByKey[key] = { key, label: name, chips: [] };
- });
- const nonDefaultParams = filterDefaultParams(
- Object.keys(queryParams || {}),
- qsConfig
- );
+ queryParamsByKey[columnKey] = { key, label, chips: [] };
- nonDefaultParams.forEach(key => {
- const columnKey = key;
- const label = columns.filter(
- ({ key: keyToCheck }) => columnKey === keyToCheck
- ).length
- ? `${
- columns.find(({ key: keyToCheck }) => columnKey === keyToCheck)
- .name
- } (${key})`
- : columnKey;
+ if (Array.isArray(queryParams[key])) {
+ queryParams[key].forEach(val =>
+ queryParamsByKey[columnKey].chips.push({
+ key: `${key}:${val}`,
+ node: getLabelFromValue(val, columnKey),
+ })
+ );
+ } else {
+ queryParamsByKey[columnKey].chips.push({
+ key: `${key}:${queryParams[key]}`,
+ node: getLabelFromValue(queryParams[key], columnKey),
+ });
+ }
+ });
+ return queryParamsByKey;
+ };
- queryParamsByKey[columnKey] = { key, label, chips: [] };
+ const chipsByKey = getChipsByKey();
- if (Array.isArray(queryParams[key])) {
- queryParams[key].forEach(val =>
- queryParamsByKey[columnKey].chips.push({
- key: `${key}:${val}`,
- node: getLabelFromValue(val, columnKey),
- })
- );
- } else {
- queryParamsByKey[columnKey].chips.push({
- key: `${key}:${queryParams[key]}`,
- node: getLabelFromValue(queryParams[key], columnKey),
- });
- }
- });
- return queryParamsByKey;
- };
+ const { name: searchColumnName } = columns.find(
+ ({ key }) => key === searchKey
+ );
- const chipsByKey = getChipsByKey();
+ const searchOptions = columns
+ .filter(({ key }) => key !== searchKey)
+ .map(({ key, name }) => (
+
+ {name}
+
+ ));
- return (
-
-
- {searchDropdownItems.length > 0 ? (
-
- {searchColumnName}
-
- }
- isOpen={isSearchDropdownOpen}
- dropdownItems={searchDropdownItems}
+ return (
+
+
+ {searchOptions.length > 0 ? (
+
+ ) : (
+ {searchColumnName}
+ )}
+
+ {columns.map(({ key, name, options, isBoolean, booleanLabels = {} }) => (
+ {
+ const [columnKey, ...value] = chip.key.split(':');
+ onRemove(columnKey, value.join(':'));
+ }}
+ categoryName={chipsByKey[key] ? chipsByKey[key].label : key}
+ key={key}
+ showToolbarItem={searchKey === key}
+ >
+ {(key === 'advanced' && (
+
- ) : (
- {searchColumnName}
- )}
-
- {columns.map(
- ({ key, name, options, isBoolean, booleanLabels = {} }) => (
- {
- const [columnKey, ...value] = chip.key.split(':');
- onRemove(columnKey, value.join(':'));
- }}
- categoryName={chipsByKey[key] ? chipsByKey[key].label : key}
- key={key}
- showToolbarItem={searchKey === key}
- >
- {(key === 'advanced' && (
-
+
+
+ )) ||
+ (isBoolean && (
+
+ )) || (
+
+ {/* TODO: add support for dates:
+ qsConfig.dateFields.filter(field => field === key).length && "date" */}
+ field === searchKey
+ ) &&
+ 'number') ||
+ 'search'
+ }
+ aria-label={i18n._(t`Search text input`)}
+ value={searchValue}
+ onChange={setSearchValue}
+ onKeyDown={handleTextKeyDown}
/>
- )) ||
- (options && (
-
-
-
- )) ||
- (isBoolean && (
-
+ )}
+
+ ))}
+ {/* Add a ToolbarFilter for any key that doesn't have it's own
+ search column so the chips show up */}
+ {Object.keys(chipsByKey)
+ .filter(val => chipsByKey[val].chips.length > 0)
+ .filter(val => columns.map(val2 => val2.key).indexOf(val) === -1)
+ .map(leftoverKey => (
+ {
+ const [columnKey, ...value] = chip.key.split(':');
+ onRemove(columnKey, value.join(':'));
+ }}
+ categoryName={
+ chipsByKey[leftoverKey]
+ ? chipsByKey[leftoverKey].label
+ : leftoverKey
+ }
+ key={leftoverKey}
+ />
+ ))}
+
+ );
}
Search.propTypes = {
diff --git a/awx/ui_next/src/components/Search/Search.test.jsx b/awx/ui_next/src/components/Search/Search.test.jsx
index 50e2178e07b2..a34b6bcc0931 100644
--- a/awx/ui_next/src/components/Search/Search.test.jsx
+++ b/awx/ui_next/src/components/Search/Search.test.jsx
@@ -49,26 +49,9 @@ describe('', () => {
expect(onSearch).toBeCalledWith('name__icontains', 'test-321');
});
- test('handleDropdownToggle properly updates state', async () => {
- const columns = [{ name: 'Name', key: 'name__icontains', isDefault: true }];
- const onSearch = jest.fn();
- const wrapper = mountWithContexts(
- {}}
- collapseListedFiltersBreakpoint="lg"
- >
-
-
-
-
- ).find('Search');
- expect(wrapper.state('isSearchDropdownOpen')).toEqual(false);
- wrapper.instance().handleDropdownToggle(true);
- expect(wrapper.state('isSearchDropdownOpen')).toEqual(true);
- });
-
- test('handleDropdownSelect properly updates state', async () => {
+ test('changing key select updates which key is called for onSearch', () => {
+ const searchButton = 'button[aria-label="Search submit button"]';
+ const searchTextInput = 'input[aria-label="Search text input"]';
const columns = [
{ name: 'Name', key: 'name__icontains', isDefault: true },
{ name: 'Description', key: 'description__icontains' },
@@ -84,12 +67,20 @@ describe('', () => {
- ).find('Search');
- expect(wrapper.state('searchKey')).toEqual('name__icontains');
- wrapper
- .instance()
- .handleDropdownSelect({ target: { innerText: 'Description' } });
- expect(wrapper.state('searchKey')).toEqual('description__icontains');
+ );
+
+ act(() => {
+ wrapper
+ .find('Select[aria-label="Simple key select"]')
+ .invoke('onSelect')({ target: { innerText: 'Description' } });
+ });
+ wrapper.update();
+ wrapper.find(searchTextInput).instance().value = 'test-321';
+ wrapper.find(searchTextInput).simulate('change');
+ wrapper.find(searchButton).simulate('click');
+
+ expect(onSearch).toHaveBeenCalledTimes(1);
+ expect(onSearch).toBeCalledWith('description__icontains', 'test-321');
});
test('attempt to search with empty string', () => {