Cleanup AddContract with store (#3981)

* Splits (WIP)

* Expand getters & setters

* Initial abi type set

* Expand

* Don't rely on passed api

* Store tests in place

* Allow RadioButtons to accept MobX arrays

* Fixes for manual testing
This commit is contained in:
Jaco Greeff 2016-12-28 18:09:54 +01:00 committed by Gav Wood
parent 7e600b5a82
commit 8677c3b91f
9 changed files with 634 additions and 180 deletions

View File

@ -14,38 +14,17 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import ContentAdd from 'material-ui/svg-icons/content/add'; import { FormattedMessage } from 'react-intl';
import ContentClear from 'material-ui/svg-icons/content/clear';
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
import { newError } from '~/redux/actions';
import { Button, Modal, Form, Input, InputAddress, RadioButtons } from '~/ui'; import { Button, Modal, Form, Input, InputAddress, RadioButtons } from '~/ui';
import { ERRORS, validateAbi, validateAddress, validateName } from '~/util/validation'; import { AddIcon, CancelIcon, NextIcon, PrevIcon } from '~/ui/Icons';
import { eip20, wallet } from '~/contracts/abi'; import Store from './store';
const ABI_TYPES = [
{
label: 'Token', readOnly: true, value: JSON.stringify(eip20),
type: 'token',
description: (<span>A standard <a href='https://github.com/ethereum/EIPs/issues/20' target='_blank'>ERC 20</a> token</span>)
},
{
label: 'Multisig Wallet', readOnly: true,
type: 'multisig',
value: JSON.stringify(wallet),
description: (<span>Official Multisig contract: <a href='https://github.com/ethereum/dapp-bin/blob/master/wallet/wallet.sol' target='_blank'>see contract code</a></span>)
},
{
label: 'Custom Contract', value: '',
type: 'custom',
description: 'Contract created from custom ABI'
}
];
const STEPS = [ 'choose a contract type', 'enter contract details' ];
@observer
export default class AddContract extends Component { export default class AddContract extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired api: PropTypes.object.isRequired
@ -56,219 +35,217 @@ export default class AddContract extends Component {
onClose: PropTypes.func onClose: PropTypes.func
}; };
state = { store = new Store(this.context.api, this.props.contracts);
abi: '',
abiError: ERRORS.invalidAbi,
abiType: ABI_TYPES[2],
abiTypeIndex: 2,
abiParsed: null,
address: '',
addressError: ERRORS.invalidAddress,
name: '',
nameError: ERRORS.invalidName,
description: '',
step: 0
};
componentDidMount () {
this.onChangeABIType(null, this.state.abiTypeIndex);
}
render () { render () {
const { step } = this.state; const { step } = this.store;
return ( return (
<Modal <Modal
visible
actions={ this.renderDialogActions() } actions={ this.renderDialogActions() }
steps={ STEPS }
current={ step } current={ step }
> steps={ [
{ this.renderStep(step) } <FormattedMessage
id='addContract.title.type'
defaultMessage='choose a contract type'
key='type' />,
<FormattedMessage
id='addContract.title.details'
defaultMessage='enter contract details'
key='details' />
] }
visible>
{ this.renderStep() }
</Modal> </Modal>
); );
} }
renderStep (step) { renderStep () {
const { step } = this.store;
switch (step) { switch (step) {
case 0: case 0:
return this.renderContractTypeSelector(); return this.renderContractTypeSelector();
default: default:
return this.renderFields(); return this.renderFields();
} }
} }
renderContractTypeSelector () { renderContractTypeSelector () {
const { abiTypeIndex } = this.state; const { abiTypeIndex, abiTypes } = this.store;
return ( return (
<RadioButtons <RadioButtons
name='contractType' name='contractType'
value={ abiTypeIndex } value={ abiTypeIndex }
values={ this.getAbiTypes() } values={ abiTypes }
onChange={ this.onChangeABIType } onChange={ this.onChangeABIType }
/> />
); );
} }
renderDialogActions () { renderDialogActions () {
const { addressError, nameError, step } = this.state; const { step } = this.store;
const hasError = !!(addressError || nameError);
const cancelBtn = ( const cancelBtn = (
<Button <Button
icon={ <ContentClear /> } icon={ <CancelIcon /> }
label='Cancel' key='cancel'
label={
<FormattedMessage
id='addContract.button.cancel'
defaultMessage='Cancel' />
}
onClick={ this.onClose } /> onClick={ this.onClose } />
); );
if (step === 0) { if (step === 0) {
const nextBtn = ( return [
cancelBtn,
<Button <Button
icon={ <NavigationArrowForward /> } icon={ <NextIcon /> }
label='Next' key='next'
label={
<FormattedMessage
id='addContract.button.next'
defaultMessage='Next' />
}
onClick={ this.onNext } /> onClick={ this.onNext } />
); ];
return [ cancelBtn, nextBtn ];
} }
const prevBtn = ( return [
cancelBtn,
<Button <Button
icon={ <NavigationArrowBack /> } icon={ <PrevIcon /> }
label='Back' key='prev'
onClick={ this.onPrev } /> label={
); <FormattedMessage
id='addContract.button.prev'
const addBtn = ( defaultMessage='Back' />
}
onClick={ this.onPrev } />,
<Button <Button
icon={ <ContentAdd /> } icon={ <AddIcon /> }
label='Add Contract' key='add'
disabled={ hasError } label={
<FormattedMessage
id='addContract.button.add'
defaultMessage='Add Contract' />
}
disabled={ this.store.hasError }
onClick={ this.onAdd } /> onClick={ this.onAdd } />
); ];
return [ cancelBtn, prevBtn, addBtn ];
} }
renderFields () { renderFields () {
const { abi, abiError, address, addressError, description, name, nameError, abiType } = this.state; const { abi, abiError, abiType, address, addressError, description, name, nameError } = this.store;
return ( return (
<Form> <Form>
<InputAddress <InputAddress
label='network address'
hint='the network address for the contract'
error={ addressError } error={ addressError }
value={ address } hint={
onSubmit={ this.onEditAddress } <FormattedMessage
id='addContract.address.hint'
defaultMessage='the network address for the contract' />
}
label={
<FormattedMessage
id='addContract.address.label'
defaultMessage='network address' />
}
onChange={ this.onChangeAddress } onChange={ this.onChangeAddress }
/> onSubmit={ this.onEditAddress }
value={ address } />
<Input <Input
label='contract name'
hint='a descriptive name for the contract'
error={ nameError } error={ nameError }
value={ name } hint={
onSubmit={ this.onEditName } /> <FormattedMessage
id='addContract.name.hint'
defaultMessage='a descriptive name for the contract' />
}
label={
<FormattedMessage
id='addContract.name.label'
defaultMessage='contract name' />
}
onSubmit={ this.onEditName }
value={ name } />
<Input <Input
multiLine hint={
rows={ 1 } <FormattedMessage
label='(optional) contract description' id='addContract.description.hint'
hint='an expanded description for the entry' defaultMessage='an expanded description for the entry' />
value={ description } }
onSubmit={ this.onEditDescription } /> label={
<FormattedMessage
id='addContract.description.label'
defaultMessage='(optional) contract description' />
}
onSubmit={ this.onEditDescription }
value={ description } />
<Input <Input
label='contract abi'
hint='the abi for the contract'
error={ abiError } error={ abiError }
value={ abi } hint={
readOnly={ abiType.readOnly } <FormattedMessage
id='addContract.abi.hint'
defaultMessage='the abi for the contract' />
}
label={
<FormattedMessage
id='addContract.abi.label'
defaultMessage='contract abi' />
}
onSubmit={ this.onEditAbi } onSubmit={ this.onEditAbi }
/> readOnly={ abiType.readOnly }
value={ abi } />
</Form> </Form>
); );
} }
getAbiTypes () {
return ABI_TYPES.map((type, index) => ({
label: type.label,
description: type.description,
key: index,
...type
}));
}
onNext = () => { onNext = () => {
this.setState({ step: this.state.step + 1 }); this.store.nextStep();
} }
onPrev = () => { onPrev = () => {
this.setState({ step: this.state.step - 1 }); this.store.prevStep();
} }
onChangeABIType = (value, index) => { onChangeABIType = (value, index) => {
const abiType = value || ABI_TYPES[index]; this.store.setAbiTypeIndex(index);
this.setState({ abiTypeIndex: index, abiType });
this.onEditAbi(abiType.value);
} }
onEditAbi = (abiIn) => { onEditAbi = (abi) => {
const { api } = this.context; this.store.setAbi(abi);
const { abi, abiError, abiParsed } = validateAbi(abiIn, api);
this.setState({ abi, abiError, abiParsed });
} }
onChangeAddress = (event, value) => { onChangeAddress = (event, address) => {
this.onEditAddress(value); this.onEditAddress(address);
} }
onEditAddress = (_address) => { onEditAddress = (address) => {
const { contracts } = this.props; this.store.setAddress(address);
let { address, addressError } = validateAddress(_address);
if (!addressError) {
const contract = contracts[address];
if (contract) {
addressError = ERRORS.duplicateAddress;
}
}
this.setState({
address,
addressError
});
} }
onEditDescription = (description) => { onEditDescription = (description) => {
this.setState({ description }); this.store.setDescription(description);
} }
onEditName = (name) => { onEditName = (name) => {
this.setState(validateName(name)); this.store.setName(name);
} }
onAdd = () => { onAdd = () => {
const { api } = this.context; return this.store
const { abiParsed, address, name, description, abiType } = this.state; .addContract()
.then(() => {
Promise.all([ this.onClose();
api.parity.setAccountName(address, name),
api.parity.setAccountMeta(address, {
contract: true,
deleted: false,
timestamp: Date.now(),
abi: abiParsed,
type: abiType.type,
description
}) })
]).catch((error) => { .catch((error) => {
console.error('onAdd', error); newError(error);
}); });
this.props.onClose();
} }
onClose = () => { onClose = () => {

View File

@ -0,0 +1,55 @@
// 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 <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import AddContract from './';
import { CONTRACTS, createApi } from './addContract.test.js';
let component;
let onClose;
function renderShallow (props) {
onClose = sinon.stub();
component = shallow(
<AddContract
{ ...props }
contracts={ CONTRACTS }
onClose={ onClose } />,
{
context: {
api: createApi()
}
}
);
return component;
}
describe('modals/AddContract', () => {
describe('rendering', () => {
beforeEach(() => {
renderShallow();
});
it('renders the defauls', () => {
expect(component).to.be.ok;
});
});
});

View File

@ -0,0 +1,38 @@
// 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 <http://www.gnu.org/licenses/>.
import sinon from 'sinon';
const ABI = '[{"constant":true,"inputs":[],"name":"totalDonated","outputs":[{"name":"","type":"uint256"}],"type":"function"}]';
const CONTRACTS = {
'0x1234567890123456789012345678901234567890': {}
};
function createApi () {
return {
parity: {
setAccountMeta: sinon.stub().resolves(),
setAccountName: sinon.stub().resolves()
}
};
}
export {
ABI,
CONTRACTS,
createApi
};

View File

@ -0,0 +1,126 @@
// 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 <http://www.gnu.org/licenses/>.
import { action, computed, observable, transaction } from 'mobx';
import { ERRORS, validateAbi, validateAddress, validateName } from '~/util/validation';
import { ABI_TYPES } from './types';
export default class Store {
@observable abi = '';
@observable abiError = ERRORS.invalidAbi;
@observable abiParsed = null;
@observable abiTypes = ABI_TYPES;
@observable abiTypeIndex = 0;
@observable address = '';
@observable addressError = ERRORS.invalidAddress;
@observable description = '';
@observable name = '';
@observable nameError = ERRORS.invalidName;
@observable step = 0;
constructor (api, contracts) {
this._api = api;
this._contracts = contracts;
this.setAbiTypeIndex(2);
}
@computed get abiType () {
return this.abiTypes[this.abiTypeIndex];
}
@computed get hasError () {
return !!(this.abiError || this.addressError || this.nameError);
}
@action nextStep = () => {
this.step++;
}
@action prevStep = () => {
this.step--;
}
@action setAbi = (_abi) => {
const { abi, abiError, abiParsed } = validateAbi(_abi);
transaction(() => {
this.abi = abi;
this.abiError = abiError;
this.abiParsed = abiParsed;
});
}
@action setAbiTypeIndex = (abiTypeIndex) => {
transaction(() => {
this.abiTypeIndex = abiTypeIndex;
this.setAbi(this.abiTypes[abiTypeIndex].value);
});
}
@action setAddress = (_address) => {
let { address, addressError } = validateAddress(_address);
if (!addressError) {
const contract = this._contracts[address];
if (contract) {
addressError = ERRORS.duplicateAddress;
}
}
transaction(() => {
this.address = address;
this.addressError = addressError;
});
}
@action setDescription = (description) => {
this.description = description;
}
@action setName = (_name) => {
const { name, nameError } = validateName(_name);
transaction(() => {
this.name = name;
this.nameError = nameError;
});
}
addContract () {
const meta = {
contract: true,
deleted: false,
timestamp: Date.now(),
abi: this.abiParsed,
type: this.abiType.type,
description: this.description
};
return Promise
.all([
this._api.parity.setAccountName(this.address, this.name),
this._api.parity.setAccountMeta(this.address, meta)
])
.catch((error) => {
console.error('addContract', error);
throw error;
});
}
}

View File

@ -0,0 +1,171 @@
// 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 <http://www.gnu.org/licenses/>.
import Store from './store';
import { ABI, CONTRACTS, createApi } from './addContract.test.js';
const INVALID_ADDR = '0x123';
const VALID_ADDR = '0x5A5eFF38DA95b0D58b6C616f2699168B480953C9';
const DUPE_ADDR = Object.keys(CONTRACTS)[0];
let api;
let store;
function createStore () {
api = createApi();
store = new Store(api, CONTRACTS);
}
describe('modals/AddContract/Store', () => {
beforeEach(() => {
createStore();
});
describe('constructor', () => {
it('creates an instance', () => {
expect(store).to.be.ok;
});
it('defaults to custom ABI', () => {
expect(store.abiType.type).to.equal('custom');
});
});
describe('@actions', () => {
describe('nextStep/prevStep', () => {
it('moves to the next/prev step', () => {
expect(store.step).to.equal(0);
store.nextStep();
expect(store.step).to.equal(1);
store.prevStep();
expect(store.step).to.equal(0);
});
});
describe('setAbiTypeIndex', () => {
beforeEach(() => {
store.setAbiTypeIndex(1);
});
it('changes the index', () => {
expect(store.abiTypeIndex).to.equal(1);
});
it('changes the abi', () => {
expect(store.abi).to.deep.equal(store.abiTypes[1].value);
});
});
describe('setAddress', () => {
it('sets a valid address', () => {
store.setAddress(VALID_ADDR);
expect(store.address).to.equal(VALID_ADDR);
expect(store.addressError).to.be.null;
});
it('sets the error on invalid address', () => {
store.setAddress(INVALID_ADDR);
expect(store.address).to.equal(INVALID_ADDR);
expect(store.addressError).not.to.be.null;
});
it('sets the error on suplicate address', () => {
store.setAddress(DUPE_ADDR);
expect(store.address).to.equal(DUPE_ADDR);
expect(store.addressError).not.to.be.null;
});
});
describe('setDescription', () => {
it('sets the description', () => {
store.setDescription('test description');
expect(store.description).to.equal('test description');
});
});
describe('setName', () => {
it('sets the name', () => {
store.setName('some name');
expect(store.name).to.equal('some name');
expect(store.nameError).to.be.null;
});
it('sets the error', () => {
store.setName('s');
expect(store.name).to.equal('s');
expect(store.nameError).not.to.be.null;
});
});
});
describe('@computed', () => {
describe('abiType', () => {
it('matches the index', () => {
expect(store.abiType).to.deep.equal(store.abiTypes[2]);
});
});
describe('hasError', () => {
beforeEach(() => {
store.setAddress(VALID_ADDR);
store.setName('valid name');
store.setAbi(ABI);
});
it('is false with no errors', () => {
expect(store.hasError).to.be.false;
});
it('is true with address error', () => {
store.setAddress(DUPE_ADDR);
expect(store.hasError).to.be.true;
});
it('is true with name error', () => {
store.setName('s');
expect(store.hasError).to.be.true;
});
it('is true with abi error', () => {
store.setAbi('');
expect(store.hasError).to.be.true;
});
});
});
describe('interactions', () => {
describe('addContract', () => {
beforeEach(() => {
store.setAddress(VALID_ADDR);
store.setName('valid name');
store.setAbi(ABI);
});
it('sets the account name', () => {
return store.addContract().then(() => {
expect(api.parity.setAccountName).to.have.been.calledWith(VALID_ADDR, 'valid name');
});
});
it('sets the account meta', () => {
return store.addContract().then(() => {
expect(api.parity.setAccountMeta).to.have.been.called;
});
});
});
});
});

View File

@ -0,0 +1,81 @@
// 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 <http://www.gnu.org/licenses/>.
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { eip20, wallet } from '~/contracts/abi';
const ABI_TYPES = [
{
description:
<FormattedMessage
id='addContract.abiType.token.description'
defaultMessage='A standard {erc20} token'
values={ {
erc20:
<a href='https://github.com/ethereum/EIPs/issues/20' target='_blank'>
<FormattedMessage
id='addContract.abiType.token.erc20'
defaultMessage='ERC 20' />
</a>
} } />,
label:
<FormattedMessage
id='addContract.abiType.token.label'
defaultMessage='Token' />,
readOnly: true,
type: 'token',
value: JSON.stringify(eip20)
},
{
description:
<FormattedMessage
id='addContract.abiType.multisigWallet.description'
defaultMessage='Ethereum Multisig contract {link}'
values={ {
link:
<a href='https://github.com/ethereum/dapp-bin/blob/master/wallet/wallet.sol' target='_blank'>
<FormattedMessage
id='addContract.abiType.multisigWallet.link'
defaultMessage='see contract code' />
</a>
} } />,
label:
<FormattedMessage
id='addContract.abiType.multisigWallet.label'
defaultMessage='Multisig Wallet' />,
readOnly: true,
type: 'multisig',
value: JSON.stringify(wallet)
},
{
description:
<FormattedMessage
id='addContract.abiType.custom.description'
defaultMessage='Contract created from custom ABI' />,
label:
<FormattedMessage
id='addContract.abiType.custom.label'
defaultMessage='Custom Contract' />,
type: 'custom',
value: ''
}
];
export {
ABI_TYPES
};

View File

@ -14,19 +14,18 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton'; import { arrayOrObjectProptype } from '~/util/proptypes';
import styles from './radioButtons.css'; import styles from './radioButtons.css';
export default class RadioButtons extends Component { export default class RadioButtons extends Component {
static propTypes = { static propTypes = {
name: PropTypes.string,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
values: PropTypes.array.isRequired,
value: PropTypes.any, value: PropTypes.any,
name: PropTypes.string values: arrayOrObjectProptype().isRequired
}; };
static defaultProps = { static defaultProps = {
@ -40,16 +39,16 @@ export default class RadioButtons extends Component {
const index = Number.isNaN(parseInt(value)) const index = Number.isNaN(parseInt(value))
? values.findIndex((val) => val.key === value) ? values.findIndex((val) => val.key === value)
: parseInt(value); : parseInt(value);
const selectedValue = typeof value !== 'object'
const selectedValue = typeof value !== 'object' ? values[index] : value; ? values[index]
: value;
const key = this.getKey(selectedValue, index); const key = this.getKey(selectedValue, index);
return ( return (
<RadioButtonGroup <RadioButtonGroup
valueSelected={ key }
name={ name } name={ name }
onChange={ this.onChange } onChange={ this.onChange }
> valueSelected={ key } >
{ this.renderContent() } { this.renderContent() }
</RadioButtonGroup> </RadioButtonGroup>
); );
@ -59,7 +58,9 @@ export default class RadioButtons extends Component {
const { values } = this.props; const { values } = this.props;
return values.map((value, index) => { return values.map((value, index) => {
const label = typeof value === 'string' ? value : value.label || ''; const label = typeof value === 'string'
? value
: value.label || '';
const description = (typeof value !== 'string' && value.description) || null; const description = (typeof value !== 'string' && value.description) || null;
const key = this.getKey(value, index); const key = this.getKey(value, index);
@ -67,28 +68,26 @@ export default class RadioButtons extends Component {
<RadioButton <RadioButton
className={ styles.spaced } className={ styles.spaced }
key={ index } key={ index }
label={
value={ key }
label={ (
<div className={ styles.typeContainer }> <div className={ styles.typeContainer }>
<span>{ label }</span> <span>{ label }</span>
{ {
description description
? ( ? <span className={ styles.desc }>{ description }</span>
<span className={ styles.desc }>{ description }</span>
)
: null : null
} }
</div> </div>
) } }
/> value={ key } />
); );
}); });
} }
getKey (value, index) { getKey (value, index) {
if (typeof value !== 'string') { if (typeof value !== 'string') {
return typeof value.key === 'undefined' ? index : value.key; return typeof value.key === 'undefined'
? index
: value.key;
} }
return index; return index;
@ -96,8 +95,8 @@ export default class RadioButtons extends Component {
onChange = (event, index) => { onChange = (event, index) => {
const { onChange, values } = this.props; const { onChange, values } = this.props;
const value = values[index] || values.find((v) => v.key === index); const value = values[index] || values.find((v) => v.key === index);
onChange(value, index); onChange(value, index);
} }
} }

View File

@ -16,6 +16,13 @@
import { PropTypes } from 'react'; import { PropTypes } from 'react';
export function arrayOrObjectProptype () {
return PropTypes.oneOfType([
PropTypes.array,
PropTypes.object
]);
}
export function nullableProptype (type) { export function nullableProptype (type) {
return PropTypes.oneOfType([ return PropTypes.oneOfType([
PropTypes.oneOf([ null ]), PropTypes.oneOf([ null ]),

View File

@ -35,21 +35,21 @@ export const ERRORS = {
gasBlockLimit: 'the transaction execution will exceed the block gas limit' gasBlockLimit: 'the transaction execution will exceed the block gas limit'
}; };
export function validateAbi (abi, api) { export function validateAbi (abi) {
let abiError = null; let abiError = null;
let abiParsed = null; let abiParsed = null;
try { try {
abiParsed = JSON.parse(abi); abiParsed = JSON.parse(abi);
if (!api.util.isArray(abiParsed)) { if (!util.isArray(abiParsed)) {
abiError = ERRORS.invalidAbi; abiError = ERRORS.invalidAbi;
return { abi, abiError, abiParsed }; return { abi, abiError, abiParsed };
} }
// Validate each elements of the Array // Validate each elements of the Array
const invalidIndex = abiParsed const invalidIndex = abiParsed
.map((o) => isValidAbiEvent(o, api) || isValidAbiFunction(o, api) || isAbiFallback(o)) .map((o) => isValidAbiEvent(o) || isValidAbiFunction(o) || isAbiFallback(o))
.findIndex((valid) => !valid); .findIndex((valid) => !valid);
if (invalidIndex !== -1) { if (invalidIndex !== -1) {
@ -70,13 +70,13 @@ export function validateAbi (abi, api) {
}; };
} }
function isValidAbiFunction (object, api) { function isValidAbiFunction (object) {
if (!object) { if (!object) {
return false; return false;
} }
return ((object.type === 'function' && object.name) || object.type === 'constructor') && return ((object.type === 'function' && object.name) || object.type === 'constructor') &&
(object.inputs && api.util.isArray(object.inputs)); (object.inputs && util.isArray(object.inputs));
} }
function isAbiFallback (object) { function isAbiFallback (object) {
@ -87,14 +87,14 @@ function isAbiFallback (object) {
return object.type === 'fallback'; return object.type === 'fallback';
} }
function isValidAbiEvent (object, api) { function isValidAbiEvent (object) {
if (!object) { if (!object) {
return false; return false;
} }
return (object.type === 'event') && return (object.type === 'event') &&
(object.name) && (object.name) &&
(object.inputs && api.util.isArray(object.inputs)); (object.inputs && util.isArray(object.inputs));
} }
export function validateAddress (address) { export function validateAddress (address) {