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 && ( - - )) || ( - - {/* 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={this.handleSearchInputChange} - onKeyDown={this.handleTextKeyDown} - /> -
- -
-
- )} -
- ) - )} - {/* 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} - /> - ))} -
- ); - } + + + + + )} + + ))} + {/* 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', () => {