Add react-window in token list for displaying tokens
This commit is contained in:
parent
6bbfd04910
commit
49252889b0
|
@ -59,6 +59,7 @@
|
|||
"react-qr-reader": "^2.2.1",
|
||||
"react-redux": "7.1.1",
|
||||
"react-router-dom": "5.1.2",
|
||||
"react-window": "^1.8.5",
|
||||
"recompose": "^0.30.0",
|
||||
"redux": "4.0.4",
|
||||
"redux-actions": "^2.6.5",
|
||||
|
@ -114,7 +115,7 @@
|
|||
"eslint-plugin-import": "2.18.2",
|
||||
"eslint-plugin-jest": "22.17.0",
|
||||
"eslint-plugin-jsx-a11y": "6.2.3",
|
||||
"eslint-plugin-react": "7.15.0",
|
||||
"eslint-plugin-react": "7.14.3",
|
||||
"ethereumjs-abi": "0.6.8",
|
||||
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
||||
"file-loader": "4.2.0",
|
||||
|
|
|
@ -6,7 +6,11 @@ const fetchTokenList = () => {
|
|||
const apiUrl = getRelayUrl()
|
||||
const url = `${apiUrl}/tokens`
|
||||
|
||||
return axios.get(url)
|
||||
return axios.get(url, {
|
||||
params: {
|
||||
limit: 300,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default fetchTokenList
|
||||
|
|
|
@ -19,7 +19,7 @@ type Props = SelectorProps & Actions
|
|||
export const loadSafe = async (
|
||||
safeName: string,
|
||||
safeAddress: string,
|
||||
owners: Array,
|
||||
owners: Array<*>,
|
||||
addSafe: Function,
|
||||
) => {
|
||||
const safeProps = await buildSafe(safeAddress, safeName)
|
||||
|
|
|
@ -12,6 +12,7 @@ import SelectField from '~/components/forms/SelectField'
|
|||
import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils'
|
||||
import { required } from '~/components/forms/validator'
|
||||
import { type Token } from '~/logic/tokens/store/model/token'
|
||||
import { formatAmount } from '~/logic/tokens/utils/formatAmount'
|
||||
import { selectedTokenStyles, selectStyles } from './style'
|
||||
|
||||
type SelectFieldProps = {
|
||||
|
@ -35,7 +36,7 @@ const SelectedToken = ({ token, classes }: SelectedTokenProps) => (
|
|||
<ListItemText
|
||||
className={classes.tokenData}
|
||||
primary={token.name}
|
||||
secondary={`${token.balance} ${token.symbol}`}
|
||||
secondary={`${formatAmount(token.balance)} ${token.symbol}`}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
@ -54,7 +55,7 @@ const TokenSelectField = ({ tokens, classes, initialValue }: SelectFieldProps) =
|
|||
const [initialToken, setInitialToken] = useState<InitialTokenType>('')
|
||||
|
||||
useEffect(() => {
|
||||
const selectedToken = tokens.find(token => token.name === initialValue)
|
||||
const selectedToken = tokens.find((token) => token.name === initialValue)
|
||||
setInitialToken(selectedToken || '')
|
||||
}, [initialValue])
|
||||
|
||||
|
@ -64,16 +65,16 @@ const TokenSelectField = ({ tokens, classes, initialValue }: SelectFieldProps) =
|
|||
component={SelectField}
|
||||
classes={{ selectMenu: classes.selectMenu }}
|
||||
validate={required}
|
||||
renderValue={token => <SelectedTokenStyled token={token} />}
|
||||
renderValue={(token) => <SelectedTokenStyled token={token} />}
|
||||
initialValue={initialToken}
|
||||
displayEmpty
|
||||
>
|
||||
{tokens.map(token => (
|
||||
{tokens.map((token) => (
|
||||
<MenuItem key={token.address} value={token}>
|
||||
<ListItemIcon>
|
||||
<Img src={token.logoUri} height={28} alt={token.name} onError={setImageToPlaceholder} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={token.name} secondary={`${token.balance} ${token.symbol}`} />
|
||||
<ListItemText primary={token.name} secondary={`${formatAmount(token.balance)} ${token.symbol}`} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</Field>
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
// @flow
|
||||
import React, { memo } from 'react'
|
||||
import { List, Set } from 'immutable'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import ListItem from '@material-ui/core/ListItem'
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon'
|
||||
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
|
||||
import ListItemText from '@material-ui/core/ListItemText'
|
||||
import Switch from '@material-ui/core/Switch'
|
||||
import Img from '~/components/layout/Img'
|
||||
import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils'
|
||||
import { ETH_ADDRESS } from '~/logic/tokens/utils/tokenHelpers'
|
||||
import { type Token } from '~/logic/tokens/store/model/token'
|
||||
import { styles } from './style'
|
||||
|
||||
export const TOGGLE_TOKEN_TEST_ID = 'toggle-token-btn'
|
||||
|
||||
type Props = {
|
||||
data: {
|
||||
activeTokensAddresses: Set<string>,
|
||||
tokens: List<Token>,
|
||||
onSwitch: Function,
|
||||
},
|
||||
style: Object,
|
||||
index: number,
|
||||
classes: Object,
|
||||
}
|
||||
|
||||
const TokenRow = memo(({
|
||||
data, index, classes, style,
|
||||
}: Props) => {
|
||||
const { tokens, activeTokensAddresses, onSwitch } = data
|
||||
const token: Token = tokens.get(index)
|
||||
const isActive = activeTokensAddresses.has(token.address)
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
<ListItem className={classes.token} classes={{ root: classes.tokenRoot }}>
|
||||
<ListItemIcon className={classes.tokenIcon}>
|
||||
<Img src={token.logoUri} height={28} alt={token.name} onError={setImageToPlaceholder} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={token.symbol} secondary={token.name} />
|
||||
{token.address !== ETH_ADDRESS && (
|
||||
<ListItemSecondaryAction>
|
||||
<Switch
|
||||
onChange={onSwitch(token)}
|
||||
checked={isActive}
|
||||
inputProps={{ 'data-testid': `${token.symbol}_${TOGGLE_TOKEN_TEST_ID}` }}
|
||||
/>
|
||||
</ListItemSecondaryAction>
|
||||
)}
|
||||
</ListItem>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default withStyles(styles)(TokenRow)
|
|
@ -2,30 +2,23 @@
|
|||
import * as React from 'react'
|
||||
import { List, Set } from 'immutable'
|
||||
import cn from 'classnames'
|
||||
import { FixedSizeList } from 'react-window'
|
||||
import SearchBar from 'material-ui-search-bar'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import MuiList from '@material-ui/core/List'
|
||||
import ListItem from '@material-ui/core/ListItem'
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon'
|
||||
import CircularProgress from '@material-ui/core/CircularProgress'
|
||||
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
|
||||
import ListItemText from '@material-ui/core/ListItemText'
|
||||
import Switch from '@material-ui/core/Switch'
|
||||
import Search from '@material-ui/icons/Search'
|
||||
import Img from '~/components/layout/Img'
|
||||
import Block from '~/components/layout/Block'
|
||||
import Button from '~/components/layout/Button'
|
||||
import Divider from '~/components/layout/Divider'
|
||||
import Hairline from '~/components/layout/Hairline'
|
||||
import Spacer from '~/components/Spacer'
|
||||
import Row from '~/components/layout/Row'
|
||||
import { ETH_ADDRESS } from '~/logic/tokens/utils/tokenHelpers'
|
||||
import { type Token } from '~/logic/tokens/store/model/token'
|
||||
import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils'
|
||||
import TokenRow from './TokenRow'
|
||||
import { styles } from './style'
|
||||
|
||||
export const ADD_CUSTOM_TOKEN_BUTTON_TEST_ID = 'add-custom-token-btn'
|
||||
export const TOGGLE_TOKEN_TEST_ID = 'toggle-token-btn'
|
||||
|
||||
type Props = {
|
||||
classes: Object,
|
||||
|
@ -110,6 +103,18 @@ class Tokens extends React.Component<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
createItemData = (tokens, activeTokensAddresses) => ({
|
||||
tokens,
|
||||
activeTokensAddresses,
|
||||
onSwitch: this.onSwitch,
|
||||
})
|
||||
|
||||
getItemKey = (index, { tokens }) => {
|
||||
const token: Token = tokens.get(index)
|
||||
|
||||
return token.address
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes, tokens, setActiveScreen } = this.props
|
||||
const { filter, activeTokensAddresses } = this.state
|
||||
|
@ -122,6 +127,7 @@ class Tokens extends React.Component<Props, State> {
|
|||
const switchToAddCustomTokenScreen = () => setActiveScreen('addCustomToken')
|
||||
|
||||
const filteredTokens = filterBy(filter, tokens)
|
||||
const itemData = this.createItemData(filteredTokens, activeTokensAddresses)
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -152,34 +158,25 @@ class Tokens extends React.Component<Props, State> {
|
|||
</Row>
|
||||
<Hairline />
|
||||
</Block>
|
||||
<MuiList className={classes.list}>
|
||||
{!tokens.size && (
|
||||
<Block justify="center" className={classes.progressContainer}>
|
||||
<CircularProgress />
|
||||
</Block>
|
||||
)}
|
||||
{filteredTokens.map((token: Token) => {
|
||||
const isActive = activeTokensAddresses.has(token.address)
|
||||
|
||||
return (
|
||||
<ListItem key={token.address} className={classes.token} classes={{ root: classes.tokenRoot }}>
|
||||
<ListItemIcon className={classes.tokenIcon}>
|
||||
<Img src={token.logoUri} height={28} alt={token.name} onError={setImageToPlaceholder} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={token.symbol} secondary={token.name} />
|
||||
{token.address !== ETH_ADDRESS && (
|
||||
<ListItemSecondaryAction>
|
||||
<Switch
|
||||
onChange={this.onSwitch(token)}
|
||||
checked={isActive}
|
||||
inputProps={{ 'data-testid': `${token.symbol}_${TOGGLE_TOKEN_TEST_ID}` }}
|
||||
/>
|
||||
</ListItemSecondaryAction>
|
||||
)}
|
||||
</ListItem>
|
||||
)
|
||||
})}
|
||||
</MuiList>
|
||||
{!tokens.size && (
|
||||
<Block justify="center" className={classes.progressContainer}>
|
||||
<CircularProgress />
|
||||
</Block>
|
||||
)}
|
||||
{tokens.size > 0 && (
|
||||
<MuiList className={classes.list}>
|
||||
<FixedSizeList
|
||||
height={413}
|
||||
width={500}
|
||||
itemCount={filteredTokens.size}
|
||||
itemData={itemData}
|
||||
itemSize={51}
|
||||
itemKey={this.getItemKey}
|
||||
>
|
||||
{TokenRow}
|
||||
</FixedSizeList>
|
||||
</MuiList>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -56,6 +56,8 @@ export const styles = () => ({
|
|||
},
|
||||
tokenIcon: {
|
||||
marginRight: md,
|
||||
height: '28px',
|
||||
width: '28px',
|
||||
},
|
||||
progressContainer: {
|
||||
width: '100%',
|
||||
|
|
29
yarn.lock
29
yarn.lock
|
@ -7335,20 +7335,20 @@ eslint-plugin-jsx-a11y@6.2.3:
|
|||
has "^1.0.3"
|
||||
jsx-ast-utils "^2.2.1"
|
||||
|
||||
eslint-plugin-react@7.15.0:
|
||||
version "7.15.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.15.0.tgz#4808b19cf7b4c439454099d4eb8f0cf0e9fe31dd"
|
||||
integrity sha512-NbIh/yVXoltm8Df28PiPRanfCZAYubGqXU391MTCpW955Vum7S0nZdQYXGAvDh9ye4aNCmOR6YcYZsfMbEQZQA==
|
||||
eslint-plugin-react@7.14.3:
|
||||
version "7.14.3"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.14.3.tgz#911030dd7e98ba49e1b2208599571846a66bdf13"
|
||||
integrity sha512-EzdyyBWC4Uz2hPYBiEJrKCUi2Fn+BJ9B/pJQcjw5X+x/H2Nm59S4MJIvL4O5NEE0+WbnQwEBxWY03oUk+Bc3FA==
|
||||
dependencies:
|
||||
array-includes "^3.0.3"
|
||||
doctrine "^2.1.0"
|
||||
has "^1.0.3"
|
||||
jsx-ast-utils "^2.2.1"
|
||||
jsx-ast-utils "^2.1.0"
|
||||
object.entries "^1.1.0"
|
||||
object.fromentries "^2.0.0"
|
||||
object.values "^1.1.0"
|
||||
prop-types "^15.7.2"
|
||||
resolve "^1.12.0"
|
||||
resolve "^1.10.1"
|
||||
|
||||
eslint-scope@^3.7.1:
|
||||
version "3.7.3"
|
||||
|
@ -11359,7 +11359,7 @@ jss@10.0.0-alpha.25:
|
|||
is-in-browser "^1.1.3"
|
||||
tiny-warning "^1.0.2"
|
||||
|
||||
jsx-ast-utils@^2.2.1:
|
||||
jsx-ast-utils@^2.1.0, jsx-ast-utils@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.2.1.tgz#4d4973ebf8b9d2837ee91a8208cc66f3a2776cfb"
|
||||
integrity sha512-v3FxCcAf20DayI+uxnCuw795+oOIkVu6EnJ1+kSzhqqTZHNkTZ7B66ZgLp4oLJ/gbA64cI0B7WRoHZMSRdyVRQ==
|
||||
|
@ -12133,6 +12133,11 @@ memdown@~3.0.0:
|
|||
ltgt "~2.2.0"
|
||||
safe-buffer "~5.1.1"
|
||||
|
||||
"memoize-one@>=3.1.1 <6":
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
|
||||
integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==
|
||||
|
||||
memoize-one@^5.0.0:
|
||||
version "5.0.5"
|
||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.0.5.tgz#8cd3809555723a07684afafcd6f756072ac75d7e"
|
||||
|
@ -14899,6 +14904,14 @@ react-transition-group@^4.3.0:
|
|||
loose-envify "^1.4.0"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
react-window@^1.8.5:
|
||||
version "1.8.5"
|
||||
resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.5.tgz#a56b39307e79979721021f5d06a67742ecca52d1"
|
||||
integrity sha512-HeTwlNa37AFa8MDZFZOKcNEkuF2YflA0hpGPiTT9vR7OawEt+GZbfM6wqkBahD3D3pUjIabQYzsnY/BSJbgq6Q==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.0.0"
|
||||
memoize-one ">=3.1.1 <6"
|
||||
|
||||
react@16.10.1:
|
||||
version "16.10.1"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-16.10.1.tgz#967c1e71a2767dfa699e6ba702a00483e3b0573f"
|
||||
|
@ -15592,7 +15605,7 @@ resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.3.2
|
|||
dependencies:
|
||||
path-parse "^1.0.6"
|
||||
|
||||
resolve@^1.12.0:
|
||||
resolve@^1.10.1, resolve@^1.12.0:
|
||||
version "1.12.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6"
|
||||
integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==
|
||||
|
|
Loading…
Reference in New Issue