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) { static create (api) {
if (instance) {
return instance;
}
return new Contracts(api); return new Contracts(api);
} }

View File

@ -15,7 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; 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'; 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 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 () { render () {
const { routerHistory } = this.props; const { routerHistory } = this.props;
@ -34,26 +51,46 @@ export default class MainApplication extends Component {
<Redirect from='/' to='/accounts' /> <Redirect from='/' to='/accounts' />
<Redirect from='/auth' to='/accounts' query={ {} } /> <Redirect from='/auth' to='/accounts' query={ {} } />
<Redirect from='/settings' to='/settings/views' /> <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='/' component={ Application }>
<Route path='accounts' component={ Accounts } /> <Route path='accounts'>
<Route path='account/:address' component={ Account } /> <IndexRoute component={ Accounts } />
<Route path='wallet/:address' component={ Wallet } /> <Route path=':address' component={ Account } />
<Route path='addresses' component={ Addresses } /> <Route path='/wallet/:address' component={ Wallet } />
<Route path='address/:address' component={ Address } /> </Route>
<Route path='addresses'>
<IndexRoute component={ Addresses } />
<Route path=':address' component={ Address } />
</Route>
<Route path='apps' component={ Dapps } /> <Route path='apps' component={ Dapps } />
<Route path='app/:id' component={ Dapp } /> <Route path='app/:id' component={ Dapp } />
<Route path='contracts' component={ Contracts } />
<Route path='contracts/write' component={ WriteContract } /> <Route path='contracts'>
<Route path='contract/:address' component={ Contract } /> <IndexRoute component={ Contracts } />
<Route path='develop' component={ WriteContract } />
<Route path=':address' component={ Contract } />
</Route>
<Route path='settings' component={ Settings }> <Route path='settings' component={ Settings }>
<Route path='background' component={ SettingsBackground } /> <Route path='background' component={ SettingsBackground } />
<Route path='proxy' component={ SettingsProxy } /> <Route path='proxy' component={ SettingsProxy } />
<Route path='views' component={ SettingsViews } /> <Route path='views' component={ SettingsViews } />
<Route path='parity' component={ SettingsParity } /> <Route path='parity' component={ SettingsParity } />
</Route> </Route>
<Route path='signer' component={ Signer } /> <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> </Route>
</Router> </Router>
); );

View File

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

View File

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

View File

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

View File

