diff --git a/js/src/modals/ExecuteContract/DetailsStep/detailsStep.spec.js b/js/src/modals/ExecuteContract/DetailsStep/detailsStep.spec.js index 5c59940eb..3622b9805 100644 --- a/js/src/modals/ExecuteContract/DetailsStep/detailsStep.spec.js +++ b/js/src/modals/ExecuteContract/DetailsStep/detailsStep.spec.js @@ -22,7 +22,7 @@ import { ContextProvider, muiTheme } from '~/ui'; import DetailsStep from './'; -import { STORE, CONTRACT } from '../executeContract.test.js'; +import { createApi, STORE, CONTRACT } from '../executeContract.test.js'; let component; let onAmountChange; @@ -41,7 +41,7 @@ function render (props) { onValueChange = sinon.stub(); component = mount( - + , - { context: { api: {}, store: STORE } } + { context: { api: createApi(), store: STORE } } ).find('ExecuteContract').shallow(); return component; diff --git a/js/src/modals/ExecuteContract/executeContract.test.js b/js/src/modals/ExecuteContract/executeContract.test.js index cf2e1d294..ce196408d 100644 --- a/js/src/modals/ExecuteContract/executeContract.test.js +++ b/js/src/modals/ExecuteContract/executeContract.test.js @@ -64,7 +64,19 @@ const STORE = { } }; +function createApi (result = true) { + return { + parity: { + registryAddress: sinon.stub().resolves('0x0000000000000000000000000000000000000000') + }, + util: { + sha3: sinon.stub().resolves('0x0000000000000000000000000000000000000000') + } + }; +} + export { + createApi, CONTRACT, STORE }; diff --git a/js/src/ui/AccountCard/accountCard.css b/js/src/ui/AccountCard/accountCard.css index 67b1704bc..5820ddf2f 100644 --- a/js/src/ui/AccountCard/accountCard.css +++ b/js/src/ui/AccountCard/accountCard.css @@ -52,6 +52,11 @@ } } +.description { + font-size: 0.75em; + color: rgba(255, 255, 255, 0.5); +} + .accountInfo { flex: 1; diff --git a/js/src/ui/AccountCard/accountCard.js b/js/src/ui/AccountCard/accountCard.js index 3c0062b22..518dfaaa6 100644 --- a/js/src/ui/AccountCard/accountCard.js +++ b/js/src/ui/AccountCard/accountCard.js @@ -43,7 +43,7 @@ export default class AccountCard extends Component { const { account } = this.props; const { copied } = this.state; - const { address, name, meta = {} } = account; + const { address, name, description, meta = {} } = account; const displayName = (name && name.toUpperCase()) || address; const { tags = [] } = meta; @@ -70,6 +70,7 @@ export default class AccountCard extends Component { { this.renderTags(tags, address) } + { this.renderDescription(description) } { this.renderAddress(displayName, address) } { this.renderBalance(address) } @@ -77,6 +78,18 @@ export default class AccountCard extends Component { ); } + renderDescription (description) { + if (!description) { + return null; + } + + return ( +
+ { description } +
+ ); + } + renderAddress (name, address) { if (name === address) { return null; diff --git a/js/src/ui/Form/AddressSelect/addressSelect.js b/js/src/ui/Form/AddressSelect/addressSelect.js index 86f4b191b..fcf48ab94 100644 --- a/js/src/ui/Form/AddressSelect/addressSelect.js +++ b/js/src/ui/Form/AddressSelect/addressSelect.js @@ -19,6 +19,7 @@ import ReactDOM from 'react-dom'; import { connect } from 'react-redux'; import keycode, { codes } from 'keycode'; import { FormattedMessage } from 'react-intl'; +import { observer } from 'mobx-react'; import TextFieldUnderline from 'material-ui/TextField/TextFieldUnderline'; @@ -26,7 +27,9 @@ import AccountCard from '~/ui/AccountCard'; import InputAddress from '~/ui/Form/InputAddress'; import Portal from '~/ui/Portal'; import { validateAddress } from '~/util/validation'; +import { nodeOrStringProptype } from '~/util/proptypes'; +import AddressSelectStore from './addressSelectStore'; import styles from './addressSelect.css'; const BOTTOM_BORDER_STYLE = { borderBottom: 'solid 3px' }; @@ -34,8 +37,10 @@ const BOTTOM_BORDER_STYLE = { borderBottom: 'solid 3px' }; // Current Form ID let currentId = 1; +@observer class AddressSelect extends Component { static contextTypes = { + api: PropTypes.object.isRequired, muiTheme: PropTypes.object.isRequired }; @@ -55,24 +60,25 @@ class AddressSelect extends Component { // Optional props allowInput: PropTypes.bool, disabled: PropTypes.bool, - error: PropTypes.string, - hint: PropTypes.string, - label: PropTypes.string, - value: PropTypes.string + error: nodeOrStringProptype(), + hint: nodeOrStringProptype(), + label: nodeOrStringProptype(), + value: nodeOrStringProptype() }; static defaultProps = { value: '' }; + store = new AddressSelectStore(this.context.api); + state = { expanded: false, focused: false, focusedCat: null, focusedItem: null, inputFocused: false, - inputValue: '', - values: [] + inputValue: '' }; componentWillMount () { @@ -80,7 +86,7 @@ class AddressSelect extends Component { } componentWillReceiveProps (nextProps) { - if (this.values && this.values.length > 0) { + if (this.store.values && this.store.values.length > 0) { return; } @@ -88,36 +94,7 @@ class AddressSelect extends Component { } setValues (props = this.props) { - const { accounts = {}, contracts = {}, contacts = {}, wallets = {} } = props; - - const accountsN = Object.keys(accounts).length; - const contractsN = Object.keys(contracts).length; - const contactsN = Object.keys(contacts).length; - const walletsN = Object.keys(wallets).length; - - if (accountsN + contractsN + contactsN + walletsN === 0) { - return; - } - - this.values = [ - { - label: 'accounts', - values: [].concat( - Object.values(wallets), - Object.values(accounts) - ) - }, - { - label: 'contacts', - values: Object.values(contacts) - }, - { - label: 'contracts', - values: Object.values(contracts) - } - ].filter((cat) => cat.values.length > 0); - - this.handleChange(); + this.store.setValues(props); } render () { @@ -216,6 +193,7 @@ class AddressSelect extends Component { { this.renderCurrentInput() } + { this.renderRegistryValues() } { this.renderAccounts() } ); @@ -241,8 +219,28 @@ class AddressSelect extends Component { ); } + renderRegistryValues () { + const { registryValues } = this.store; + + if (registryValues.length === 0) { + return null; + } + + const accounts = registryValues + .map((registryValue, index) => { + const account = { ...registryValue, index: `${registryValue.address}_${index}` }; + return this.renderAccountCard(account); + }); + + return ( +
+ { accounts } +
+ ); + } + renderAccounts () { - const { values } = this.state; + const { values } = this.store; if (values.length === 0) { return ( @@ -257,8 +255,8 @@ class AddressSelect extends Component { ); } - const categories = values.map((category) => { - return this.renderCategory(category.label, category.values); + const categories = values.map((category, index) => { + return this.renderCategory(category, index); }); return ( @@ -268,7 +266,8 @@ class AddressSelect extends Component { ); } - renderCategory (name, values = []) { + renderCategory (category, index) { + const { label, key, values = [] } = category; let content; if (values.length === 0) { @@ -292,8 +291,8 @@ class AddressSelect extends Component { } return ( -
-
{ name }
+
+
{ label }
{ content }
); @@ -306,7 +305,7 @@ class AddressSelect extends Component { const balance = balances[address]; const account = { ...accountsInfo[address], - address, index + ..._account }; return ( @@ -325,9 +324,10 @@ class AddressSelect extends Component { this.inputRef = refId; } - handleCustomInput = () => { + validateCustomInput = () => { const { allowInput } = this.props; - const { inputValue, values } = this.state; + const { inputValue } = this.store; + const { values } = this.store; // If input is HEX and allowInput === true, send it if (allowInput && inputValue && /^(0x)?([0-9a-f])+$/i.test(inputValue)) { @@ -335,8 +335,8 @@ class AddressSelect extends Component { } // If only one value, select it - if (values.length === 1 && values[0].values.length === 1) { - const value = values[0].values[0]; + if (values.reduce((cur, cat) => cur + cat.values.length, 0) === 1) { + const value = values.find((cat) => cat.values.length > 0).values[0]; return this.handleClick(value.address); } } @@ -361,7 +361,7 @@ class AddressSelect extends Component { case 'enter': const index = this.state.focusedItem; if (!index) { - return this.handleCustomInput(); + return this.validateCustomInput(); } return this.handleDOMAction(`account_${index}`, 'click'); @@ -408,10 +408,11 @@ class AddressSelect extends Component { } handleNavigation = (direction, event) => { - const { focusedItem, focusedCat, values } = this.state; + const { focusedItem, focusedCat } = this.state; + const { values } = this.store; // Don't do anything if no values - if (values.length === 0) { + if (values.reduce((cur, cat) => cur + cat.values.length, 0) === 0) { return event; } @@ -423,7 +424,12 @@ class AddressSelect extends Component { event.preventDefault(); - const nextValues = values[focusedCat || 0]; + const firstCat = values.findIndex((cat) => cat.values.length > 0); + const nextCat = focusedCat && values[focusedCat].values.length > 0 + ? focusedCat + : firstCat; + + const nextValues = values[nextCat]; const nextFocus = nextValues ? nextValues.values[0] : null; return this.focusItem(nextFocus && nextFocus.index || 1); } @@ -457,12 +463,21 @@ class AddressSelect extends Component { // If right: next category if (direction === 'right') { - nextCategory = Math.min(prevCategoryIndex + 1, values.length - 1); + const categoryShift = values + .slice(prevCategoryIndex + 1, values.length) + .findIndex((cat) => cat.values.length > 0) + 1; + + nextCategory = Math.min(prevCategoryIndex + categoryShift, values.length - 1); } // If right: previous category if (direction === 'left') { - nextCategory = Math.max(prevCategoryIndex - 1, 0); + const categoryShift = values + .slice(0, prevCategoryIndex) + .reverse() + .findIndex((cat) => cat.values.length > 0) + 1; + + nextCategory = Math.max(prevCategoryIndex - categoryShift, 0); } // If left or right: try to keep the horizontal index @@ -525,43 +540,6 @@ class AddressSelect extends Component { this.setState({ expanded: false }); } - /** - * Filter the given values based on the given - * filter - */ - filterValues = (values = [], _filter = '') => { - const filter = _filter.toLowerCase(); - - return values - // Remove empty accounts - .filter((a) => a) - .filter((account) => { - const address = account.address.toLowerCase(); - const inAddress = address.includes(filter); - - if (!account.name || inAddress) { - return inAddress; - } - - const name = account.name.toLowerCase(); - const inName = name.includes(filter); - const { meta = {} } = account; - - if (!meta.tags || inName) { - return inName; - } - - const tags = (meta.tags || []).join(''); - return tags.includes(filter); - }) - .sort((accA, accB) => { - const nameA = accA.name || accA.address; - const nameB = accB.name || accB.address; - - return nameA.localeCompare(nameB); - }); - } - handleInputBlur = () => { this.setState({ inputFocused: false }); } @@ -572,25 +550,10 @@ class AddressSelect extends Component { handleChange = (event = { target: {} }) => { const { value = '' } = event.target; - let index = 0; - const values = this.values - .map((category) => { - const filteredValues = this - .filterValues(category.values, value) - .map((value) => { - index++; - return { ...value, index: parseInt(index) }; - }); - - return { - label: category.label, - values: filteredValues - }; - }); + this.store.handleChange(value); this.setState({ - values, focusedItem: null, inputValue: value }); diff --git a/js/src/ui/Form/AddressSelect/addressSelectStore.js b/js/src/ui/Form/AddressSelect/addressSelectStore.js new file mode 100644 index 000000000..61eec13fb --- /dev/null +++ b/js/src/ui/Form/AddressSelect/addressSelectStore.js @@ -0,0 +1,222 @@ +// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import React from 'react'; +import { observable, action } from 'mobx'; +import { flatMap } from 'lodash'; +import { FormattedMessage } from 'react-intl'; + +import Contracts from '~/contracts'; +import { sha3 } from '~/api/util/sha3'; + +export default class AddressSelectStore { + + @observable values = []; + @observable registryValues = []; + + initValues = []; + regLookups = []; + + constructor (api) { + this.api = api; + + const { registry } = Contracts.create(api); + + registry + .getContract('emailverification') + .then((emailVerification) => { + this.regLookups.push({ + lookup: (value) => { + return emailVerification + .instance + .reverse.call({}, [ sha3(value) ]); + }, + describe: (value) => ( + + ) + }); + }); + + registry + .getInstance() + .then((registryInstance) => { + this.regLookups.push({ + lookup: (value) => { + return registryInstance + .getAddress.call({}, [ sha3(value), 'A' ]); + }, + describe: (value) => ( + + ) + }); + }); + } + + @action setValues (props) { + const { accounts = {}, contracts = {}, contacts = {}, wallets = {} } = props; + + const accountsN = Object.keys(accounts).length; + const contractsN = Object.keys(contracts).length; + const contactsN = Object.keys(contacts).length; + const walletsN = Object.keys(wallets).length; + + if (accountsN + contractsN + contactsN + walletsN === 0) { + return; + } + + this.initValues = [ + { + key: 'accounts', + label: ( + + ), + values: [].concat( + Object.values(wallets), + Object.values(accounts) + ) + }, + { + key: 'contacts', + label: ( + + ), + values: Object.values(contacts) + }, + { + key: 'contracts', + label: ( + + ), + values: Object.values(contracts) + } + ].filter((cat) => cat.values.length > 0); + + this.handleChange(); + } + + @action handleChange = (value = '') => { + let index = 0; + + this.values = this.initValues + .map((category) => { + const filteredValues = this + .filterValues(category.values, value) + .map((value) => { + index++; + + return { + index: parseInt(index), + ...value + }; + }); + + return { + label: category.label, + values: filteredValues + }; + }); + + // Registries Lookup + this.registryValues = []; + + const lookups = this.regLookups.map((regLookup) => regLookup.lookup(value)); + + Promise + .all(lookups) + .then((results) => { + return results + .map((result, index) => { + if (/^(0x)?0*$/.test(result)) { + return; + } + + const lowercaseResult = result.toLowerCase(); + + const account = flatMap(this.initValues, (cat) => cat.values) + .find((account) => account.address.toLowerCase() === lowercaseResult); + + return { + description: this.regLookups[index].describe(value), + address: result, + name: account && account.name || value + }; + }) + .filter((data) => data); + }) + .then((registryValues) => { + this.registryValues = registryValues; + }); + } + + /** + * Filter the given values based on the given + * filter + */ + filterValues = (values = [], _filter = '') => { + const filter = _filter.toLowerCase(); + + return values + // Remove empty accounts + .filter((a) => a) + .filter((account) => { + const address = account.address.toLowerCase(); + const inAddress = address.includes(filter); + + if (!account.name || inAddress) { + return inAddress; + } + + const name = account.name.toLowerCase(); + const inName = name.includes(filter); + const { meta = {} } = account; + + if (!meta.tags || inName) { + return inName; + } + + const tags = (meta.tags || []).join(''); + return tags.includes(filter); + }) + .sort((accA, accB) => { + const nameA = accA.name || accA.address; + const nameB = accB.name || accB.address; + + return nameA.localeCompare(nameB); + }); + } + +}