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:
Jaco Greeff 2017-01-31 12:21:50 +01:00 committed by GitHub
parent ee906467ad
commit 223c474487
7 changed files with 234 additions and 81 deletions

View File

@ -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 {

View File

@ -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,6 +58,7 @@ export default class AccountCard extends Component {
onFocus={ this.onFocus } onFocus={ this.onFocus }
onKeyDown={ this.handleKeyDown } onKeyDown={ this.handleKeyDown }
> >
<div className={ styles.infoContainer }>
<IdentityIcon address={ address } /> <IdentityIcon address={ address } />
<div className={ styles.accountInfo }> <div className={ styles.accountInfo }>
<div className={ styles.accountName }> <div className={ styles.accountName }>
@ -68,13 +68,19 @@ export default class AccountCard extends Component {
unknown unknown
/> />
</div> </div>
{ this.renderTags(tags, address) }
{ this.renderDescription(description) } { this.renderDescription(description) }
{ this.renderAddress(address) } { this.renderAddress(address) }
{ this.renderBalance(address) }
</div> </div>
</div> </div>
<Tags tags={ tags } />
<Balance
balance={ balance }
className={ styles.balance }
showOnlyEth
showZeroValues
/>
</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) => {

View File

@ -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);
});
});
}); });
}); });

View File

@ -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>
); );

View 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);
});
});
});

View File

@ -28,14 +28,21 @@ export default class Tags extends Component {
} }
render () { render () {
return (<div className={ styles.tags }> const { tags } = this.props;
if (!tags || tags.length === 0) {
return null;
}
return (
<div className={ styles.tags }>
{ this.renderTags() } { this.renderTags() }
</div>); </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 }

View File

@ -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,