From 5ae737f307b13c6797ddd8d1edfceb1e455e7da3 Mon Sep 17 00:00:00 2001 From: Jannis Redmann Date: Thu, 3 Nov 2016 11:57:43 +0100 Subject: [PATCH] new InputAddressSelect component (#3071) * basic address autocomplete * validate input, propagate changes * show IdentityIcon in menu * show IdentityIcon next to input * refactoring, better variable names, linting * show default IdentityIcon if search by name * port #3065 over * show accounts in the beginning * show accounts before contacts * filter deleted accounts * UX improvements - limit number of search results shown - hint text * only render identity icon if valid address * UX improvements - align IdentityIcon - better hint text * align label & error with other inputs This probably needs to be changed soon again. Therefore this ugly hack has been put in place. * Align component with coding style for app * Use standard/tested AddressAutocmplete (WIP) * Address selection & inputs operational * Update TODOs, remove unused CSS * only handle input changes when editing * Simplify * Cleanup unused modules * Add contracts to address search * Updates Address Selector to handle valid input address #3071 * Added Address Selector to contracts read queries --- js/src/ui/Form/AddressSelect/addressSelect.js | 97 +++++++++++++------ js/src/ui/Form/AutoComplete/autocomplete.js | 20 ++-- .../InputAddressSelect/inputAddressSelect.css | 31 ------ .../InputAddressSelect/inputAddressSelect.js | 79 +++------------ js/src/views/Contract/Queries/inputQuery.js | 19 +++- 5 files changed, 110 insertions(+), 136 deletions(-) delete mode 100644 js/src/ui/Form/InputAddressSelect/inputAddressSelect.css diff --git a/js/src/ui/Form/AddressSelect/addressSelect.js b/js/src/ui/Form/AddressSelect/addressSelect.js index 72e80f432..ac7f2da51 100644 --- a/js/src/ui/Form/AddressSelect/addressSelect.js +++ b/js/src/ui/Form/AddressSelect/addressSelect.js @@ -16,6 +16,7 @@ import React, { Component, PropTypes } from 'react'; import { MenuItem } from 'material-ui'; +import { isEqual } from 'lodash'; import AutoComplete from '../AutoComplete'; import IdentityIcon from '../../IdentityIcon'; @@ -24,57 +25,82 @@ import IdentityName from '../../IdentityName'; import styles from './addressSelect.css'; export default class AddressSelect extends Component { + static contextTypes = { + api: PropTypes.object.isRequired + } + static propTypes = { disabled: PropTypes.bool, accounts: PropTypes.object, contacts: PropTypes.object, + contracts: PropTypes.object, label: PropTypes.string, hint: PropTypes.string, error: PropTypes.string, value: PropTypes.string, tokens: PropTypes.object, - onChange: PropTypes.func.isRequired + onChange: PropTypes.func.isRequired, + allowInput: PropTypes.bool } state = { entries: {}, + addresses: [], value: '' } + entriesFromProps (props = this.props) { + const { accounts, contacts, contracts } = props; + const entries = Object.assign({}, accounts || {}, contacts || {}, contracts || {}); + return entries; + } + componentWillMount () { - const { accounts, contacts, value } = this.props; - const entries = Object.assign({}, accounts || {}, contacts || {}); - this.setState({ entries, value }); + const { value } = this.props; + const entries = this.entriesFromProps(); + const addresses = Object.keys(entries).sort(); + + this.setState({ entries, addresses, value }); } componentWillReceiveProps (newProps) { - const { accounts, contacts } = newProps; - const entries = Object.assign({}, accounts || {}, contacts || {}); - this.setState({ entries }); + const entries = this.entriesFromProps(); + const addresses = Object.keys(entries).sort(); + + if (!isEqual(addresses, this.state.addresses)) { + this.setState({ entries, addresses }); + } + + if (newProps.value !== this.props.value) { + this.setState({ value: newProps.value }); + } } render () { - const { disabled, error, hint, label } = this.props; - const { entries } = this.state; - const value = this.getSearchText(); + const { allowInput, disabled, error, hint, label } = this.props; + const { entries, value } = this.state; + + const searchText = this.getSearchText(); + const icon = this.renderIdentityIcon(value); return (
- - { this.renderIdentityIcon(value) } + { icon }
); } @@ -82,7 +108,7 @@ export default class AddressSelect extends Component { renderIdentityIcon (inputValue) { const { error, value } = this.props; - if (error || !inputValue) { + if (error || !inputValue || value.length !== 42) { return null; } @@ -96,8 +122,9 @@ export default class AddressSelect extends Component { renderItem = (entry) => { return { - text: entry.address, - value: this.renderSelectEntry(entry) + text: entry.name && entry.name.toUpperCase() || entry.address, + value: this.renderSelectEntry(entry), + address: entry.address }; } @@ -127,32 +154,48 @@ export default class AddressSelect extends Component { getSearchText () { const entry = this.getEntry(); - if (!entry) return ''; + const { value } = this.state; - return entry.name ? entry.name.toUpperCase() : ''; + return entry && entry.name + ? entry.name.toUpperCase() + : value; } getEntry () { - const { value } = this.props; - if (!value) return ''; - - const { entries } = this.state; - return entries[value]; + const { entries, value } = this.state; + return value ? entries[value] : null; } - handleFilter = (searchText, address) => { + handleFilter = (searchText, name, item) => { + const { address } = item; const entry = this.state.entries[address]; const lowCaseSearch = searchText.toLowerCase(); - return [ entry.name, entry.address ] + return [entry.name, entry.address] .some(text => text.toLowerCase().indexOf(lowCaseSearch) !== -1); } onChange = (entry, empty) => { + const { allowInput } = this.props; + const { value } = this.state; + const address = entry && entry.address ? entry.address - : (empty ? '' : this.state.value); + : ((empty && !allowInput) ? '' : value); this.props.onChange(null, address); } + + onUpdateInput = (query, choices) => { + const { api } = this.context; + + const address = query.trim(); + + if (!/^0x/.test(address) && api.util.isAddressValid(`0x${address}`)) { + const checksumed = api.util.toChecksumAddress(`0x${address}`); + return this.props.onChange(null, checksumed); + } + + this.props.onChange(null, address); + }; } diff --git a/js/src/ui/Form/AutoComplete/autocomplete.js b/js/src/ui/Form/AutoComplete/autocomplete.js index 353b4d73c..b0958da31 100644 --- a/js/src/ui/Form/AutoComplete/autocomplete.js +++ b/js/src/ui/Form/AutoComplete/autocomplete.js @@ -21,6 +21,7 @@ import { PopoverAnimationVertical } from 'material-ui/Popover'; export default class AutoComplete extends Component { static propTypes = { onChange: PropTypes.func.isRequired, + onUpdateInput: PropTypes.func, disabled: PropTypes.bool, label: PropTypes.string, hint: PropTypes.string, @@ -43,7 +44,7 @@ export default class AutoComplete extends Component { } render () { - const { disabled, error, hint, label, value, className, filter } = this.props; + const { disabled, error, hint, label, value, className, filter, onUpdateInput } = this.props; const { open } = this.state; return ( @@ -54,11 +55,11 @@ export default class AutoComplete extends Component { hintText={ hint } errorText={ error } onNewRequest={ this.onChange } + onUpdateInput={ onUpdateInput } searchText={ value } onFocus={ this.onFocus } onBlur={ this.onBlur } animation={ PopoverAnimationVertical } - filter={ filter } popoverProps={ { open } } openOnFocus @@ -108,11 +109,17 @@ export default class AutoComplete extends Component { } onBlur = () => { - window.setTimeout(() => { - const { entry } = this.state; + const { onUpdateInput } = this.props; - this.handleOnChange(entry); - }, 100); + // TODO: Handle blur gracefully where we use onUpdateInput (currently replaces input + // input where text is allowed with the last selected value from the dropdown) + if (!onUpdateInput) { + window.setTimeout(() => { + const { entry } = this.state; + + this.handleOnChange(entry); + }, 100); + } } onFocus = () => { @@ -131,5 +138,4 @@ export default class AutoComplete extends Component { this.props.onChange(value, empty); } } - } diff --git a/js/src/ui/Form/InputAddressSelect/inputAddressSelect.css b/js/src/ui/Form/InputAddressSelect/inputAddressSelect.css deleted file mode 100644 index 0211ef568..000000000 --- a/js/src/ui/Form/InputAddressSelect/inputAddressSelect.css +++ /dev/null @@ -1,31 +0,0 @@ -/* Copyright 2015, 2016 Ethcore (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 . -*/ -.inputselect { - position: relative; -} - -.inputselect svg { - padding-right: 84px; -} - -.toggle { - position: absolute !important; - top: 38px; - right: 0; - display: inline-block !important; - width: auto !important; -} diff --git a/js/src/ui/Form/InputAddressSelect/inputAddressSelect.js b/js/src/ui/Form/InputAddressSelect/inputAddressSelect.js index e2c5e8a1d..e77043aed 100644 --- a/js/src/ui/Form/InputAddressSelect/inputAddressSelect.js +++ b/js/src/ui/Form/InputAddressSelect/inputAddressSelect.js @@ -17,103 +17,46 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { Toggle } from 'material-ui'; import AddressSelect from '../AddressSelect'; -import InputAddress from '../InputAddress'; - -import styles from './inputAddressSelect.css'; class InputAddressSelect extends Component { static propTypes = { - accounts: PropTypes.object, - contacts: PropTypes.object, - disabled: PropTypes.bool, - editing: PropTypes.bool, + accounts: PropTypes.object.isRequired, + contacts: PropTypes.object.isRequired, + contracts: PropTypes.object.isRequired, error: PropTypes.string, label: PropTypes.string, hint: PropTypes.string, value: PropTypes.string, - tokens: PropTypes.object, onChange: PropTypes.func }; - state = { - editing: this.props.editing || false, - entries: [] - } - render () { - const { editing } = this.state; - - return ( -
- { editing ? this.renderInput() : this.renderSelect() } - -
- ); - } - - renderInput () { - const { disabled, error, hint, label, value, tokens } = this.props; - - return ( - - ); - } - - renderSelect () { - const { accounts, contacts, disabled, error, hint, label, value, tokens } = this.props; + const { accounts, contacts, contracts, label, hint, error, value, onChange } = this.props; return ( + onChange={ onChange } /> ); } - - onToggle = () => { - const { editing } = this.state; - - this.setState({ - editing: !editing - }); - } - - onChangeInput = (event, value) => { - this.props.onChange(event, value); - } - - onChangeSelect = (event, value) => { - this.props.onChange(event, value); - } } function mapStateToProps (state) { - const { accounts, contacts } = state.personal; + const { accounts, contacts, contracts } = state.personal; return { accounts, - contacts + contacts, + contracts }; } diff --git a/js/src/views/Contract/Queries/inputQuery.js b/js/src/views/Contract/Queries/inputQuery.js index f21379a23..94c569412 100644 --- a/js/src/views/Contract/Queries/inputQuery.js +++ b/js/src/views/Contract/Queries/inputQuery.js @@ -19,7 +19,7 @@ import React, { Component, PropTypes } from 'react'; import LinearProgress from 'material-ui/LinearProgress'; import { Card, CardActions, CardTitle, CardText } from 'material-ui/Card'; -import { Button, Input } from '../../../ui'; +import { Button, Input, InputAddressSelect } from '../../../ui'; import styles from './queries.css'; @@ -124,8 +124,8 @@ export default class InputQuery extends Component { const { name, type } = input; const label = `${name ? `${name}: ` : ''}${type}`; - const onChange = (event) => { - const value = event.target.value; + const onChange = (event, input) => { + const value = event && event.target.value || input; const { values } = this.state; this.setState({ @@ -136,6 +136,19 @@ export default class InputQuery extends Component { }); }; + if (type === 'address') { + return ( +
+ +
+ ); + } + return (