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