From f6564dcc2f61f892d9a85f057209db5f802cf5b0 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Sat, 10 Dec 2016 01:32:39 +0100 Subject: [PATCH 01/29] Fix dapps separation --- js/src/views/Dapps/dapps.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/js/src/views/Dapps/dapps.js b/js/src/views/Dapps/dapps.js index cf847202a..b0b731b93 100644 --- a/js/src/views/Dapps/dapps.js +++ b/js/src/views/Dapps/dapps.js @@ -72,9 +72,17 @@ export default class Dapps extends Component { ] } /> - { this.renderList(this.store.visibleLocal) } - { this.renderList(this.store.visibleBuiltin) } - { this.renderList(this.store.visibleNetwork, externalOverlay) } +
+ { this.renderList(this.store.visibleLocal) } +
+ +
+ { this.renderList(this.store.visibleBuiltin) } +
+ +
+ { this.renderList(this.store.visibleNetwork, externalOverlay) } +
); From 0d9b1882a31346ab5ab831ea373767c22723b2f9 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Sat, 10 Dec 2016 01:56:38 +0100 Subject: [PATCH 02/29] Treat tabs as real link (enable Ctrl+Click for new Tab) --- js/src/views/Application/TabBar/tabBar.css | 31 +++++++---- js/src/views/Application/TabBar/tabBar.js | 64 +++++----------------- 2 files changed, 33 insertions(+), 62 deletions(-) diff --git a/js/src/views/Application/TabBar/tabBar.css b/js/src/views/Application/TabBar/tabBar.css index b11df4e40..7b74622d3 100644 --- a/js/src/views/Application/TabBar/tabBar.css +++ b/js/src/views/Application/TabBar/tabBar.css @@ -30,24 +30,31 @@ } } -.tabs button, +.tabLink { + display: flex; + + > * { + flex: 1; + } + + &:hover { + background: rgba(0, 0, 0, 0.4) !important; + } + + &.tabactive, &.tabactive:hover { + color: white !important; + background: rgba(0, 0, 0, 0.25) !important; + border-radius: 4px 4px 0 0; + } +} + +.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; diff --git a/js/src/views/Application/TabBar/tabBar.js b/js/src/views/Application/TabBar/tabBar.js index 9d9f55874..ccf1290c5 100644 --- a/js/src/views/Application/TabBar/tabBar.js +++ b/js/src/views/Application/TabBar/tabBar.js @@ -16,6 +16,7 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; +import { Link } from 'react-router'; import { bindActionCreators } from 'redux'; import { Toolbar, ToolbarGroup } from 'material-ui/Toolbar'; import { Tab as MUITab } from 'material-ui/Tabs'; @@ -40,8 +41,7 @@ class Tab extends Component { active: PropTypes.bool, view: PropTypes.object, children: PropTypes.node, - pendings: PropTypes.number, - onChange: PropTypes.func + pendings: PropTypes.number }; shouldComponentUpdate (nextProps) { @@ -60,7 +60,6 @@ class Tab extends Component { selected={ active } icon={ view.icon } label={ label } - onTouchTap={ this.handleClick } > { children } @@ -118,11 +117,6 @@ class Tab extends Component { return this.renderLabel(label, null); } - - handleClick = () => { - const { onChange, view } = this.props; - onChange(view); - } } class TabBar extends Component { @@ -142,34 +136,12 @@ 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) || (!isEqual(prevViews, nextViews)); } @@ -206,7 +178,6 @@ class TabBar extends Component { renderTabs () { const { views, pending } = this.props; - const { activeViewId } = this.state; const items = views .map((view, index) => { @@ -216,36 +187,29 @@ class TabBar extends Component { ) : null; - const active = activeViewId === view.id; - return ( - - { body } - + + { body } + + ); }); return ( -
+
{ items }
); } - - onChange = (view) => { - const { router } = this.context; - - router.push(view.route); - this.setState({ activeViewId: view.id }); - } } function mapStateToProps (state) { From 51b9034a5e7a0839945e79e237d154f470c20466 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Sat, 10 Dec 2016 12:44:48 +0100 Subject: [PATCH 03/29] Don't show addresses while loading balances (fix flash of unsorted) --- js/src/views/Addresses/addresses.js | 37 +++++++++++++++++++---------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/js/src/views/Addresses/addresses.js b/js/src/views/Addresses/addresses.js index 5e0ed4e18..a3e52ec55 100644 --- a/js/src/views/Addresses/addresses.js +++ b/js/src/views/Addresses/addresses.js @@ -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,27 +72,40 @@ class Addresses extends Component { } render () { - const { balances, contacts, hasContacts } = this.props; - const { searchValues, sortOrder } = this.state; - return (
{ this.renderActionbar() } { this.renderAddAddress() } - + { this.renderAccountsList() }
); } + renderAccountsList () { + const { balances, contacts, hasContacts } = this.props; + const { searchValues, sortOrder } = this.state; + + if (hasContacts && Object.keys(balances).length === 0) { + return ( + + ); + } + + return ( + + ); + } + renderSortButton () { const onChange = (sortOrder) => { this.setState({ sortOrder }); From 591d086f42d4ba5e92936df277f14054e42050d2 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Sat, 10 Dec 2016 14:19:15 +0100 Subject: [PATCH 04/29] Better use of React-Router (maintaining old routes) --- js/src/main.js | 59 +++++++++++++++++++----- js/src/views/Accounts/Summary/summary.js | 2 +- js/src/views/Addresses/addresses.js | 2 +- js/src/views/Contracts/contracts.js | 2 +- 4 files changed, 51 insertions(+), 14 deletions(-) diff --git a/js/src/main.js b/js/src/main.js index d508c50fc..c1dda9d57 100644 --- a/js/src/main.js +++ b/js/src/main.js @@ -15,7 +15,7 @@ // along with Parity. If not, see . 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 { + + { /** Backward Compatible links */ } + + + + - - - - - + + + + + + + + + + + - - - + + + + + + + + - - + + + + + ); diff --git a/js/src/views/Accounts/Summary/summary.js b/js/src/views/Accounts/Summary/summary.js index 3183a2903..a19b9a9de 100644 --- a/js/src/views/Accounts/Summary/summary.js +++ b/js/src/views/Accounts/Summary/summary.js @@ -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 = ( diff --git a/js/src/views/Addresses/addresses.js b/js/src/views/Addresses/addresses.js index a3e52ec55..fd26d94e5 100644 --- a/js/src/views/Addresses/addresses.js +++ b/js/src/views/Addresses/addresses.js @@ -95,7 +95,7 @@ class Addresses extends Component { return ( Date: Sat, 10 Dec 2016 14:26:35 +0100 Subject: [PATCH 05/29] Better use of Tab Bar --- js/src/views/Application/TabBar/tabBar.js | 59 +++++++++++------------ 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/js/src/views/Application/TabBar/tabBar.js b/js/src/views/Application/TabBar/tabBar.js index ccf1290c5..63f569f1f 100644 --- a/js/src/views/Application/TabBar/tabBar.js +++ b/js/src/views/Application/TabBar/tabBar.js @@ -17,7 +17,6 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { Link } from 'react-router'; -import { bindActionCreators } from 'redux'; import { Toolbar, ToolbarGroup } from 'material-ui/Toolbar'; import { Tab as MUITab } from 'material-ui/Tabs'; import { isEqual } from 'lodash'; @@ -27,37 +26,24 @@ 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 }; 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 ( @@ -126,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 @@ -140,8 +125,7 @@ class TabBar extends Component { 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) || + return (nextProps.pending.length !== this.props.pending.length) || (!isEqual(prevViews, nextViews)); } @@ -212,28 +196,41 @@ class TabBar extends Component { } } -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); From 81c5085b35ad05313d9999ff80018e375d5c8413 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Sat, 10 Dec 2016 14:31:20 +0100 Subject: [PATCH 06/29] Don't create new Contracts instance if already exists --- js/src/contracts/contracts.js | 4 ++++ js/src/views/Account/account.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/js/src/contracts/contracts.js b/js/src/contracts/contracts.js index f61a63690..a8020b825 100644 --- a/js/src/contracts/contracts.js +++ b/js/src/contracts/contracts.js @@ -62,6 +62,10 @@ export default class Contracts { } static create (api) { + if (instance) { + return instance; + } + return new Contracts(api); } diff --git a/js/src/views/Account/account.js b/js/src/views/Account/account.js index cbacd5280..840de05b9 100644 --- a/js/src/views/Account/account.js +++ b/js/src/views/Account/account.js @@ -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'; From 65f586ed1478f8dc4fc389b94d9e582319703f92 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Sat, 10 Dec 2016 14:32:54 +0100 Subject: [PATCH 07/29] Fix tab bar active style --- js/src/views/Application/TabBar/tabBar.css | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/js/src/views/Application/TabBar/tabBar.css b/js/src/views/Application/TabBar/tabBar.css index 7b74622d3..172750c8f 100644 --- a/js/src/views/Application/TabBar/tabBar.css +++ b/js/src/views/Application/TabBar/tabBar.css @@ -42,9 +42,12 @@ } &.tabactive, &.tabactive:hover { - color: white !important; background: rgba(0, 0, 0, 0.25) !important; border-radius: 4px 4px 0 0; + + * { + color: white !important; + } } } From ef93262311c97ef7f1b0f845a45652894c3c5121 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Sat, 10 Dec 2016 15:19:05 +0100 Subject: [PATCH 08/29] See addresses outside address book + Save them --- js/src/modals/AddAddress/addAddress.js | 9 ++ js/src/views/Account/Header/header.css | 4 + js/src/views/Account/Header/header.js | 27 ++++-- js/src/views/Address/address.js | 109 +++++++++++++++++++------ 4 files changed, 119 insertions(+), 30 deletions(-) diff --git a/js/src/modals/AddAddress/addAddress.js b/js/src/modals/AddAddress/addAddress.js index e44cb0b3c..a72158cc7 100644 --- a/js/src/modals/AddAddress/addAddress.js +++ b/js/src/modals/AddAddress/addAddress.js @@ -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 (
- } /> -
+ { this.renderName(address) } + +
{ address }
+ { uuidText }
{ meta.description }
{ this.renderTxCount() }
+
@@ -89,6 +94,18 @@ export default class Header extends Component { ); } + renderName (address) { + const { hideName } = this.props; + + if (hideName) { + return null; + } + + return ( + } /> + ); + } + renderTxCount () { const { balance, isContract } = this.props; diff --git a/js/src/views/Address/address.js b/js/src/views/Address/address.js index c1427b2be..9c39203ba 100644 --- a/js/src/views/Address/address.js +++ b/js/src/views/Address/address.js @@ -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 ( +
+ { this.renderAddAddress(contact, address) } + { this.renderEditDialog(contact) } + { this.renderActionbar(contact) } + { this.renderDelete(contact) } + +
+ + +
+ ); + } + + renderAddAddress (contact, address) { + if (contact) { + return null; + } + + const { contacts } = this.props; + const { showAdd } = this.state; + + if (!showAdd) { return null; } return ( -
- { this.renderEditDialog(contact) } - { this.renderActionbar(contact) } - - -
- - -
+ + ); + } + + renderDelete (contact) { + if (!contact) { + return null; + } + + const { showDeleteDialog } = this.state; + + return ( + ); } @@ -116,17 +155,27 @@ class Address extends Component { onClick={ this.showDeleteDialog } /> ]; + const addToBook = ( +
); diff --git a/js/src/modals/Transfer/store.js b/js/src/modals/Transfer/store.js index 3a8f54f92..e08d7203d 100644 --- a/js/src/modals/Transfer/store.js +++ b/js/src/modals/Transfer/store.js @@ -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; diff --git a/js/src/modals/Transfer/transfer.js b/js/src/modals/Transfer/transfer.js index 0c96a1168..57dc569f2 100644 --- a/js/src/modals/Transfer/transfer.js +++ b/js/src/modals/Transfer/transfer.js @@ -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 (
tok.token.tag === tag); + const tokenIndex = nextTokens.findIndex((tok) => tok.token && tok.token.tag === tag); if (tokenIndex === -1) { nextTokens.push({ diff --git a/js/src/ui/Form/AddressSelect/addressSelect.css b/js/src/ui/Form/AddressSelect/addressSelect.css index 30671db73..01bc8901d 100644 --- a/js/src/ui/Form/AddressSelect/addressSelect.css +++ b/js/src/ui/Form/AddressSelect/addressSelect.css @@ -15,7 +15,9 @@ /* along with Parity. If not, see . */ .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; diff --git a/js/src/ui/Form/AddressSelect/addressSelect.js b/js/src/ui/Form/AddressSelect/addressSelect.js index d0f331c34..2fbcc80bf 100644 --- a/js/src/ui/Form/AddressSelect/addressSelect.js +++ b/js/src/ui/Form/AddressSelect/addressSelect.js @@ -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,7 +42,8 @@ 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 = { @@ -129,7 +132,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((t) => t.token.tag.toLowerCase() === 'eth'); + + if (!ethToken) { + return null; + } + + const value = fromWei(ethToken.value); + + return ( +
+ { value.toFormat(3) } { 'ETH' } +
+ ); + } + renderMenuItem (address) { + const balance = this.props.balances + ? this.renderBalance(address) + : null; + const item = (
+ + { balance }
); @@ -155,11 +187,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 () { From 84116130f6b93ad6cfaa094818e28d459998174a Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Sat, 10 Dec 2016 16:58:03 +0100 Subject: [PATCH 11/29] Add sender balances to contract (exec/deploy) --- .../DeployContract/DetailsStep/detailsStep.js | 3 +++ .../modals/DeployContract/deployContract.js | 22 +++++++++++++++++-- .../DetailsStep/detailsStep.js | 15 ++++++++----- .../modals/ExecuteContract/executeContract.js | 14 +++++++++--- 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/js/src/modals/DeployContract/DetailsStep/detailsStep.js b/js/src/modals/DeployContract/DetailsStep/detailsStep.js index aa0a30e55..9db223793 100644 --- a/js/src/modals/DeployContract/DetailsStep/detailsStep.js +++ b/js/src/modals/DeployContract/DetailsStep/detailsStep.js @@ -37,6 +37,7 @@ export default class DetailsStep extends Component { onParamsChange: PropTypes.func.isRequired, onInputsChange: PropTypes.func.isRequired, + balances: PropTypes.object, fromAddress: PropTypes.string, fromAddressError: PropTypes.string, name: PropTypes.string, @@ -77,6 +78,7 @@ export default class DetailsStep extends Component { render () { const { accounts, + balances, readOnly, fromAddress, fromAddressError, @@ -97,6 +99,7 @@ export default class DetailsStep extends Component { value={ fromAddress } error={ fromAddressError } accounts={ accounts } + balances={ balances } onChange={ this.onFromAddressChange } /> . 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 { { + const balances = pick(state.balances.balances, fromAddresses); + return { balances }; + }; +} + +export default connect( + mapStateToProps +)(DeployContract); + diff --git a/js/src/modals/ExecuteContract/DetailsStep/detailsStep.js b/js/src/modals/ExecuteContract/DetailsStep/detailsStep.js index 3ffb929a9..7bbe7be84 100644 --- a/js/src/modals/ExecuteContract/DetailsStep/detailsStep.js +++ b/js/src/modals/ExecuteContract/DetailsStep/detailsStep.js @@ -35,22 +35,24 @@ export default class DetailsStep extends Component { amount: PropTypes.string, amountError: PropTypes.string, onAmountChange: PropTypes.func.isRequired, + onFromAddressChange: PropTypes.func.isRequired, + onValueChange: PropTypes.func.isRequired, + values: PropTypes.array.isRequired, + valuesError: PropTypes.array.isRequired, + + balances: PropTypes.object, 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, - values: PropTypes.array.isRequired, - valuesError: PropTypes.array.isRequired, - warning: PropTypes.string, - onValueChange: PropTypes.func.isRequired + 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 (
@@ -61,6 +63,7 @@ export default class DetailsStep extends Component { value={ fromAddress } error={ fromAddressError } accounts={ accounts } + balances={ balances } onChange={ onFromAddressChange } /> { this.renderFunctionSelect() } { this.renderParameters() } diff --git a/js/src/modals/ExecuteContract/executeContract.js b/js/src/modals/ExecuteContract/executeContract.js index 7b4e8ccd2..c3ac96490 100644 --- a/js/src/modals/ExecuteContract/executeContract.js +++ b/js/src/modals/ExecuteContract/executeContract.js @@ -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) { - const { gasLimit } = state.nodeStatus; +function mapStateToProps (initState, initProps) { + const fromAddresses = Object.keys(initProps.accounts); - return { gasLimit }; + return (state) => { + const balances = pick(state.balances.balances, fromAddresses); + const { gasLimit } = state.nodeStatus; + + return { gasLimit, balances }; + }; } function mapDispatchToProps (dispatch) { From cd6ab072170bfe30ac04a79008c9024d784141e6 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Sat, 10 Dec 2016 17:06:44 +0100 Subject: [PATCH 12/29] Use the new `onClose` autocomplete prop --- js/src/ui/Form/AutoComplete/autocomplete.js | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/js/src/ui/Form/AutoComplete/autocomplete.js b/js/src/ui/Form/AutoComplete/autocomplete.js index c7a5dd141..f5ad43201 100644 --- a/js/src/ui/Form/AutoComplete/autocomplete.js +++ b/js/src/ui/Form/AutoComplete/autocomplete.js @@ -44,7 +44,6 @@ export default class AutoComplete extends Component { lastChangedValue: undefined, entry: null, open: false, - fakeBlur: false, dataSource: [] } @@ -78,7 +77,7 @@ export default class AutoComplete extends Component { onUpdateInput={ onUpdateInput } searchText={ value } onFocus={ this.onFocus } - onBlur={ this.onBlur } + onClose={ this.onClose } animation={ PopoverAnimationVertical } filter={ filter } popoverProps={ { open } } @@ -121,7 +120,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 +153,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; - } - - this.handleOnChange(entry); - }, 200); + const { entry } = this.state; + this.handleOnChange(entry); } } From 2346f29731da1816eab4115c8e130c6ad4745aa3 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Sat, 10 Dec 2016 18:35:54 +0100 Subject: [PATCH 13/29] Add dividers to AutoComplete --- js/src/ui/Form/AddressSelect/addressSelect.js | 32 +++++-- js/src/ui/Form/AutoComplete/autocomplete.js | 94 +++++++++++++++---- 2 files changed, 103 insertions(+), 23 deletions(-) diff --git a/js/src/ui/Form/AddressSelect/addressSelect.js b/js/src/ui/Form/AddressSelect/addressSelect.js index 2fbcc80bf..4bd93caa9 100644 --- a/js/src/ui/Form/AddressSelect/addressSelect.js +++ b/js/src/ui/Form/AddressSelect/addressSelect.js @@ -47,23 +47,41 @@ export default class AddressSelect extends Component { } 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) { @@ -74,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); @@ -92,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 } /> diff --git a/js/src/ui/Form/AutoComplete/autocomplete.js b/js/src/ui/Form/AutoComplete/autocomplete.js index f5ad43201..0e2acc98b 100644 --- a/js/src/ui/Form/AutoComplete/autocomplete.js +++ b/js/src/ui/Form/AutoComplete/autocomplete.js @@ -17,9 +17,10 @@ import React, { Component, PropTypes } from 'react'; import keycode from 'keycode'; import { MenuItem, AutoComplete as MUIAutoComplete } from 'material-ui'; +import Divider from 'material-ui/Divider'; import { PopoverAnimationVertical } from 'material-ui/Popover'; -import { isEqual } from 'lodash'; +import { isEqual, range } from 'lodash'; export default class AutoComplete extends Component { static propTypes = { @@ -38,14 +39,17 @@ export default class AutoComplete extends Component { PropTypes.array, PropTypes.object ]) - } + }; state = { lastChangedValue: undefined, entry: null, open: false, - dataSource: [] - } + dataSource: [], + dividerBreaks: [] + }; + + dividersVisibility = {}; componentWillMount () { const dataSource = this.getDataSource(); @@ -63,7 +67,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 ( @@ -79,7 +83,7 @@ export default class AutoComplete extends Component { onFocus={ this.onFocus } onClose={ this.onClose } animation={ PopoverAnimationVertical } - filter={ filter } + filter={ this.handleFilter } popoverProps={ { open } } openOnFocus menuCloseDelay={ 0 } @@ -99,18 +103,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; + } + + const item = { + text: '', + divider: currentDivider, + isDivider: true, + value: ( + + ) + }; + + currentDivider++; + return item; + } + + let item; + + if (renderItem && typeof renderItem === 'function') { + item = renderItem(entry); + } else { + item = { + text: entry, + value: ( + + ) + }; + } + + 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]; } - return entriesArray.map(entry => ({ - text: entry, - value: ( - - ) - })); + 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) => { From 69c0086ada5b55a0924662b301e0b1fefbb93f4b Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Sat, 10 Dec 2016 19:14:29 +0100 Subject: [PATCH 14/29] Better Autocomplete Divider --- js/src/ui/Form/AutoComplete/autocomplete.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/js/src/ui/Form/AutoComplete/autocomplete.js b/js/src/ui/Form/AutoComplete/autocomplete.js index 0e2acc98b..f78ab41dc 100644 --- a/js/src/ui/Form/AutoComplete/autocomplete.js +++ b/js/src/ui/Form/AutoComplete/autocomplete.js @@ -16,11 +16,23 @@ import React, { Component, PropTypes } from 'react'; import keycode from 'keycode'; -import { MenuItem, AutoComplete as MUIAutoComplete } from 'material-ui'; -import Divider from 'material-ui/Divider'; +import { MenuItem, AutoComplete as MUIAutoComplete, Divider as MUIDivider } from 'material-ui'; import { PopoverAnimationVertical } from 'material-ui/Popover'; -import { isEqual, range } from 'lodash'; +import { isEqual } from 'lodash'; + +// Hack to prevent "Unknown prop `disableFocusRipple` on
tag" error +class Divider extends Component { + static muiName = MUIDivider.muiName; + + render () { + return ( +
+ +
+ ); + } +} export default class AutoComplete extends Component { static propTypes = { From 0f6681d3e819aa1498d28be4c24313c369754f02 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Sat, 10 Dec 2016 19:15:45 +0100 Subject: [PATCH 15/29] Linting issue --- js/src/ui/Form/AutoComplete/autocomplete.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/src/ui/Form/AutoComplete/autocomplete.js b/js/src/ui/Form/AutoComplete/autocomplete.js index f78ab41dc..d11ae7cc5 100644 --- a/js/src/ui/Form/AutoComplete/autocomplete.js +++ b/js/src/ui/Form/AutoComplete/autocomplete.js @@ -28,7 +28,7 @@ class Divider extends Component { render () { return (
- +
); } From 70eab0da0331c7867c619aa12a6f6a86831aea3f Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Sat, 10 Dec 2016 20:29:22 +0100 Subject: [PATCH 16/29] PR grumbles --- .../DeployContract/DetailsStep/detailsStep.js | 22 +++++++++---------- .../DetailsStep/detailsStep.js | 6 ++--- js/src/ui/Form/AddressSelect/addressSelect.js | 3 +-- js/src/views/Addresses/addresses.js | 2 +- 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/js/src/modals/DeployContract/DetailsStep/detailsStep.js b/js/src/modals/DeployContract/DetailsStep/detailsStep.js index 9db223793..3de7a8a44 100644 --- a/js/src/modals/DeployContract/DetailsStep/detailsStep.js +++ b/js/src/modals/DeployContract/DetailsStep/detailsStep.js @@ -28,27 +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 }; diff --git a/js/src/modals/ExecuteContract/DetailsStep/detailsStep.js b/js/src/modals/ExecuteContract/DetailsStep/detailsStep.js index 7bbe7be84..fde7fa1b2 100644 --- a/js/src/modals/ExecuteContract/DetailsStep/detailsStep.js +++ b/js/src/modals/ExecuteContract/DetailsStep/detailsStep.js @@ -32,20 +32,20 @@ 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, onFromAddressChange: PropTypes.func.isRequired, onValueChange: PropTypes.func.isRequired, values: PropTypes.array.isRequired, valuesError: PropTypes.array.isRequired, + amount: PropTypes.string, + amountError: PropTypes.string, balances: PropTypes.object, fromAddress: PropTypes.string, fromAddressError: PropTypes.string, - gasEdit: PropTypes.bool, func: PropTypes.object, funcError: PropTypes.string, + gasEdit: PropTypes.bool, onFuncChange: PropTypes.func, onGasEditClick: PropTypes.func, warning: PropTypes.string diff --git a/js/src/ui/Form/AddressSelect/addressSelect.js b/js/src/ui/Form/AddressSelect/addressSelect.js index 4bd93caa9..0cd92c5c8 100644 --- a/js/src/ui/Form/AddressSelect/addressSelect.js +++ b/js/src/ui/Form/AddressSelect/addressSelect.js @@ -158,7 +158,7 @@ export default class AddressSelect extends Component { return null; } - const ethToken = balance.tokens.find((t) => t.token.tag.toLowerCase() === 'eth'); + const ethToken = balance.tokens.find((tok) => tok.token && tok.token.tag && tok.token.tag.toLowerCase() === 'eth'); if (!ethToken) { return null; @@ -187,7 +187,6 @@ export default class AddressSelect extends Component { - { balance }
); diff --git a/js/src/views/Addresses/addresses.js b/js/src/views/Addresses/addresses.js index fd26d94e5..609f029c7 100644 --- a/js/src/views/Addresses/addresses.js +++ b/js/src/views/Addresses/addresses.js @@ -89,7 +89,7 @@ class Addresses extends Component { if (hasContacts && Object.keys(balances).length === 0) { return ( - + ); } From 7401358543f8e4b18f55cebc4cc1d73ae5337ab6 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Sat, 10 Dec 2016 22:15:56 +0100 Subject: [PATCH 17/29] PR grumbles --- .../DeployContract/DetailsStep/detailsStep.js | 23 +++++++++++-------- js/src/views/Contracts/contracts.js | 2 +- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/js/src/modals/DeployContract/DetailsStep/detailsStep.js b/js/src/modals/DeployContract/DetailsStep/detailsStep.js index 3de7a8a44..51c5d3cfb 100644 --- a/js/src/modals/DeployContract/DetailsStep/detailsStep.js +++ b/js/src/modals/DeployContract/DetailsStep/detailsStep.js @@ -94,25 +94,28 @@ export default class DetailsStep extends Component { + error={ fromAddressError } + onChange={ this.onFromAddressChange } + value={ fromAddress } + /> + /> + /> { this.renderContractSelect() } @@ -120,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 } + /> + readOnly={ readOnly || solc } + value={ code } + /> ); diff --git a/js/src/views/Contracts/contracts.js b/js/src/views/Contracts/contracts.js index 524954f80..7b74654da 100644 --- a/js/src/views/Contracts/contracts.js +++ b/js/src/views/Contracts/contracts.js @@ -147,7 +147,7 @@ class Contracts extends Component { >