Update CreateWallet with FormattedMessage (#4298)

* Allow FormattedMessage as hint & label

* tests for basic rendering

* convert component messages

* Typo

* id typos (insubstantial, but annoying)

* 2015-2017

* 2015-2017

* 2015-2017

* 2015-2017

* 2015-2017
This commit is contained in:
Jaco Greeff 2017-01-26 16:11:04 +01:00 committed by GitHub
parent 82a7a17e6e
commit 2ac7655355
11 changed files with 612 additions and 113 deletions

View File

@ -14,8 +14,9 @@
// 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, { Component, PropTypes } from 'react';
import { omitBy } from 'lodash';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { Form, TypedInput, Input, AddressSelect, InputAddress } from '~/ui';
@ -46,24 +47,54 @@ export default class WalletDetails extends Component {
return (
<Form>
<InputAddress
label='wallet address'
hint='the wallet contract address'
hint={
<FormattedMessage
id='createWallet.details.address.hint'
defaultMessage='the wallet contract address'
/>
}
label={
<FormattedMessage
id='createWallet.details.address.label'
defaultMessage='wallet address'
/>
}
value={ wallet.address }
error={ errors.address }
onChange={ this.onAddressChange }
/>
<Input
label='wallet name'
hint='the local name for this wallet'
value={ wallet.name }
error={ errors.name }
hint={
<FormattedMessage
id='createWallet.details.name.hint'
defaultMessage='the local name for this wallet'
/>
}
label={
<FormattedMessage
id='createWallet.details.name.label'
defaultMessage='wallet name'
/>
}
value={ wallet.name }
onChange={ this.onNameChange }
/>
<Input
label='wallet description (optional)'
hint='the local description for this wallet'
hint={
<FormattedMessage
id='createWallet.details.description.hint'
defaultMessage='the local description for this wallet'
/>
}
label={
<FormattedMessage
id='createWallet.details.description.label'
defaultMessage='wallet description (optional)'
/>
}
value={ wallet.description }
onChange={ this.onDescriptionChange }
/>
@ -80,43 +111,88 @@ export default class WalletDetails extends Component {
return (
<Form>
<AddressSelect
label='from account (contract owner)'
hint='the owner account for this contract'
value={ wallet.account }
error={ errors.account }
onChange={ this.onAccoutChange }
accounts={ _accounts }
error={ errors.account }
hint={
<FormattedMessage
id='createWallet.details.ownerMulti.hint'
defaultMessage='the owner account for this contract'
/>
}
label={
<FormattedMessage
id='createWallet.details.ownerMulti.label'
defaultMessage='from account (contract owner)'
/>
}
value={ wallet.account }
onChange={ this.onAccoutChange }
/>
<Input
label='wallet name'
hint='the local name for this wallet'
value={ wallet.name }
error={ errors.name }
hint={
<FormattedMessage
id='createWallet.details.nameMulti.hint'
defaultMessage='the local name for this wallet'
/>
}
label={
<FormattedMessage
id='createWallet.details.nameMulti.label'
defaultMessage='wallet name'
/>
}
value={ wallet.name }
onChange={ this.onNameChange }
/>
<Input
label='wallet description (optional)'
hint='the local description for this wallet'
hint={
<FormattedMessage
id='createWallet.details.descriptionMulti.hint'
defaultMessage='the local description for this wallet'
/>
}
label={
<FormattedMessage
id='createWallet.details.descriptionMulti.label'
defaultMessage='wallet description (optional)'
/>
}
value={ wallet.description }
onChange={ this.onDescriptionChange }
/>
<TypedInput
label='other wallet owners'
value={ wallet.owners.slice() }
onChange={ this.onOwnersChange }
accounts={ accounts }
label={
<FormattedMessage
id='createWallet.details.ownersMulti.label'
defaultMessage='other wallet owners'
/>
}
onChange={ this.onOwnersChange }
param='address[]'
value={ wallet.owners.slice() }
/>
<div className={ styles.splitInput }>
<TypedInput
label='required owners'
hint='number of required owners to accept a transaction'
value={ wallet.required }
error={ errors.required }
hint={
<FormattedMessage
id='createWallet.details.ownersMultiReq.hint'
defaultMessage='number of required owners to accept a transaction'
/>
}
label={
<FormattedMessage
id='createWallet.details.ownersMultiReq.label'
defaultMessage='required owners'
/>
}
value={ wallet.required }
onChange={ this.onRequiredChange }
param='uint'
min={ 1 }
@ -124,13 +200,23 @@ export default class WalletDetails extends Component {
/>
<TypedInput
label='wallet day limit'
hint='amount of ETH spendable without confirmations'
value={ wallet.daylimit }
error={ errors.daylimit }
hint={
<FormattedMessage
id='createWallet.details.dayLimitMulti.hint'
defaultMessage='amount of ETH spendable without confirmations'
/>
}
isEth
label={
<FormattedMessage
id='createWallet.details.dayLimitMulti.label'
defaultMessage='wallet day limit'
/>
}
onChange={ this.onDaylimitChange }
param='uint'
isEth
value={ wallet.daylimit }
/>
</div>
</Form>

View File

@ -0,0 +1,49 @@
// Copyright 2015-2017 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 WalletDetails from './';
import { ACCOUNTS } from '../createWallet.test.js';
let component;
let onChange;
function render (walletType = 'MULTISIG') {
onChange = sinon.stub();
component = shallow(
<WalletDetails
accounts={ ACCOUNTS }
errors={ {} }
onChange={ onChange }
wallet={ {
owners: []
} }
walletType={ walletType }
/>
);
return component;
}
describe('WalletDetails', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
});

View File

@ -15,9 +15,10 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { CompletedStep, IdentityIcon, CopyToClipboard } from '~/ui';
import { fromWei } from '~/api/util/wei';
import { CompletedStep, IdentityIcon, CopyToClipboard } from '~/ui';
import styles from '../createWallet.css';
@ -48,24 +49,73 @@ export default class WalletInfo extends Component {
return (
<CompletedStep>
<div>
<code>{ name }</code>
<span> has been </span>
<span> { deployed ? 'deployed' : 'added' } at </span>
<span>
<FormattedMessage
id='createWallet.info.created'
defaultMessage='{name} has been {deployedOrAdded} at '
values={ {
name: <code>{ name }</code>,
deployedOrAdded: deployed
? (
<FormattedMessage
id='createWallet.info.deployed'
defaultMessage='deployed'
/>
)
: (
<FormattedMessage
id='createWallet.info.added'
defaultMessage='added'
/>
)
} }
/>
</span>
</div>
<div>
<CopyToClipboard data={ address } label='copy address to clipboard' />
<IdentityIcon address={ address } inline center className={ styles.identityicon } />
<CopyToClipboard
data={ address }
label={
<FormattedMessage
id='createWallet.info.copyAddress'
defaultMessage='copy address to clipboard'
/>
}
/>
<IdentityIcon
address={ address }
className={ styles.identityicon }
center
inline
/>
<div className={ styles.address }>{ address }</div>
</div>
<div>with the following owners</div>
<div>
<FormattedMessage
id='createWallet.info.owners'
defaultMessage='The following are wallet owners'
/>
</div>
<div>
{ this.renderOwners() }
</div>
<p>
<code>{ required }</code> owners are required to confirm a transaction.
<FormattedMessage
id='createWallet.info.numOwners'
defaultMessage='{numOwners} owners are required to confirm a transaction.'
values={ {
numOwners: <code>{ required }</code>
} }
/>
</p>
<p>
The daily limit is set to <code>{ fromWei(daylimit).toFormat() }</code> ETH.
<FormattedMessage
id='createWallet.info.dayLimit'
defaultMessage='The daily limit is set to {dayLimit} ETH.'
values={ {
dayLimit: <code>{ fromWei(daylimit).toFormat() }</code>
} }
/>
</p>
</CompletedStep>
);
@ -74,12 +124,27 @@ export default class WalletInfo extends Component {
renderOwners () {
const { account, owners, deployed } = this.props;
return [].concat(deployed ? account : null, owners).filter((a) => a).map((address, id) => (
<div key={ id } className={ styles.owner }>
<IdentityIcon address={ address } inline center className={ styles.identityicon } />
<div className={ styles.address }>{ this.addressToString(address) }</div>
</div>
));
return []
.concat(deployed ? account : null, owners)
.filter((account) => account)
.map((address, id) => {
return (
<div
className={ styles.owner }
key={ id }
>
<IdentityIcon
address={ address }
className={ styles.identityicon }
center
inline
/>
<div className={ styles.address }>
{ this.addressToString(address) }
</div>
</div>
);
});
}
addressToString (address) {

View File

@ -0,0 +1,46 @@
// Copyright 2015-2017 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 WalletInfo from './';
import { ACCOUNTS } from '../createWallet.test.js';
let component;
function render () {
component = shallow(
<WalletInfo
accounts={ ACCOUNTS }
account='0x1234567890123456789012345678901234567890'
address='0x0987654321098765432109876543210987654321'
daylimit='5'
name='testWallet'
owners={ [] }
required='5'
/>
);
return component;
}
describe('WalletInfo', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
});

View File

@ -15,11 +15,53 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { RadioButtons } from '~/ui';
import { walletSourceURL } from '~/contracts/code/wallet';
import { RadioButtons } from '~/ui';
// import styles from '../createWallet.css';
const TYPES = [
{
label: (
<FormattedMessage
id='createWallet.type.multisig.label'
defaultMessage='Multi-Sig wallet'
/>
),
key: 'MULTISIG',
description: (
<FormattedMessage
id='createWallet.type.multisig.description'
defaultMessage='Create/Deploy a {link} Wallet'
values={ {
link: (
<a href={ walletSourceURL } target='_blank'>
<FormattedMessage
id='createWallet.type.multisig.link'
defaultMessage='standard multi-signature'
/>
</a>
)
} }
/>
)
},
{
label: (
<FormattedMessage
id='createWallet.type.watch.label'
defaultMessage='Watch a wallet'
/>
),
key: 'WATCH',
description: (
<FormattedMessage
id='createWallet.type.watch.description'
defaultMessage='Add an existing wallet to your accounts'
/>
)
}
];
export default class WalletType extends Component {
static propTypes = {
@ -33,34 +75,13 @@ export default class WalletType extends Component {
return (
<RadioButtons
name='contractType'
value={ type }
values={ this.getTypes() }
onChange={ this.onTypeChange }
value={ type }
values={ TYPES }
/>
);
}
getTypes () {
return [
{
label: 'Multi-Sig wallet', key: 'MULTISIG',
description: (
<span>
<span>Create/Deploy a </span>
<a href={ walletSourceURL } target='_blank'>
standard multi-signature
</a>
<span> Wallet</span>
</span>
)
},
{
label: 'Watch a wallet', key: 'WATCH',
description: 'Add an existing wallet to your accounts'
}
];
}
onTypeChange = (type) => {
this.props.onChange(type.key);
}

View File

@ -0,0 +1,42 @@
// Copyright 2015-2017 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 WalletType from './';
let component;
let onChange;
function render (walletType = 'MULTISIG') {
onChange = sinon.stub();
component = shallow(
<WalletType
onChange={ onChange }
type={ walletType }
/>
);
return component;
}
describe('WalletType', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
});

View File

@ -14,20 +14,17 @@
// 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, { Component, PropTypes } from 'react';
import { observer } from 'mobx-react';
import ActionDone from 'material-ui/svg-icons/action/done';
import ContentClear from 'material-ui/svg-icons/content/clear';
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { Button, Modal, TxHash, BusyStep } from '~/ui';
import { CancelIcon, DoneIcon, NextIcon } from '~/ui/Icons';
import WalletType from './WalletType';
import WalletDetails from './WalletDetails';
import WalletInfo from './WalletInfo';
import CreateWalletStore from './createWalletStore';
// import styles from './createWallet.css';
@observer
export default class CreateWallet extends Component {
@ -49,12 +46,27 @@ export default class CreateWallet extends Component {
return (
<Modal
visible
title='rejected'
title={
<FormattedMessage
id='createWallet.rejected.title'
defaultMessage='rejected'
/>
}
actions={ this.renderDialogActions() }
>
<BusyStep
title='The deployment has been rejected'
state='The wallet will not be created. You can safely close this window.'
title={
<FormattedMessage
id='createWallet.rejected.message'
defaultMessage='The deployment has been rejected'
/>
}
state={
<FormattedMessage
id='createWallet.rejected.state'
defaultMessage='The wallet will not be created. You can safely close this window.'
/>
}
/>
</Modal>
);
@ -65,7 +77,7 @@ export default class CreateWallet extends Component {
visible
actions={ this.renderDialogActions() }
current={ stage }
steps={ steps.map((s) => s.title) }
steps={ steps.map((step) => step.title) }
waiting={ waiting }
>
{ this.renderPage() }
@ -81,10 +93,19 @@ export default class CreateWallet extends Component {
case 'DEPLOYMENT':
return (
<BusyStep
title='The deployment is currently in progress'
title={
<FormattedMessage
id='createWallet.deployment.message'
defaultMessage='The deployment is currently in progress'
/>
}
state={ this.store.deployState }
>
{ this.store.txhash ? (<TxHash hash={ this.store.txhash } />) : null }
{
this.store.txhash
? <TxHash hash={ this.store.txhash } />
: null
}
</BusyStep>
);
@ -92,15 +113,13 @@ export default class CreateWallet extends Component {
return (
<WalletInfo
accounts={ accounts }
account={ this.store.wallet.account }
address={ this.store.wallet.address }
daylimit={ this.store.wallet.daylimit }
deployed={ this.store.deployed }
name={ this.store.wallet.name }
owners={ this.store.wallet.owners.slice() }
required={ this.store.wallet.required }
daylimit={ this.store.wallet.daylimit }
name={ this.store.wallet.name }
deployed={ this.store.deployed }
/>
);
@ -108,10 +127,10 @@ export default class CreateWallet extends Component {
return (
<WalletDetails
accounts={ accounts }
wallet={ this.store.wallet }
errors={ this.store.errors }
walletType={ this.store.walletType }
onChange={ this.store.onChange }
wallet={ this.store.wallet }
walletType={ this.store.walletType }
/>
);
@ -131,40 +150,65 @@ export default class CreateWallet extends Component {
const cancelBtn = (
<Button
icon={ <ContentClear /> }
label='Cancel'
icon={ <CancelIcon /> }
label={
<FormattedMessage
id='createWallet.button.cancel'
defaultMessage='Cancel'
/>
}
onClick={ this.onClose }
/>
);
const closeBtn = (
<Button
icon={ <ContentClear /> }
label='Close'
icon={ <CancelIcon /> }
label={
<FormattedMessage
id='createWallet.button.close'
defaultMessage='Close'
/>
}
onClick={ this.onClose }
/>
);
const doneBtn = (
<Button
icon={ <ActionDone /> }
label='Done'
icon={ <DoneIcon /> }
label={
<FormattedMessage
id='createWallet.button.done'
defaultMessage='Done'
/>
}
onClick={ this.onClose }
/>
);
const sendingBtn = (
<Button
icon={ <ActionDone /> }
label='Sending...'
icon={ <DoneIcon /> }
label={
<FormattedMessage
id='createWallet.button.sending'
defaultMessage='Sending...'
/>
}
disabled
/>
);
const nextBtn = (
<Button
icon={ <NavigationArrowForward /> }
label='Next'
icon={ <NextIcon /> }
label={
<FormattedMessage
id='createWallet.button.next'
defaultMessage='Next'
/>
}
onClick={ onNext }
/>
);
@ -184,9 +228,14 @@ export default class CreateWallet extends Component {
if (this.store.walletType === 'WATCH') {
return [ cancelBtn, (
<Button
icon={ <NavigationArrowForward /> }
label='Add'
disabled={ hasErrors }
icon={ <NextIcon /> }
label={
<FormattedMessage
id='createWallet.button.add'
defaultMessage='Add'
/>
}
onClick={ onAdd }
/>
) ];
@ -194,9 +243,14 @@ export default class CreateWallet extends Component {
return [ cancelBtn, (
<Button
icon={ <NavigationArrowForward /> }
label='Create'
disabled={ hasErrors }
icon={ <NextIcon /> }
label={
<FormattedMessage
id='createWallet.button.create'
defaultMessage='Create'
/>
}
onClick={ onCreate }
/>
) ];

View File

@ -0,0 +1,54 @@
// Copyright 2015-2017 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 CreateWallet from './';
import { ACCOUNTS } from './createWallet.test.js';
let api;
let component;
let onClose;
function createApi () {
api = {};
return api;
}
function render () {
onClose = sinon.stub();
component = shallow(
<CreateWallet
accounts={ ACCOUNTS }
onClose={ onClose }
/>,
{
context: { api: createApi() }
}
);
return component;
}
describe('CreateWallet', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
});

View File

@ -0,0 +1,25 @@
// Copyright 2015-2017 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/>.
const ACCOUNTS = {
'0x1234567890123456789012345678901234567890': {
address: '0x1234567890123456789012345678901234567890'
}
};
export {
ACCOUNTS
};

View File

@ -15,10 +15,12 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { observable, computed, action, transaction } from 'mobx';
import React from 'react';
import { FormattedMessage } from 'react-intl';
import Contract from '~/api/contract';
import Contracts from '~/contracts';
import { ERROR_CODES } from '~/api/transport/error';
import Contracts from '~/contracts';
import { wallet as walletAbi } from '~/contracts/abi';
import { wallet as walletCode, walletLibraryRegKey, fullWalletCode } from '~/contracts/code/wallet';
@ -27,10 +29,39 @@ import { toWei } from '~/api/util/wei';
import WalletsUtils from '~/util/wallets';
const STEPS = {
TYPE: { title: 'wallet type' },
DETAILS: { title: 'wallet details' },
DEPLOYMENT: { title: 'wallet deployment', waiting: true },
INFO: { title: 'wallet informaton' }
TYPE: {
title: (
<FormattedMessage
id='createWallet.steps.type'
defaultMessage='wallet type'
/>
)
},
DETAILS: {
title: (
<FormattedMessage
id='createWallet.steps.details'
defaultMessage='wallet details'
/>
)
},
DEPLOYMENT: {
title: (
<FormattedMessage
id='createWallet.steps.deployment'
defaultMessage='wallet deployment'
/>
),
waiting: true
},
INFO: {
title: (
<FormattedMessage
id='createWallet.steps.info'
defaultMessage='wallet informaton'
/>
)
}
};
export default class CreateWalletStore {
@ -227,25 +258,50 @@ export default class CreateWalletStore {
switch (data.state) {
case 'estimateGas':
case 'postTransaction':
this.deployState = 'Preparing transaction for network transmission';
this.deployState = (
<FormattedMessage
id='createWallet.states.preparing'
defaultMessage='Preparing transaction for network transmission'
/>
);
return;
case 'checkRequest':
this.deployState = 'Waiting for confirmation of the transaction in the Parity Secure Signer';
this.deployState = (
<FormattedMessage
id='createWallet.states.waitingConfirm'
defaultMessage='Waiting for confirmation of the transaction in the Parity Secure Signer'
/>
);
return;
case 'getTransactionReceipt':
this.deployState = 'Waiting for the contract deployment transaction receipt';
this.deployState = (
<FormattedMessage
id='createWallet.states.waitingReceipt'
defaultMessage='Waiting for the contract deployment transaction receipt'
/>
);
this.txhash = data.txhash;
return;
case 'hasReceipt':
case 'getCode':
this.deployState = 'Validating the deployed contract code';
this.deployState = (
<FormattedMessage
id='createWallet.states.validatingCode'
defaultMessage='Validating the deployed contract code'
/>
);
return;
case 'completed':
this.deployState = 'The contract deployment has been completed';
this.deployState = (
<FormattedMessage
id='createWallet.states.completed'
defaultMessage='The contract deployment has been completed'
/>
);
return;
default:

View File

@ -28,6 +28,7 @@ import Input from '~/ui/Form/Input';
import InputAddressSelect from '~/ui/Form/InputAddressSelect';
import Select from '~/ui/Form/Select';
import { ABI_TYPES, parseAbiType } from '~/util/abi';
import { nodeOrStringProptype } from '~/util/proptypes';
import styles from './typedInput.css';
@ -42,9 +43,9 @@ export default class TypedInput extends Component {
allowCopy: PropTypes.bool,
className: PropTypes.string,
error: PropTypes.any,
hint: PropTypes.string,
hint: nodeOrStringProptype(),
isEth: PropTypes.bool,
label: PropTypes.string,
label: nodeOrStringProptype(),
max: PropTypes.number,
min: PropTypes.number,
onChange: PropTypes.func,