Redesign screen/page "List of networks" (#13560)
Co-authored-by: George Marshall <george.marshall@consensys.net> Co-authored-by: Elliot Winkler <elliot.winkler@gmail.com>feature/default_network_editable
parent
8174aaa6b5
commit
8184b150cb
@ -0,0 +1,114 @@ |
||||
import React, { useState, useContext } from 'react'; |
||||
import PropTypes from 'prop-types'; |
||||
import Fuse from 'fuse.js'; |
||||
import InputAdornment from '@material-ui/core/InputAdornment'; |
||||
import TextField from '../../../../components/ui/text-field'; |
||||
import { I18nContext } from '../../../../contexts/i18n'; |
||||
import SearchIcon from '../../../../components/ui/search-icon'; |
||||
|
||||
export default function CustomContentSearch({ |
||||
onSearch, |
||||
error, |
||||
networksList, |
||||
searchQueryInput, |
||||
}) { |
||||
const t = useContext(I18nContext); |
||||
const [searchIconColor, setSearchIconColor] = useState( |
||||
'var(--color-icon-muted)', |
||||
); |
||||
|
||||
const networksListArray = Object.values(networksList); |
||||
const networksSearchFuse = new Fuse(networksListArray, { |
||||
shouldSort: true, |
||||
threshold: 0.2, |
||||
location: 0, |
||||
distance: 100, |
||||
maxPatternLength: 32, |
||||
minMatchCharLength: 1, |
||||
keys: ['label', 'labelKey'], |
||||
}); |
||||
|
||||
const handleSearch = async (searchQuery) => { |
||||
if (searchQuery === '') { |
||||
setSearchIconColor('var(--color-icon-muted)'); |
||||
} else { |
||||
setSearchIconColor('var(--color-icon-default)'); |
||||
} |
||||
|
||||
const fuseSearchResult = networksSearchFuse.search(searchQuery); |
||||
const results = searchQuery ? [...fuseSearchResult] : networksListArray; |
||||
await onSearch({ searchQuery, results }); |
||||
}; |
||||
|
||||
const renderStartAdornment = () => { |
||||
return ( |
||||
<InputAdornment position="start"> |
||||
<SearchIcon color={searchIconColor} /> |
||||
</InputAdornment> |
||||
); |
||||
}; |
||||
|
||||
const renderEndAdornment = () => { |
||||
return ( |
||||
<> |
||||
{searchQueryInput && ( |
||||
<InputAdornment |
||||
className="imageclosectn" |
||||
position="end" |
||||
onClick={() => handleSearch('')} |
||||
> |
||||
<i |
||||
className="fa fa-times networks-tab__imageclose" |
||||
width="17" |
||||
heigth="17" |
||||
title="Close" |
||||
/> |
||||
</InputAdornment> |
||||
)} |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
return ( |
||||
<TextField |
||||
id="search-networks" |
||||
data-testid="search-networks" |
||||
placeholder={t('customContentSearch')} |
||||
type="text" |
||||
value={searchQueryInput} |
||||
onChange={(e) => handleSearch(e.target.value)} |
||||
error={error} |
||||
fullWidth |
||||
autoFocus |
||||
autoComplete="off" |
||||
classes={{ |
||||
inputRoot: 'networks-tab__networks-list__custom-search-network', |
||||
}} |
||||
startAdornment={renderStartAdornment()} |
||||
endAdornment={renderEndAdornment()} |
||||
/> |
||||
); |
||||
} |
||||
|
||||
CustomContentSearch.propTypes = { |
||||
/** |
||||
* The function searches the list of networks depending on |
||||
* the entered parameter and returns the entire list of |
||||
* networks when the user clicks on 'X' on the search tab |
||||
*/ |
||||
onSearch: PropTypes.func, |
||||
/** |
||||
* An error message is displayed when a user searches for a specific |
||||
* network on the search tab and that network does not exist |
||||
* in the networks list |
||||
*/ |
||||
error: PropTypes.string, |
||||
/** |
||||
* The list of networks available for search. |
||||
*/ |
||||
networksList: PropTypes.array, |
||||
/** |
||||
* Search for a specific network(s) by label or labelKey |
||||
*/ |
||||
searchQueryInput: PropTypes.string, |
||||
}; |
@ -0,0 +1,23 @@ |
||||
import React from 'react'; |
||||
import testData from '../../../../../.storybook/test-data'; |
||||
import CustomContentSearch from './custom-content-search'; |
||||
|
||||
export default { |
||||
title: 'Pages/Settings/NetworksTab/CustomContentSearch', |
||||
id: __filename, |
||||
argTypes: { |
||||
error: { |
||||
control: 'text', |
||||
}, |
||||
searchQueryInput: { |
||||
control: 'text', |
||||
}, |
||||
onSearch: { |
||||
action: 'onSearch', |
||||
}, |
||||
}, |
||||
}; |
||||
|
||||
export const CustomContentSearchComponent = (args) => { |
||||
return <CustomContentSearch {...args} networksList={testData.networkList} />; |
||||
}; |
@ -0,0 +1,110 @@ |
||||
import React from 'react'; |
||||
import { fireEvent, render, screen } from '@testing-library/react'; |
||||
import Fuse from 'fuse.js'; |
||||
import configureStore from '../../../../store/store'; |
||||
import { renderWithProvider } from '../../../../../test/lib/render-helpers'; |
||||
import testData from '../../../../../.storybook/test-data'; |
||||
import CustomContentSearch from './custom-content-search'; |
||||
|
||||
function renderComponent({ componentProps = {} } = {}) { |
||||
const store = configureStore({}); |
||||
return renderWithProvider(<CustomContentSearch {...componentProps} />, store); |
||||
} |
||||
|
||||
describe('CustomContentSearch', () => { |
||||
it('should render custom content search correctly', () => { |
||||
const onSearch = jest.fn(); |
||||
const wrapper = renderComponent({ |
||||
componentProps: { onSearch, networksList: testData.networkList }, |
||||
}); |
||||
expect(wrapper.getByTestId('search-networks')).toBeDefined(); |
||||
}); |
||||
|
||||
it('should check placeholder text in TextField input', () => { |
||||
const onSearch = jest.fn(); |
||||
const wrapper = renderComponent({ |
||||
componentProps: { onSearch, networksList: testData.networkList }, |
||||
}); |
||||
const { getByPlaceholderText } = wrapper; |
||||
expect( |
||||
getByPlaceholderText('Search for a previously added network'), |
||||
).toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('re-render the same component with different props', () => { |
||||
const onSearch = jest.fn(); |
||||
const { rerender } = render( |
||||
<CustomContentSearch |
||||
onSearch={onSearch} |
||||
networksList={[]} |
||||
searchQueryInput="" |
||||
/>, |
||||
); |
||||
const input = screen.getByTestId('search-networks'); |
||||
expect(input.value).toBe(''); |
||||
rerender( |
||||
<CustomContentSearch |
||||
onSearch={onSearch} |
||||
networksList={[]} |
||||
searchQueryInput="Polygon" |
||||
/>, |
||||
); |
||||
expect(input.value).toBe('Polygon'); |
||||
}); |
||||
|
||||
it('should call onSearch prop with input value', () => { |
||||
const onSearch = jest.fn(); |
||||
const wrapper = renderComponent({ |
||||
componentProps: { |
||||
onSearch, |
||||
networksList: [], |
||||
searchQueryInput: 'Avalanche', |
||||
}, |
||||
}); |
||||
const input = wrapper.getByTestId('search-networks'); |
||||
fireEvent.change(input, { target: { value: 'Polygon' } }); |
||||
expect(input.value).toBe('Avalanche'); |
||||
}); |
||||
|
||||
it('should check if error is shown if search does not return any network from the list', () => { |
||||
const onSearch = jest.fn(); |
||||
const networksSearchFuse = new Fuse(testData.networkList, { |
||||
keys: ['label', 'labelKey'], |
||||
}); |
||||
const fuseSearchResult = networksSearchFuse.search('Optimism'); |
||||
const wrapper = renderComponent({ |
||||
componentProps: { |
||||
onSearch, |
||||
networksList: testData.networkList, |
||||
searchQueryInput: 'Optimism', |
||||
error: 'No matching results found.', |
||||
}, |
||||
}); |
||||
const input = wrapper.getByTestId('search-networks'); |
||||
expect(fuseSearchResult).toHaveLength(0); |
||||
fireEvent.change(input, { |
||||
target: { error: 'No matching results found.' }, |
||||
}); |
||||
expect(input.error).toBe('No matching results found.'); |
||||
}); |
||||
|
||||
it('should check if error is not shown if search return some network from the list', () => { |
||||
const onSearch = jest.fn(); |
||||
const networksSearchFuse = new Fuse(testData.networkList, { |
||||
keys: ['label', 'labelKey'], |
||||
}); |
||||
const fuseSearchResult = networksSearchFuse.search('ropsten'); |
||||
const wrapper = renderComponent({ |
||||
componentProps: { |
||||
onSearch, |
||||
networksList: testData.networkList, |
||||
searchQueryInput: 'Avalanche', |
||||
error: '', |
||||
}, |
||||
}); |
||||
const input = wrapper.getByTestId('search-networks'); |
||||
expect(fuseSearchResult).toHaveLength(1); |
||||
fireEvent.change(input, { target: { error: '' } }); |
||||
expect(input.error).toBe(''); |
||||
}); |
||||
}); |
@ -0,0 +1 @@ |
||||
export { default } from './custom-content-search'; |
@ -0,0 +1,28 @@ |
||||
import React from 'react'; |
||||
import testData from '../../../../../.storybook/test-data'; |
||||
import NetworksList from './networks-list'; |
||||
|
||||
export default { |
||||
title: 'Pages/Settings/NetworksTab/NetworksList', |
||||
id: __filename, |
||||
argTypes: { |
||||
networkDefaultedToProvider: { |
||||
control: 'boolean', |
||||
}, |
||||
networkIsSelected: { |
||||
control: 'boolean', |
||||
}, |
||||
networksToRender: { |
||||
control: 'array', |
||||
}, |
||||
}, |
||||
args: { |
||||
networkDefaultedToProvider: false, |
||||
networkIsSelected: false, |
||||
networksToRender: testData.networkList, |
||||
}, |
||||
}; |
||||
|
||||
export const NetworksListComponent = (args) => { |
||||
return <NetworksList {...args} />; |
||||
}; |
Loading…
Reference in new issue