mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 01:47:00 +01:00
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>
This commit is contained in:
parent
8174aaa6b5
commit
8184b150cb
@ -13,6 +13,103 @@ const state = {
|
||||
protocol: 'https:',
|
||||
url: 'https://metamask.github.io/test-dapp/',
|
||||
},
|
||||
networkList: [
|
||||
{
|
||||
blockExplorerUrl: "https://etherscan.io",
|
||||
chainId: "0x1",
|
||||
iconColor: 'var(--mainnet)',
|
||||
isATestNetwork: false,
|
||||
labelKey: "mainnet",
|
||||
providerType: "mainnet",
|
||||
rpcUrl: "https://mainnet.infura.io/v3/",
|
||||
ticker: "ETH",
|
||||
viewOnly: true,
|
||||
},
|
||||
{
|
||||
blockExplorerUrl: "https://ropsten.etherscan.io",
|
||||
chainId: "0x3",
|
||||
iconColor: 'var(--ropsten)',
|
||||
isATestNetwork: true,
|
||||
labelKey: "ropsten",
|
||||
providerType: "ropsten",
|
||||
rpcUrl: "https://ropsten.infura.io/v3/",
|
||||
ticker: "ETH",
|
||||
viewOnly: true,
|
||||
},
|
||||
{
|
||||
blockExplorerUrl: "https://rinkeby.etherscan.io",
|
||||
chainId: "0x4",
|
||||
iconColor: 'var(--rinkeby)',
|
||||
isATestNetwork: true,
|
||||
labelKey: "rinkeby",
|
||||
providerType: "rinkeby",
|
||||
rpcUrl: "https://rinkeby.infura.io/v3/",
|
||||
ticker: "ETH",
|
||||
viewOnly: true,
|
||||
},
|
||||
{
|
||||
blockExplorerUrl: "https://goerli.etherscan.io",
|
||||
chainId: "0x5",
|
||||
iconColor: 'var(--goerli)',
|
||||
isATestNetwork: true,
|
||||
labelKey: "goerli",
|
||||
providerType: "goerli",
|
||||
rpcUrl: "https://goerli.infura.io/v3/",
|
||||
ticker: "ETH",
|
||||
viewOnly: true,
|
||||
},
|
||||
{
|
||||
blockExplorerUrl: "https://kovan.etherscan.io",
|
||||
chainId: "0x2a",
|
||||
iconColor: 'var(--kovan)',
|
||||
isATestNetwork: true,
|
||||
labelKey: "kovan",
|
||||
providerType: "kovan",
|
||||
rpcUrl: "https://kovan.infura.io/v3/",
|
||||
ticker: "ETH",
|
||||
viewOnly: true,
|
||||
},
|
||||
{
|
||||
blockExplorerUrl: "",
|
||||
chainId: "0x539",
|
||||
iconColor: 'var(--localhost)',
|
||||
isATestNetwork: true,
|
||||
label: "Localhost 8545",
|
||||
providerType: "rpc",
|
||||
rpcUrl: "http://localhost:8545",
|
||||
ticker: "ETH",
|
||||
},
|
||||
{
|
||||
blockExplorerUrl: "https://bscscan.com",
|
||||
chainId: "0x38",
|
||||
iconColor: 'var(--localhost)',
|
||||
isATestNetwork: false,
|
||||
label: "Binance Smart Chain",
|
||||
providerType: "rpc",
|
||||
rpcUrl: "https://bsc-dataseed.binance.org/",
|
||||
ticker: "BNB",
|
||||
},
|
||||
{
|
||||
blockExplorerUrl: "https://cchain.explorer.avax.network/",
|
||||
chainId: "0xa86a",
|
||||
iconColor: 'var(--localhost)',
|
||||
isATestNetwork: false,
|
||||
label: "Avalanche",
|
||||
providerType: "rpc",
|
||||
rpcUrl: "https://api.avax.network/ext/bc/C/rpc",
|
||||
ticker: "AVAX",
|
||||
},
|
||||
{
|
||||
blockExplorerUrl: "https://polygonscan.com",
|
||||
chainId: "0x89",
|
||||
iconColor: 'var(--localhost)',
|
||||
isATestNetwork: false,
|
||||
label: "Polygon",
|
||||
providerType: "rpc",
|
||||
rpcUrl: "https://polygon-rpc.com",
|
||||
ticker: "MATIC",
|
||||
},
|
||||
],
|
||||
metamask: {
|
||||
tokenList: {
|
||||
'0x6b175474e89094c44da98b954eedeac495271d0f': {
|
||||
|
8
app/_locales/en/messages.json
generated
8
app/_locales/en/messages.json
generated
@ -714,6 +714,9 @@
|
||||
"custom": {
|
||||
"message": "Advanced"
|
||||
},
|
||||
"customContentSearch": {
|
||||
"message": "Search for a previously added network"
|
||||
},
|
||||
"customGas": {
|
||||
"message": "Customize Gas"
|
||||
},
|
||||
@ -2798,7 +2801,7 @@
|
||||
"message": "Settings"
|
||||
},
|
||||
"settingsSearchMatchingNotFound": {
|
||||
"message": "No matching results found"
|
||||
"message": "No matching results found."
|
||||
},
|
||||
"shorthandVersion": {
|
||||
"message": "v$1",
|
||||
@ -3539,6 +3542,9 @@
|
||||
"testFaucet": {
|
||||
"message": "Test Faucet"
|
||||
},
|
||||
"testNetworks": {
|
||||
"message": "Test networks"
|
||||
},
|
||||
"theme": {
|
||||
"message": "Theme"
|
||||
},
|
||||
|
@ -130,6 +130,13 @@ export const CHAIN_ID_TO_RPC_URL_MAP = {
|
||||
[LOCALHOST_CHAIN_ID]: LOCALHOST_RPC_URL,
|
||||
};
|
||||
|
||||
export const CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP = {
|
||||
[MAINNET_CHAIN_ID]: ETH_TOKEN_IMAGE_URL,
|
||||
[AVALANCHE_CHAIN_ID]: AVAX_TOKEN_IMAGE_URL,
|
||||
[BSC_CHAIN_ID]: BNB_TOKEN_IMAGE_URL,
|
||||
[POLYGON_CHAIN_ID]: MATIC_TOKEN_IMAGE_URL,
|
||||
};
|
||||
|
||||
export const CHAIN_ID_TO_NETWORK_ID_MAP = Object.values(
|
||||
NETWORK_TYPE_TO_ID_MAP,
|
||||
).reduce((chainIdToNetworkIdMap, { chainId, networkId }) => {
|
||||
|
@ -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';
|
@ -1,10 +1,46 @@
|
||||
@use "design-system";
|
||||
|
||||
.networks-tab {
|
||||
&__imageclose {
|
||||
cursor: pointer;
|
||||
color: var(--color-icon-default);
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
max-width: 739px;
|
||||
max-width: 779px;
|
||||
justify-content: space-between;
|
||||
|
||||
&__custom-image {
|
||||
border: 1px solid var(--color-border-default);
|
||||
margin-inline-start: 8px;
|
||||
}
|
||||
|
||||
&__check-icon {
|
||||
margin-inline-end: 10px;
|
||||
color: var(--color-success-default);
|
||||
|
||||
&__transparent {
|
||||
color: transparent;
|
||||
width: 16px;
|
||||
margin-inline-end: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon-with-fallback {
|
||||
padding: 0 1px 2px 2px;
|
||||
color: var(--color-primary-inverse); // TODO: design-tokens needs network colors
|
||||
margin-inline-start: 8px;
|
||||
|
||||
@each $variant, $color in design-system.$color-map {
|
||||
&--color-#{$variant} {
|
||||
background: var($color);
|
||||
color: var(--color-primary-inverse); // TODO: design-tokens needs network colors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
margin-top: 0;
|
||||
flex-direction: column;
|
||||
@ -52,7 +88,9 @@
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
max-height: 465px;
|
||||
max-width: 400px;
|
||||
margin-top: 24px;
|
||||
padding-inline-start: 24px;
|
||||
|
||||
.page-container__footer {
|
||||
border-top: none;
|
||||
@ -74,6 +112,7 @@
|
||||
align-items: center;
|
||||
width: 90%;
|
||||
margin-top: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,8 +142,38 @@
|
||||
|
||||
&__networks-list {
|
||||
flex: 0.5 0 auto;
|
||||
max-width: 343px;
|
||||
margin-top: 24px;
|
||||
max-width: 350px;
|
||||
border-right: 1px solid var(--color-border-default);
|
||||
|
||||
&__custom-search-network {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.MuiInput-input {
|
||||
font-size: 14px;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.MuiTextField-root {
|
||||
padding-inline-end: 16px;
|
||||
|
||||
#search-networks-helper-text {
|
||||
color: var(--color-text-alternative);
|
||||
}
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
padding: 0 24px 0 24px;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
@media screen and (max-width: $break-small) {
|
||||
margin-inline-start: 58px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
flex: 1;
|
||||
@ -149,9 +218,10 @@
|
||||
|
||||
&__networks-list-item {
|
||||
display: flex;
|
||||
padding: 13px 0 13px 17px;
|
||||
padding: 12px 24px 12px 0;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
width: 311px;
|
||||
|
||||
.color-indicator {
|
||||
&:hover {
|
||||
@ -159,13 +229,12 @@
|
||||
}
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
margin: 0 4px 0 10px;
|
||||
margin: 0 4px 0 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
padding: 20px 23px 21px 17px;
|
||||
border-bottom: 1px solid var(--color-border-default);
|
||||
padding: 12px 0 12px 24px;
|
||||
max-width: 351px;
|
||||
}
|
||||
}
|
||||
@ -191,35 +260,31 @@
|
||||
}
|
||||
|
||||
svg {
|
||||
margin-left: 10px;
|
||||
margin-inline-start: 15px;
|
||||
padding-top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&__networks-list-arrow {
|
||||
display: none;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
display: block;
|
||||
right: 10px;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
margin: 0 5px;
|
||||
|
||||
[dir='rtl'] & {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
color: var(--color-text-default);
|
||||
}
|
||||
}
|
||||
|
||||
&__networks-list-name--selected {
|
||||
font-weight: bold;
|
||||
color: var(--color-text-default);
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
font-weight: normal;
|
||||
color: var(--color-text-default);
|
||||
}
|
||||
}
|
||||
|
||||
&__networks-list-name--disabled {
|
||||
font-weight: 300;
|
||||
color: var(--color-text-muted);
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
color: var(--color-text-default);
|
||||
}
|
||||
}
|
||||
|
||||
&__network-form-footer {
|
||||
|
@ -4,16 +4,19 @@ import classnames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useI18nContext } from '../../../../hooks/useI18nContext';
|
||||
import { NETWORK_TYPE_RPC } from '../../../../../shared/constants/network';
|
||||
import { SIZES } from '../../../../helpers/constants/design-system';
|
||||
import ColorIndicator from '../../../../components/ui/color-indicator';
|
||||
import {
|
||||
NETWORK_TYPE_RPC,
|
||||
CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP,
|
||||
} from '../../../../../shared/constants/network';
|
||||
import LockIcon from '../../../../components/ui/lock-icon';
|
||||
import IconCaretRight from '../../../../components/ui/icon/icon-caret-right';
|
||||
import IconCheck from '../../../../components/ui/icon/icon-check';
|
||||
import { NETWORKS_FORM_ROUTE } from '../../../../helpers/constants/routes';
|
||||
import { setSelectedSettingsRpcUrl } from '../../../../store/actions';
|
||||
import { getEnvironmentType } from '../../../../../app/scripts/lib/util';
|
||||
import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../../shared/constants/app';
|
||||
import { getProvider } from '../../../../selectors';
|
||||
import Identicon from '../../../../components/ui/identicon';
|
||||
import UrlIcon from '../../../../components/ui/url-icon';
|
||||
|
||||
import { handleHooksSettingsRefs } from '../../../../helpers/utils/settings-search';
|
||||
|
||||
@ -22,6 +25,8 @@ const NetworksListItem = ({
|
||||
networkIsSelected,
|
||||
selectedRpcUrl,
|
||||
networkIndex,
|
||||
setSearchQuery,
|
||||
setSearchedNetworks,
|
||||
}) => {
|
||||
const t = useI18nContext();
|
||||
const history = useHistory();
|
||||
@ -45,6 +50,9 @@ const NetworksListItem = ({
|
||||
(listItemUrlIsProviderUrl || listItemTypeIsProviderNonRpcType);
|
||||
const displayNetworkListItemAsSelected =
|
||||
listItemNetworkIsSelected || listItemNetworkIsCurrentProvider;
|
||||
const isCurrentRpcTarget =
|
||||
listItemUrlIsProviderUrl || listItemTypeIsProviderNonRpcType;
|
||||
|
||||
const settingsRefs = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
@ -57,17 +65,46 @@ const NetworksListItem = ({
|
||||
key={`settings-network-list-item:${rpcUrl}`}
|
||||
className="networks-tab__networks-list-item"
|
||||
onClick={() => {
|
||||
setSearchQuery('');
|
||||
setSearchedNetworks([]);
|
||||
dispatch(setSelectedSettingsRpcUrl(rpcUrl));
|
||||
if (!isFullScreen) {
|
||||
history.push(NETWORKS_FORM_ROUTE);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ColorIndicator
|
||||
color={labelKey}
|
||||
type={ColorIndicator.TYPES.FILLED}
|
||||
size={SIZES.LG}
|
||||
/>
|
||||
{isCurrentRpcTarget ? (
|
||||
<IconCheck color="var(--color-success-default)" />
|
||||
) : (
|
||||
<div className="networks-tab__content__check-icon__transparent">✓</div>
|
||||
)}
|
||||
{network.chainId in CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP ? (
|
||||
<Identicon
|
||||
className="networks-tab__content__custom-image"
|
||||
diameter={24}
|
||||
image={CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP[network.chainId]}
|
||||
imageBorder
|
||||
/>
|
||||
) : (
|
||||
!network.isATestNetwork && (
|
||||
<UrlIcon
|
||||
className="networks-tab__content__icon-with-fallback"
|
||||
fallbackClassName="networks-tab__content__icon-with-fallback"
|
||||
name={label}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{network.isATestNetwork && (
|
||||
<UrlIcon
|
||||
name={label || labelKey}
|
||||
fallbackClassName={classnames(
|
||||
'networks-tab__content__icon-with-fallback',
|
||||
{
|
||||
[`networks-tab__content__icon-with-fallback--color-${labelKey}`]: true,
|
||||
},
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={classnames('networks-tab__networks-list-name', {
|
||||
'networks-tab__networks-list-name--selected': displayNetworkListItemAsSelected,
|
||||
@ -81,7 +118,6 @@ const NetworksListItem = ({
|
||||
<LockIcon width="14px" height="17px" fill="var(--color-icon-muted)" />
|
||||
)}
|
||||
</div>
|
||||
<IconCaretRight className="networks-tab__networks-list-arrow" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -91,6 +127,8 @@ NetworksListItem.propTypes = {
|
||||
networkIsSelected: PropTypes.bool,
|
||||
selectedRpcUrl: PropTypes.string,
|
||||
networkIndex: PropTypes.number,
|
||||
setSearchQuery: PropTypes.func,
|
||||
setSearchedNetworks: PropTypes.func,
|
||||
};
|
||||
|
||||
export default NetworksListItem;
|
||||
|
@ -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} />;
|
||||
};
|
@ -1,6 +1,13 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { useI18nContext } from '../../../../hooks/useI18nContext';
|
||||
import CustomContentSearch from '../custom-content-search';
|
||||
import Typography from '../../../../components/ui/typography';
|
||||
import {
|
||||
COLORS,
|
||||
TYPOGRAPHY,
|
||||
} from '../../../../helpers/constants/design-system';
|
||||
import NetworksListItem from '../networks-list-item';
|
||||
|
||||
const NetworksList = ({
|
||||
@ -9,6 +16,20 @@ const NetworksList = ({
|
||||
networkDefaultedToProvider,
|
||||
selectedRpcUrl,
|
||||
}) => {
|
||||
const t = useI18nContext();
|
||||
const [searchedNetworks, setSearchedNetworks] = useState([]);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const searchedNetworksToRender =
|
||||
searchedNetworks.length === 0 && searchQuery === ''
|
||||
? networksToRender
|
||||
: searchedNetworks;
|
||||
const searchedNetworksToRenderThatAreNotTestNetworks = searchedNetworksToRender.filter(
|
||||
(network) => !network.isATestNetwork,
|
||||
);
|
||||
const searchedNetworksToRenderThatAreTestNetworks = searchedNetworksToRender.filter(
|
||||
(network) => network.isATestNetwork,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('networks-tab__networks-list', {
|
||||
@ -16,13 +37,52 @@ const NetworksList = ({
|
||||
networkIsSelected && !networkDefaultedToProvider,
|
||||
})}
|
||||
>
|
||||
{networksToRender.map((network, index) => (
|
||||
<CustomContentSearch
|
||||
onSearch={({
|
||||
searchQuery: newSearchQuery = '',
|
||||
results: newResults = [],
|
||||
}) => {
|
||||
setSearchedNetworks(newResults);
|
||||
setSearchQuery(newSearchQuery);
|
||||
}}
|
||||
error={
|
||||
searchedNetworksToRender.length === 0
|
||||
? t('settingsSearchMatchingNotFound')
|
||||
: null
|
||||
}
|
||||
networksList={networksToRender}
|
||||
searchQueryInput={searchQuery}
|
||||
/>
|
||||
{searchedNetworksToRenderThatAreNotTestNetworks.map((network, index) => (
|
||||
<NetworksListItem
|
||||
key={`settings-network-list:${network.rpcUrl}`}
|
||||
network={network}
|
||||
networkIsSelected={networkIsSelected}
|
||||
selectedRpcUrl={selectedRpcUrl}
|
||||
networkIndex={index}
|
||||
setSearchQuery={setSearchQuery}
|
||||
setSearchedNetworks={setSearchedNetworks}
|
||||
/>
|
||||
))}
|
||||
{searchQuery === '' && (
|
||||
<Typography
|
||||
variant={TYPOGRAPHY.H6}
|
||||
margin={[6, 0, 0, 9]}
|
||||
color={COLORS.TEXT_MUTED}
|
||||
className="networks-tab__networks-list__label"
|
||||
>
|
||||
{t('testNetworks')}
|
||||
</Typography>
|
||||
)}
|
||||
{searchedNetworksToRenderThatAreTestNetworks.map((network, index) => (
|
||||
<NetworksListItem
|
||||
key={`settings-network-list:${network.rpcUrl}`}
|
||||
network={network}
|
||||
networkIsSelected={networkIsSelected}
|
||||
selectedRpcUrl={selectedRpcUrl}
|
||||
networkIndex={index}
|
||||
setSearchQuery={setSearchQuery}
|
||||
setSearchedNetworks={setSearchedNetworks}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -25,6 +25,7 @@ const renderComponent = (props) => {
|
||||
const defaultNetworks = defaultNetworksData.map((network) => ({
|
||||
...network,
|
||||
viewOnly: true,
|
||||
isATestNetwork: true,
|
||||
}));
|
||||
|
||||
const props = {
|
||||
@ -32,6 +33,7 @@ const props = {
|
||||
networkIsSelected: false,
|
||||
networksToRender: defaultNetworks,
|
||||
selectedRpcUrl: 'http://localhost:8545',
|
||||
isATestNetwork: true,
|
||||
};
|
||||
|
||||
describe('NetworksList Component', () => {
|
||||
|
@ -26,6 +26,7 @@ const renderComponent = (props) => {
|
||||
const defaultNetworks = defaultNetworksData.map((network) => ({
|
||||
...network,
|
||||
viewOnly: true,
|
||||
isATestNetwork: true,
|
||||
}));
|
||||
|
||||
const props = {
|
||||
@ -40,13 +41,16 @@ const props = {
|
||||
blockExplorerUrl: '',
|
||||
viewOnly: false,
|
||||
rpcPrefs: {},
|
||||
isATestNetwork: true,
|
||||
},
|
||||
shouldRenderNetworkForm: true,
|
||||
};
|
||||
|
||||
describe('NetworksTabContent Component', () => {
|
||||
it('should render networks tab content correctly', async () => {
|
||||
const { queryByText, getByDisplayValue } = renderComponent(props);
|
||||
const { queryByText, getByDisplayValue, getAllByText } = renderComponent(
|
||||
props,
|
||||
);
|
||||
|
||||
expect(queryByText('Ethereum Mainnet')).toBeInTheDocument();
|
||||
expect(queryByText('Ropsten Test Network')).toBeInTheDocument();
|
||||
@ -68,9 +72,7 @@ describe('NetworksTabContent Component', () => {
|
||||
getByDisplayValue(props.selectedNetwork.chainId),
|
||||
).toBeInTheDocument();
|
||||
expect(getByDisplayValue(props.selectedNetwork.ticker)).toBeInTheDocument();
|
||||
expect(
|
||||
getByDisplayValue(props.selectedNetwork.blockExplorerUrl),
|
||||
).toBeInTheDocument();
|
||||
expect(getAllByText(props.selectedNetwork.blockExplorerUrl)).toBeDefined();
|
||||
|
||||
fireEvent.change(getByDisplayValue(props.selectedNetwork.label), {
|
||||
target: { value: 'LocalHost 8545' },
|
||||
|
@ -16,7 +16,10 @@ import {
|
||||
getNetworksTabSelectedRpcUrl,
|
||||
getProvider,
|
||||
} from '../../../selectors';
|
||||
import { NETWORK_TYPE_RPC } from '../../../../shared/constants/network';
|
||||
import {
|
||||
NETWORK_TYPE_RPC,
|
||||
TEST_CHAINS,
|
||||
} from '../../../../shared/constants/network';
|
||||
import { defaultNetworksData } from './networks-tab.constants';
|
||||
import NetworksTabContent from './networks-tab-content';
|
||||
import NetworksForm from './networks-form';
|
||||
@ -25,6 +28,7 @@ import NetworksFormSubheader from './networks-tab-subheader';
|
||||
const defaultNetworks = defaultNetworksData.map((network) => ({
|
||||
...network,
|
||||
viewOnly: true,
|
||||
isATestNetwork: TEST_CHAINS.includes(network.chainId),
|
||||
}));
|
||||
|
||||
const NetworksTab = ({ addNewNetwork }) => {
|
||||
@ -50,6 +54,7 @@ const NetworksTab = ({ addNewNetwork }) => {
|
||||
chainId: rpc.chainId,
|
||||
ticker: rpc.ticker,
|
||||
blockExplorerUrl: rpc.rpcPrefs?.blockExplorerUrl || '',
|
||||
isATestNetwork: TEST_CHAINS.includes(rpc.chainId),
|
||||
};
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user