@ -15,8 +15,10 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import ActionDoneAll from 'material-ui/svg-icons/action/done-all'; import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
import ContentClear from 'material-ui/svg-icons/content/clear'; import ContentClear from 'material-ui/svg-icons/content/clear';
import { pick } from 'lodash';
import { BusyStep, CompletedStep, CopyToClipboard, Button, IdentityIcon, Modal, TxHash } from '~/ui'; import { BusyStep, CompletedStep, CopyToClipboard, Button, IdentityIcon, Modal, TxHash } from '~/ui';
import { ERRORS, validateAbi, validateCode, validateName } from '~/util/validation'; import { ERRORS, validateAbi, validateCode, validateName } from '~/util/validation';
@ -36,7 +38,7 @@ const STEPS = {
COMPLETED: { title: 'completed' } COMPLETED: { title: 'completed' }
}; };
export default class DeployContract extends Component { class DeployContract extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired, api: PropTypes.object.isRequired,
store: PropTypes.object.isRequired store: PropTypes.object.isRequired
@ -45,6 +47,7 @@ export default class DeployContract extends Component {
static propTypes = { static propTypes = {
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
balances: PropTypes.object,
abi: PropTypes.string, abi: PropTypes.string,
code: PropTypes.string, code: PropTypes.string,
readOnly: PropTypes.bool, readOnly: PropTypes.bool,
@ -192,7 +195,7 @@ export default class DeployContract extends Component {
} }
renderStep () { renderStep () {
const { accounts, readOnly } = this.props; const { accounts, readOnly, balances } = this.props;
const { address, deployError, step, deployState, txhash, rejected } = this.state; const { address, deployError, step, deployState, txhash, rejected } = this.state;
if (deployError) { if (deployError) {
@ -216,6 +219,7 @@ export default class DeployContract extends Component {
<DetailsStep <DetailsStep
{ ...this.state } { ...this.state }
accounts={ accounts } accounts={ accounts }
balances={ balances }
readOnly={ readOnly } readOnly={ readOnly }
onFromAddressChange={ this.onFromAddressChange } onFromAddressChange={ this.onFromAddressChange }
onDescriptionChange={ this.onDescriptionChange } onDescriptionChange={ this.onDescriptionChange }
@ -394,3 +398,17 @@ export default class DeployContract extends Component {
this.props.onClose(); 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 = { static propTypes = {
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
contract: PropTypes.object.isRequired, contract: PropTypes.object.isRequired,
amount: PropTypes.string,
amountError: PropTypes.string,
onAmountChange: PropTypes.func.isRequired, onAmountChange: PropTypes.func.isRequired,
fromAddress: PropTypes.string,
fromAddressError: PropTypes.string,
gasEdit: PropTypes.bool,
onFromAddressChange: PropTypes.func.isRequired, onFromAddressChange: PropTypes.func.isRequired,
func: PropTypes.object, onValueChange: PropTypes.func.isRequired,
funcError: PropTypes.string,
onFuncChange: PropTypes.func,
onGasEditClick: PropTypes.func,
values: PropTypes.array.isRequired, values: PropTypes.array.isRequired,
valuesError: 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 () { 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 ( return (
<Form> <Form>
@ -61,6 +63,7 @@ export default class DetailsStep extends Component {
value={ fromAddress } value={ fromAddress }
error={ fromAddressError } error={ fromAddressError }
accounts={ accounts } accounts={ accounts }
balances={ balances }
onChange={ onFromAddressChange } /> onChange={ onFromAddressChange } />
{ this.renderFunctionSelect() } { this.renderFunctionSelect() }
{ this.renderParameters() } { this.renderParameters() }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,11 +16,24 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import keycode from 'keycode'; 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 { PopoverAnimationVertical } from 'material-ui/Popover';
import { isEqual } from 'lodash'; 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 { export default class AutoComplete extends Component {
static propTypes = { static propTypes = {
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
@ -38,15 +51,17 @@ export default class AutoComplete extends Component {
PropTypes.array, PropTypes.array,
PropTypes.object PropTypes.object
]) ])
} };
state = { state = {
lastChangedValue: undefined, lastChangedValue: undefined,
entry: null, entry: null,
open: false, open: false,
fakeBlur: false, dataSource: [],
dataSource: [] dividerBreaks: []
} };
dividersVisibility = {};
componentWillMount () { componentWillMount () {
const dataSource = this.getDataSource(); const dataSource = this.getDataSource();
@ -64,7 +79,7 @@ export default class AutoComplete extends Component {
} }
render () { 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; const { open, dataSource } = this.state;
return ( return (
@ -78,9 +93,9 @@ export default class AutoComplete extends Component {
onUpdateInput={ onUpdateInput } onUpdateInput={ onUpdateInput }
searchText={ value } searchText={ value }
onFocus={ this.onFocus } onFocus={ this.onFocus }
onBlur={ this.onBlur } onClose={ this.onClose }
animation={ PopoverAnimationVertical } animation={ PopoverAnimationVertical }
filter={ filter } filter={ this.handleFilter }
popoverProps={ { open } } popoverProps={ { open } }
openOnFocus openOnFocus
menuCloseDelay={ 0 } menuCloseDelay={ 0 }
@ -100,18 +115,76 @@ export default class AutoComplete extends Component {
? entries ? entries
: Object.values(entries); : Object.values(entries);
if (renderItem && typeof renderItem === 'function') { let currentDivider = 0;
return entriesArray.map(entry => renderItem(entry)); 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, text: entry,
value: ( value: (
<MenuItem <MenuItem
primaryText={ entry } 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) => { onKeyDown = (event) => {
@ -121,7 +194,6 @@ export default class AutoComplete extends Component {
case 'down': case 'down':
const { menu } = muiAutocomplete.refs; const { menu } = muiAutocomplete.refs;
menu && menu.handleKeyDown(event); menu && menu.handleKeyDown(event);
this.setState({ fakeBlur: true });
break; break;
case 'enter': case 'enter':
@ -155,22 +227,12 @@ export default class AutoComplete extends Component {
this.setState({ entry, open: false }); this.setState({ entry, open: false });
} }
onBlur = (event) => { onClose = (event) => {
const { onUpdateInput } = this.props; 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) { if (!onUpdateInput) {
window.setTimeout(() => { const { entry } = this.state;
const { entry, fakeBlur } = this.state;
if (fakeBlur) {
this.setState({ fakeBlur: false });
return;
}
this.handleOnChange(entry); this.handleOnChange(entry);
}, 200);
} }
} }

View File

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

View File

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

View File

@ -32,18 +32,20 @@ export default class Header extends Component {
balance: PropTypes.object, balance: PropTypes.object,
className: PropTypes.string, className: PropTypes.string,
children: PropTypes.node, children: PropTypes.node,
isContract: PropTypes.bool isContract: PropTypes.bool,
hideName: PropTypes.bool
}; };
static defaultProps = { static defaultProps = {
className: '', className: '',
children: null, children: null,
isContract: false isContract: false,
hideName: false
}; };
render () { render () {
const { api } = this.context; const { api } = this.context;
const { account, balance, className, children } = this.props; const { account, balance, className, children, hideName } = this.props;
const { address, meta, uuid } = account; const { address, meta, uuid } = account;
if (!account) { if (!account) {
@ -60,17 +62,20 @@ export default class Header extends Component {
<IdentityIcon <IdentityIcon
address={ address } /> address={ address } />
<div className={ styles.floatleft }> <div className={ styles.floatleft }>
<ContainerTitle title={ <IdentityName address={ address } unknown /> } /> { this.renderName(address) }
<div className={ styles.addressline }>
<div className={ [ hideName ? styles.bigaddress : '', styles.addressline ].join(' ') }>
<CopyToClipboard data={ address } /> <CopyToClipboard data={ address } />
<div className={ styles.address }>{ address }</div> <div className={ styles.address }>{ address }</div>
</div> </div>
{ uuidText } { uuidText }
<div className={ styles.infoline }> <div className={ styles.infoline }>
{ meta.description } { meta.description }
</div> </div>
{ this.renderTxCount() } { this.renderTxCount() }
</div> </div>
<div className={ styles.tags }> <div className={ styles.tags }>
<Tags tags={ meta.tags } /> <Tags tags={ meta.tags } />
</div> </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 () { renderTxCount () {
const { balance, isContract } = this.props; 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 { EditMeta, DeleteAccount, Shapeshift, SMSVerification, Transfer, PasswordManager } from '~/modals';
import { Actionbar, Button, Page } from '~/ui'; 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 Header from './Header';
import Transactions from './Transactions'; import Transactions from './Transactions';

View File

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

View File

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

View File

@ -23,7 +23,7 @@ import { uniq, isEqual } from 'lodash';
import List from '../Accounts/List'; import List from '../Accounts/List';
import Summary from '../Accounts/Summary'; import Summary from '../Accounts/Summary';
import { AddAddress } from '~/modals'; 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 { setVisibleAccounts } from '~/redux/providers/personalActions';
import styles from './addresses.css'; import styles from './addresses.css';
@ -72,24 +72,37 @@ class Addresses extends Component {
} }
render () { render () {
const { balances, contacts, hasContacts } = this.props;
const { searchValues, sortOrder } = this.state;
return ( return (
<div> <div>
{ this.renderActionbar() } { this.renderActionbar() }
{ this.renderAddAddress() } { this.renderAddAddress() }
<Page> <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 <List
link='address' link='addresses'
search={ searchValues } search={ searchValues }
accounts={ contacts } accounts={ contacts }
balances={ balances } balances={ balances }
empty={ !hasContacts } empty={ !hasContacts }
order={ sortOrder } order={ sortOrder }
handleAddSearchToken={ this.onAddSearchToken } /> handleAddSearchToken={ this.onAddSearchToken }
</Page> />
</div>
); );
} }

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, .settings,
.logo, .logo,
.last { .last {
background: rgba(0, 0, 0, 0.5) !important; /* rgba(0, 0, 0, 0.25) !important; */ 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 { .tabbarTooltip {
left: 3.3em; left: 3.3em;
top: 0.5em; top: 0.5em;

View File

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

View File

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

View File

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

View File

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