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
This commit is contained in:
Jannis Redmann 2016-11-03 11:57:43 +01:00 committed by Gav Wood
parent 0f9451efe8
commit 5ae737f307
5 changed files with 110 additions and 136 deletions

View File

@ -16,6 +16,7 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { MenuItem } from 'material-ui'; import { MenuItem } from 'material-ui';
import { isEqual } from 'lodash';
import AutoComplete from '../AutoComplete'; import AutoComplete from '../AutoComplete';
import IdentityIcon from '../../IdentityIcon'; import IdentityIcon from '../../IdentityIcon';
@ -24,57 +25,82 @@ import IdentityName from '../../IdentityName';
import styles from './addressSelect.css'; import styles from './addressSelect.css';
export default class AddressSelect extends Component { export default class AddressSelect extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
}
static propTypes = { static propTypes = {
disabled: PropTypes.bool, disabled: PropTypes.bool,
accounts: PropTypes.object, accounts: PropTypes.object,
contacts: PropTypes.object, contacts: PropTypes.object,
contracts: PropTypes.object,
label: PropTypes.string, label: PropTypes.string,
hint: PropTypes.string, hint: PropTypes.string,
error: PropTypes.string, error: PropTypes.string,
value: PropTypes.string, value: PropTypes.string,
tokens: PropTypes.object, tokens: PropTypes.object,
onChange: PropTypes.func.isRequired onChange: PropTypes.func.isRequired,
allowInput: PropTypes.bool
} }
state = { state = {
entries: {}, entries: {},
addresses: [],
value: '' value: ''
} }
entriesFromProps (props = this.props) {
const { accounts, contacts, contracts } = props;
const entries = Object.assign({}, accounts || {}, contacts || {}, contracts || {});
return entries;
}
componentWillMount () { componentWillMount () {
const { accounts, contacts, value } = this.props; const { value } = this.props;
const entries = Object.assign({}, accounts || {}, contacts || {}); const entries = this.entriesFromProps();
this.setState({ entries, value }); const addresses = Object.keys(entries).sort();
this.setState({ entries, addresses, value });
} }
componentWillReceiveProps (newProps) { componentWillReceiveProps (newProps) {
const { accounts, contacts } = newProps; const entries = this.entriesFromProps();
const entries = Object.assign({}, accounts || {}, contacts || {}); const addresses = Object.keys(entries).sort();
this.setState({ entries });
if (!isEqual(addresses, this.state.addresses)) {
this.setState({ entries, addresses });
}
if (newProps.value !== this.props.value) {
this.setState({ value: newProps.value });
}
} }
render () { render () {
const { disabled, error, hint, label } = this.props; const { allowInput, disabled, error, hint, label } = this.props;
const { entries } = this.state; const { entries, value } = this.state;
const value = this.getSearchText();
const searchText = this.getSearchText();
const icon = this.renderIdentityIcon(value);
return ( return (
<div className={ styles.container }> <div className={ styles.container }>
<AutoComplete <AutoComplete
className={ (error || !value) ? '' : styles.paddedInput } className={ !icon ? '' : styles.paddedInput }
disabled={ disabled } disabled={ disabled }
label={ label } label={ label }
hint={ hint ? `search for ${hint}` : 'search for an address' } hint={ hint ? `search for ${hint}` : 'search for an address' }
error={ error } error={ error }
onChange={ this.onChange } onChange={ this.onChange }
value={ value } onBlur={ this.onBlur }
onUpdateInput={ allowInput && this.onUpdateInput }
value={ searchText }
filter={ this.handleFilter } filter={ this.handleFilter }
entries={ entries } entries={ entries }
entry={ this.getEntry() || {} } entry={ this.getEntry() || {} }
renderItem={ this.renderItem } renderItem={ this.renderItem }
/> />
{ icon }
{ this.renderIdentityIcon(value) }
</div> </div>
); );
} }
@ -82,7 +108,7 @@ export default class AddressSelect extends Component {
renderIdentityIcon (inputValue) { renderIdentityIcon (inputValue) {
const { error, value } = this.props; const { error, value } = this.props;
if (error || !inputValue) { if (error || !inputValue || value.length !== 42) {
return null; return null;
} }
@ -96,8 +122,9 @@ export default class AddressSelect extends Component {
renderItem = (entry) => { renderItem = (entry) => {
return { return {
text: entry.address, text: entry.name && entry.name.toUpperCase() || entry.address,
value: this.renderSelectEntry(entry) value: this.renderSelectEntry(entry),
address: entry.address
}; };
} }
@ -127,32 +154,48 @@ export default class AddressSelect extends Component {
getSearchText () { getSearchText () {
const entry = this.getEntry(); 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 () { getEntry () {
const { value } = this.props; const { entries, value } = this.state;
if (!value) return ''; return value ? entries[value] : null;
const { entries } = this.state;
return entries[value];
} }
handleFilter = (searchText, address) => { handleFilter = (searchText, name, item) => {
const { address } = item;
const entry = this.state.entries[address]; const entry = this.state.entries[address];
const lowCaseSearch = searchText.toLowerCase(); const lowCaseSearch = searchText.toLowerCase();
return [ entry.name, entry.address ] return [entry.name, entry.address]
.some(text => text.toLowerCase().indexOf(lowCaseSearch) !== -1); .some(text => text.toLowerCase().indexOf(lowCaseSearch) !== -1);
} }
onChange = (entry, empty) => { onChange = (entry, empty) => {
const { allowInput } = this.props;
const { value } = this.state;
const address = entry && entry.address const address = entry && entry.address
? entry.address ? entry.address
: (empty ? '' : this.state.value); : ((empty && !allowInput) ? '' : value);
this.props.onChange(null, address); 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);
};
} }

View File

@ -21,6 +21,7 @@ import { PopoverAnimationVertical } from 'material-ui/Popover';
export default class AutoComplete extends Component { export default class AutoComplete extends Component {
static propTypes = { static propTypes = {
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
onUpdateInput: PropTypes.func,
disabled: PropTypes.bool, disabled: PropTypes.bool,
label: PropTypes.string, label: PropTypes.string,
hint: PropTypes.string, hint: PropTypes.string,
@ -43,7 +44,7 @@ export default class AutoComplete extends Component {
} }
render () { 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; const { open } = this.state;
return ( return (
@ -54,11 +55,11 @@ export default class AutoComplete extends Component {
hintText={ hint } hintText={ hint }
errorText={ error } errorText={ error }
onNewRequest={ this.onChange } onNewRequest={ this.onChange }
onUpdateInput={ onUpdateInput }
searchText={ value } searchText={ value }
onFocus={ this.onFocus } onFocus={ this.onFocus }
onBlur={ this.onBlur } onBlur={ this.onBlur }
animation={ PopoverAnimationVertical } animation={ PopoverAnimationVertical }
filter={ filter } filter={ filter }
popoverProps={ { open } } popoverProps={ { open } }
openOnFocus openOnFocus
@ -108,11 +109,17 @@ export default class AutoComplete extends Component {
} }
onBlur = () => { onBlur = () => {
window.setTimeout(() => { const { onUpdateInput } = this.props;
const { entry } = this.state;
this.handleOnChange(entry); // TODO: Handle blur gracefully where we use onUpdateInput (currently replaces input
}, 100); // 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 = () => { onFocus = () => {
@ -131,5 +138,4 @@ export default class AutoComplete extends Component {
this.props.onChange(value, empty); this.props.onChange(value, empty);
} }
} }
} }

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
.inputselect {
position: relative;
}
.inputselect svg {
padding-right: 84px;
}
.toggle {
position: absolute !important;
top: 38px;
right: 0;
display: inline-block !important;
width: auto !important;
}

View File

@ -17,103 +17,46 @@
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 { bindActionCreators } from 'redux';
import { Toggle } from 'material-ui';
import AddressSelect from '../AddressSelect'; import AddressSelect from '../AddressSelect';
import InputAddress from '../InputAddress';
import styles from './inputAddressSelect.css';
class InputAddressSelect extends Component { class InputAddressSelect extends Component {
static propTypes = { static propTypes = {
accounts: PropTypes.object, accounts: PropTypes.object.isRequired,
contacts: PropTypes.object, contacts: PropTypes.object.isRequired,
disabled: PropTypes.bool, contracts: PropTypes.object.isRequired,
editing: PropTypes.bool,
error: PropTypes.string, error: PropTypes.string,
label: PropTypes.string, label: PropTypes.string,
hint: PropTypes.string, hint: PropTypes.string,
value: PropTypes.string, value: PropTypes.string,
tokens: PropTypes.object,
onChange: PropTypes.func onChange: PropTypes.func
}; };
state = {
editing: this.props.editing || false,
entries: []
}
render () { render () {
const { editing } = this.state; const { accounts, contacts, contracts, label, hint, error, value, onChange } = this.props;
return (
<div className={ styles.inputselect }>
{ editing ? this.renderInput() : this.renderSelect() }
<Toggle
className={ styles.toggle }
label='Edit'
labelPosition='right'
toggled={ editing }
onToggle={ this.onToggle } />
</div>
);
}
renderInput () {
const { disabled, error, hint, label, value, tokens } = this.props;
return (
<InputAddress
disabled={ disabled }
error={ error }
hint={ hint }
label={ label }
value={ value }
tokens={ tokens }
onChange={ this.onChangeInput } />
);
}
renderSelect () {
const { accounts, contacts, disabled, error, hint, label, value, tokens } = this.props;
return ( return (
<AddressSelect <AddressSelect
allowInput
accounts={ accounts } accounts={ accounts }
contacts={ contacts } contacts={ contacts }
disabled={ disabled } contracts={ contracts }
error={ error }
label={ label } label={ label }
hint={ hint } hint={ hint }
error={ error }
value={ value } value={ value }
tokens={ tokens } onChange={ onChange } />
onChange={ this.onChangeSelect } />
); );
} }
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) { function mapStateToProps (state) {
const { accounts, contacts } = state.personal; const { accounts, contacts, contracts } = state.personal;
return { return {
accounts, accounts,
contacts contacts,
contracts
}; };
} }

View File

@ -19,7 +19,7 @@ import React, { Component, PropTypes } from 'react';
import LinearProgress from 'material-ui/LinearProgress'; import LinearProgress from 'material-ui/LinearProgress';
import { Card, CardActions, CardTitle, CardText } from 'material-ui/Card'; 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'; import styles from './queries.css';
@ -124,8 +124,8 @@ export default class InputQuery extends Component {
const { name, type } = input; const { name, type } = input;
const label = `${name ? `${name}: ` : ''}${type}`; const label = `${name ? `${name}: ` : ''}${type}`;
const onChange = (event) => { const onChange = (event, input) => {
const value = event.target.value; const value = event && event.target.value || input;
const { values } = this.state; const { values } = this.state;
this.setState({ this.setState({
@ -136,6 +136,19 @@ export default class InputQuery extends Component {
}); });
}; };
if (type === 'address') {
return (
<div key={ name }>
<InputAddressSelect
hint={ type }
label={ label }
required
onChange={ onChange }
/>
</div>
);
}
return ( return (
<div key={ name }> <div key={ name }>
<Input <Input