Update AccountCard for re-use (#4350)
* Tags handle empty values * Export AccountCard in ~/ui * Allow ETH-only & zero display * Use ui/Balance for balance display * Add tests for Balance & Tags component availability * Add className, optional handlers only * Remove debug logging * AccountCard UI update
This commit is contained in:
parent
ee906467ad
commit
223c474487
@ -20,8 +20,8 @@
|
|||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
|
|
||||||
background-color: rgba(0, 0, 0, 0.8);
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
|
||||||
@ -53,6 +53,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.infoContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
font-size: 0.75em;
|
font-size: 0.75em;
|
||||||
color: rgba(255, 255, 255, 0.5);
|
color: rgba(255, 255, 255, 0.5);
|
||||||
@ -86,14 +93,10 @@
|
|||||||
.accountName {
|
.accountName {
|
||||||
font-weight: 700 !important;
|
font-weight: 700 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.balance {
|
.balance {
|
||||||
.tag {
|
margin-top: 0;
|
||||||
margin-left: 0.5em;
|
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes copied {
|
@keyframes copied {
|
||||||
|
@ -18,21 +18,20 @@ import React, { Component, PropTypes } from 'react';
|
|||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import keycode from 'keycode';
|
import keycode from 'keycode';
|
||||||
|
|
||||||
|
import Balance from '~/ui/Balance';
|
||||||
import IdentityIcon from '~/ui/IdentityIcon';
|
import IdentityIcon from '~/ui/IdentityIcon';
|
||||||
import IdentityName from '~/ui/IdentityName';
|
import IdentityName from '~/ui/IdentityName';
|
||||||
import Tags from '~/ui/Tags';
|
import Tags from '~/ui/Tags';
|
||||||
|
|
||||||
import { fromWei } from '~/api/util/wei';
|
|
||||||
|
|
||||||
import styles from './accountCard.css';
|
import styles from './accountCard.css';
|
||||||
|
|
||||||
export default class AccountCard extends Component {
|
export default class AccountCard extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: PropTypes.object.isRequired,
|
account: PropTypes.object.isRequired,
|
||||||
onClick: PropTypes.func.isRequired,
|
balance: PropTypes.object,
|
||||||
onFocus: PropTypes.func.isRequired,
|
className: PropTypes.string,
|
||||||
|
onClick: PropTypes.func,
|
||||||
balance: PropTypes.object
|
onFocus: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -40,11 +39,11 @@ export default class AccountCard extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account } = this.props;
|
const { account, balance, className } = this.props;
|
||||||
const { copied } = this.state;
|
const { copied } = this.state;
|
||||||
const { address, description, meta = {}, name } = account;
|
const { address, description, meta = {}, name } = account;
|
||||||
const { tags = [] } = meta;
|
const { tags = [] } = meta;
|
||||||
const classes = [ styles.account ];
|
const classes = [ styles.account, className ];
|
||||||
|
|
||||||
if (copied) {
|
if (copied) {
|
||||||
classes.push(styles.copied);
|
classes.push(styles.copied);
|
||||||
@ -59,21 +58,28 @@ export default class AccountCard extends Component {
|
|||||||
onFocus={ this.onFocus }
|
onFocus={ this.onFocus }
|
||||||
onKeyDown={ this.handleKeyDown }
|
onKeyDown={ this.handleKeyDown }
|
||||||
>
|
>
|
||||||
<IdentityIcon address={ address } />
|
<div className={ styles.infoContainer }>
|
||||||
<div className={ styles.accountInfo }>
|
<IdentityIcon address={ address } />
|
||||||
<div className={ styles.accountName }>
|
<div className={ styles.accountInfo }>
|
||||||
<IdentityName
|
<div className={ styles.accountName }>
|
||||||
address={ address }
|
<IdentityName
|
||||||
name={ name }
|
address={ address }
|
||||||
unknown
|
name={ name }
|
||||||
/>
|
unknown
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{ this.renderDescription(description) }
|
||||||
|
{ this.renderAddress(address) }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ this.renderTags(tags, address) }
|
|
||||||
{ this.renderDescription(description) }
|
|
||||||
{ this.renderAddress(address) }
|
|
||||||
{ this.renderBalance(address) }
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Tags tags={ tags } />
|
||||||
|
<Balance
|
||||||
|
balance={ balance }
|
||||||
|
className={ styles.balance }
|
||||||
|
showOnlyEth
|
||||||
|
showZeroValues
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -105,40 +111,6 @@ export default class AccountCard extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTags (tags = [], address) {
|
|
||||||
if (tags.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tags tags={ tags } />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderBalance (address) {
|
|
||||||
const { balance = {} } = this.props;
|
|
||||||
|
|
||||||
if (!balance.tokens) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ethToken = balance.tokens
|
|
||||||
.find((tok) => tok.token && (tok.token.tag || '').toLowerCase() === 'eth');
|
|
||||||
|
|
||||||
if (!ethToken) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = fromWei(ethToken.value).toFormat(3);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={ styles.balance }>
|
|
||||||
<span>{ value }</span>
|
|
||||||
<span className={ styles.tag }>ETH</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleKeyDown = (event) => {
|
handleKeyDown = (event) => {
|
||||||
const codeName = keycode(event);
|
const codeName = keycode(event);
|
||||||
|
|
||||||
@ -182,13 +154,13 @@ export default class AccountCard extends Component {
|
|||||||
onClick = () => {
|
onClick = () => {
|
||||||
const { account, onClick } = this.props;
|
const { account, onClick } = this.props;
|
||||||
|
|
||||||
onClick(account.address);
|
onClick && onClick(account.address);
|
||||||
}
|
}
|
||||||
|
|
||||||
onFocus = () => {
|
onFocus = () => {
|
||||||
const { account, onFocus } = this.props;
|
const { account, onFocus } = this.props;
|
||||||
|
|
||||||
onFocus(account.index);
|
onFocus && onFocus(account.index);
|
||||||
}
|
}
|
||||||
|
|
||||||
preventEvent = (e) => {
|
preventEvent = (e) => {
|
||||||
|
@ -61,6 +61,23 @@ describe('ui/AccountCard', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('components', () => {
|
describe('components', () => {
|
||||||
|
describe('Balance', () => {
|
||||||
|
let balance;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
balance = component.find('Connect(Balance)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the balance', () => {
|
||||||
|
expect(balance.length).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets showOnlyEth & showZeroValues', () => {
|
||||||
|
expect(balance.props().showOnlyEth).to.be.true;
|
||||||
|
expect(balance.props().showZeroValues).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('IdentityIcon', () => {
|
describe('IdentityIcon', () => {
|
||||||
let icon;
|
let icon;
|
||||||
|
|
||||||
@ -100,5 +117,17 @@ describe('ui/AccountCard', () => {
|
|||||||
expect(name.props().unknown).to.be.true;
|
expect(name.props().unknown).to.be.true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Tags', () => {
|
||||||
|
let tags;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tags = component.find('Tags');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the tags', () => {
|
||||||
|
expect(tags.length).to.equal(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -16,31 +16,46 @@
|
|||||||
|
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import unknownImage from '../../../assets/images/contracts/unknown-64x64.png';
|
import unknownImage from '~/../assets/images/contracts/unknown-64x64.png';
|
||||||
|
|
||||||
import styles from './balance.css';
|
import styles from './balance.css';
|
||||||
|
|
||||||
class Balance extends Component {
|
class Balance extends Component {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
api: PropTypes.object
|
api: PropTypes.object
|
||||||
}
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
balance: PropTypes.object,
|
balance: PropTypes.object,
|
||||||
images: PropTypes.object.isRequired
|
className: PropTypes.string,
|
||||||
}
|
images: PropTypes.object.isRequired,
|
||||||
|
showOnlyEth: PropTypes.bool,
|
||||||
|
showZeroValues: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
showOnlyEth: false,
|
||||||
|
showZeroValues: false
|
||||||
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { api } = this.context;
|
const { api } = this.context;
|
||||||
const { balance, images } = this.props;
|
const { balance, className, images, showZeroValues, showOnlyEth } = this.props;
|
||||||
|
|
||||||
if (!balance) {
|
if (!balance || !balance.tokens) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let body = (balance.tokens || [])
|
let body = balance.tokens
|
||||||
.filter((balance) => new BigNumber(balance.value).gt(0))
|
.filter((balance) => {
|
||||||
|
const hasBalance = showZeroValues || new BigNumber(balance.value).gt(0);
|
||||||
|
const isValidToken = !showOnlyEth || (balance.token.tag || '').toLowerCase() === 'eth';
|
||||||
|
|
||||||
|
return hasBalance && isValidToken;
|
||||||
|
})
|
||||||
.map((balance, index) => {
|
.map((balance, index) => {
|
||||||
const token = balance.token;
|
const token = balance.token;
|
||||||
|
|
||||||
@ -95,13 +110,16 @@ class Balance extends Component {
|
|||||||
if (!body.length) {
|
if (!body.length) {
|
||||||
body = (
|
body = (
|
||||||
<div className={ styles.empty }>
|
<div className={ styles.empty }>
|
||||||
There are no balances associated with this account
|
<FormattedMessage
|
||||||
|
id='ui.balance.none'
|
||||||
|
defaultMessage='There are no balances associated with this account'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ styles.balances }>
|
<div className={ [styles.balances, className].join(' ') }>
|
||||||
{ body }
|
{ body }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
122
js/src/ui/Balance/balance.spec.js
Normal file
122
js/src/ui/Balance/balance.spec.js
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import apiutil from '~/api/util';
|
||||||
|
|
||||||
|
import Balance from './';
|
||||||
|
|
||||||
|
const BALANCE = {
|
||||||
|
tokens: [
|
||||||
|
{ value: '122', token: { tag: 'ETH' } },
|
||||||
|
{ value: '345', token: { tag: 'GAV', format: 1 } },
|
||||||
|
{ value: '0', token: { tag: 'TST', format: 1 } }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
let api;
|
||||||
|
let component;
|
||||||
|
let store;
|
||||||
|
|
||||||
|
function createApi () {
|
||||||
|
api = {
|
||||||
|
dappsUrl: 'http://testDapps:1234/',
|
||||||
|
util: apiutil
|
||||||
|
};
|
||||||
|
|
||||||
|
return api;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStore () {
|
||||||
|
store = {
|
||||||
|
dispatch: sinon.stub(),
|
||||||
|
subscribe: sinon.stub(),
|
||||||
|
getState: () => {
|
||||||
|
return {
|
||||||
|
images: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render (props = {}) {
|
||||||
|
if (!props.balance) {
|
||||||
|
props.balance = BALANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
component = shallow(
|
||||||
|
<Balance
|
||||||
|
className='testClass'
|
||||||
|
{ ...props }
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
store: createStore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).find('Balance').shallow({ context: { api: createApi() } });
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ui/Balance', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders defaults', () => {
|
||||||
|
expect(component).to.be.ok;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes the specified className', () => {
|
||||||
|
expect(component.hasClass('testClass')).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders all the non-zero balances', () => {
|
||||||
|
expect(component.find('img')).to.have.length(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('render specifiers', () => {
|
||||||
|
it('renders only the single token with showOnlyEth', () => {
|
||||||
|
render({ showOnlyEth: true });
|
||||||
|
expect(component.find('img')).to.have.length(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders all the tokens with showZeroValues', () => {
|
||||||
|
render({ showZeroValues: true });
|
||||||
|
expect(component.find('img')).to.have.length(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows ETH with zero value with showOnlyEth & showZeroValues', () => {
|
||||||
|
render({
|
||||||
|
showOnlyEth: true,
|
||||||
|
showZeroValues: true,
|
||||||
|
balance: {
|
||||||
|
tokens: [
|
||||||
|
{ value: '0', token: { tag: 'ETH' } },
|
||||||
|
{ value: '345', token: { tag: 'GAV', format: 1 } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(component.find('img')).to.have.length(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -28,14 +28,21 @@ export default class Tags extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return (<div className={ styles.tags }>
|
const { tags } = this.props;
|
||||||
{ this.renderTags() }
|
|
||||||
</div>);
|
if (!tags || tags.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={ styles.tags }>
|
||||||
|
{ this.renderTags() }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTags () {
|
renderTags () {
|
||||||
const { handleAddSearchToken, setRefs } = this.props;
|
const { handleAddSearchToken, setRefs, tags } = this.props;
|
||||||
const tags = this.props.tags || [];
|
|
||||||
|
|
||||||
const tagClasses = handleAddSearchToken
|
const tagClasses = handleAddSearchToken
|
||||||
? [ styles.tag, styles.tagClickable ]
|
? [ styles.tag, styles.tagClickable ]
|
||||||
@ -47,14 +54,14 @@ export default class Tags extends Component {
|
|||||||
|
|
||||||
return tags
|
return tags
|
||||||
.sort()
|
.sort()
|
||||||
.map((tag, idx) => {
|
.map((tag, index) => {
|
||||||
const onClick = handleAddSearchToken
|
const onClick = handleAddSearchToken
|
||||||
? () => handleAddSearchToken(tag)
|
? () => handleAddSearchToken(tag)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={ idx }
|
key={ `tag_${index}` }
|
||||||
className={ tagClasses.join(' ') }
|
className={ tagClasses.join(' ') }
|
||||||
onClick={ onClick }
|
onClick={ onClick }
|
||||||
ref={ setRef }
|
ref={ setRef }
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import AccountCard from './AccountCard';
|
||||||
import Actionbar from './Actionbar';
|
import Actionbar from './Actionbar';
|
||||||
import ActionbarExport from './Actionbar/Export';
|
import ActionbarExport from './Actionbar/Export';
|
||||||
import ActionbarImport from './Actionbar/Import';
|
import ActionbarImport from './Actionbar/Import';
|
||||||
@ -59,6 +60,7 @@ import TxList from './TxList';
|
|||||||
import Warning from './Warning';
|
import Warning from './Warning';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
AccountCard,
|
||||||
Actionbar,
|
Actionbar,
|
||||||
ActionbarExport,
|
ActionbarExport,
|
||||||
ActionbarImport,
|
ActionbarImport,
|
||||||
|
Loading…
Reference in New Issue
Block a user