mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Search accounts by name (#7261)
Co-Authored-By: Paweł Lula <pavloblack@hotmail.com> Co-Authored-By: Mark Stacey <markjstacey@gmail.com>
This commit is contained in:
parent
6e0b8f80ad
commit
638e242ce6
@ -1199,6 +1199,12 @@
|
|||||||
"searchTokens": {
|
"searchTokens": {
|
||||||
"message": "Search Tokens"
|
"message": "Search Tokens"
|
||||||
},
|
},
|
||||||
|
"searchAccounts": {
|
||||||
|
"message": "Search Accounts"
|
||||||
|
},
|
||||||
|
"noAccountsFound": {
|
||||||
|
"message": "No accounts found for the given search query"
|
||||||
|
},
|
||||||
"selectAnAccount": {
|
"selectAnAccount": {
|
||||||
"message": "Select an Account"
|
"message": "Select an Account"
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import React, { PureComponent } from 'react'
|
import React, { Component } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import debounce from 'lodash.debounce'
|
import debounce from 'lodash.debounce'
|
||||||
|
import Fuse from 'fuse.js'
|
||||||
|
import InputAdornment from '@material-ui/core/InputAdornment'
|
||||||
|
|
||||||
import { Menu, Item, Divider, CloseArea } from '../dropdowns/components/menu'
|
import { Menu, Item, Divider, CloseArea } from '../dropdowns/components/menu'
|
||||||
import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums'
|
import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums'
|
||||||
import { getEnvironmentType } from '../../../../../app/scripts/lib/util'
|
import { getEnvironmentType } from '../../../../../app/scripts/lib/util'
|
||||||
@ -17,17 +20,19 @@ import {
|
|||||||
CONNECT_HARDWARE_ROUTE,
|
CONNECT_HARDWARE_ROUTE,
|
||||||
DEFAULT_ROUTE,
|
DEFAULT_ROUTE,
|
||||||
} from '../../../helpers/constants/routes'
|
} from '../../../helpers/constants/routes'
|
||||||
|
import TextField from '../../ui/text-field'
|
||||||
|
import SearchIcon from '../../ui/search-icon'
|
||||||
|
|
||||||
export default class AccountMenu extends PureComponent {
|
export default class AccountMenu extends Component {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
t: PropTypes.func,
|
t: PropTypes.func,
|
||||||
metricsEvent: PropTypes.func,
|
metricsEvent: PropTypes.func,
|
||||||
}
|
}
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
accounts: PropTypes.object,
|
shouldShowAccountsSearch: PropTypes.bool,
|
||||||
|
accounts: PropTypes.array,
|
||||||
history: PropTypes.object,
|
history: PropTypes.object,
|
||||||
identities: PropTypes.object,
|
|
||||||
isAccountMenuOpen: PropTypes.bool,
|
isAccountMenuOpen: PropTypes.bool,
|
||||||
keyrings: PropTypes.array,
|
keyrings: PropTypes.array,
|
||||||
lockMetamask: PropTypes.func,
|
lockMetamask: PropTypes.func,
|
||||||
@ -39,22 +44,76 @@ export default class AccountMenu extends PureComponent {
|
|||||||
originOfCurrentTab: PropTypes.string,
|
originOfCurrentTab: PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
accountsRef;
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
atAccountListBottom: false,
|
shouldShowScrollButton: false,
|
||||||
|
searchQuery: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
addressFuse = new Fuse([], {
|
||||||
|
shouldSort: false,
|
||||||
|
threshold: 0.45,
|
||||||
|
location: 0,
|
||||||
|
distance: 100,
|
||||||
|
maxPatternLength: 32,
|
||||||
|
minMatchCharLength: 1,
|
||||||
|
keys: [
|
||||||
|
{ name: 'name', weight: 0.5 },
|
||||||
|
{ name: 'address', weight: 0.5 },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
componentDidUpdate (prevProps, prevState) {
|
||||||
const { isAccountMenuOpen: prevIsAccountMenuOpen } = prevProps
|
const { isAccountMenuOpen: prevIsAccountMenuOpen } = prevProps
|
||||||
|
const { searchQuery: prevSearchQuery } = prevState
|
||||||
const { isAccountMenuOpen } = this.props
|
const { isAccountMenuOpen } = this.props
|
||||||
|
const { searchQuery } = this.state
|
||||||
|
|
||||||
if (!prevIsAccountMenuOpen && isAccountMenuOpen) {
|
if (!prevIsAccountMenuOpen && isAccountMenuOpen) {
|
||||||
this.setAtAccountListBottom()
|
this.setShouldShowScrollButton()
|
||||||
|
this.resetSearchQuery()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// recalculate on each search query change
|
||||||
|
// whether we can show scroll down button
|
||||||
|
if (isAccountMenuOpen && prevSearchQuery !== searchQuery) {
|
||||||
|
this.setShouldShowScrollButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAccountsSearch () {
|
||||||
|
const inputAdornment = (
|
||||||
|
<InputAdornment
|
||||||
|
position="start"
|
||||||
|
style={{
|
||||||
|
maxHeight: 'none',
|
||||||
|
marginRight: 0,
|
||||||
|
marginLeft: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SearchIcon />
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
|
||||||
|
return [
|
||||||
|
<TextField
|
||||||
|
key="search-text-field"
|
||||||
|
id="search-accounts"
|
||||||
|
placeholder={this.context.t('searchAccounts')}
|
||||||
|
type="text"
|
||||||
|
value={this.state.searchQuery}
|
||||||
|
onChange={e => this.setSearchQuery(e.target.value)}
|
||||||
|
startAdornment={inputAdornment}
|
||||||
|
fullWidth
|
||||||
|
theme="material-white-padded"
|
||||||
|
/>,
|
||||||
|
<Divider key="search-divider" />,
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
renderAccounts () {
|
renderAccounts () {
|
||||||
const {
|
const {
|
||||||
identities,
|
|
||||||
accounts,
|
accounts,
|
||||||
selectedAddress,
|
selectedAddress,
|
||||||
keyrings,
|
keyrings,
|
||||||
@ -62,14 +121,21 @@ export default class AccountMenu extends PureComponent {
|
|||||||
addressConnectedDomainMap,
|
addressConnectedDomainMap,
|
||||||
originOfCurrentTab,
|
originOfCurrentTab,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
const { searchQuery } = this.state
|
||||||
|
|
||||||
const accountOrder = keyrings.reduce((list, keyring) => list.concat(keyring.accounts), [])
|
let filteredIdentities = accounts
|
||||||
|
if (searchQuery) {
|
||||||
|
this.addressFuse.setCollection(accounts)
|
||||||
|
filteredIdentities = this.addressFuse.search(searchQuery)
|
||||||
|
}
|
||||||
|
|
||||||
return accountOrder.filter(address => !!identities[address]).map(address => {
|
if (filteredIdentities.length === 0) {
|
||||||
const identity = identities[address]
|
return <p className="account-menu__no-accounts">{this.context.t('noAccountsFound')}</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredIdentities.map(identity => {
|
||||||
const isSelected = identity.address === selectedAddress
|
const isSelected = identity.address === selectedAddress
|
||||||
|
|
||||||
const balanceValue = accounts[address] ? accounts[address].balance : ''
|
|
||||||
const simpleAddress = identity.address.substring(2).toLowerCase()
|
const simpleAddress = identity.address.substring(2).toLowerCase()
|
||||||
|
|
||||||
const keyring = keyrings.find(kr => {
|
const keyring = keyrings.find(kr => {
|
||||||
@ -106,7 +172,7 @@ export default class AccountMenu extends PureComponent {
|
|||||||
</div>
|
</div>
|
||||||
<UserPreferencedCurrencyDisplay
|
<UserPreferencedCurrencyDisplay
|
||||||
className="account-menu__balance"
|
className="account-menu__balance"
|
||||||
value={balanceValue}
|
value={identity.balance}
|
||||||
type={PRIMARY}
|
type={PRIMARY}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -181,28 +247,41 @@ export default class AccountMenu extends PureComponent {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
setAtAccountListBottom = () => {
|
resetSearchQuery () {
|
||||||
const target = document.querySelector('.account-menu__accounts')
|
this.setSearchQuery('')
|
||||||
const { scrollTop, offsetHeight, scrollHeight } = target
|
|
||||||
const atAccountListBottom = scrollTop + offsetHeight >= scrollHeight
|
|
||||||
this.setState({ atAccountListBottom })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onScroll = debounce(this.setAtAccountListBottom, 25)
|
setSearchQuery (searchQuery) {
|
||||||
|
this.setState({ searchQuery })
|
||||||
|
}
|
||||||
|
|
||||||
|
setShouldShowScrollButton = () => {
|
||||||
|
const { scrollTop, offsetHeight, scrollHeight } = this.accountsRef
|
||||||
|
|
||||||
|
const canScroll = scrollHeight > offsetHeight
|
||||||
|
|
||||||
|
const atAccountListBottom = scrollTop + offsetHeight >= scrollHeight
|
||||||
|
|
||||||
|
const shouldShowScrollButton = canScroll && !atAccountListBottom
|
||||||
|
|
||||||
|
this.setState({ shouldShowScrollButton})
|
||||||
|
}
|
||||||
|
|
||||||
|
onScroll = debounce(this.setShouldShowScrollButton, 25)
|
||||||
|
|
||||||
handleScrollDown = e => {
|
handleScrollDown = e => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
const target = document.querySelector('.account-menu__accounts')
|
|
||||||
const { scrollHeight } = target
|
const { scrollHeight } = this.accountsRef
|
||||||
target.scroll({ left: 0, top: scrollHeight, behavior: 'smooth' })
|
this.accountsRef.scroll({ left: 0, top: scrollHeight, behavior: 'smooth' })
|
||||||
this.setAtAccountListBottom()
|
|
||||||
|
this.setShouldShowScrollButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
renderScrollButton () {
|
renderScrollButton () {
|
||||||
const { accounts } = this.props
|
const { shouldShowScrollButton } = this.state
|
||||||
const { atAccountListBottom } = this.state
|
|
||||||
|
|
||||||
return !atAccountListBottom && Object.keys(accounts).length > 3 && (
|
return shouldShowScrollButton && (
|
||||||
<div
|
<div
|
||||||
className="account-menu__scroll-button"
|
className="account-menu__scroll-button"
|
||||||
onClick={this.handleScrollDown}
|
onClick={this.handleScrollDown}
|
||||||
@ -211,20 +290,21 @@ export default class AccountMenu extends PureComponent {
|
|||||||
src="./images/icons/down-arrow.svg"
|
src="./images/icons/down-arrow.svg"
|
||||||
width={28}
|
width={28}
|
||||||
height={28}
|
height={28}
|
||||||
|
alt="scroll down"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { t } = this.context
|
const { t, metricsEvent } = this.context
|
||||||
const {
|
const {
|
||||||
|
shouldShowAccountsSearch,
|
||||||
isAccountMenuOpen,
|
isAccountMenuOpen,
|
||||||
toggleAccountMenu,
|
toggleAccountMenu,
|
||||||
lockMetamask,
|
lockMetamask,
|
||||||
history,
|
history,
|
||||||
} = this.props
|
} = this.props
|
||||||
const { metricsEvent } = this.context
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
@ -246,9 +326,13 @@ export default class AccountMenu extends PureComponent {
|
|||||||
</Item>
|
</Item>
|
||||||
<Divider />
|
<Divider />
|
||||||
<div className="account-menu__accounts-container">
|
<div className="account-menu__accounts-container">
|
||||||
|
{shouldShowAccountsSearch ? this.renderAccountsSearch() : null}
|
||||||
<div
|
<div
|
||||||
className="account-menu__accounts"
|
className="account-menu__accounts"
|
||||||
onScroll={this.onScroll}
|
onScroll={this.onScroll}
|
||||||
|
ref={ref => {
|
||||||
|
this.accountsRef = ref
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{ this.renderAccounts() }
|
{ this.renderAccounts() }
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { compose } from 'recompose'
|
import { compose, withProps } from 'recompose'
|
||||||
import { withRouter } from 'react-router-dom'
|
import { withRouter } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
toggleAccountMenu,
|
toggleAccountMenu,
|
||||||
@ -13,23 +13,28 @@ import {
|
|||||||
} from '../../../store/actions'
|
} from '../../../store/actions'
|
||||||
import {
|
import {
|
||||||
getAddressConnectedDomainMap,
|
getAddressConnectedDomainMap,
|
||||||
getMetaMaskAccounts,
|
getMetaMaskAccountsOrdered,
|
||||||
|
getMetaMaskKeyrings,
|
||||||
getOriginOfCurrentTab,
|
getOriginOfCurrentTab,
|
||||||
|
getSelectedAddress,
|
||||||
} from '../../../selectors/selectors'
|
} from '../../../selectors/selectors'
|
||||||
|
|
||||||
import AccountMenu from './account-menu.component'
|
import AccountMenu from './account-menu.component'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The min amount of accounts to show search field
|
||||||
|
*/
|
||||||
|
const SHOW_SEARCH_ACCOUNTS_MIN_COUNT = 5
|
||||||
|
|
||||||
function mapStateToProps (state) {
|
function mapStateToProps (state) {
|
||||||
const { metamask: { selectedAddress, isAccountMenuOpen, keyrings, identities } } = state
|
const { metamask: { isAccountMenuOpen } } = state
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectedAddress,
|
|
||||||
isAccountMenuOpen,
|
isAccountMenuOpen,
|
||||||
keyrings,
|
|
||||||
identities,
|
|
||||||
accounts: getMetaMaskAccounts(state),
|
|
||||||
addressConnectedDomainMap: getAddressConnectedDomainMap(state),
|
addressConnectedDomainMap: getAddressConnectedDomainMap(state),
|
||||||
originOfCurrentTab: getOriginOfCurrentTab(state),
|
originOfCurrentTab: getOriginOfCurrentTab(state),
|
||||||
|
selectedAddress: getSelectedAddress(state),
|
||||||
|
keyrings: getMetaMaskKeyrings(state),
|
||||||
|
accounts: getMetaMaskAccountsOrdered(state),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,5 +70,6 @@ function mapDispatchToProps (dispatch) {
|
|||||||
|
|
||||||
export default compose(
|
export default compose(
|
||||||
withRouter,
|
withRouter,
|
||||||
connect(mapStateToProps, mapDispatchToProps)
|
connect(mapStateToProps, mapDispatchToProps),
|
||||||
|
withProps(({ accounts }) => ({ shouldShowAccountsSearch: accounts.length >= SHOW_SEARCH_ACCOUNTS_MIN_COUNT}))
|
||||||
)(AccountMenu)
|
)(AccountMenu)
|
||||||
|
@ -51,22 +51,26 @@
|
|||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__accounts {
|
&__accounts-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column nowrap;
|
flex-direction: column;
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 256px;
|
|
||||||
position: relative;
|
|
||||||
z-index: 200;
|
z-index: 200;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
max-height: 256px;
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 575px) {
|
@media screen and (max-width: 575px) {
|
||||||
max-height: 228px;
|
max-height: 228px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__accounts {
|
||||||
|
overflow-y: auto;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.keyring-label {
|
.keyring-label {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
@ -77,6 +81,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__no-accounts {
|
||||||
|
font-size: 0.8em;
|
||||||
|
padding: 16px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
&__account {
|
&__account {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row nowrap;
|
flex-flow: row nowrap;
|
||||||
|
1
ui/app/components/ui/search-icon/index.js
Normal file
1
ui/app/components/ui/search-icon/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './search-icon.component'
|
12
ui/app/components/ui/search-icon/search-icon.component.js
Normal file
12
ui/app/components/ui/search-icon/search-icon.component.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default function SearchIcon () {
|
||||||
|
return (
|
||||||
|
<svg height="20" width="20" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
|
||||||
|
<g clipRule="evenodd" fillRule="evenodd">
|
||||||
|
<path d="M9.167 3.333a5.833 5.833 0 100 11.667 5.833 5.833 0 000-11.667zm-7.5 5.834a7.5 7.5 0 1115 0 7.5 7.5 0 01-15 0z" />
|
||||||
|
<path d="M13.286 13.286a.833.833 0 011.178 0l3.625 3.625a.833.833 0 11-1.178 1.178l-3.625-3.625a.833.833 0 010-1.178z" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
@ -28,6 +28,24 @@ const styles = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
materialError: {},
|
materialError: {},
|
||||||
|
materialWhitePaddedRoot: {
|
||||||
|
color: '#aeaeae',
|
||||||
|
},
|
||||||
|
materialWhitePaddedInput: {
|
||||||
|
padding: '8px',
|
||||||
|
|
||||||
|
'&::placeholder': {
|
||||||
|
color: '#aeaeae',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
materialWhitePaddedFocused: {
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
materialWhitePaddedUnderline: {
|
||||||
|
'&:after': {
|
||||||
|
borderBottom: '2px solid #fff',
|
||||||
|
},
|
||||||
|
},
|
||||||
// Non-material styles
|
// Non-material styles
|
||||||
formLabel: {
|
formLabel: {
|
||||||
'&$formLabelFocused': {
|
'&$formLabelFocused': {
|
||||||
@ -66,35 +84,99 @@ const styles = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const TextField = props => {
|
const getMaterialThemeInputProps = ({
|
||||||
const { error, classes, material, startAdornment, largeLabel, dir, ...textFieldProps } = props
|
dir,
|
||||||
|
classes: { materialLabel, materialFocused, materialError, materialUnderline },
|
||||||
|
startAdornment,
|
||||||
|
}) => ({
|
||||||
|
InputLabelProps: {
|
||||||
|
FormLabelClasses: {
|
||||||
|
root: materialLabel,
|
||||||
|
focused: materialFocused,
|
||||||
|
error: materialError,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InputProps: {
|
||||||
|
startAdornment,
|
||||||
|
classes: {
|
||||||
|
underline: materialUnderline,
|
||||||
|
},
|
||||||
|
inputProps: {
|
||||||
|
dir,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const getMaterialWhitePaddedThemeInputProps = ({
|
||||||
|
dir,
|
||||||
|
classes: { materialWhitePaddedRoot, materialWhitePaddedFocused, materialWhitePaddedInput, materialWhitePaddedUnderline },
|
||||||
|
startAdornment,
|
||||||
|
}) => ({
|
||||||
|
InputProps: {
|
||||||
|
startAdornment,
|
||||||
|
classes: {
|
||||||
|
root: materialWhitePaddedRoot,
|
||||||
|
focused: materialWhitePaddedFocused,
|
||||||
|
input: materialWhitePaddedInput,
|
||||||
|
underline: materialWhitePaddedUnderline,
|
||||||
|
},
|
||||||
|
inputProps: {
|
||||||
|
dir,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const getBorderedThemeInputProps = ({
|
||||||
|
dir,
|
||||||
|
classes: { formLabel, formLabelFocused, materialError, largeInputLabel, inputLabel, inputRoot, input, inputFocused },
|
||||||
|
largeLabel,
|
||||||
|
startAdornment,
|
||||||
|
}) => ({
|
||||||
|
InputLabelProps: {
|
||||||
|
shrink: true,
|
||||||
|
className: largeLabel ? largeInputLabel : inputLabel,
|
||||||
|
FormLabelClasses: {
|
||||||
|
root: formLabel,
|
||||||
|
focused: formLabelFocused,
|
||||||
|
error: materialError,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InputProps: {
|
||||||
|
startAdornment,
|
||||||
|
disableUnderline: true,
|
||||||
|
classes: {
|
||||||
|
root: inputRoot,
|
||||||
|
input: input,
|
||||||
|
focused: inputFocused,
|
||||||
|
},
|
||||||
|
inputProps: {
|
||||||
|
dir,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const themeToInputProps = {
|
||||||
|
'material': getMaterialThemeInputProps,
|
||||||
|
'bordered': getBorderedThemeInputProps,
|
||||||
|
'material-white-padded': getMaterialWhitePaddedThemeInputProps,
|
||||||
|
}
|
||||||
|
|
||||||
|
const TextField = ({
|
||||||
|
error,
|
||||||
|
classes,
|
||||||
|
theme,
|
||||||
|
startAdornment,
|
||||||
|
largeLabel,
|
||||||
|
dir,
|
||||||
|
...textFieldProps
|
||||||
|
}) => {
|
||||||
|
const inputProps = themeToInputProps[theme]({ classes, startAdornment, largeLabel, dir })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MaterialTextField
|
<MaterialTextField
|
||||||
error={Boolean(error)}
|
error={Boolean(error)}
|
||||||
helperText={error}
|
helperText={error}
|
||||||
InputLabelProps={{
|
{...inputProps}
|
||||||
shrink: material ? undefined : true,
|
|
||||||
className: material ? '' : (largeLabel ? classes.largeInputLabel : classes.inputLabel),
|
|
||||||
FormLabelClasses: {
|
|
||||||
root: material ? classes.materialLabel : classes.formLabel,
|
|
||||||
focused: material ? classes.materialFocused : classes.formLabelFocused,
|
|
||||||
error: classes.materialError,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
InputProps={{
|
|
||||||
startAdornment: startAdornment || undefined,
|
|
||||||
disableUnderline: !material,
|
|
||||||
classes: {
|
|
||||||
root: material ? '' : classes.inputRoot,
|
|
||||||
input: material ? '' : classes.input,
|
|
||||||
underline: material ? classes.materialUnderline : '',
|
|
||||||
focused: material ? '' : classes.inputFocused,
|
|
||||||
},
|
|
||||||
inputProps: {
|
|
||||||
dir,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
{...textFieldProps}
|
{...textFieldProps}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@ -103,13 +185,14 @@ const TextField = props => {
|
|||||||
TextField.defaultProps = {
|
TextField.defaultProps = {
|
||||||
error: null,
|
error: null,
|
||||||
dir: 'auto',
|
dir: 'auto',
|
||||||
|
theme: 'bordered',
|
||||||
}
|
}
|
||||||
|
|
||||||
TextField.propTypes = {
|
TextField.propTypes = {
|
||||||
error: PropTypes.string,
|
error: PropTypes.string,
|
||||||
classes: PropTypes.object,
|
classes: PropTypes.object,
|
||||||
dir: PropTypes.string,
|
dir: PropTypes.string,
|
||||||
material: PropTypes.bool,
|
theme: PropTypes.oneOf(['bordered', 'material', 'material-white-padded']),
|
||||||
startAdornment: PropTypes.element,
|
startAdornment: PropTypes.element,
|
||||||
largeLabel: PropTypes.bool,
|
largeLabel: PropTypes.bool,
|
||||||
}
|
}
|
||||||
|
@ -33,14 +33,14 @@ storiesOf('TextField', module)
|
|||||||
<TextField
|
<TextField
|
||||||
label="Text"
|
label="Text"
|
||||||
type="text"
|
type="text"
|
||||||
material
|
theme="material"
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.add('Material password', () => (
|
.add('Material password', () => (
|
||||||
<TextField
|
<TextField
|
||||||
label="Password"
|
label="Password"
|
||||||
type="password"
|
type="password"
|
||||||
material
|
theme="material"
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.add('Material error', () => (
|
.add('Material error', () => (
|
||||||
@ -48,6 +48,6 @@ storiesOf('TextField', module)
|
|||||||
type="text"
|
type="text"
|
||||||
label="Name"
|
label="Name"
|
||||||
error="Invalid value"
|
error="Invalid value"
|
||||||
material
|
theme="material"
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
@ -36,13 +36,9 @@ export default class TokenSearch extends Component {
|
|||||||
error: PropTypes.string,
|
error: PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor (props) {
|
state = {
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
handleSearch (searchQuery) {
|
handleSearch (searchQuery) {
|
||||||
this.setState({ searchQuery })
|
this.setState({ searchQuery })
|
||||||
|
@ -165,7 +165,7 @@ export default class UnlockPage extends Component {
|
|||||||
error={error}
|
error={error}
|
||||||
autoFocus
|
autoFocus
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
material
|
theme="material"
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { NETWORK_TYPES } from '../helpers/constants/common'
|
import { NETWORK_TYPES } from '../helpers/constants/common'
|
||||||
import { mapObjectValues } from '../../../app/scripts/lib/util'
|
import { mapObjectValues } from '../../../app/scripts/lib/util'
|
||||||
import { stripHexPrefix, addHexPrefix } from 'ethereumjs-util'
|
import { stripHexPrefix, addHexPrefix } from 'ethereumjs-util'
|
||||||
|
import { createSelector } from 'reselect'
|
||||||
|
|
||||||
import abi from 'human-standard-token-abi'
|
import abi from 'human-standard-token-abi'
|
||||||
import { multiplyCurrencies } from '../helpers/utils/conversion-util'
|
import { multiplyCurrencies } from '../helpers/utils/conversion-util'
|
||||||
@ -58,6 +59,28 @@ export function getCurrentNetworkId (state) {
|
|||||||
return state.metamask.network
|
return state.metamask.network
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getMetaMaskAccounts = createSelector(
|
||||||
|
getMetaMaskAccountsRaw,
|
||||||
|
getMetaMaskCachedBalances,
|
||||||
|
(currentAccounts, cachedBalances) => Object.entries(currentAccounts).reduce((selectedAccounts, [accountID, account]) => {
|
||||||
|
if (account.balance === null || account.balance === undefined) {
|
||||||
|
return {
|
||||||
|
...selectedAccounts,
|
||||||
|
[accountID]: {
|
||||||
|
...account,
|
||||||
|
balance: cachedBalances && cachedBalances[accountID],
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
...selectedAccounts,
|
||||||
|
[accountID]: account,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {})
|
||||||
|
)
|
||||||
|
|
||||||
export function getSelectedAddress (state) {
|
export function getSelectedAddress (state) {
|
||||||
const selectedAddress = state.metamask.selectedAddress || Object.keys(getMetaMaskAccounts(state))[0]
|
const selectedAddress = state.metamask.selectedAddress || Object.keys(getMetaMaskAccounts(state))[0]
|
||||||
|
|
||||||
@ -80,25 +103,37 @@ export function getNumberOfTokens (state) {
|
|||||||
return tokens ? tokens.length : 0
|
return tokens ? tokens.length : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMetaMaskAccounts (state) {
|
export function getMetaMaskKeyrings (state) {
|
||||||
const currentAccounts = state.metamask.accounts
|
return state.metamask.keyrings
|
||||||
const cachedBalances = state.metamask.cachedBalances[state.metamask.network]
|
}
|
||||||
const selectedAccounts = {}
|
|
||||||
|
|
||||||
Object.keys(currentAccounts).forEach(accountID => {
|
export function getMetaMaskIdentities (state) {
|
||||||
const account = currentAccounts[accountID]
|
return state.metamask.identities
|
||||||
if (account && account.balance === null || account.balance === undefined) {
|
|
||||||
selectedAccounts[accountID] = {
|
|
||||||
...account,
|
|
||||||
balance: cachedBalances && cachedBalances[accountID],
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
selectedAccounts[accountID] = account
|
export function getMetaMaskAccountsRaw (state) {
|
||||||
|
return state.metamask.accounts
|
||||||
}
|
}
|
||||||
})
|
|
||||||
return selectedAccounts
|
export function getMetaMaskCachedBalances (state) {
|
||||||
|
const network = getCurrentNetworkId(state)
|
||||||
|
|
||||||
|
return state.metamask.cachedBalances[network]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get ordered (by keyrings) accounts with identity and balance
|
||||||
|
*/
|
||||||
|
export const getMetaMaskAccountsOrdered = createSelector(
|
||||||
|
getMetaMaskKeyrings,
|
||||||
|
getMetaMaskIdentities,
|
||||||
|
getMetaMaskAccounts,
|
||||||
|
(keyrings, identities, accounts) => keyrings
|
||||||
|
.reduce((list, keyring) => list.concat(keyring.accounts), [])
|
||||||
|
.filter(address => !!identities[address])
|
||||||
|
.map(address => ({ ...identities[address], ...accounts[address]}))
|
||||||
|
)
|
||||||
|
|
||||||
export function isBalanceCached (state) {
|
export function isBalanceCached (state) {
|
||||||
const selectedAccountBalance = state.metamask.accounts[getSelectedAddress(state)].balance
|
const selectedAccountBalance = state.metamask.accounts[getSelectedAddress(state)].balance
|
||||||
const cachedBalance = getSelectedAccountCachedBalance(state)
|
const cachedBalance = getSelectedAccountCachedBalance(state)
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
import assert from 'assert'
|
import assert from 'assert'
|
||||||
import * as selectors from '../selectors'
|
import { getAddressBook } from '../selectors.js'
|
||||||
const {
|
|
||||||
getAddressBook,
|
|
||||||
} = selectors
|
|
||||||
import mockState from './selectors-test-data'
|
import mockState from './selectors-test-data'
|
||||||
|
|
||||||
describe('selectors', () => {
|
describe('selectors', () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user