Merge pull request #3799 from ethcore/ng-ui-fixes

Several Fixes to the UI
This commit is contained in:
Gav Wood 2016-12-11 02:16:21 +01:00 committed by GitHub
commit 078feaadd5
26 changed files with 520 additions and 241 deletions

View File

@ -62,6 +62,10 @@ export default class Contracts {
}
static create (api) {
if (instance) {
return instance;
}
return new Contracts(api);
}

View File

@ -15,7 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { Redirect, Router, Route } from 'react-router';
import { Redirect, Router, Route, IndexRoute } from 'react-router';
import { Accounts, Account, Addresses, Address, Application, Contract, Contracts, WriteContract, Wallet, Dapp, Dapps, Settings, SettingsBackground, SettingsParity, SettingsProxy, SettingsViews, Signer, Status } from '~/views';
@ -26,6 +26,23 @@ export default class MainApplication extends Component {
routerHistory: PropTypes.any.isRequired
};
handleDeprecatedRoute = (nextState, replace) => {
const { address } = nextState.params;
const redirectMap = {
account: 'accounts',
address: 'addresses',
contract: 'contracts'
};
const oldRoute = nextState.routes[0].path;
const newRoute = Object.keys(redirectMap).reduce((newRoute, key) => {
return newRoute.replace(new RegExp(`^/${key}`), '/' + redirectMap[key]);
}, oldRoute);
console.warn(`Route "${oldRoute}" is deprecated. Please use "${newRoute}"`);
replace(newRoute.replace(':address', address));
}
render () {
const { routerHistory } = this.props;
@ -34,26 +51,46 @@ export default class MainApplication extends Component {
<Redirect from='/' to='/accounts' />
<Redirect from='/auth' to='/accounts' query={ {} } />
<Redirect from='/settings' to='/settings/views' />
{ /** Backward Compatible links */ }
<Route path='/account/:address' onEnter={ this.handleDeprecatedRoute } />
<Route path='/address/:address' onEnter={ this.handleDeprecatedRoute } />
<Route path='/contract/:address' onEnter={ this.handleDeprecatedRoute } />
<Route path='/' component={ Application }>
<Route path='accounts' component={ Accounts } />
<Route path='account/:address' component={ Account } />
<Route path='wallet/:address' component={ Wallet } />
<Route path='addresses' component={ Addresses } />
<Route path='address/:address' component={ Address } />
<Route path='accounts'>
<IndexRoute component={ Accounts } />
<Route path=':address' component={ Account } />
<Route path='/wallet/:address' component={ Wallet } />
</Route>
<Route path='addresses'>
<IndexRoute component={ Addresses } />
<Route path=':address' component={ Address } />
</Route>
<Route path='apps' component={ Dapps } />
<Route path='app/:id' component={ Dapp } />
<Route path='contracts' component={ Contracts } />
<Route path='contracts/write' component={ WriteContract } />
<Route path='contract/:address' component={ Contract } />
<Route path='contracts'>
<IndexRoute component={ Contracts } />
<Route path='develop' component={ WriteContract } />
<Route path=':address' component={ Contract } />
</Route>
<Route path='settings' component={ Settings }>
<Route path='background' component={ SettingsBackground } />
<Route path='proxy' component={ SettingsProxy } />
<Route path='views' component={ SettingsViews } />
<Route path='parity' component={ SettingsParity } />
</Route>
<Route path='signer' component={ Signer } />
<Route path='status' component={ Status } />
<Route path='status/:subpage' component={ Status } />
<Route path='status'>
<IndexRoute component={ Status } />
<Route path=':subpage' component={ Status } />
</Route>
</Route>
</Router>
);

View File

@ -28,6 +28,7 @@ export default class AddAddress extends Component {
static propTypes = {
contacts: PropTypes.object.isRequired,
address: PropTypes.string,
onClose: PropTypes.func
};
@ -39,6 +40,12 @@ export default class AddAddress extends Component {
description: ''
};
componentWillMount () {
if (this.props.address) {
this.onEditAddress(null, this.props.address);
}
}
render () {
return (
<Modal
@ -77,6 +84,8 @@ export default class AddAddress extends Component {
hint='the network address for the entry'
error={ addressError }
value={ address }
disabled={ !!this.props.address }
allowCopy={ false }
onChange={ this.onEditAddress } />
<Input
label='address name'

View File

@ -30,10 +30,12 @@ export default class WalletInfo extends Component {
owners: PropTypes.array.isRequired,
required: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
PropTypes.number
]).isRequired,
daylimit: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
PropTypes.number
]).isRequired,

View File

@ -28,26 +28,25 @@ export default class DetailsStep extends Component {
static propTypes = {
accounts: PropTypes.object.isRequired,
onFromAddressChange: PropTypes.func.isRequired,
onNameChange: PropTypes.func.isRequired,
onDescriptionChange: PropTypes.func.isRequired,
onAbiChange: PropTypes.func.isRequired,
onCodeChange: PropTypes.func.isRequired,
onParamsChange: PropTypes.func.isRequired,
onDescriptionChange: PropTypes.func.isRequired,
onFromAddressChange: PropTypes.func.isRequired,
onInputsChange: PropTypes.func.isRequired,
onNameChange: PropTypes.func.isRequired,
onParamsChange: PropTypes.func.isRequired,
abi: PropTypes.string,
abiError: PropTypes.string,
balances: PropTypes.object,
code: PropTypes.string,
codeError: PropTypes.string,
description: PropTypes.string,
descriptionError: PropTypes.string,
fromAddress: PropTypes.string,
fromAddressError: PropTypes.string,
name: PropTypes.string,
nameError: PropTypes.string,
description: PropTypes.string,
descriptionError: PropTypes.string,
abi: PropTypes.string,
abiError: PropTypes.string,
code: PropTypes.string,
codeError: PropTypes.string,
readOnly: PropTypes.bool
};
@ -77,6 +76,7 @@ export default class DetailsStep extends Component {
render () {
const {
accounts,
balances,
readOnly,
fromAddress, fromAddressError,
@ -94,24 +94,28 @@ export default class DetailsStep extends Component {
<AddressSelect
label='from account (contract owner)'
hint='the owner account for this contract'
value={ fromAddress }
error={ fromAddressError }
accounts={ accounts }
onChange={ this.onFromAddressChange } />
balances={ balances }
error={ fromAddressError }
onChange={ this.onFromAddressChange }
value={ fromAddress }
/>
<Input
label='contract name'
hint='a name for the deployed contract'
error={ nameError }
onChange={ this.onNameChange }
value={ name || '' }
onChange={ this.onNameChange } />
/>
<Input
label='contract description (optional)'
hint='a description for the contract'
error={ descriptionError }
onChange={ this.onDescriptionChange }
value={ description }
onChange={ this.onDescriptionChange } />
/>
{ this.renderContractSelect() }
@ -119,17 +123,19 @@ export default class DetailsStep extends Component {
label='abi / solc combined-output'
hint='the abi of the contract to deploy or solc combined-output'
error={ abiError }
value={ solcOutput }
onChange={ this.onSolcChange }
onSubmit={ this.onSolcSubmit }
readOnly={ readOnly } />
readOnly={ readOnly }
value={ solcOutput }
/>
<Input
label='code'
hint='the compiled code of the contract to deploy'
error={ codeError }
value={ code }
onSubmit={ this.onCodeChange }
readOnly={ readOnly || solc } />
readOnly={ readOnly || solc }
value={ code }
/>
</Form>
);

View File

@ -15,8 +15,10 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
import ContentClear from 'material-ui/svg-icons/content/clear';
import { pick } from 'lodash';
import { BusyStep, CompletedStep, CopyToClipboard, Button, IdentityIcon, Modal, TxHash } from '~/ui';
import { ERRORS, validateAbi, validateCode, validateName } from '~/util/validation';
@ -36,7 +38,7 @@ const STEPS = {
COMPLETED: { title: 'completed' }
};
export default class DeployContract extends Component {
class DeployContract extends Component {
static contextTypes = {
api: PropTypes.object.isRequired,
store: PropTypes.object.isRequired
@ -45,6 +47,7 @@ export default class DeployContract extends Component {
static propTypes = {
accounts: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired,
balances: PropTypes.object,
abi: PropTypes.string,
code: PropTypes.string,
readOnly: PropTypes.bool,
@ -192,7 +195,7 @@ export default class DeployContract extends Component {
}
renderStep () {
const { accounts, readOnly } = this.props;
const { accounts, readOnly, balances } = this.props;
const { address, deployError, step, deployState, txhash, rejected } = this.state;
if (deployError) {
@ -216,6 +219,7 @@ export default class DeployContract extends Component {
<DetailsStep
{ ...this.state }
accounts={ accounts }
balances={ balances }
readOnly={ readOnly }
onFromAddressChange={ this.onFromAddressChange }
onDescriptionChange={ this.onDescriptionChange }
@ -394,3 +398,17 @@ export default class DeployContract extends Component {
this.props.onClose();
}
}
function mapStateToProps (initState, initProps) {
const fromAddresses = Object.keys(initProps.accounts);
return (state) => {
const balances = pick(state.balances.balances, fromAddresses);
return { balances };
};
}
export default connect(
mapStateToProps
)(DeployContract);

View File

@ -32,25 +32,27 @@ export default class DetailsStep extends Component {
static propTypes = {
accounts: PropTypes.object.isRequired,
contract: PropTypes.object.isRequired,
amount: PropTypes.string,
amountError: PropTypes.string,
onAmountChange: PropTypes.func.isRequired,
fromAddress: PropTypes.string,
fromAddressError: PropTypes.string,
gasEdit: PropTypes.bool,
onFromAddressChange: PropTypes.func.isRequired,
func: PropTypes.object,
funcError: PropTypes.string,
onFuncChange: PropTypes.func,
onGasEditClick: PropTypes.func,
onValueChange: PropTypes.func.isRequired,
values: PropTypes.array.isRequired,
valuesError: PropTypes.array.isRequired,
warning: PropTypes.string,
onValueChange: PropTypes.func.isRequired
amount: PropTypes.string,
amountError: PropTypes.string,
balances: PropTypes.object,
fromAddress: PropTypes.string,
fromAddressError: PropTypes.string,
func: PropTypes.object,
funcError: PropTypes.string,
gasEdit: PropTypes.bool,
onFuncChange: PropTypes.func,
onGasEditClick: PropTypes.func,
warning: PropTypes.string
}
render () {
const { accounts, amount, amountError, fromAddress, fromAddressError, gasEdit, onGasEditClick, onFromAddressChange, onAmountChange } = this.props;
const { accounts, amount, amountError, balances, fromAddress, fromAddressError, gasEdit, onGasEditClick, onFromAddressChange, onAmountChange } = this.props;
return (
<Form>
@ -61,6 +63,7 @@ export default class DetailsStep extends Component {
value={ fromAddress }
error={ fromAddressError }
accounts={ accounts }
balances={ balances }
onChange={ onFromAddressChange } />
{ this.renderFunctionSelect() }
{ this.renderParameters() }

View File

@ -18,6 +18,8 @@ import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { observer } from 'mobx-react';
import { pick } from 'lodash';
import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
import ContentClear from 'material-ui/svg-icons/content/clear';
import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
@ -57,6 +59,7 @@ class ExecuteContract extends Component {
isTest: PropTypes.bool,
fromAddress: PropTypes.string,
accounts: PropTypes.object,
balances: PropTypes.object,
contract: PropTypes.object,
gasLimit: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired,
@ -362,10 +365,15 @@ class ExecuteContract extends Component {
}
}
function mapStateToProps (state) {
function mapStateToProps (initState, initProps) {
const fromAddresses = Object.keys(initProps.accounts);
return (state) => {
const balances = pick(state.balances.balances, fromAddresses);
const { gasLimit } = state.nodeStatus;
return { gasLimit };
return { gasLimit, balances };
};
}
function mapDispatchToProps (dispatch) {

View File

@ -134,6 +134,7 @@ export default class Details extends Component {
images: PropTypes.object.isRequired,
sender: PropTypes.string,
senderError: PropTypes.string,
sendersBalances: PropTypes.object,
recipient: PropTypes.string,
recipientError: PropTypes.string,
tag: PropTypes.string,
@ -203,7 +204,7 @@ export default class Details extends Component {
}
renderFromAddress () {
const { sender, senderError, senders } = this.props;
const { sender, senderError, senders, sendersBalances } = this.props;
if (!senders) {
return null;
@ -218,6 +219,7 @@ export default class Details extends Component {
hint='the sender address'
value={ sender }
onChange={ this.onEditSender }
balances={ sendersBalances }
/>
</div>
);

View File

@ -54,6 +54,7 @@ export default class TransferStore {
@observable sender = '';
@observable senderError = null;
@observable sendersBalances = {};
@observable total = '0.0';
@observable totalError = null;
@ -66,8 +67,6 @@ export default class TransferStore {
onClose = null;
senders = null;
sendersBalances = null;
isWallet = false;
wallet = null;

View File

@ -155,8 +155,8 @@ class Transfer extends Component {
renderDetailsPage () {
const { account, balance, images, senders } = this.props;
const { valueAll, extras, recipient, recipientError, sender, senderError } = this.store;
const { tag, total, totalError, value, valueError } = this.store;
const { recipient, recipientError, sender, senderError, sendersBalances } = this.store;
const { valueAll, extras, tag, total, totalError, value, valueError } = this.store;
return (
<Details
@ -170,6 +170,7 @@ class Transfer extends Component {
recipientError={ recipientError }
sender={ sender }
senderError={ senderError }
sendersBalances={ sendersBalances }
tag={ tag }
total={ total }
totalError={ totalError }

View File

@ -15,7 +15,9 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.account {
padding: 4px 0 0 0;
padding: 0.25em 0;
display: flex;
align-items: center;
}
.name {
@ -27,6 +29,11 @@
padding: 0 0 0 1em;
}
.balance {
color: #aaa;
padding-left: 1em;
}
.image {
display: inline-block;
height: 32px;

View File

@ -21,6 +21,8 @@ import AutoComplete from '../AutoComplete';
import IdentityIcon from '../../IdentityIcon';
import IdentityName from '../../IdentityName';
import { fromWei } from '~/api/util/wei';
import styles from './addressSelect.css';
export default class AddressSelect extends Component {
@ -40,27 +42,46 @@ export default class AddressSelect extends Component {
value: PropTypes.string,
tokens: PropTypes.object,
onChange: PropTypes.func.isRequired,
allowInput: PropTypes.bool
allowInput: PropTypes.bool,
balances: PropTypes.object
}
state = {
autocompleteEntries: [],
entries: {},
addresses: [],
value: ''
}
entriesFromProps (props = this.props) {
const { accounts, contacts, contracts, wallets } = props;
const entries = Object.assign({}, accounts || {}, wallets || {}, contacts || {}, contracts || {});
return entries;
const { accounts = {}, contacts = {}, contracts = {}, wallets = {} } = props;
const autocompleteEntries = [].concat(
Object.values(wallets),
'divider',
Object.values(accounts),
'divider',
Object.values(contacts),
'divider',
Object.values(contracts)
);
const entries = {
...wallets,
...accounts,
...contacts,
...contracts
};
return { autocompleteEntries, entries };
}
componentWillMount () {
const { value } = this.props;
const entries = this.entriesFromProps();
const { entries, autocompleteEntries } = this.entriesFromProps();
const addresses = Object.keys(entries).sort();
this.setState({ entries, addresses, value });
this.setState({ autocompleteEntries, entries, addresses, value });
}
componentWillReceiveProps (newProps) {
@ -71,7 +92,7 @@ export default class AddressSelect extends Component {
render () {
const { allowInput, disabled, error, hint, label } = this.props;
const { entries, value } = this.state;
const { autocompleteEntries, value } = this.state;
const searchText = this.getSearchText();
const icon = this.renderIdentityIcon(value);
@ -89,7 +110,7 @@ export default class AddressSelect extends Component {
onUpdateInput={ allowInput && this.onUpdateInput }
value={ searchText }
filter={ this.handleFilter }
entries={ entries }
entries={ autocompleteEntries }
entry={ this.getEntry() || {} }
renderItem={ this.renderItem }
/>
@ -129,7 +150,34 @@ export default class AddressSelect extends Component {
};
}
renderBalance (address) {
const { balances = {} } = this.props;
const balance = balances[address];
if (!balance) {
return null;
}
const ethToken = balance.tokens.find((tok) => tok.token && tok.token.tag && tok.token.tag.toLowerCase() === 'eth');
if (!ethToken) {
return null;
}
const value = fromWei(ethToken.value);
return (
<div className={ styles.balance }>
{ value.toFormat(3) }<small> { 'ETH' }</small>
</div>
);
}
renderMenuItem (address) {
const balance = this.props.balances
? this.renderBalance(address)
: null;
const item = (
<div className={ styles.account }>
<IdentityIcon
@ -139,6 +187,7 @@ export default class AddressSelect extends Component {
<IdentityName
className={ styles.name }
address={ address } />
{ balance }
</div>
);
@ -155,11 +204,10 @@ export default class AddressSelect extends Component {
getSearchText () {
const entry = this.getEntry();
const { value } = this.state;
return entry && entry.name
? entry.name.toUpperCase()
: value;
: this.state.value;
}
getEntry () {

View File

@ -16,11 +16,24 @@
import React, { Component, PropTypes } from 'react';
import keycode from 'keycode';
import { MenuItem, AutoComplete as MUIAutoComplete } from 'material-ui';
import { MenuItem, AutoComplete as MUIAutoComplete, Divider as MUIDivider } from 'material-ui';
import { PopoverAnimationVertical } from 'material-ui/Popover';
import { isEqual } from 'lodash';
// Hack to prevent "Unknown prop `disableFocusRipple` on <hr> tag" error
class Divider extends Component {
static muiName = MUIDivider.muiName;
render () {
return (
<div style={ { margin: '0.25em 0' } }>
<MUIDivider style={ { height: 2 } } />
</div>
);
}
}
export default class AutoComplete extends Component {
static propTypes = {
onChange: PropTypes.func.isRequired,
@ -38,15 +51,17 @@ export default class AutoComplete extends Component {
PropTypes.array,
PropTypes.object
])
}
};
state = {
lastChangedValue: undefined,
entry: null,
open: false,
fakeBlur: false,
dataSource: []
}
dataSource: [],
dividerBreaks: []
};
dividersVisibility = {};
componentWillMount () {
const dataSource = this.getDataSource();
@ -64,7 +79,7 @@ export default class AutoComplete extends Component {
}
render () {
const { disabled, error, hint, label, value, className, filter, onUpdateInput } = this.props;
const { disabled, error, hint, label, value, className, onUpdateInput } = this.props;
const { open, dataSource } = this.state;
return (
@ -78,9 +93,9 @@ export default class AutoComplete extends Component {
onUpdateInput={ onUpdateInput }
searchText={ value }
onFocus={ this.onFocus }
onBlur={ this.onBlur }
onClose={ this.onClose }
animation={ PopoverAnimationVertical }
filter={ filter }
filter={ this.handleFilter }
popoverProps={ { open } }
openOnFocus
menuCloseDelay={ 0 }
@ -100,18 +115,76 @@ export default class AutoComplete extends Component {
? entries
: Object.values(entries);
if (renderItem && typeof renderItem === 'function') {
return entriesArray.map(entry => renderItem(entry));
let currentDivider = 0;
let firstSet = false;
const dataSource = entriesArray.map((entry, index) => {
// Render divider
if (typeof entry === 'string' && entry.toLowerCase() === 'divider') {
// Don't add divider if nothing before
if (!firstSet) {
return undefined;
}
return entriesArray.map(entry => ({
const item = {
text: '',
divider: currentDivider,
isDivider: true,
value: (
<Divider />
)
};
currentDivider++;
return item;
}
let item;
if (renderItem && typeof renderItem === 'function') {
item = renderItem(entry);
} else {
item = {
text: entry,
value: (
<MenuItem
primaryText={ entry }
/>
)
}));
};
}
if (!firstSet) {
item.first = true;
firstSet = true;
}
item.divider = currentDivider;
return item;
}).filter((item) => item !== undefined);
return dataSource;
}
handleFilter = (searchText, name, item) => {
if (item.isDivider) {
return this.dividersVisibility[item.divider];
}
if (item.first) {
this.dividersVisibility = {};
}
const { filter } = this.props;
const show = filter(searchText, name, item);
// Show the related divider
if (show) {
this.dividersVisibility[item.divider] = true;
}
return show;
}
onKeyDown = (event) => {
@ -121,7 +194,6 @@ export default class AutoComplete extends Component {
case 'down':
const { menu } = muiAutocomplete.refs;
menu && menu.handleKeyDown(event);
this.setState({ fakeBlur: true });
break;
case 'enter':
@ -155,22 +227,12 @@ export default class AutoComplete extends Component {
this.setState({ entry, open: false });
}
onBlur = (event) => {
onClose = (event) => {
const { onUpdateInput } = this.props;
// TODO: Handle blur gracefully where we use onUpdateInput (currently replaces
// input where text is allowed with the last selected value from the dropdown)
if (!onUpdateInput) {
window.setTimeout(() => {
const { entry, fakeBlur } = this.state;
if (fakeBlur) {
this.setState({ fakeBlur: false });
return;
}
const { entry } = this.state;
this.handleOnChange(entry);
}, 200);
}
}

View File

@ -16,7 +16,7 @@
*/
.layout {
padding: 0.25em 0.25em 1em 0.25em;
padding: 0.25em;
&>div {
margin-bottom: 0.75em;

View File

@ -31,6 +31,10 @@
.infoline,
.uuidline {
line-height: 1.618em;
&.bigaddress {
font-size: 1.25em;
}
}
.infoline,

View File

@ -32,18 +32,20 @@ export default class Header extends Component {
balance: PropTypes.object,
className: PropTypes.string,
children: PropTypes.node,
isContract: PropTypes.bool
isContract: PropTypes.bool,
hideName: PropTypes.bool
};
static defaultProps = {
className: '',
children: null,
isContract: false
isContract: false,
hideName: false
};
render () {
const { api } = this.context;
const { account, balance, className, children } = this.props;
const { account, balance, className, children, hideName } = this.props;
const { address, meta, uuid } = account;
if (!account) {
@ -60,17 +62,20 @@ export default class Header extends Component {
<IdentityIcon
address={ address } />
<div className={ styles.floatleft }>
<ContainerTitle title={ <IdentityName address={ address } unknown /> } />
<div className={ styles.addressline }>
{ this.renderName(address) }
<div className={ [ hideName ? styles.bigaddress : '', styles.addressline ].join(' ') }>
<CopyToClipboard data={ address } />
<div className={ styles.address }>{ address }</div>
</div>
{ uuidText }
<div className={ styles.infoline }>
{ meta.description }
</div>
{ this.renderTxCount() }
</div>
<div className={ styles.tags }>
<Tags tags={ meta.tags } />
</div>
@ -89,6 +94,18 @@ export default class Header extends Component {
);
}
renderName (address) {
const { hideName } = this.props;
if (hideName) {
return null;
}
return (
<ContainerTitle title={ <IdentityName address={ address } unknown /> } />
);
}
renderTxCount () {
const { balance, isContract } = this.props;

View File

@ -26,7 +26,7 @@ import VerifyIcon from 'material-ui/svg-icons/action/verified-user';
import { EditMeta, DeleteAccount, Shapeshift, SMSVerification, Transfer, PasswordManager } from '~/modals';
import { Actionbar, Button, Page } from '~/ui';
import shapeshiftBtn from '../../../assets/images/shapeshift-btn.png';
import shapeshiftBtn from '~/../assets/images/shapeshift-btn.png';
import Header from './Header';
import Transactions from './Transactions';

View File

@ -153,7 +153,7 @@ export default class Summary extends Component {
const { link, noLink, account, name } = this.props;
const { address } = account;
const viewLink = `/${link || 'account'}/${address}`;
const viewLink = `/${link || 'accounts'}/${address}`;
const content = (
<IdentityName address={ address } name={ name } unknown />

View File

@ -19,8 +19,9 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import ActionDelete from 'material-ui/svg-icons/action/delete';
import ContentCreate from 'material-ui/svg-icons/content/create';
import ContentAdd from 'material-ui/svg-icons/content/add';
import { EditMeta } from '~/modals';
import { EditMeta, AddAddress } from '~/modals';
import { Actionbar, Button, Page } from '~/ui';
import Header from '../Account/Header';
@ -32,7 +33,7 @@ class Address extends Component {
static contextTypes = {
api: PropTypes.object.isRequired,
router: PropTypes.object.isRequired
}
};
static propTypes = {
setVisibleAccounts: PropTypes.func.isRequired,
@ -40,12 +41,13 @@ class Address extends Component {
contacts: PropTypes.object,
balances: PropTypes.object,
params: PropTypes.object
}
};
state = {
showDeleteDialog: false,
showEditDialog: false
}
showEditDialog: false,
showAdd: false
};
componentDidMount () {
this.setVisibleAccounts();
@ -73,32 +75,69 @@ class Address extends Component {
render () {
const { contacts, balances } = this.props;
const { address } = this.props.params;
const { showDeleteDialog } = this.state;
if (Object.keys(contacts).length === 0) {
return null;
}
const contact = (contacts || {})[address];
const balance = (balances || {})[address];
if (!contact) {
return (
<div>
{ this.renderAddAddress(contact, address) }
{ this.renderEditDialog(contact) }
{ this.renderActionbar(contact) }
{ this.renderDelete(contact) }
<Page>
<Header
account={ contact || { address, meta: {} } }
balance={ balance }
hideName={ !contact }
/>
<Transactions
address={ address }
/>
</Page>
</div>
);
}
renderAddAddress (contact, address) {
if (contact) {
return null;
}
const { contacts } = this.props;
const { showAdd } = this.state;
if (!showAdd) {
return null;
}
return (
<div>
{ this.renderEditDialog(contact) }
{ this.renderActionbar(contact) }
<AddAddress
contacts={ contacts }
onClose={ this.onCloseAdd }
address={ address }
/>
);
}
renderDelete (contact) {
if (!contact) {
return null;
}
const { showDeleteDialog } = this.state;
return (
<Delete
account={ contact }
visible={ showDeleteDialog }
route='/addresses'
onClose={ this.closeDeleteDialog } />
<Page>
<Header
account={ contact }
balance={ balance } />
<Transactions
address={ address } />
</Page>
</div>
onClose={ this.closeDeleteDialog }
/>
);
}
@ -116,17 +155,27 @@ class Address extends Component {
onClick={ this.showDeleteDialog } />
];
const addToBook = (
<Button
key='newAddress'
icon={ <ContentAdd /> }
label='save address'
onClick={ this.onOpenAdd }
/>
);
return (
<Actionbar
title='Address Information'
buttons={ !contact ? [] : buttons } />
buttons={ !contact ? [ addToBook ] : buttons }
/>
);
}
renderEditDialog (contact) {
const { showEditDialog } = this.state;
if (!showEditDialog) {
if (!contact || !showEditDialog) {
return null;
}
@ -151,6 +200,16 @@ class Address extends Component {
showDeleteDialog = () => {
this.setState({ showDeleteDialog: true });
}
onOpenAdd = () => {
this.setState({
showAdd: true
});
}
onCloseAdd = () => {
this.setState({ showAdd: false });
}
}
function mapStateToProps (state) {

View File

@ -23,7 +23,7 @@ import { uniq, isEqual } from 'lodash';
import List from '../Accounts/List';
import Summary from '../Accounts/Summary';
import { AddAddress } from '~/modals';
import { Actionbar, ActionbarExport, ActionbarImport, ActionbarSearch, ActionbarSort, Button, Page } from '~/ui';
import { Actionbar, ActionbarExport, ActionbarImport, ActionbarSearch, ActionbarSort, Button, Page, Loading } from '~/ui';
import { setVisibleAccounts } from '~/redux/providers/personalActions';
import styles from './addresses.css';
@ -72,24 +72,37 @@ class Addresses extends Component {
}
render () {
const { balances, contacts, hasContacts } = this.props;
const { searchValues, sortOrder } = this.state;
return (
<div>
{ this.renderActionbar() }
{ this.renderAddAddress() }
<Page>
{ this.renderAccountsList() }
</Page>
</div>
);
}
renderAccountsList () {
const { balances, contacts, hasContacts } = this.props;
const { searchValues, sortOrder } = this.state;
if (hasContacts && Object.keys(balances).length === 0) {
return (
<Loading />
);
}
return (
<List
link='address'
link='addresses'
search={ searchValues }
accounts={ contacts }
balances={ balances }
empty={ !hasContacts }
order={ sortOrder }
handleAddSearchToken={ this.onAddSearchToken } />
</Page>
</div>
handleAddSearchToken={ this.onAddSearchToken }
/>
);
}

View File

@ -30,24 +30,34 @@
}
}
.tabs button,
.tabLink {
display: flex;
> * {
flex: 1;
}
&:hover {
background: rgba(0, 0, 0, 0.4) !important;
}
&.tabactive, &.tabactive:hover {
background: rgba(0, 0, 0, 0.25) !important;
border-radius: 4px 4px 0 0;
* {
color: white !important;
}
}
}
.tabLink,
.settings,
.logo,
.last {
background: rgba(0, 0, 0, 0.5) !important; /* rgba(0, 0, 0, 0.25) !important; */
}
.tabs button:hover {
background: rgba(0, 0, 0, 0.4) !important;
}
button.tabactive,
button.tabactive:hover {
color: white !important;
background: rgba(0, 0, 0, 0.25) !important;
border-radius: 4px 4px 0 0;
}
.tabbarTooltip {
left: 3.3em;
top: 0.5em;

View File

@ -16,7 +16,7 @@
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Link } from 'react-router';
import { Toolbar, ToolbarGroup } from 'material-ui/Toolbar';
import { Tab as MUITab } from 'material-ui/Tabs';
import { isEqual } from 'lodash';
@ -26,41 +26,26 @@ import { Badge, Tooltip } from '~/ui';
import styles from './tabBar.css';
import imagesEthcoreBlock from '../../../../assets/images/parity-logo-white-no-text.svg';
const TABMAP = {
accounts: 'account',
wallet: 'account',
addresses: 'address',
apps: 'app',
contracts: 'contract',
deploy: 'contract'
};
class Tab extends Component {
static propTypes = {
active: PropTypes.bool,
view: PropTypes.object,
children: PropTypes.node,
pendings: PropTypes.number,
onChange: PropTypes.func
pendings: PropTypes.number
};
shouldComponentUpdate (nextProps) {
return nextProps.active !== this.props.active ||
(nextProps.view.id === 'signer' && nextProps.pendings !== this.props.pendings);
return (nextProps.view.id === 'signer' && nextProps.pendings !== this.props.pendings);
}
render () {
const { active, view, children } = this.props;
const { view, children } = this.props;
const label = this.getLabel(view);
return (
<MUITab
className={ active ? styles.tabactive : '' }
selected={ active }
icon={ view.icon }
label={ label }
onTouchTap={ this.handleClick }
>
{ children }
</MUITab>
@ -118,11 +103,6 @@ class Tab extends Component {
return this.renderLabel(label, null);
}
handleClick = () => {
const { onChange, view } = this.props;
onChange(view);
}
}
class TabBar extends Component {
@ -132,7 +112,6 @@ class TabBar extends Component {
static propTypes = {
views: PropTypes.array.isRequired,
hash: PropTypes.string.isRequired,
pending: PropTypes.array,
isTest: PropTypes.bool,
netChain: PropTypes.string
@ -142,34 +121,11 @@ class TabBar extends Component {
pending: []
};
state = {
activeViewId: ''
};
setActiveView (props = this.props) {
const { hash, views } = props;
const view = views.find((view) => view.value === hash);
this.setState({ activeViewId: view.id });
}
componentWillMount () {
this.setActiveView();
}
componentWillReceiveProps (nextProps) {
if (nextProps.hash !== this.props.hash) {
this.setActiveView(nextProps);
}
}
shouldComponentUpdate (nextProps, nextState) {
const prevViews = this.props.views.map((v) => v.id).sort();
const nextViews = nextProps.views.map((v) => v.id).sort();
return (nextProps.hash !== this.props.hash) ||
(nextProps.pending.length !== this.props.pending.length) ||
(nextState.activeViewId !== this.state.activeViewId) ||
return (nextProps.pending.length !== this.props.pending.length) ||
(!isEqual(prevViews, nextViews));
}
@ -206,7 +162,6 @@ class TabBar extends Component {
renderTabs () {
const { views, pending } = this.props;
const { activeViewId } = this.state;
const items = views
.map((view, index) => {
@ -216,60 +171,66 @@ class TabBar extends Component {
)
: null;
const active = activeViewId === view.id;
return (
<Tab
active={ active }
view={ view }
onChange={ this.onChange }
<Link
key={ view.id }
to={ view.route }
activeClassName={ styles.tabactive }
className={ styles.tabLink }
>
<Tab
view={ view }
pendings={ pending.length }
>
{ body }
</Tab>
</Link>
);
});
return (
<div
className={ styles.tabs }
onChange={ this.onChange }>
<div className={ styles.tabs }>
{ items }
</div>
);
}
onChange = (view) => {
const { router } = this.context;
router.push(view.route);
this.setState({ activeViewId: view.id });
}
}
function mapStateToProps (state) {
const { views } = state.settings;
function mapStateToProps (initState) {
const { views } = initState.settings;
const filteredViews = Object
let filteredViewIds = Object
.keys(views)
.filter((id) => views[id].fixed || views[id].active)
.filter((id) => views[id].fixed || views[id].active);
let filteredViews = filteredViewIds
.map((id) => ({
...views[id],
id
}));
const windowHash = (window.location.hash || '').split('?')[0].split('/')[1];
const hash = TABMAP[windowHash] || windowHash;
return (state) => {
const { views } = state.settings;
return { views: filteredViews, hash };
}
const viewIds = Object
.keys(views)
.filter((id) => views[id].fixed || views[id].active);
function mapDispatchToProps (dispatch) {
return bindActionCreators({}, dispatch);
if (isEqual(viewIds, filteredViewIds)) {
return { views: filteredViews };
}
filteredViewIds = viewIds;
filteredViews = viewIds
.map((id) => ({
...views[id],
id
}));
return { views: filteredViews };
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
mapStateToProps
)(TabBar);

View File

@ -19,4 +19,5 @@
display: flex;
flex-direction: column;
min-height: 100vh;
padding-bottom: 1em;
}

View File

@ -85,7 +85,7 @@ class Contracts extends Component {
{ this.renderDeployContract() }
<Page>
<List
link='contract'
link='contracts'
search={ searchValues }
accounts={ contracts }
balances={ balances }
@ -142,12 +142,12 @@ class Contracts extends Component {
label='deploy contract'
onClick={ this.onDeployContract } />,
<Link
to='/contracts/write'
to='/contracts/develop'
key='writeContract'
>
<Button
icon={ <FileIcon /> }
label='write contract'
label='develop contract'
/>
</Link>,

View File

@ -72,9 +72,17 @@ export default class Dapps extends Component {
] }
/>
<Page>
<div>
{ this.renderList(this.store.visibleLocal) }
</div>
<div>
{ this.renderList(this.store.visibleBuiltin) }
</div>
<div>
{ this.renderList(this.store.visibleNetwork, externalOverlay) }
</div>
</Page>
</div>
);