[beta] UI updates for 1.5.1 (#4429)

* s/Delete Contract/Forget Contract/ (#4237)

* Adjust the location of the signer snippet (#4155)

* Additional building-block UI components (#4239)

* Currency WIP

* Expand tests

* Pass className

* Add QrCode

* Export new components in ~/ui

* s/this.props.netSymbol/netSymbol/

* Fix import case

* ui/SectionList component (#4292)

* array chunking utility

* add SectionList component

* Add TODOs to indicate possible future work

* Add missing overlay style (as used in dapps at present)

* Add a Playground for the UI Components (#4301)

* Playground // WIP

* Linting

* Add Examples with code

* CSS Linting

* Linting

* Add Connected Currency Symbol

* 2015-2017

* 2015-2017

* 2015-2017

* 2015-2017

* 2015-2017

* 2015-2017

* 2015-2017

* Added `renderSymbol` tests

* PR grumbles

* Add Eth and Btc QRCode examples

* 2015-2017

* Add tests for playground

* Fixing tests

* Split Dapp icon into ui/DappIcon (#4308)

* Add QrCode & Copy to ShapeShift (#4322)

* Extract CopyIcon to ~/ui/Icons

* Add copy & QrCode address

* Default size 4

* Add bitcoin: link

* use protocol links applicable to coin exchanged

* Remove .only

* Display QrCode for accounts, addresses & contracts (#4329)

* Allow Portal to be used as top-level modal (#4338)

* Portal

* Allow Portal to be used in as both top-level and popover

* modal/popover variable naming

* export Portal in ~/ui

* Properly handle optional onKeyDown

* Add simple Playground Example

* Add proper event listener to Portal (#4359)

* Display AccountCard name via IdentityName (#4235)

* Fix signing (#4363)

* Dapp Account Selection & Defaults (#4355)

* Add parity_defaultAccount RPC (with subscription) (#4383)

* Default Account selector in Signer overlay (#4375)

* Typo, fixes #4271 (#4391)

* Fix ParityBar account selection overflows (#4405)

* Available Dapp selection alignment with Permissions (Portal) (#4374)

* registry dapp: make lookup use lower case (#4409)

* Dapps use defaultAccount instead of own selectors (#4386)

* Poll for defaultAccount to update dapp & overlay subscriptions (#4417)

* Poll for defaultAccount (Fixes #4413)

* Fix nextTimeout on catch

* Store timers

* Re-enable default updates on change detection

* Add block & timestamp conditions to Signer (#4411)

* Extension installation overlay (#4423)

* Extension installation overlay

* Pr gumbles

* Spelling

* Update Chrome URL

* Fix for non-included jsonrpc

* Extend Portal component (as per Modal) #4392
This commit is contained in:
Jaco Greeff 2017-02-04 09:42:36 +01:00 committed by Gav Wood
parent f76b94c2c5
commit fb817fcdca
136 changed files with 5775 additions and 1230 deletions

View File

@ -163,6 +163,7 @@
"phoneformat.js": "1.0.3",
"promise-worker": "1.1.1",
"push.js": "0.0.11",
"qrcode-npm": "0.0.3",
"qs": "6.3.0",
"react": "15.4.1",
"react-ace": "4.1.0",
@ -170,6 +171,8 @@
"react-copy-to-clipboard": "4.2.3",
"react-dom": "15.4.1",
"react-dropzone": "3.7.3",
"react-element-to-jsx-string": "6.0.0",
"react-event-listener": "0.4.1",
"react-intl": "2.1.5",
"react-portal": "3.0.0",
"react-redux": "4.4.6",
@ -185,6 +188,7 @@
"scryptsy": "2.0.0",
"solc": "ngotchac/solc-js",
"store": "1.3.20",
"useragent.js": "0.5.6",
"utf8": "2.1.2",
"valid-url": "1.0.9",
"validator": "6.2.0",

View File

@ -127,6 +127,18 @@ export function inNumber16 (number) {
return inHex(bn.toString(16));
}
export function inOptionsCondition (condition) {
if (condition) {
if (condition.block) {
condition.block = condition.block ? inNumber10(condition.block) : null;
} else if (condition.time) {
condition.time = inNumber10(Math.floor(condition.time.getTime() / 1000));
}
}
return condition;
}
export function inOptions (options) {
if (options) {
Object.keys(options).forEach((key) => {
@ -136,6 +148,10 @@ export function inOptions (options) {
options[key] = inAddress(options[key]);
break;
case 'condition':
options[key] = inOptionsCondition(options[key]);
break;
case 'gas':
case 'gasPrice':
options[key] = inNumber16((new BigNumber(options[key])).round());

View File

@ -200,6 +200,18 @@ export function outSyncing (syncing) {
return syncing;
}
export function outTransactionCondition (condition) {
if (condition) {
if (condition.block) {
condition.block = outNumber(condition.block);
} else if (condition.time) {
condition.time = outDate(condition.time);
}
}
return condition;
}
export function outTransaction (tx) {
if (tx) {
Object.keys(tx).forEach((key) => {
@ -213,8 +225,14 @@ export function outTransaction (tx) {
tx[key] = outNumber(tx[key]);
break;
case 'condition':
tx[key] = outTransactionCondition(tx[key]);
break;
case 'minBlock':
tx[key] = tx[key] ? outNumber(tx[key]) : null;
tx[key] = tx[key]
? outNumber(tx[key])
: null;
break;
case 'creates':

View File

@ -1,4 +1,4 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { inAddress, inAddresses, inData, inHex, inNumber16, inOptions } from '../../format/input';
import { inAddress, inAddresses, inData, inHex, inNumber16, inOptions, inBlockNumber } from '../../format/input';
import { outAccountInfo, outAddress, outAddresses, outChainStatus, outHistogram, outNumber, outPeers, outTransaction } from '../../format/output';
export default class Parity {
@ -76,6 +76,17 @@ export default class Parity {
.execute('parity_dappsInterface');
}
decryptMessage (address, data) {
return this._transport
.execute('parity_decryptMessage', inAddress(address), inHex(data));
}
defaultAccount () {
return this._transport
.execute('parity_defaultAccount')
.then(outAddress);
}
defaultExtraData () {
return this._transport
.execute('parity_defaultExtraData');
@ -101,6 +112,11 @@ export default class Parity {
.execute('parity_enode');
}
encryptMessage (pubkey, data) {
return this._transport
.execute('parity_encryptMessage', inHex(pubkey), inHex(data));
}
executeUpgrade () {
return this._transport
.execute('parity_executeUpgrade');
@ -111,6 +127,17 @@ export default class Parity {
.execute('parity_extraData');
}
futureTransactions () {
return this._transport
.execute('parity_futureTransactions');
}
gasCeilTarget () {
return this._transport
.execute('parity_gasCeilTarget')
.then(outNumber);
}
gasFloorTarget () {
return this._transport
.execute('parity_gasFloorTarget')
@ -156,11 +183,22 @@ export default class Parity {
.execute('parity_killAccount', inAddress(account), password);
}
listAccounts (count, offset = null, blockNumber = 'latest') {
return this._transport
.execute('parity_listAccounts', count, inAddress(offset), inBlockNumber(blockNumber))
.then((accounts) => (accounts || []).map(outAddress));
}
listRecentDapps () {
return this._transport
.execute('parity_listRecentDapps');
}
listStorageKeys (address, count, hash = null, blockNumber = 'latest') {
return this._transport
.execute('parity_listStorageKeys', inAddress(address), count, inHex(hash), inBlockNumber(blockNumber));
}
removeAddress (address) {
return this._transport
.execute('parity_removeAddress', inAddress(address));
@ -265,6 +303,11 @@ export default class Parity {
.then(outAddress);
}
postSign (address, hash) {
return this._transport
.execute('parity_postSign', inAddress(address), inHex(hash));
}
postTransaction (options) {
return this._transport
.execute('parity_postTransaction', inOptions(options));
@ -311,16 +354,31 @@ export default class Parity {
.execute('parity_setDappsAddresses', dappId, inAddresses(addresses));
}
setEngineSigner (address, password) {
return this._transport
.execute('parity_setEngineSigner', inAddress(address), password);
}
setExtraData (data) {
return this._transport
.execute('parity_setExtraData', inData(data));
}
setGasCeilTarget (quantity) {
return this._transport
.execute('parity_setGasCeilTarget', inNumber16(quantity));
}
setGasFloorTarget (quantity) {
return this._transport
.execute('parity_setGasFloorTarget', inNumber16(quantity));
}
setMaxTransactionGas (quantity) {
return this._transport
.execute('parity_setMaxTransactionGas', inNumber16(quantity));
}
setMinGasPrice (quantity) {
return this._transport
.execute('parity_setMinGasPrice', inNumber16(quantity));

View File

@ -23,6 +23,7 @@ export default class Eth {
this._started = false;
this._lastBlock = new BigNumber(-1);
this._pollTimerId = null;
}
get isStarted () {
@ -37,7 +38,7 @@ export default class Eth {
_blockNumber = () => {
const nextTimeout = (timeout = 1000) => {
setTimeout(() => {
this._pollTimerId = setTimeout(() => {
this._blockNumber();
}, timeout);
};
@ -57,6 +58,6 @@ export default class Eth {
nextTimeout();
})
.catch(nextTimeout);
.catch(() => nextTimeout());
}
}

View File

@ -25,6 +25,7 @@ const events = {
'logging': { module: 'logging' },
'eth_blockNumber': { module: 'eth' },
'parity_allAccountsInfo': { module: 'personal' },
'parity_defaultAccount': { module: 'personal' },
'eth_accounts': { module: 'personal' },
'signer_requestsToConfirm': { module: 'signer' }
};

View File

@ -1,4 +1,4 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
@ -20,6 +20,9 @@ export default class Personal {
this._api = api;
this._updateSubscriptions = updateSubscriptions;
this._started = false;
this._lastDefaultAccount = '0x0';
this._pollTimerId = null;
}
get isStarted () {
@ -30,12 +33,44 @@ export default class Personal {
this._started = true;
return Promise.all([
this._defaultAccount(),
this._listAccounts(),
this._accountsInfo(),
this._loggingSubscribe()
]);
}
// FIXME: Because of the different API instances, the "wait for valid changes" approach
// doesn't work. Since the defaultAccount is critical to operation, we poll in exactly
// same way we do in ../eth (ala same as eth_blockNumber) and update. This should be moved
// to pub-sub as it becomes available
_defaultAccount = (timerDisabled = false) => {
const nextTimeout = (timeout = 1000) => {
if (!timerDisabled) {
this._pollTimerId = setTimeout(() => {
this._defaultAccount();
}, timeout);
}
};
if (!this._api.transport.isConnected) {
nextTimeout(500);
return;
}
return this._api.parity
.defaultAccount()
.then((defaultAccount) => {
if (this._lastDefaultAccount !== defaultAccount) {
this._lastDefaultAccount = defaultAccount;
this._updateSubscriptions('parity_defaultAccount', null, defaultAccount);
}
nextTimeout();
})
.catch(() => nextTimeout());
}
_listAccounts = () => {
return this._api.eth
.accounts()
@ -46,9 +81,19 @@ export default class Personal {
_accountsInfo = () => {
return this._api.parity
.allAccountsInfo()
.accountsInfo()
.then((info) => {
this._updateSubscriptions('parity_allAccountsInfo', null, info);
this._updateSubscriptions('parity_accountsInfo', null, info);
return this._api.parity
.allAccountsInfo()
.catch(() => {
// NOTE: This fails on non-secure APIs, swallow error
return {};
})
.then((allInfo) => {
this._updateSubscriptions('parity_allAccountsInfo', null, allInfo);
});
});
}
@ -73,6 +118,11 @@ export default class Personal {
case 'parity_setAccountMeta':
this._accountsInfo();
return;
case 'parity_setDappsAddresses':
case 'parity_setNewDappsWhitelist':
this._defaultAccount(true);
return;
}
});
}

View File

@ -1,4 +1,4 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
@ -18,31 +18,51 @@ import sinon from 'sinon';
import Personal from './personal';
const TEST_DEFAULT = '0xfa64203C044691aA57251aF95f4b48d85eC00Dd5';
const TEST_INFO = {
'0xfa64203C044691aA57251aF95f4b48d85eC00Dd5': {
[TEST_DEFAULT]: {
name: 'test'
}
};
const TEST_LIST = ['0xfa64203C044691aA57251aF95f4b48d85eC00Dd5'];
const TEST_LIST = [TEST_DEFAULT];
function stubApi (accounts, info) {
const _calls = {
accountsInfo: [],
allAccountsInfo: [],
listAccounts: []
listAccounts: [],
defaultAccount: []
};
return {
_calls,
transport: {
isConnected: true
},
parity: {
accountsInfo: () => {
const stub = sinon.stub().resolves(info || TEST_INFO)();
_calls.accountsInfo.push(stub);
return stub;
},
allAccountsInfo: () => {
const stub = sinon.stub().resolves(info || TEST_INFO)();
_calls.allAccountsInfo.push(stub);
return stub;
},
defaultAccount: () => {
const stub = sinon.stub().resolves(Object.keys(info || TEST_INFO)[0])();
_calls.defaultAccount.push(stub);
return stub;
}
},
eth: {
accounts: () => {
const stub = sinon.stub().resolves(accounts || TEST_LIST)();
_calls.listAccounts.push(stub);
return stub;
}
@ -85,6 +105,10 @@ describe('api/subscriptions/personal', () => {
expect(personal.isStarted).to.be.true;
});
it('calls parity_accountsInfo', () => {
expect(api._calls.accountsInfo.length).to.be.ok;
});
it('calls parity_allAccountsInfo', () => {
expect(api._calls.allAccountsInfo.length).to.be.ok;
});
@ -94,8 +118,10 @@ describe('api/subscriptions/personal', () => {
});
it('updates subscribers', () => {
expect(cb.firstCall).to.have.been.calledWith('eth_accounts', null, TEST_LIST);
expect(cb.secondCall).to.have.been.calledWith('parity_allAccountsInfo', null, TEST_INFO);
expect(cb).to.have.been.calledWith('parity_defaultAccount', null, TEST_DEFAULT);
expect(cb).to.have.been.calledWith('eth_accounts', null, TEST_LIST);
expect(cb).to.have.been.calledWith('parity_accountsInfo', null, TEST_INFO);
expect(cb).to.have.been.calledWith('parity_allAccountsInfo', null, TEST_INFO);
});
});
@ -110,7 +136,15 @@ describe('api/subscriptions/personal', () => {
expect(personal.isStarted).to.be.true;
});
it('calls parity_defaultAccount', () => {
expect(api._calls.defaultAccount.length).to.be.ok;
});
it('calls personal_accountsInfo', () => {
expect(api._calls.accountsInfo.length).to.be.ok;
});
it('calls personal_allAccountsInfo', () => {
expect(api._calls.allAccountsInfo.length).to.be.ok;
});

View File

@ -45,7 +45,7 @@ export default class Application extends Component {
}
componentDidMount () {
this.attachInstance();
return this.attachInstance();
}
render () {
@ -80,12 +80,12 @@ export default class Application extends Component {
}
attachInstance () {
Promise
return Promise
.all([
attachInstances(),
api.parity.accountsInfo()
api.parity.accountsInfo(),
attachInstances()
])
.then(([{ managerInstance, registryInstance, tokenregInstance }, accountsInfo]) => {
.then(([accountsInfo, { managerInstance, registryInstance, tokenregInstance }]) => {
accountsInfo = accountsInfo || {};
this.setState({
loading: false,

View File

@ -1,4 +1,4 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
@ -17,7 +17,6 @@
import React, { Component, PropTypes } from 'react';
import { api } from '../../parity';
import AddressSelect from '../../AddressSelect';
import Container from '../../Container';
import styles from './deployment.css';
@ -122,41 +121,20 @@ export default class Deployment extends Component {
}
renderForm () {
const { accounts } = this.context;
const { baseText, name, nameError, tla, tlaError, totalSupply, totalSupplyError } = this.state;
const hasError = !!(nameError || tlaError || totalSupplyError);
const error = `${styles.input} ${styles.error}`;
const addresses = Object.keys(accounts);
// <div className={ styles.input }>
// <label>global registration</label>
// <select onChange={ this.onChangeRegistrar }>
// <option value='no'>No, only for me</option>
// <option value='yes'>Yes, for everybody</option>
// </select>
// <div className={ styles.hint }>
// register on network (fee: { globalFeeText }ETH)
// </div>
// </div>
return (
<Container>
<div className={ styles.form }>
<div className={ styles.input }>
<label>deployment account</label>
<AddressSelect
addresses={ addresses }
onChange={ this.onChangeFrom } />
<div className={ styles.hint }>
the owner account to deploy from
</div>
</div>
<div className={ nameError ? error : styles.input }>
<label>token name</label>
<input
value={ name }
name='name'
onChange={ this.onChangeName } />
onChange={ this.onChangeName }
/>
<div className={ styles.hint }>
{ nameError || 'an identifying name for the token' }
</div>
@ -167,7 +145,8 @@ export default class Deployment extends Component {
className={ styles.small }
name='tla'
value={ tla }
onChange={ this.onChangeTla } />
onChange={ this.onChangeTla }
/>
<div className={ styles.hint }>
{ tlaError || 'unique network acronym for this token' }
</div>
@ -180,7 +159,8 @@ export default class Deployment extends Component {
max='999999999999'
name='totalSupply'
value={ totalSupply }
onChange={ this.onChangeSupply } />
onChange={ this.onChangeSupply }
/>
<div className={ styles.hint }>
{ totalSupplyError || `number of tokens (base: ${baseText})` }
</div>
@ -191,7 +171,8 @@ export default class Deployment extends Component {
<div
className={ styles.button }
disabled={ hasError }
onClick={ this.onDeploy }>
onClick={ this.onDeploy }
>
Deploy Token
</div>
</div>
@ -201,12 +182,6 @@ export default class Deployment extends Component {
);
}
onChangeFrom = (event) => {
const fromAddress = event.target.value;
this.setState({ fromAddress });
}
onChangeName = (event) => {
const name = event.target.value;
const nameError = name && (name.length > 2) && (name.length < 32)
@ -266,7 +241,7 @@ export default class Deployment extends Component {
onDeploy = () => {
const { managerInstance, registryInstance, tokenregInstance } = this.context;
const { base, deployBusy, fromAddress, globalReg, globalFee, name, nameError, tla, tlaError, totalSupply, totalSupplyError } = this.state;
const { base, deployBusy, globalReg, globalFee, name, nameError, tla, tlaError, totalSupply, totalSupplyError } = this.state;
const hasError = !!(nameError || tlaError || totalSupplyError);
if (hasError || deployBusy) {
@ -276,18 +251,23 @@ export default class Deployment extends Component {
const tokenreg = (globalReg ? tokenregInstance : registryInstance).address;
const values = [base.mul(totalSupply), tla, name, tokenreg];
const options = {
from: fromAddress,
value: globalReg ? globalFee : 0
};
this.setState({ deployBusy: true, deployState: 'Estimating gas for the transaction' });
managerInstance
.deploy.estimateGas(options, values)
return api.parity
.defaultAccount()
.then((defaultAddress) => {
options.from = defaultAddress;
return managerInstance.deploy.estimateGas(options, values);
})
.then((gas) => {
this.setState({ deployState: 'Gas estimated, Posting transaction to the network' });
const gasPassed = gas.mul(1.2);
options.gas = gasPassed.toFixed(0);
console.log(`gas estimated at ${gas.toFormat(0)}, passing ${gasPassed.toFormat(0)}`);

View File

@ -25,6 +25,8 @@ let registryInstance;
const registries = {};
const subscriptions = {};
let defaultSubscriptionId;
let nextSubscriptionId = 1000;
let isTest = false;
@ -65,6 +67,20 @@ export function unsubscribeEvents (subscriptionId) {
delete subscriptions[subscriptionId];
}
export function subscribeDefaultAddress (callback) {
return api
.subscribe('parity_defaultAccount', callback)
.then((subscriptionId) => {
defaultSubscriptionId = subscriptionId;
return defaultSubscriptionId;
});
}
export function unsubscribeDefaultAddress () {
return api.unsubscribe(defaultSubscriptionId);
}
function pollEvents () {
const loop = Object.values(subscriptions);
const timeout = () => setTimeout(pollEvents, 1000);

View File

@ -1,4 +1,4 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
@ -17,10 +17,9 @@
import React, { Component } from 'react';
import { api } from '../parity';
import { attachInterface } from '../services';
import { attachInterface, subscribeDefaultAddress, unsubscribeDefaultAddress } from '../services';
import Button from '../Button';
import Events from '../Events';
import IdentityIcon from '../IdentityIcon';
import Loading from '../Loading';
import styles from './application.css';
@ -32,7 +31,7 @@ let nextEventId = 0;
export default class Application extends Component {
state = {
fromAddress: null,
defaultAddress: null,
loading: true,
url: '',
urlError: null,
@ -47,19 +46,32 @@ export default class Application extends Component {
registerType: 'file',
repo: '',
repoError: null,
subscriptionId: null,
events: {},
eventIds: []
}
componentDidMount () {
attachInterface()
.then((state) => {
this.setState(state, () => {
this.setState({ loading: false });
});
return Promise
.all([
attachInterface(),
subscribeDefaultAddress((error, defaultAddress) => {
if (!error) {
this.setState({ defaultAddress });
}
})
])
.then(([state]) => {
this.setState(Object.assign({}, state, {
loading: false
}));
});
}
componentWillUnmount () {
return unsubscribeDefaultAddress();
}
render () {
const { loading } = this.state;
@ -75,16 +87,20 @@ export default class Application extends Component {
}
renderPage () {
const { fromAddress, registerBusy, url, urlError, contentHash, contentHashError, contentHashOwner, commit, commitError, registerType, repo, repoError } = this.state;
const { defaultAddress, registerBusy, url, urlError, contentHash, contentHashError, contentHashOwner, commit, commitError, registerType, repo, repoError } = this.state;
let hashClass = null;
if (contentHashError) {
hashClass = contentHashOwner !== fromAddress ? styles.hashError : styles.hashWarning;
hashClass = contentHashOwner !== defaultAddress
? styles.hashError
: styles.hashWarning;
} else if (contentHash) {
hashClass = styles.hashOk;
}
let valueInputs = null;
if (registerType === 'content') {
valueInputs = [
<div className={ styles.capture } key='repo'>
@ -94,7 +110,8 @@ export default class Application extends Component {
disabled={ registerBusy }
value={ repo }
className={ repoError ? styles.error : null }
onChange={ this.onChangeRepo } />
onChange={ this.onChangeRepo }
/>
</div>,
<div className={ styles.capture } key='hash'>
<input
@ -103,7 +120,8 @@ export default class Application extends Component {
disabled={ registerBusy }
value={ commit }
className={ commitError ? styles.error : null }
onChange={ this.onChangeCommit } />
onChange={ this.onChangeCommit }
/>
</div>
];
} else {
@ -115,7 +133,8 @@ export default class Application extends Component {
disabled={ registerBusy }
value={ url }
className={ urlError ? styles.error : null }
onChange={ this.onChangeUrl } />
onChange={ this.onChangeUrl }
/>
</div>
);
}
@ -128,11 +147,17 @@ export default class Application extends Component {
<Button
disabled={ registerBusy }
invert={ registerType !== 'file' }
onClick={ this.onClickTypeNormal }>File Link</Button>
onClick={ this.onClickTypeNormal }
>
File Link
</Button>
<Button
disabled={ registerBusy }
invert={ registerType !== 'content' }
onClick={ this.onClickTypeContent }>Content Bundle</Button>
onClick={ this.onClickTypeContent }
>
Content Bundle
</Button>
</div>
<div className={ styles.box }>
<div className={ styles.description }>
@ -148,26 +173,21 @@ export default class Application extends Component {
</div>
<Events
eventIds={ this.state.eventIds }
events={ this.state.events } />
events={ this.state.events }
/>
</div>
);
}
renderButtons () {
const { accounts, fromAddress, urlError, repoError, commitError, contentHashError, contentHashOwner } = this.state;
const account = accounts[fromAddress];
const { defaultAddress, urlError, repoError, commitError, contentHashError, contentHashOwner } = this.state;
return (
<div className={ styles.buttons }>
<div className={ styles.addressSelect }>
<Button invert onClick={ this.onSelectFromAddress }>
<IdentityIcon address={ account.address } />
<div>{ account.name || account.address }</div>
</Button>
</div>
<Button
onClick={ this.onClickRegister }
disabled={ (contentHashError && contentHashOwner !== fromAddress) || urlError || repoError || commitError }>register url</Button>
disabled={ (contentHashError && contentHashOwner !== defaultAddress) || urlError || repoError || commitError }
>register url</Button>
</div>
);
}
@ -264,6 +284,7 @@ export default class Application extends Component {
// TODO: field validation
if (!urlError) {
const parts = url.split('/');
hasContent = parts.length !== 0;
if (parts[2] === 'github.com' || parts[2] === 'raw.githubusercontent.com') {
@ -280,11 +301,11 @@ export default class Application extends Component {
}
onClickRegister = () => {
const { commit, commitError, contentHashError, contentHashOwner, fromAddress, url, urlError, registerType, repo, repoError } = this.state;
const { defaultAddress, commit, commitError, contentHashError, contentHashOwner, url, urlError, registerType, repo, repoError } = this.state;
// TODO: No errors are currently set, validation to be expanded and added for each
// field (query is fast to pick up the issues, so not burning atm)
if ((contentHashError && contentHashOwner !== fromAddress) || repoError || urlError || commitError) {
if ((contentHashError && contentHashOwner !== defaultAddress) || repoError || urlError || commitError) {
return;
}
@ -354,12 +375,15 @@ export default class Application extends Component {
}
registerContent (contentRepo, contentCommit) {
const { contentHash, fromAddress, instance } = this.state;
contentCommit = contentCommit.substr(0, 2) === '0x' ? contentCommit : `0x${contentCommit}`;
const { defaultAddress, contentHash, instance } = this.state;
contentCommit = contentCommit.substr(0, 2) === '0x'
? contentCommit
: `0x${contentCommit}`;
const eventId = nextEventId++;
const values = [contentHash, contentRepo, contentCommit];
const options = { from: fromAddress };
const options = { from: defaultAddress };
this.setState({
eventIds: [eventId].concat(this.state.eventIds),
@ -368,7 +392,7 @@ export default class Application extends Component {
contentHash,
contentRepo,
contentCommit,
fromAddress,
defaultAddress,
registerBusy: true,
registerState: 'Estimating gas for the transaction',
timestamp: new Date()
@ -396,6 +420,7 @@ export default class Application extends Component {
});
const gasPassed = gas.mul(1.2);
options.gas = gasPassed.toFixed(0);
console.log(`gas estimated at ${gas.toFormat(0)}, passing ${gasPassed.toFormat(0)}`);
@ -405,11 +430,11 @@ export default class Application extends Component {
}
registerUrl (contentUrl) {
const { contentHash, fromAddress, instance } = this.state;
const { contentHash, defaultAddress, instance } = this.state;
const eventId = nextEventId++;
const values = [contentHash, contentUrl];
const options = { from: fromAddress };
const options = { from: defaultAddress };
this.setState({
eventIds: [eventId].concat(this.state.eventIds),
@ -417,7 +442,7 @@ export default class Application extends Component {
[eventId]: {
contentHash,
contentUrl,
fromAddress,
defaultAddress,
registerBusy: true,
registerState: 'Estimating gas for the transaction',
timestamp: new Date()
@ -445,6 +470,7 @@ export default class Application extends Component {
});
const gasPassed = gas.mul(1.2);
options.gas = gasPassed.toFixed(0);
console.log(`gas estimated at ${gas.toFormat(0)}, passing ${gasPassed.toFormat(0)}`);
@ -453,25 +479,6 @@ export default class Application extends Component {
);
}
onSelectFromAddress = () => {
const { accounts, fromAddress } = this.state;
const addresses = Object.keys(accounts);
let index = 0;
addresses.forEach((address, _index) => {
if (address === fromAddress) {
index = _index;
}
});
index++;
if (index >= addresses.length) {
index = 0;
}
this.setState({ fromAddress: addresses[index] });
}
lookupHash (url) {
const { instance } = this.state;

View File

@ -17,48 +17,44 @@
import * as abis from '~/contracts/abi';
import { api } from './parity';
let defaultSubscriptionId;
export function attachInterface () {
return api.parity
.registryAddress()
.then((registryAddress) => {
console.log(`the registry was found at ${registryAddress}`);
const registry = api.newContract(abis.registry, registryAddress).instance;
return Promise
.all([
registry.getAddress.call({}, [api.util.sha3('githubhint'), 'A']),
api.parity.accountsInfo()
]);
return api
.newContract(abis.registry, registryAddress).instance
.getAddress.call({}, [api.util.sha3('githubhint'), 'A']);
})
.then(([address, accountsInfo]) => {
.then((address) => {
console.log(`githubhint was found at ${address}`);
const contract = api.newContract(abis.githubhint, address);
const accounts = Object
.keys(accountsInfo)
.reduce((obj, address) => {
const account = accountsInfo[address];
return Object.assign(obj, {
[address]: {
address,
name: account.name
}
});
}, {});
const fromAddress = Object.keys(accounts)[0];
return {
accounts,
address,
accountsInfo,
contract,
instance: contract.instance,
fromAddress
instance: contract.instance
};
})
.catch((error) => {
console.error('attachInterface', error);
});
}
export function subscribeDefaultAddress (callback) {
return api
.subscribe('parity_defaultAccount', callback)
.then((subscriptionId) => {
defaultSubscriptionId = subscriptionId;
return defaultSubscriptionId;
});
}
export function unsubscribeDefaultAddress () {
return api.unsubscribe(defaultSubscriptionId);
}

View File

@ -22,7 +22,7 @@ import { api } from '../parity';
import styles from './transaction.css';
import IdentityIcon from '../../githubhint/IdentityIcon';
import IdentityIcon from '../IdentityIcon';
class BaseTransaction extends Component {

View File

@ -81,6 +81,7 @@ export const ownerLookup = (name) => (dispatch, getState) => {
return;
}
name = name.toLowerCase();
dispatch(ownerLookupStart(name));
return getOwner(contract, name)

View File

@ -70,7 +70,7 @@ class Lookup extends Component {
<MenuItem value='IMG' primaryText='IMG  hash of a picture in the blockchain' />
<MenuItem value='CONTENT' primaryText='CONTENT  hash of a data in the blockchain' />
<MenuItem value='reverse' primaryText='reverse find a name for an address' />
<MenuItem value='owner' primaryText='owner find a the owner' />
<MenuItem value='owner' primaryText='owner find the owner' />
</DropDownMenu>
<RaisedButton
label='Lookup'

View File

@ -1,4 +1,4 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
@ -30,7 +30,6 @@ export default class Application extends Component {
state = {
accounts: {},
address: null,
fromAddress: null,
accountsInfo: {},
blockNumber: new BigNumber(0),
contract: null,
@ -41,11 +40,9 @@ export default class Application extends Component {
}
componentDidMount () {
attachInterface()
return attachInterface()
.then((state) => {
this.setState(state, () => {
this.setState({ loading: false });
});
this.setState(Object.assign({}, state, { loading: false }));
return attachBlockNumber(state.instance, (state) => {
this.setState(state);
@ -80,22 +77,21 @@ export default class Application extends Component {
return (
<Header
blockNumber={ blockNumber }
totalSignatures={ totalSignatures } />
totalSignatures={ totalSignatures }
/>
);
}
renderImport () {
const { accounts, fromAddress, instance, showImport } = this.state;
const { instance, showImport } = this.state;
if (showImport) {
return (
<Import
accounts={ accounts }
fromAddress={ fromAddress }
instance={ instance }
visible={ showImport }
onClose={ this.toggleImport }
onSetFromAddress={ this.setFromAddress } />
/>
);
}
@ -112,7 +108,8 @@ export default class Application extends Component {
return (
<Events
accountsInfo={ accountsInfo }
contract={ contract } />
contract={ contract }
/>
);
}
@ -121,10 +118,4 @@ export default class Application extends Component {
showImport: !this.state.showImport
});
}
setFromAddress = (fromAddress) => {
this.setState({
fromAddress
});
}
}

View File

@ -19,18 +19,14 @@ import React, { Component, PropTypes } from 'react';
import { api } from '../parity';
import { callRegister, postRegister } from '../services';
import Button from '../Button';
import IdentityIcon from '../IdentityIcon';
import styles from './import.css';
export default class Import extends Component {
static propTypes = {
accounts: PropTypes.object.isRequired,
fromAddress: PropTypes.string.isRequired,
instance: PropTypes.object.isRequired,
visible: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
onSetFromAddress: PropTypes.func.isRequired
onClose: PropTypes.func.isRequired
}
state = {
@ -83,21 +79,12 @@ export default class Import extends Component {
}
renderRegister () {
const { accounts, fromAddress } = this.props;
const account = accounts[fromAddress];
const count = this.countFunctions();
let buttons = null;
if (count) {
buttons = (
<div className={ styles.buttonrow }>
<div className={ styles.addressSelect }>
<Button invert onClick={ this.onSelectFromAddress }>
<IdentityIcon address={ account.address } />
<div>{ account.name || account.address }</div>
</Button>
</div>
<Button onClick={ this.onRegister }>
register functions
</Button>
@ -197,15 +184,15 @@ export default class Import extends Component {
}
onRegister = () => {
const { instance, fromAddress, onClose } = this.props;
const { instance, onClose } = this.props;
const { functions, fnstate } = this.state;
Promise
return Promise
.all(
functions
.filter((fn) => !fn.constant)
.filter((fn) => fnstate[fn.signature] === 'fntodo')
.map((fn) => postRegister(instance, fn.id, { from: fromAddress }))
.map((fn) => postRegister(instance, fn.id, {}))
)
.then(() => {
onClose();
@ -214,23 +201,4 @@ export default class Import extends Component {
console.error('onRegister', error);
});
}
onSelectFromAddress = () => {
const { accounts, fromAddress, onSetFromAddress } = this.props;
const addresses = Object.keys(accounts);
let index = 0;
addresses.forEach((address, _index) => {
if (address === fromAddress) {
index = _index;
}
});
index++;
if (index >= addresses.length) {
index = 0;
}
onSetFromAddress(addresses[index]);
}
}

View File

@ -166,8 +166,13 @@ export function callRegister (instance, id, options = {}) {
}
export function postRegister (instance, id, options = {}) {
return instance.register
.estimateGas(options, [id])
return api.parity
.defaultAccount()
.then((defaultAddress) => {
options.from = defaultAddress;
return instance.register.estimateGas(options, [id]);
})
.then((gas) => {
options.gas = gas.mul(1.2).toFixed(0);
console.log('postRegister', `gas estimated at ${gas.toFormat(0)}, setting to ${gas.mul(1.2).toFormat(0)}`);

View File

@ -16,6 +16,9 @@
import { Address, Data, Hash, Quantity } from '../types';
// DUMMY for beta
const SECTION_ACCOUNTS = null;
export default {
acceptNonReservedPeers: {
desc: '?',
@ -135,6 +138,17 @@ export default {
}
},
defaultAccount: {
section: SECTION_ACCOUNTS,
desc: 'Returns the defaultAccount that is to be used with transactions',
params: [],
returns: {
type: Address,
desc: 'The account address',
example: '0x63Cf90D3f0410092FC0fca41846f596223979195'
}
},
defaultExtraData: {
desc: 'Returns the default extra data',
params: [],

View File

@ -15,15 +15,23 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.modal {
flex-direction: column;
}
.container {
overflow-y: auto;
}
.description {
margin-top: .5em !important;
}
.list {
margin-bottom: 1.5em;
.background {
background: rgba(255, 255, 255, 0.2);
margin: 0 -1.5em;
padding: 0.5em 1.5em;
padding: 0.5em 0;
}
.header {
@ -37,3 +45,26 @@
opacity: 0.75;
}
}
.selectIcon {
position: absolute;
right: 0.5em;
top: 0.5em;
}
.selected,
.unselected {
position: relative;
}
.unselected {
background: rgba(0, 0, 0, 0.4) !important;
.selectIcon {
opacity: 0.15;
}
}
.selected {
background: rgba(255, 255, 255, 0.15) !important;
}

View File

@ -1,4 +1,4 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
@ -14,14 +14,12 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { Checkbox } from 'material-ui';
import { List, ListItem } from 'material-ui/List';
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { Modal, Button } from '~/ui';
import { DoneIcon } from '~/ui/Icons';
import { DappCard, Portal, SectionList } from '~/ui';
import { CheckIcon } from '~/ui/Icons';
import styles from './addDapps.css';
@ -39,61 +37,61 @@ export default class AddDapps extends Component {
}
return (
<Modal
actions={ [
<Button
icon={ <DoneIcon /> }
key='done'
label={
<FormattedMessage
id='dapps.add.button.done'
defaultMessage='Done' />
}
onClick={ store.closeModal } />
] }
compact
<Portal
className={ styles.modal }
onClose={ store.closeModal }
open
title={
<FormattedMessage
id='dapps.add.label'
defaultMessage='visible applications' />
defaultMessage='visible applications'
/>
}
visible>
<div className={ styles.warning } />
{
this.renderList(store.sortedLocal,
<FormattedMessage
id='dapps.add.local.label'
defaultMessage='Applications locally available' />,
<FormattedMessage
id='dapps.add.local.desc'
defaultMessage='All applications installed locally on the machine by the user for access by the Parity client.' />
)
}
{
this.renderList(store.sortedBuiltin,
<FormattedMessage
id='dapps.add.builtin.label'
defaultMessage='Applications bundled with Parity' />,
<FormattedMessage
id='dapps.add.builtin.desc'
defaultMessage='Experimental applications developed by the Parity team to show off dapp capabilities, integration, experimental features and to control certain network-wide client behaviour.' />
)
}
{
this.renderList(store.sortedNetwork,
<FormattedMessage
id='dapps.add.network.label'
defaultMessage='Applications on the global network' />,
<FormattedMessage
id='dapps.add.network.desc'
defaultMessage='These applications are not affiliated with Parity nor are they published by Parity. Each remain under the control of their respective authors. Please ensure that you understand the goals for each application before interacting.' />
)
}
</Modal>
>
<div className={ styles.container }>
<div className={ styles.warning } />
{
this.renderList(store.sortedLocal, store.displayApps,
<FormattedMessage
id='dapps.add.local.label'
defaultMessage='Applications locally available'
/>,
<FormattedMessage
id='dapps.add.local.desc'
defaultMessage='All applications installed locally on the machine by the user for access by the Parity client.'
/>
)
}
{
this.renderList(store.sortedBuiltin, store.displayApps,
<FormattedMessage
id='dapps.add.builtin.label'
defaultMessage='Applications bundled with Parity'
/>,
<FormattedMessage
id='dapps.add.builtin.desc'
defaultMessage='Experimental applications developed by the Parity team to show off dapp capabilities, integration, experimental features and to control certain network-wide client behaviour.'
/>
)
}
{
this.renderList(store.sortedNetwork, store.displayApps,
<FormattedMessage
id='dapps.add.network.label'
defaultMessage='Applications on the global network'
/>,
<FormattedMessage
id='dapps.add.network.desc'
defaultMessage='These applications are not affiliated with Parity nor are they published by Parity. Each remain under the control of their respective authors. Please ensure that you understand the goals for each application before interacting.'
/>
)
}
</div>
</Portal>
);
}
renderList (items, header, byline) {
renderList (items, visibleItems, header, byline) {
if (!items || !items.length) {
return null;
}
@ -104,41 +102,40 @@ export default class AddDapps extends Component {
<div className={ styles.header }>{ header }</div>
<div className={ styles.byline }>{ byline }</div>
</div>
<List>
{ items.map(this.renderApp) }
</List>
<SectionList
items={ items }
noStretch
renderItem={ this.renderApp }
/>
</div>
);
}
renderApp = (app) => {
const { store } = this.props;
const isHidden = !store.displayApps[app.id].visible;
const isVisible = store.displayApps[app.id].visible;
const onCheck = () => {
if (isHidden) {
store.showApp(app.id);
} else {
const onClick = () => {
if (isVisible) {
store.hideApp(app.id);
} else {
store.showApp(app.id);
}
};
return (
<ListItem
<DappCard
app={ app }
className={
isVisible
? styles.selected
: styles.unselected
}
key={ app.id }
leftCheckbox={
<Checkbox
checked={ !isHidden }
onCheck={ onCheck }
/>
}
primaryText={ app.name }
secondaryText={
<div className={ styles.description }>
{ app.description }
</div>
}
/>
onClick={ onClick }
>
<CheckIcon className={ styles.selectIcon } />
</DappCard>
);
}
}

View File

@ -33,13 +33,13 @@ describe('modals/AddDapps', () => {
it('does not render the modal with modalOpen = false', () => {
expect(
renderShallow({ modalOpen: false }).find('Connect(Modal)')
renderShallow({ modalOpen: false }).find('Portal')
).to.have.length(0);
});
it('does render the modal with modalOpen = true', () => {
expect(
renderShallow({ modalOpen: true }).find('Connect(Modal)')
renderShallow({ modalOpen: true }).find('Portal')
).to.have.length(1);
});
});

View File

@ -15,33 +15,54 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.container {
overflow-y: auto;
}
.item {
.info {
display: inline-block;
display: flex;
flex: 1;
position: relative;
.address {
opacity: 0.75;
}
.description {
margin-top: 0.5em;
opacity: 0.75;
}
.name {
margin: 0.5em 0;
text-transform: uppercase;
}
.overlay {
position: absolute;
right: 0.5em;
top: 0.5em;
}
}
.selected, .unselected {
margin-bottom: 0.25em;
&:focus {
outline: none;
}
}
.unselected {
background: rgba(0, 0, 0, 0.4) !important;
}
.selected {
background: rgba(255, 255, 255, 0.15);
background: rgba(255, 255, 255, 0.15) !important;
&.default {
background: rgba(255, 255, 255, 0.35) !important;
}
}
.unselected {
}
.iconDisabled {
opacity: 0.15;
}
.legend {
opacity: 0.75;
span {
line-height: 24px;
vertical-align: top;
}
}

View File

@ -1,4 +1,4 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
@ -14,14 +14,12 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { Checkbox } from 'material-ui';
import { List, ListItem } from 'material-ui/List';
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { Button, IdentityIcon, Modal } from '~/ui';
import { DoneIcon } from '~/ui/Icons';
import { AccountCard, Portal, SectionList } from '~/ui';
import { CheckIcon, StarIcon, StarOutlineIcon } from '~/ui/Icons';
import styles from './dappPermissions.css';
@ -39,74 +37,80 @@ export default class DappPermissions extends Component {
}
return (
<Modal
actions={ [
<Button
icon={ <DoneIcon /> }
key='done'
label={
<FormattedMessage
id='dapps.permissions.button.done'
defaultMessage='Done' />
}
onClick={ store.closeModal } />
] }
compact
<Portal
buttons={
<div className={ styles.legend }>
<FormattedMessage
id='dapps.permissions.description'
defaultMessage='{activeIcon} account is available to application, {defaultIcon} account is the default account'
values={ {
activeIcon: <CheckIcon />,
defaultIcon: <StarIcon />
} }
/>
</div>
}
onClose={ store.closeModal }
open
title={
<FormattedMessage
id='dapps.permissions.label'
defaultMessage='visible dapp accounts' />
defaultMessage='visible dapp accounts'
/>
}
visible>
<List>
{ this.renderListItems() }
</List>
</Modal>
>
<div className={ styles.container }>
<SectionList
items={ store.accounts }
noStretch
renderItem={ this.renderAccount }
/>
</div>
</Portal>
);
}
renderListItems () {
renderAccount = (account) => {
const { store } = this.props;
return store.accounts.map((account) => {
const onCheck = () => {
store.selectAccount(account.address);
};
const onMakeDefault = () => {
store.setDefaultAccount(account.address);
};
// TODO: Once new modal & account selection is in, this should be updated
// to conform to the new (as of this code WIP) look & feel for selection.
// For now in the current/old style, not as pretty but consistent.
return (
<ListItem
className={
const onSelect = () => {
store.selectAccount(account.address);
};
let className;
if (account.checked) {
className = account.default
? `${styles.selected} ${styles.default}`
: styles.selected;
} else {
className = styles.unselected;
}
return (
<div className={ styles.item }>
<AccountCard
account={ account }
className={ className }
onClick={ onSelect }
/>
<div className={ styles.overlay }>
{
account.checked && account.default
? <StarIcon />
: <StarOutlineIcon className={ styles.iconDisabled } onClick={ onMakeDefault } />
}
{
account.checked
? styles.selected
: styles.unselected
? <CheckIcon onClick={ onSelect } />
: <CheckIcon className={ styles.iconDisabled } onClick={ onSelect } />
}
key={ account.address }
leftCheckbox={
<Checkbox
checked={ account.checked }
onCheck={ onCheck }
/>
}
primaryText={
<div className={ styles.item }>
<IdentityIcon address={ account.address } />
<div className={ styles.info }>
<h3 className={ styles.name }>
{ account.name }
</h3>
<div className={ styles.address }>
{ account.address }
</div>
<div className={ styles.description }>
{ account.description }
</div>
</div>
</div>
} />
);
});
</div>
</div>
);
}
}

View File

@ -33,13 +33,13 @@ describe('modals/DappPermissions', () => {
it('does not render the modal with modalOpen = false', () => {
expect(
renderShallow({ modalOpen: false }).find('Connect(Modal)')
renderShallow({ modalOpen: false }).find('Portal')
).to.have.length(0);
});
it('does render the modal with modalOpen = true', () => {
expect(
renderShallow({ modalOpen: true, accounts: [] }).find('Connect(Modal)')
renderShallow({ modalOpen: true, accounts: [] }).find('Portal')
).to.have.length(1);
});
});

View File

@ -29,12 +29,17 @@ export default class Store {
@action closeModal = () => {
transaction(() => {
const accounts = this.accounts
.filter((account) => account.checked)
.map((account) => account.address);
let addresses = null;
const checkedAccounts = this.accounts.filter((account) => account.checked);
if (checkedAccounts.length) {
addresses = checkedAccounts.filter((account) => account.default)
.concat(checkedAccounts.filter((account) => !account.default))
.map((account) => account.address);
}
this.modalOpen = false;
this.updateWhitelist(accounts.length === this.accounts.length ? null : accounts);
this.updateWhitelist(addresses);
});
}
@ -42,12 +47,15 @@ export default class Store {
transaction(() => {
this.accounts = Object
.values(accounts)
.map((account) => {
.map((account, index) => {
return {
address: account.address,
checked: this.whitelist
? this.whitelist.includes(account.address)
: true,
default: this.whitelist
? this.whitelist[0] === account.address
: index === 0,
description: account.meta.description,
name: account.name
};
@ -57,9 +65,31 @@ export default class Store {
}
@action selectAccount = (address) => {
transaction(() => {
this.accounts = this.accounts.map((account) => {
if (account.address === address) {
account.checked = !account.checked;
account.default = false;
}
return account;
});
this.setDefaultAccount((
this.accounts.find((account) => account.default) ||
this.accounts.find((account) => account.checked) ||
{}
).address);
});
}
@action setDefaultAccount = (address) => {
this.accounts = this.accounts.map((account) => {
if (account.address === address) {
account.checked = !account.checked;
account.checked = true;
account.default = true;
} else if (account.default) {
account.default = false;
}
return account;

View File

@ -23,21 +23,25 @@ const ACCOUNTS = {
'456': { address: '456', name: '456', meta: { description: '456' } },
'789': { address: '789', name: '789', meta: { description: '789' } }
};
const WHITELIST = ['123', '456'];
const WHITELIST = ['456', '789'];
let api;
let store;
function create () {
api = {
parity: {
getNewDappsWhitelist: sinon.stub().resolves(WHITELIST),
setNewDappsWhitelist: sinon.stub().resolves(true)
}
};
store = new Store(api);
}
describe('modals/DappPermissions/store', () => {
let api;
let store;
beforeEach(() => {
api = {
parity: {
getNewDappsWhitelist: sinon.stub().resolves(WHITELIST),
setNewDappsWhitelist: sinon.stub().resolves(true)
}
};
store = new Store(api);
create();
});
describe('constructor', () => {
@ -51,49 +55,71 @@ describe('modals/DappPermissions/store', () => {
});
describe('@actions', () => {
describe('openModal', () => {
beforeEach(() => {
store.openModal(ACCOUNTS);
});
beforeEach(() => {
store.openModal(ACCOUNTS);
});
describe('openModal', () => {
it('sets the modalOpen status', () => {
expect(store.modalOpen).to.be.true;
});
it('sets accounts with checked interfaces', () => {
expect(store.accounts.peek()).to.deep.equal([
{ address: '123', name: '123', description: '123', checked: true },
{ address: '456', name: '456', description: '456', checked: true },
{ address: '789', name: '789', description: '789', checked: false }
{ address: '123', name: '123', description: '123', default: false, checked: false },
{ address: '456', name: '456', description: '456', default: true, checked: true },
{ address: '789', name: '789', description: '789', default: false, checked: true }
]);
});
});
describe('closeModal', () => {
beforeEach(() => {
store.openModal(ACCOUNTS);
store.selectAccount('789');
store.setDefaultAccount('789');
store.closeModal();
});
it('calls setNewDappsWhitelist', () => {
expect(api.parity.setNewDappsWhitelist).to.have.been.calledOnce;
});
it('has the default account in first position', () => {
expect(api.parity.setNewDappsWhitelist).to.have.been.calledWith(['789', '456']);
});
});
describe('selectAccount', () => {
beforeEach(() => {
store.openModal(ACCOUNTS);
store.selectAccount('123');
store.selectAccount('789');
});
it('unselects previous selected accounts', () => {
expect(store.accounts.find((account) => account.address === '123').checked).to.be.false;
expect(store.accounts.find((account) => account.address === '123').checked).to.be.true;
});
it('selects previous unselected accounts', () => {
expect(store.accounts.find((account) => account.address === '789').checked).to.be.true;
expect(store.accounts.find((account) => account.address === '789').checked).to.be.false;
});
it('sets a new default when default was unselected', () => {
store.selectAccount('456');
expect(store.accounts.find((account) => account.address === '456').default).to.be.false;
expect(store.accounts.find((account) => account.address === '123').default).to.be.true;
});
});
describe('setDefaultAccount', () => {
beforeEach(() => {
store.setDefaultAccount('789');
});
it('unselects previous default', () => {
expect(store.accounts.find((account) => account.address === '456').default).to.be.false;
});
it('selects new default', () => {
expect(store.accounts.find((account) => account.address === '789').default).to.be.true;
});
});
});

View File

@ -1,4 +1,4 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
@ -15,42 +15,22 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { Input, GasPriceEditor } from '~/ui';
import { GasPriceEditor } from '~/ui';
import styles from '../executeContract.css';
export default class AdvancedStep extends Component {
static propTypes = {
gasStore: PropTypes.object.isRequired,
minBlock: PropTypes.string,
minBlockError: PropTypes.string,
onMinBlockChange: PropTypes.func
gasStore: PropTypes.object.isRequired
};
render () {
const { gasStore, minBlock, minBlockError, onMinBlockChange } = this.props;
const { gasStore } = this.props;
return (
<div>
<Input
error={ minBlockError }
hint={
<FormattedMessage
id='executeContract.advanced.minBlock.hint'
defaultMessage='Only post the transaction after this block' />
}
label={
<FormattedMessage
id='executeContract.advanced.minBlock.label'
defaultMessage='BlockNumber to send from' />
}
value={ minBlock }
onSubmit={ onMinBlockChange } />
<div className={ styles.gaseditor }>
<GasPriceEditor store={ gasStore } />
</div>
<div className={ styles.gaseditor }>
<GasPriceEditor store={ gasStore } />
</div>
);
}

View File

@ -1,4 +1,4 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
@ -14,7 +14,6 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import BigNumber from 'bignumber.js';
import { pick } from 'lodash';
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
@ -41,27 +40,32 @@ const TITLES = {
transfer: (
<FormattedMessage
id='executeContract.steps.transfer'
defaultMessage='function details' />
defaultMessage='function details'
/>
),
sending: (
<FormattedMessage
id='executeContract.steps.sending'
defaultMessage='sending' />
defaultMessage='sending'
/>
),
complete: (
<FormattedMessage
id='executeContract.steps.complete'
defaultMessage='complete' />
defaultMessage='complete'
/>
),
advanced: (
<FormattedMessage
id='executeContract.steps.advanced'
defaultMessage='advanced options' />
defaultMessage='advanced options'
/>
),
rejected: (
<FormattedMessage
id='executeContract.steps.rejected'
defaultMessage='rejected' />
defaultMessage='rejected'
/>
)
};
const STAGES_BASIC = [TITLES.transfer, TITLES.sending, TITLES.complete];
@ -95,8 +99,6 @@ class ExecuteContract extends Component {
fromAddressError: null,
func: null,
funcError: null,
minBlock: '0',
minBlockError: null,
rejected: false,
sending: false,
step: STEP_DETAILS,
@ -139,7 +141,8 @@ class ExecuteContract extends Component {
advancedOptions
? [STEP_BUSY]
: [STEP_BUSY_OR_ADVANCED]
}>
}
>
{ this.renderExceptionWarning() }
{ this.renderStep() }
</Modal>
@ -161,8 +164,8 @@ class ExecuteContract extends Component {
renderDialogActions () {
const { onClose, fromAddress } = this.props;
const { advancedOptions, sending, step, fromAddressError, minBlockError, valuesError } = this.state;
const hasError = fromAddressError || minBlockError || valuesError.find((error) => error);
const { advancedOptions, sending, step, fromAddressError, valuesError } = this.state;
const hasError = fromAddressError || valuesError.find((error) => error);
const cancelBtn = (
<Button
@ -170,10 +173,12 @@ class ExecuteContract extends Component {
label={
<FormattedMessage
id='executeContract.button.cancel'
defaultMessage='cancel' />
defaultMessage='cancel'
/>
}
icon={ <CancelIcon /> }
onClick={ onClose } />
onClick={ onClose }
/>
);
const postBtn = (
<Button
@ -181,11 +186,13 @@ class ExecuteContract extends Component {
label={
<FormattedMessage
id='executeContract.button.post'
defaultMessage='post transaction' />
defaultMessage='post transaction'
/>
}
disabled={ !!(sending || hasError) }
icon={ <IdentityIcon address={ fromAddress } button /> }
onClick={ this.postTransaction } />
onClick={ this.postTransaction }
/>
);
const nextBtn = (
<Button
@ -193,10 +200,12 @@ class ExecuteContract extends Component {
label={
<FormattedMessage
id='executeContract.button.next'
defaultMessage='next' />
defaultMessage='next'
/>
}
icon={ <NextIcon /> }
onClick={ this.onNextClick } />
onClick={ this.onNextClick }
/>
);
const prevBtn = (
<Button
@ -204,10 +213,12 @@ class ExecuteContract extends Component {
label={
<FormattedMessage
id='executeContract.button.prev'
defaultMessage='prev' />
defaultMessage='prev'
/>
}
icon={ <PrevIcon /> }
onClick={ this.onPrevClick } />
onClick={ this.onPrevClick }
/>
);
if (step === STEP_DETAILS) {
@ -233,16 +244,18 @@ class ExecuteContract extends Component {
label={
<FormattedMessage
id='executeContract.button.done'
defaultMessage='done' />
defaultMessage='done'
/>
}
icon={ <DoneIcon /> }
onClick={ onClose } />
onClick={ onClose }
/>
];
}
renderStep () {
const { onFromAddressChange } = this.props;
const { advancedOptions, step, busyState, minBlock, minBlockError, txhash, rejected } = this.state;
const { advancedOptions, step, busyState, txhash, rejected } = this.state;
if (rejected) {
return (
@ -250,13 +263,16 @@ class ExecuteContract extends Component {
title={
<FormattedMessage
id='executeContract.rejected.title'
defaultMessage='The execution has been rejected' />
defaultMessage='The execution has been rejected'
/>
}
state={
<FormattedMessage
id='executeContract.rejected.state'
defaultMessage='You can safely close this window, the function execution will not occur.' />
} />
defaultMessage='You can safely close this window, the function execution will not occur.'
/>
}
/>
);
}
@ -269,7 +285,8 @@ class ExecuteContract extends Component {
onFromAddressChange={ onFromAddressChange }
onFuncChange={ this.onFuncChange }
onAdvancedClick={ this.onAdvancedClick }
onValueChange={ this.onValueChange } />
onValueChange={ this.onValueChange }
/>
);
} else if (step === (advancedOptions ? STEP_BUSY : STEP_BUSY_OR_ADVANCED)) {
return (
@ -277,17 +294,15 @@ class ExecuteContract extends Component {
title={
<FormattedMessage
id='executeContract.busy.title'
defaultMessage='The function execution is in progress' />
defaultMessage='The function execution is in progress'
/>
}
state={ busyState } />
state={ busyState }
/>
);
} else if (advancedOptions && (step === STEP_BUSY_OR_ADVANCED)) {
return (
<AdvancedStep
gasStore={ this.gasStore }
minBlock={ minBlock }
minBlockError={ minBlockError }
onMinBlockChange={ this.onMinBlockChange } />
<AdvancedStep gasStore={ this.gasStore } />
);
}
@ -306,6 +321,7 @@ class ExecuteContract extends Component {
onFuncChange = (event, func) => {
const values = (func.abi.inputs || []).map((input) => {
const parsedType = parseAbiType(input.type);
return parsedType.default;
});
@ -315,15 +331,6 @@ class ExecuteContract extends Component {
}, this.estimateGas);
}
onMinBlockChange = (minBlock) => {
const minBlockError = validateUint(minBlock).valueError;
this.setState({
minBlock,
minBlockError
});
}
onValueChange = (event, index, _value) => {
const { func, values, valuesError } = this.state;
const input = func.inputs.find((input, _index) => index === _index);
@ -385,17 +392,14 @@ class ExecuteContract extends Component {
postTransaction = () => {
const { api, store } = this.context;
const { fromAddress } = this.props;
const { advancedOptions, amount, func, minBlock, values } = this.state;
const { advancedOptions, amount, func, values } = this.state;
const steps = advancedOptions ? STAGES_ADVANCED : STAGES_BASIC;
const finalstep = steps.length - 1;
const options = {
gas: this.gasStore.gas,
gasPrice: this.gasStore.price,
const options = this.gasStore.overrideTransaction({
from: fromAddress,
minBlock: new BigNumber(minBlock || 0).gt(0) ? minBlock : null,
value: api.util.toWei(amount || 0)
};
});
this.setState({ sending: true, step: advancedOptions ? STEP_BUSY : STEP_BUSY_OR_ADVANCED });
@ -406,7 +410,8 @@ class ExecuteContract extends Component {
busyState: (
<FormattedMessage
id='executeContract.busy.waitAuth'
defaultMessage='Waiting for authorization in the Parity Signer' />
defaultMessage='Waiting for authorization in the Parity Signer'
/>
)
});
@ -429,7 +434,8 @@ class ExecuteContract extends Component {
busyState: (
<FormattedMessage
id='executeContract.busy.posted'
defaultMessage='Your transaction has been posted to the network' />
defaultMessage='Your transaction has been posted to the network'
/>
)
});
})

View File

@ -18,6 +18,8 @@ import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { CopyToClipboard, QrCode } from '~/ui';
import Value from '../Value';
import styles from '../shapeshift.css';
@ -59,9 +61,7 @@ export default class AwaitingDepositStep extends Component {
typeSymbol
} } />
</div>
<div className={ styles.hero }>
{ depositAddress }
</div>
{ this.renderAddress(depositAddress, coinSymbol) }
<div className={ styles.price }>
<div>
<FormattedMessage
@ -76,4 +76,42 @@ export default class AwaitingDepositStep extends Component {
</div>
);
}
renderAddress (depositAddress, coinSymbol) {
const qrcode = (
<QrCode
className={ styles.qrcode }
value={ depositAddress }
/>
);
let protocolLink = null;
// TODO: Expand for other coins where protocols are available
switch (coinSymbol) {
case 'BTC':
protocolLink = `bitcoin:${depositAddress}`;
break;
}
return (
<div className={ styles.addressInfo }>
{
protocolLink
? (
<a
href={ protocolLink }
target='_blank'
>
{ qrcode }
</a>
)
: qrcode
}
<div className={ styles.address }>
<CopyToClipboard data={ depositAddress } />
<span>{ depositAddress }</span>
</div>
</div>
);
}
}

View File

@ -19,7 +19,10 @@ import React from 'react';
import AwaitingDepositStep from './';
const TEST_ADDRESS = '0x123456789123456789123456789123456789';
let component;
let instance;
function render () {
component = shallow(
@ -29,6 +32,7 @@ function render () {
price: { rate: 0.001, minimum: 0, limit: 1.999 }
} } />
);
instance = component.instance();
return component;
}
@ -47,4 +51,61 @@ describe('modals/Shapeshift/AwaitingDepositStep', () => {
render({ depositAddress: 'xyz' });
expect(component.find('FormattedMessage').first().props().id).to.match(/awaitingDeposit/);
});
describe('instance methods', () => {
describe('renderAddress', () => {
let address;
beforeEach(() => {
address = shallow(instance.renderAddress(TEST_ADDRESS));
});
it('renders the address', () => {
expect(address.text()).to.contain(TEST_ADDRESS);
});
describe('CopyToClipboard', () => {
let copy;
beforeEach(() => {
copy = address.find('Connect(CopyToClipboard)');
});
it('renders the copy', () => {
expect(copy.length).to.equal(1);
});
it('passes the address', () => {
expect(copy.props().data).to.equal(TEST_ADDRESS);
});
});
describe('QrCode', () => {
let qr;
beforeEach(() => {
qr = address.find('QrCode');
});
it('renders the QrCode', () => {
expect(qr.length).to.equal(1);
});
it('passed the address', () => {
expect(qr.props().value).to.equal(TEST_ADDRESS);
});
describe('protocol link', () => {
it('does not render a protocol link (unlinked type)', () => {
expect(address.find('a')).to.have.length(0);
});
it('renders protocol link for BTC', () => {
address = shallow(instance.renderAddress(TEST_ADDRESS, 'BTC'));
expect(address.find('a').props().href).to.equal(`bitcoin:${TEST_ADDRESS}`);
});
});
});
});
});
});

View File

@ -14,9 +14,28 @@
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.body {
}
.addressInfo {
text-align: center;
.address {
background: rgba(255, 255, 255, 0.1);
margin: 0.75em 0;
padding: 1em;
span {
margin-left: 0.75em;
}
}
.qrcode {
margin: 0.75em 0;
}
}
.shapeshift {
position: absolute;
bottom: 0.5em;

View File

@ -1,4 +1,4 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
@ -27,37 +27,22 @@ export default class Extras extends Component {
dataError: PropTypes.string,
gasStore: PropTypes.object.isRequired,
isEth: PropTypes.bool,
minBlock: PropTypes.string,
minBlockError: PropTypes.string,
onChange: PropTypes.func.isRequired,
total: PropTypes.string,
totalError: PropTypes.string
}
render () {
const { gasStore, minBlock, minBlockError, onChange } = this.props;
const { gasStore, onChange } = this.props;
return (
<Form>
{ this.renderData() }
<Input
error={ minBlockError }
hint={
<FormattedMessage
id='transferModal.minBlock.hint'
defaultMessage='Only post the transaction after this block' />
}
label={
<FormattedMessage
id='transferModal.minBlock.label'
defaultMessage='BlockNumber to send from' />
}
value={ minBlock }
onChange={ this.onEditMinBlock } />
<div className={ styles.gaseditor }>
<GasPriceEditor
store={ gasStore }
onChange={ onChange } />
onChange={ onChange }
/>
</div>
</Form>
);
@ -76,23 +61,22 @@ export default class Extras extends Component {
hint={
<FormattedMessage
id='transfer.advanced.data.hint'
defaultMessage='the data to pass through with the transaction' />
defaultMessage='the data to pass through with the transaction'
/>
}
label={
<FormattedMessage
id='transfer.advanced.data.label'
defaultMessage='transaction data' />
defaultMessage='transaction data'
/>
}
onChange={ this.onEditData }
value={ data } />
value={ data }
/>
);
}
onEditData = (event) => {
this.props.onChange('data', event.target.value);
}
onEditMinBlock = (event) => {
this.props.onChange('minBlock', event.target.value);
}
}

View File

@ -1,4 +1,4 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
@ -52,9 +52,6 @@ export default class TransferStore {
@observable data = '';
@observable dataError = null;
@observable minBlock = '0';
@observable minBlockError = null;
@observable recipient = '';
@observable recipientError = ERRORS.requireRecipient;
@ -78,6 +75,30 @@ export default class TransferStore {
gasStore = null;
constructor (api, props) {
this.api = api;
const { account, balance, gasLimit, senders, newError, sendersBalances } = props;
this.account = account;
this.balance = balance;
this.isWallet = account && account.wallet;
this.newError = newError;
this.gasStore = new GasPriceStore(api, { gasLimit });
if (this.isWallet) {
this.wallet = props.wallet;
this.walletContract = new Contract(this.api, walletAbi);
}
if (senders) {
this.senders = senders;
this.sendersBalances = sendersBalances;
this.senderError = ERRORS.requireSender;
}
}
@computed get steps () {
const steps = [].concat(this.extras ? STAGES_EXTRA : STAGES_BASIC);
@ -90,7 +111,7 @@ export default class TransferStore {
@computed get isValid () {
const detailsValid = !this.recipientError && !this.valueError && !this.totalError && !this.senderError;
const extrasValid = !this.gasStore.errorGas && !this.gasStore.errorPrice && !this.minBlockError && !this.totalError;
const extrasValid = !this.gasStore.errorGas && !this.gasStore.errorPrice && !this.gasStore.conditionBlockError && !this.totalError;
const verifyValid = !this.passwordError;
switch (this.stage) {
@ -111,29 +132,6 @@ export default class TransferStore {
return this.balance.tokens.find((balance) => balance.token.tag === this.tag).token;
}
constructor (api, props) {
this.api = api;
const { account, balance, gasLimit, senders, newError, sendersBalances } = props;
this.account = account;
this.balance = balance;
this.isWallet = account && account.wallet;
this.newError = newError;
this.gasStore = new GasPriceStore(api, { gasLimit });
if (this.isWallet) {
this.wallet = props.wallet;
this.walletContract = new Contract(this.api, walletAbi);
}
if (senders) {
this.senders = senders;
this.sendersBalances = sendersBalances;
this.senderError = ERRORS.requireSender;
}
}
@action onNext = () => {
this.stage += 1;
}
@ -163,9 +161,6 @@ export default class TransferStore {
case 'gasPrice':
return this._onUpdateGasPrice(value);
case 'minBlock':
return this._onUpdateMinBlock(value);
case 'recipient':
return this._onUpdateRecipient(value);
@ -283,14 +278,6 @@ export default class TransferStore {
this.recalculate();
}
@action _onUpdateMinBlock = (minBlock) => {
console.log('minBlock', minBlock);
transaction(() => {
this.minBlock = minBlock;
this.minBlockError = this._validatePositiveNumber(minBlock);
});
}
@action _onUpdateGasPrice = (gasPrice) => {
this.recalculate();
}
@ -588,7 +575,7 @@ export default class TransferStore {
send () {
const { options, values } = this._getTransferParams();
options.minBlock = new BigNumber(this.minBlock || 0).gt(0) ? this.minBlock : null;
log.debug('@send', 'transfer value', options.value && options.value.toFormat());
return this._getTransferMethod().postTransaction(options, values);
@ -596,6 +583,7 @@ export default class TransferStore {
_estimateGas (forceToken = false) {
const { options, values } = this._getTransferParams(true, forceToken);
return this._getTransferMethod(true, forceToken).estimateGas(options, values);
}
@ -636,15 +624,12 @@ export default class TransferStore {
const to = (isEth && !isWallet) ? this.recipient
: (this.isWallet ? this.wallet.address : this.token.address);
const options = {
const options = this.gasStore.overrideTransaction({
from: this.sender || this.account.address,
to
};
});
if (!gas) {
options.gas = this.gasStore.gas;
options.gasPrice = this.gasStore.price;
} else {
if (gas) {
options.gas = MAX_GAS_ESTIMATION;
}
@ -681,6 +666,7 @@ export default class TransferStore {
_validatePositiveNumber (num) {
try {
const v = new BigNumber(num);
if (v.lt(0)) {
return ERRORS.invalidAmount;
}

View File

@ -206,7 +206,7 @@ class Transfer extends Component {
return null;
}
const { isEth, data, dataError, minBlock, minBlockError, total, totalError } = this.store;
const { isEth, data, dataError, total, totalError } = this.store;
return (
<Extras
@ -214,8 +214,6 @@ class Transfer extends Component {
dataError={ dataError }
gasStore={ this.store.gasStore }
isEth={ isEth }
minBlock={ minBlock }
minBlockError={ minBlockError }
onChange={ this.store.onUpdateDetails }
total={ total }
totalError={ totalError } />

View File

@ -16,10 +16,10 @@
import AddAddress from './AddAddress';
import AddContract from './AddContract';
import AddDapps from './AddDapps';
import CreateAccount from './CreateAccount';
import CreateWallet from './CreateWallet';
import DappPermissions from './DappPermissions';
import DappsVisible from './AddDapps';
import DeleteAccount from './DeleteAccount';
import DeployContract from './DeployContract';
import EditMeta from './EditMeta';
@ -37,10 +37,10 @@ import WalletSettings from './WalletSettings';
export {
AddAddress,
AddContract,
AddDapps,
CreateAccount,
CreateWallet,
DappPermissions,
DappsVisible,
DeleteAccount,
DeployContract,
EditMeta,

View File

@ -0,0 +1,17 @@
// 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/>.
export default from './playground';

View File

@ -0,0 +1,90 @@
/* 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/>.
*/
$codeBackground: #002b36;
$codeColor: #93a1a1;
.container {
background-color: rgba(0, 0, 0, 0.5);
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
padding: 1em;
display: flex;
flex-direction: column;
.examples {
flex: 1;
overflow: auto;
}
}
.title {
font-size: 2.25em;
margin-bottom: 1em;
.select {
font-size: 0.85em;
font-family: monospace;
display: inline-block;
height: 1.5em;
border: 1px solid #aaa;
padding: 0 0.5em;
color: #555;
appearance: none;
}
}
.exampleContainer {
background-color: rgba(0, 0, 0, 0.5);
padding: 1em;
margin-bottom: 1em;
&:last-child {
margin-bottom: 0;
}
p {
font-size: 1.25em;
margin-top: 0;
}
}
.example {
display: flex;
flex-direction: row;
.code {
flex: 1;
overflow: auto;
padding: 0.5em;
background-color: #$codeBackground;
color: $codeColor;
font-size: 0.75em;
code {
white-space: pre;
}
}
.component {
flex: 3;
padding-left: 0.5em;
}
}

View File

@ -0,0 +1,90 @@
// 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 { observer } from 'mobx-react';
import React, { Component } from 'react';
import CurrencySymbol from '~/ui/CurrencySymbol/currencySymbol.example';
import QrCode from '~/ui/QrCode/qrCode.example';
import SectionList from '~/ui/SectionList/sectionList.example';
import Portal from '~/ui/Portal/portal.example';
import PlaygroundStore from './store';
import styles from './playground.css';
PlaygroundStore.register(<CurrencySymbol />);
PlaygroundStore.register(<QrCode />);
PlaygroundStore.register(<SectionList />);
PlaygroundStore.register(<Portal />);
@observer
export default class Playground extends Component {
state = {
selectedIndex: 0
};
store = PlaygroundStore.get();
render () {
return (
<div className={ styles.container }>
<div className={ styles.title }>
<span>Playground > </span>
<select
className={ styles.select }
onChange={ this.handleChange }
>
{ this.renderOptions() }
</select>
</div>
<div className={ styles.examples }>
{ this.renderComponent() }
</div>
</div>
);
}
renderOptions () {
const { components } = this.store;
return components.map((element, index) => {
const name = element.type.displayName || element.type.name;
return (
<option
key={ `${name}_${index}` }
value={ index }
>
{ name }
</option>
);
});
}
renderComponent () {
const { components } = this.store;
const { selectedIndex } = this.state;
return components[selectedIndex];
}
handleChange = (event) => {
const { value } = event.target;
this.setState({ selectedIndex: value });
}
}

View File

@ -0,0 +1,47 @@
// 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 Playground from './playground';
let component;
let options;
function render (props = {}) {
component = shallow(
<Playground />
);
options = component.find('option');
return component;
}
describe('playground', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
it('renders multiple options', () => {
expect(options.length).to.be.greaterThan(2);
});
});

View File

@ -0,0 +1,55 @@
// 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 React, { Component, PropTypes } from 'react';
import reactElementToJSXString from 'react-element-to-jsx-string';
import styles from './playground.css';
export default class PlaygroundExample extends Component {
static propTypes = {
children: PropTypes.node,
name: PropTypes.string
};
render () {
const { children, name } = this.props;
return (
<div className={ styles.exampleContainer }>
{ this.renderName(name) }
<div className={ styles.example }>
<div className={ styles.code }>
<code>{ reactElementToJSXString(children) }</code>
</div>
<div className={ styles.component }>
{ children }
</div>
</div>
</div>
);
}
renderName (name) {
if (!name) {
return null;
}
return (
<p>{ name }</p>
);
}
}

View File

@ -0,0 +1,51 @@
// 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 { action, observable } from 'mobx';
let instance = null;
export default class PlaygroundStore {
@observable components = [];
static get () {
if (!instance) {
instance = new PlaygroundStore();
}
return instance;
}
static register (component) {
PlaygroundStore.get().add(component);
}
@action
add (component) {
const name = component.type.displayName || component.type.name;
const hasComponent = this.components.find((c) => {
const cName = c.type.displayName || c.type.name;
return name && cName && cName === name;
});
if (hasComponent) {
return;
}
this.components.push(component);
}
}

View File

@ -0,0 +1,41 @@
// 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 React from 'react';
import QrCode from '~/ui/QrCode/qrCode.example';
import PlaygroundStore from './store';
describe('playground/store', () => {
let store = PlaygroundStore.get();
it('is available', () => {
expect(PlaygroundStore.get()).to.be.ok;
});
it('adds new Components', () => {
PlaygroundStore.register(<QrCode />);
expect(store.components.length).greaterThan(0);
});
it('adds new Components only once', () => {
PlaygroundStore.register(<QrCode />);
PlaygroundStore.register(<QrCode />);
expect(store.components.filter((c) => /QrCode/i.test(c.type.name)).length).equal(1);
});
});

View File

@ -52,7 +52,7 @@ export default class SignerMiddleware {
}
onConfirmStart = (store, action) => {
const { gas, gasPrice, id, password, payload, wallet } = action.payload;
const { condition, gas = 0, gasPrice = 0, id, password, payload, wallet } = action.payload;
const handlePromise = (promise) => {
promise
@ -119,7 +119,7 @@ export default class SignerMiddleware {
});
}
handlePromise(this._api.signer.confirmRequest(id, { gas, gasPrice }, password));
handlePromise(this._api.signer.confirmRequest(id, { gas, gasPrice, condition }, password));
}
onRejectStart = (store, action) => {

View File

@ -78,45 +78,57 @@ const routes = [
{ path: '/', onEnter: redirectTo('/accounts') },
{ path: '/auth', onEnter: redirectTo('/accounts') },
{ path: '/settings', onEnter: redirectTo('/settings/views') },
{
path: '/',
component: Application,
childRoutes: [
{
path: 'accounts',
indexRoute: { component: Accounts },
childRoutes: accountsRoutes
},
{
path: 'addresses',
indexRoute: { component: Addresses },
childRoutes: addressesRoutes
},
{
path: 'contracts',
indexRoute: { component: Contracts },
childRoutes: contractsRoutes
},
{
path: 'status',
indexRoute: { component: Status },
childRoutes: statusRoutes
},
{
path: 'settings',
component: Settings,
childRoutes: settingsRoutes
},
{ path: 'apps', component: Dapps },
{ path: 'app/:id', component: Dapp },
{ path: 'web', component: Web },
{ path: 'web/:url', component: Web },
{ path: 'signer', component: Signer }
]
}
{ path: '/settings', onEnter: redirectTo('/settings/views') }
];
const appRoutes = [
{
path: 'accounts',
indexRoute: { component: Accounts },
childRoutes: accountsRoutes
},
{
path: 'addresses',
indexRoute: { component: Addresses },
childRoutes: addressesRoutes
},
{
path: 'contracts',
indexRoute: { component: Contracts },
childRoutes: contractsRoutes
},
{
path: 'status',
indexRoute: { component: Status },
childRoutes: statusRoutes
},
{
path: 'settings',
component: Settings,
childRoutes: settingsRoutes
},
{ path: 'apps', component: Dapps },
{ path: 'app/:id', component: Dapp },
{ path: 'web', component: Web },
{ path: 'web/:url', component: Web },
{ path: 'signer', component: Signer }
];
// TODO : use ES6 imports when supported
if (process.env.NODE_ENV !== 'production') {
const Playground = require('./playground').default;
appRoutes.push({
path: 'playground',
component: Playground
});
}
routes.push({
path: '/',
component: Application,
childRoutes: appRoutes
});
export default routes;

View File

@ -20,8 +20,8 @@
margin: 0.5em 0;
display: flex;
flex-direction: row;
align-items: center;
flex-direction: column;
align-items: flex-start;
background-color: rgba(0, 0, 0, 0.8);
@ -53,6 +53,13 @@
}
}
.infoContainer {
display: flex;
flex-direction: row;
margin-bottom: 0.5em;
width: 100%;
}
.description {
font-size: 0.75em;
color: rgba(255, 255, 255, 0.5);
@ -86,14 +93,10 @@
.accountName {
font-weight: 700 !important;
}
}
.balance {
.tag {
margin-left: 0.5em;
font-size: 0.85em;
}
margin-top: 0;
}
@keyframes copied {

View File

@ -1,4 +1,4 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
@ -18,21 +18,20 @@ import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import keycode from 'keycode';
import Balance from '~/ui/Balance';
import IdentityIcon from '~/ui/IdentityIcon';
import IdentityName from '~/ui/IdentityName';
import Tags from '~/ui/Tags';
import { fromWei } from '~/api/util/wei';
import styles from './accountCard.css';
export default class AccountCard extends Component {
static propTypes = {
account: PropTypes.object.isRequired,
onClick: PropTypes.func.isRequired,
onFocus: PropTypes.func.isRequired,
balance: PropTypes.object
balance: PropTypes.object,
className: PropTypes.string,
onClick: PropTypes.func,
onFocus: PropTypes.func
};
state = {
@ -40,15 +39,11 @@ export default class AccountCard extends Component {
};
render () {
const { account } = this.props;
const { account, balance, className } = this.props;
const { copied } = this.state;
const { address, name, description, meta = {} } = account;
const displayName = (name && name.toUpperCase()) || address;
const { address, description, meta = {}, name } = account;
const { tags = [] } = meta;
const classes = [ styles.account ];
const classes = [ styles.account, className ];
if (copied) {
classes.push(styles.copied);
@ -63,17 +58,28 @@ export default class AccountCard extends Component {
onFocus={ this.onFocus }
onKeyDown={ this.handleKeyDown }
>
<IdentityIcon address={ address } />
<div className={ styles.accountInfo }>
<div className={ styles.accountName }>
<span>{ displayName }</span>
<div className={ styles.infoContainer }>
<IdentityIcon address={ address } />
<div className={ styles.accountInfo }>
<div className={ styles.accountName }>
<IdentityName
address={ address }
name={ name }
unknown
/>
</div>
{ this.renderDescription(description) }
{ this.renderAddress(address) }
</div>
{ this.renderTags(tags, address) }
{ this.renderDescription(description) }
{ this.renderAddress(displayName, address) }
{ this.renderBalance(address) }
</div>
<Tags tags={ tags } />
<Balance
balance={ balance }
className={ styles.balance }
showOnlyEth
showZeroValues
/>
</div>
);
}
@ -90,11 +96,7 @@ export default class AccountCard extends Component {
);
}
renderAddress (name, address) {
if (name === address) {
return null;
}
renderAddress (address) {
return (
<div className={ styles.addressContainer }>
<span
@ -109,40 +111,6 @@ export default class AccountCard extends Component {
);
}
renderTags (tags = [], address) {
if (tags.length === 0) {
return null;
}
return (
<Tags tags={ tags } />
);
}
renderBalance (address) {
const { balance = {} } = this.props;
if (!balance.tokens) {
return null;
}
const ethToken = balance.tokens
.find((tok) => tok.token && (tok.token.tag || '').toLowerCase() === 'eth');
if (!ethToken) {
return null;
}
const value = fromWei(ethToken.value).toFormat(3);
return (
<div className={ styles.balance }>
<span>{ value }</span>
<span className={ styles.tag }>ETH</span>
</div>
);
}
handleKeyDown = (event) => {
const codeName = keycode(event);
@ -158,6 +126,7 @@ export default class AccountCard extends Component {
// @see https://developers.google.com/web/updates/2015/04/cut-and-copy-commands
try {
const range = document.createRange();
range.selectNode(element);
window.getSelection().addRange(range);
document.execCommand('copy');
@ -184,12 +153,14 @@ export default class AccountCard extends Component {
onClick = () => {
const { account, onClick } = this.props;
onClick(account.address);
onClick && onClick(account.address);
}
onFocus = () => {
const { account, onFocus } = this.props;
onFocus(account.index);
onFocus && onFocus(account.index);
}
preventEvent = (e) => {

View File

@ -0,0 +1,133 @@
// 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 AccountCard from './';
const TEST_ADDRESS = '0x1234567890123456789012345678901234567890';
const TEST_NAME = 'Jimmy';
let component;
let onClick;
let onFocus;
function render (props = {}) {
if (!props.account) {
props.account = {
address: TEST_ADDRESS,
description: 'testDescription',
name: TEST_NAME,
meta: {}
};
}
onClick = sinon.stub();
onFocus = sinon.stub();
component = shallow(
<AccountCard
{ ...props }
onClick={ onClick }
onFocus={ onFocus }
/>
);
return component;
}
describe('ui/AccountCard', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
describe('components', () => {
describe('Balance', () => {
let balance;
beforeEach(() => {
balance = component.find('Connect(Balance)');
});
it('renders the balance', () => {
expect(balance.length).to.equal(1);
});
it('sets showOnlyEth & showZeroValues', () => {
expect(balance.props().showOnlyEth).to.be.true;
expect(balance.props().showZeroValues).to.be.true;
});
});
describe('IdentityIcon', () => {
let icon;
beforeEach(() => {
icon = component.find('Connect(IdentityIcon)');
});
it('renders the icon', () => {
expect(icon.length).to.equal(1);
});
it('passes the address through', () => {
expect(icon.props().address).to.equal(TEST_ADDRESS);
});
});
describe('IdentityName', () => {
let name;
beforeEach(() => {
name = component.find('Connect(IdentityName)');
});
it('renders the name', () => {
expect(name.length).to.equal(1);
});
it('passes the address through', () => {
expect(name.props().address).to.equal(TEST_ADDRESS);
});
it('passes the name through', () => {
expect(name.props().name).to.equal(TEST_NAME);
});
it('renders unknown (no name)', () => {
expect(name.props().unknown).to.be.true;
});
});
describe('Tags', () => {
let tags;
beforeEach(() => {
tags = component.find('Tags');
});
it('renders the tags', () => {
expect(tags.length).to.equal(1);
});
});
});
});

View File

@ -16,31 +16,46 @@
import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import unknownImage from '../../../assets/images/contracts/unknown-64x64.png';
import unknownImage from '~/../assets/images/contracts/unknown-64x64.png';
import styles from './balance.css';
class Balance extends Component {
static contextTypes = {
api: PropTypes.object
}
};
static propTypes = {
balance: PropTypes.object,
images: PropTypes.object.isRequired
}
className: PropTypes.string,
images: PropTypes.object.isRequired,
showOnlyEth: PropTypes.bool,
showZeroValues: PropTypes.bool
};
static defaultProps = {
showOnlyEth: false,
showZeroValues: false
};
render () {
const { api } = this.context;
const { balance, images } = this.props;
const { balance, className, images, showZeroValues, showOnlyEth } = this.props;
if (!balance) {
if (!balance || !balance.tokens) {
return null;
}
let body = (balance.tokens || [])
.filter((balance) => new BigNumber(balance.value).gt(0))
let body = balance.tokens
.filter((balance) => {
const hasBalance = showZeroValues || new BigNumber(balance.value).gt(0);
const isValidToken = !showOnlyEth || (balance.token.tag || '').toLowerCase() === 'eth';
return hasBalance && isValidToken;
})
.map((balance, index) => {
const token = balance.token;
@ -92,13 +107,16 @@ class Balance extends Component {
if (!body.length) {
body = (
<div className={ styles.empty }>
There are no balances associated with this account
<FormattedMessage
id='ui.balance.none'
defaultMessage='There are no balances associated with this account'
/>
</div>
);
}
return (
<div className={ styles.balances }>
<div className={ [styles.balances, className].join(' ') }>
{ body }
</div>
);

View File

@ -0,0 +1,122 @@
// 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 apiutil from '~/api/util';
import Balance from './';
const BALANCE = {
tokens: [
{ value: '122', token: { tag: 'ETH' } },
{ value: '345', token: { tag: 'GAV', format: 1 } },
{ value: '0', token: { tag: 'TST', format: 1 } }
]
};
let api;
let component;
let store;
function createApi () {
api = {
dappsUrl: 'http://testDapps:1234/',
util: apiutil
};
return api;
}
function createStore () {
store = {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
images: {}
};
}
};
return store;
}
function render (props = {}) {
if (!props.balance) {
props.balance = BALANCE;
}
component = shallow(
<Balance
className='testClass'
{ ...props }
/>,
{
context: {
store: createStore()
}
}
).find('Balance').shallow({ context: { api: createApi() } });
return component;
}
describe('ui/Balance', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
it('passes the specified className', () => {
expect(component.hasClass('testClass')).to.be.true;
});
it('renders all the non-zero balances', () => {
expect(component.find('img')).to.have.length(2);
});
describe('render specifiers', () => {
it('renders only the single token with showOnlyEth', () => {
render({ showOnlyEth: true });
expect(component.find('img')).to.have.length(1);
});
it('renders all the tokens with showZeroValues', () => {
render({ showZeroValues: true });
expect(component.find('img')).to.have.length(3);
});
it('shows ETH with zero value with showOnlyEth & showZeroValues', () => {
render({
showOnlyEth: true,
showZeroValues: true,
balance: {
tokens: [
{ value: '0', token: { tag: 'ETH' } },
{ value: '345', token: { tag: 'GAV', format: 1 } }
]
}
});
expect(component.find('img')).to.have.length(1);
});
});
});

View File

@ -14,30 +14,36 @@
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.byline, .description {
$bylineColor: #aaa;
$bylineLineHeight: 1.2rem;
$bylineMaxHeight: 2.4rem;
$titleLineHeight: 2rem;
$smallFontSize: 0.75rem;
.byline,
.description {
color: $bylineColor;
display: -webkit-box;
line-height: $bylineLineHeight;
max-height: $bylineMaxHeight;
overflow: hidden;
position: relative;
line-height: 1.2em;
max-height: 2.4em;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
color: #aaa;
* {
color: #aaa !important;
color: $bylineColor !important;
}
}
.description {
font-size: 0.75em;
font-size: $smallFontSize;
margin: 0.5em 0 0;
}
.title {
text-transform: uppercase;
line-height: $titleLineHeight;
margin: 0;
line-height: 34px;
text-transform: uppercase;
}

View File

@ -29,29 +29,41 @@ export default class Title extends Component {
}
render () {
const { byline, className, title } = this.props;
const byLine = typeof byline === 'string'
? (
<span title={ byline }>
{ byline }
</span>
)
: byline;
const { className, title } = this.props;
return (
<div className={ className }>
<h3 className={ styles.title }>
{ title }
</h3>
<div className={ styles.byline }>
{ byLine }
</div>
{ this.renderByline() }
{ this.renderDescription() }
</div>
);
}
renderByline () {
const { byline } = this.props;
if (!byline) {
return null;
}
return (
<div className={ styles.byline }>
{
typeof byline === 'string'
? (
<span title={ byline }>
{ byline }
</span>
)
: byline
}
</div>
);
}
renderDescription () {
const { description } = this.props;
@ -59,17 +71,17 @@ export default class Title extends Component {
return null;
}
const desc = typeof description === 'string'
? (
<span title={ description }>
{ description }
</span>
)
: description;
return (
<div className={ styles.description }>
{ desc }
{
typeof description === 'string'
? (
<span title={ description }>
{ description }
</span>
)
: description
}
</div>
);
}

View File

@ -29,15 +29,14 @@ export default class Container extends Component {
className: PropTypes.string,
compact: PropTypes.bool,
light: PropTypes.bool,
onClick: PropTypes.func,
style: PropTypes.object,
tabIndex: PropTypes.number,
title: nodeOrStringProptype()
}
render () {
const { children, className, compact, light, style, tabIndex } = this.props;
const classes = `${styles.container} ${light ? styles.light : ''} ${className}`;
const { children, className, compact, light, onClick, style, tabIndex } = this.props;
const props = {};
if (Number.isInteger(tabIndex)) {
@ -45,8 +44,27 @@ export default class Container extends Component {
}
return (
<div className={ classes } style={ style } { ...props }>
<Card className={ compact ? styles.compact : styles.padded }>
<div
className={
[
styles.container,
light
? styles.light
: '',
className
].join(' ')
}
style={ style }
{ ...props }
>
<Card
className={
compact
? styles.compact
: styles.padded
}
onClick={ onClick }
>
{ this.renderTitle() }
{ children }
</Card>

View File

@ -14,21 +14,21 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { IconButton } from 'material-ui';
import React, { Component, PropTypes } from 'react';
import Clipboard from 'react-copy-to-clipboard';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { IconButton } from 'material-ui';
import Clipboard from 'react-copy-to-clipboard';
import CopyIcon from 'material-ui/svg-icons/content/content-copy';
import Theme from '../Theme';
import { showSnackbar } from '~/redux/providers/snackbarActions';
const { textColor, disabledTextColor } = Theme.flatButton;
import { CopyIcon } from '../Icons';
import Theme from '../Theme';
import styles from './copyToClipboard.css';
const { textColor, disabledTextColor } = Theme.flatButton;
class CopyToClipboard extends Component {
static propTypes = {
showSnackbar: PropTypes.func.isRequired,

View File

@ -0,0 +1,51 @@
// 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 React, { Component } from 'react';
import PlaygroundExample from '~/playground/playgroundExample';
import ConnectedCurrencySymbol, { CurrencySymbol } from './currencySymbol';
export default class CurrencySymbolExample extends Component {
render () {
return (
<div>
<PlaygroundExample name='Connected Currency Symbol'>
<ConnectedCurrencySymbol />
</PlaygroundExample>
<PlaygroundExample name='Simple Currency Symbol'>
<CurrencySymbol
netChain='testnet'
/>
</PlaygroundExample>
<PlaygroundExample name='ETC Currency Symbol'>
<CurrencySymbol
netChain='classic'
/>
</PlaygroundExample>
<PlaygroundExample name='EXP Currency Symbol'>
<CurrencySymbol
netChain='expanse'
/>
</PlaygroundExample>
</div>
);
}
}

View File

@ -0,0 +1,65 @@
// 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, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
const SYMBOL_ETC = 'ETC';
const SYMBOL_ETH = 'ETH';
const SYMBOL_EXP = 'EXP';
export class CurrencySymbol extends Component {
static propTypes = {
className: PropTypes.string,
netChain: PropTypes.string.isRequired
}
render () {
const { className } = this.props;
return (
<span className={ className }>{ this.renderSymbol() }</span>
);
}
renderSymbol () {
const { netChain } = this.props;
switch (netChain) {
case 'classic':
return SYMBOL_ETC;
case 'expanse':
return SYMBOL_EXP;
default:
return SYMBOL_ETH;
}
}
}
function mapStateToProps (state) {
const { netChain } = state.nodeStatus;
return {
netChain
};
}
export default connect(
mapStateToProps,
null
)(CurrencySymbol);

View File

@ -0,0 +1,99 @@
// 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 CurrencySymbol from './';
let component;
let store;
function createRedux (netChain = 'ropsten') {
store = {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
nodeStatus: {
netChain
}
};
}
};
return store;
}
function render (netChain, props = {}) {
component = shallow(
<CurrencySymbol { ...props } />,
{
context: {
store: createRedux(netChain)
}
}
).find('CurrencySymbol').shallow();
return component;
}
describe('ui/CurrencySymbol', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
it('passes the className as provided', () => {
expect(render('ropsten', { className: 'test' }).find('span').hasClass('test')).to.be.true;
});
describe('currencies', () => {
it('renders ETH as default', () => {
expect(render().text()).equal('ETH');
});
it('renders ETC for classic', () => {
expect(render('classic').text()).equal('ETC');
});
it('renders EXP for expanse', () => {
expect(render('expanse').text()).equal('EXP');
});
it('renders ETH as default', () => {
expect(render('somethingElse').text()).equal('ETH');
});
});
describe('renderSymbol', () => {
it('render defaults', () => {
expect(render().instance().renderSymbol()).to.be.ok;
});
it('render ETH as default', () => {
expect(render().instance().renderSymbol()).equal('ETH');
});
it('render ETC', () => {
expect(render('classic').instance().renderSymbol()).equal('ETC');
});
it('render EXP', () => {
expect(render('expanse').instance().renderSymbol()).equal('EXP');
});
});
});

View File

@ -0,0 +1,17 @@
// 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/>.
export default from './currencySymbol';

View File

@ -16,17 +16,14 @@
*/
.container {
position: relative;
height: 100%;
position: relative;
}
.image {
left: 1.5em;
position: absolute;
top: 1.5em;
left: 1.5em;
border-radius: 50%;
width: 56px;
height: 56px;
}
.description {

View File

@ -17,74 +17,80 @@
import React, { Component, PropTypes } from 'react';
import { Link } from 'react-router';
import { Container, ContainerTitle, Tags } from '~/ui';
import Container, { Title as ContainerTitle } from '~/ui/Container';
import DappIcon from '~/ui/DappIcon';
import Tags from '~/ui/Tags';
import styles from './summary.css';
export default class Summary extends Component {
static contextTypes = {
api: React.PropTypes.object
}
import styles from './dappCard.css';
export default class DappCard extends Component {
static propTypes = {
app: PropTypes.object.isRequired,
children: PropTypes.node
}
children: PropTypes.node,
className: PropTypes.string,
onClick: PropTypes.func,
showLink: PropTypes.bool,
showTags: PropTypes.bool
};
static defaultProps = {
showLink: false,
showTags: false
};
render () {
const { dappsUrl } = this.context.api;
const { app } = this.props;
const { app, children, className, onClick, showLink, showTags } = this.props;
if (!app) {
return null;
}
const image = this.renderImage(dappsUrl, app);
const link = this.renderLink(app);
return (
<Container className={ styles.container }>
{ image }
<Tags tags={ [app.type] } />
<Container
className={
[styles.container, className].join(' ')
}
onClick={ onClick }
>
<DappIcon
app={ app }
className={ styles.image }
/>
<Tags
tags={
showTags
? [app.type]
: null
}
/>
<div className={ styles.description }>
<ContainerTitle
className={ styles.title }
title={ link }
title={
showLink
? this.renderLink(app)
: app.name
}
byline={ app.description }
/>
<div className={ styles.author }>
{ app.author }, v{ app.version }
</div>
{ this.props.children }
{ children }
</div>
</Container>
);
}
renderImage (dappsUrl, app) {
if (app.type === 'local') {
return (
<img src={ `${dappsUrl}/${app.id}/${app.iconUrl}` } className={ styles.image } />
);
}
return (
<img src={ `${dappsUrl}${app.image}` } className={ styles.image } />
);
}
renderLink (app) {
// Special case for web dapp
if (app.url === 'web') {
return (
<Link to={ `/web` }>
{ app.name }
</Link>
);
}
return (
<Link to={ `/app/${app.id}` }>
<Link
to={
app.url === 'web'
? '/web'
: `/app/${app.id}`
}
>
{ app.name }
</Link>
);

View File

@ -0,0 +1,17 @@
// 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/>.
export default from './dappCard';

View File

@ -0,0 +1,31 @@
/* 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/>.
*/
.icon {
border-radius: 50%;
margin: 0 0.75em 0 0;
}
.normal {
height: 56px;
width: 56px;
}
.small {
height: 32px;
width: 32px;
}

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 React, { Component, PropTypes } from 'react';
import styles from './dappIcon.css';
export default class DappIcon extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
app: PropTypes.object.isRequired,
className: PropTypes.string,
small: PropTypes.bool
};
render () {
const { dappsUrl } = this.context.api;
const { app, className, small } = this.props;
return (
<img
className={
[styles.icon, styles[small ? 'small' : 'normal'], className].join(' ')
}
src={
app.type === 'local'
? `${dappsUrl}/${app.id}/${app.iconUrl}`
: `${dappsUrl}${app.image}`
}
/>
);
}
}

View File

@ -0,0 +1,70 @@
// 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 DappIcon from './';
const DAPPS_URL = 'http://test';
let api;
let component;
function createApi () {
api = {
dappsUrl: DAPPS_URL
};
return api;
}
function render (props = {}) {
if (!props.app) {
props.app = {};
}
component = shallow(
<DappIcon { ...props } />,
{
context: { api: createApi() }
}
);
return component;
}
describe('ui/DappIcon', () => {
it('renders defaults', () => {
expect(render()).to.be.ok;
});
it('adds specified className', () => {
expect(render({ className: 'testClass' }).hasClass('testClass')).to.be.true;
});
it('renders local apps with correct URL', () => {
expect(render({ app: { id: 'test', type: 'local', iconUrl: 'test.img' } }).props().src).to.equal(
`${DAPPS_URL}/test/test.img`
);
});
it('renders other apps with correct URL', () => {
expect(render({ app: { id: 'test', image: '/test.img' } }).props().src).to.equal(
`${DAPPS_URL}/test.img`
);
});
});

View File

@ -0,0 +1,17 @@
// 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/>.
export default from './dappIcon';

View File

@ -73,6 +73,12 @@
}
}
.title {
display: flex;
flex-direction: column;
position: relative;
}
.label {
margin: 1rem 0.5rem 0.25em;
color: rgba(255, 255, 255, 0.498039);
@ -102,14 +108,11 @@
}
.categories {
flex: 1;
display: flex;
flex: 1;
flex-direction: row;
justify-content: flex-start;
margin: 2rem 0 0;
> * {
flex: 1;
}

View File

@ -176,37 +176,42 @@ class AddressSelect extends Component {
return (
<Portal
className={ styles.inputContainer }
isChildModal
onClose={ this.handleClose }
onKeyDown={ this.handleKeyDown }
open={ expanded }
title={
<div className={ styles.title }>
<label className={ styles.label } htmlFor={ id }>
{ label }
</label>
<div className={ styles.outerInput }>
<input
id={ id }
className={ styles.input }
placeholder={ ilHint }
onBlur={ this.handleInputBlur }
onFocus={ this.handleInputFocus }
onChange={ this.handleChange }
ref={ this.setInputRef }
/>
{ this.renderLoader() }
</div>
<div className={ styles.underline }>
<TextFieldUnderline
focus={ inputFocused }
focusStyle={ BOTTOM_BORDER_STYLE }
muiTheme={ muiTheme }
style={ BOTTOM_BORDER_STYLE }
/>
</div>
{ this.renderCurrentInput() }
{ this.renderRegistryValues() }
</div>
}
>
<label className={ styles.label } htmlFor={ id }>
{ label }
</label>
<div className={ styles.outerInput }>
<input
id={ id }
className={ styles.input }
placeholder={ ilHint }
onBlur={ this.handleInputBlur }
onFocus={ this.handleInputFocus }
onChange={ this.handleChange }
ref={ this.setInputRef }
/>
{ this.renderLoader() }
</div>
<div className={ styles.underline }>
<TextFieldUnderline
focus={ inputFocused }
focusStyle={ BOTTOM_BORDER_STYLE }
muiTheme={ muiTheme }
style={ BOTTOM_BORDER_STYLE }
/>
</div>
{ this.renderCurrentInput() }
{ this.renderRegistryValues() }
{ this.renderAccounts() }
</Portal>
);

View File

@ -80,6 +80,9 @@ class InputAddress extends Component {
props.focused = focused;
}
// FIXME: The is not advisable, fixes the display issue, however the name should come from
// a common component.
// account.name || (value ? 'UNNAMED' : value)
return (
<div className={ containerClasses.join(' ') }>
<Input
@ -98,7 +101,7 @@ class InputAddress extends Component {
tabIndex={ tabIndex }
value={
text && account
? account.name
? (account.name || (value ? 'UNNAMED' : value))
: (nullName || value)
}
{ ...props }

View File

@ -0,0 +1,17 @@
// 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/>.
export default from './inputDate';

View File

@ -0,0 +1,22 @@
/* 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/>.
*/
.container {
.input {
width: 100%;
}
}

View File

@ -0,0 +1,53 @@
// 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 { DatePicker } from 'material-ui';
import React, { Component, PropTypes } from 'react';
import Label from '../Label';
import styles from './inputDate.css';
// NOTE: Has to be larger than Signer overlay Z, aligns with ../InputTime
const DIALOG_STYLE = { zIndex: 10010 };
export default class InputDate extends Component {
static propTypes = {
className: PropTypes.string,
hint: PropTypes.node,
label: PropTypes.node,
onChange: PropTypes.func,
value: PropTypes.object.isRequired
};
render () {
const { className, hint, label, onChange, value } = this.props;
return (
<div className={ [styles.container, className].join(' ') }>
<Label label={ label } />
<DatePicker
autoOk
className={ styles.input }
dialogContainerStyle={ DIALOG_STYLE }
hintText={ hint }
onChange={ onChange }
value={ value }
/>
</div>
);
}
}

View File

@ -0,0 +1,17 @@
// 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/>.
export default from './inputTime';

View File

@ -0,0 +1,22 @@
/* 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/>.
*/
.container {
.input {
width: 100%;
}
}

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 { TimePicker } from 'material-ui';
import React, { Component, PropTypes } from 'react';
import Label from '../Label';
import styles from './inputTime.css';
// NOTE: Has to be larger than Signer overlay Z, aligns with ../InputDate
const DIALOG_STYLE = { zIndex: 10010 };
export default class InputTime extends Component {
static propTypes = {
className: PropTypes.string,
hint: PropTypes.node,
label: PropTypes.node,
onChange: PropTypes.func,
value: PropTypes.object.isRequired
}
render () {
const { className, hint, label, onChange, value } = this.props;
return (
<div className={ [styles.container, className].join(' ') }>
<Label label={ label } />
<TimePicker
autoOk
className={ styles.input }
dialogStyle={ DIALOG_STYLE }
format='24hr'
hintText={ hint }
onChange={ onChange }
value={ value }
/>
</div>
);
}
}

View File

@ -0,0 +1,17 @@
// 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/>.
export default from './label';

View File

@ -0,0 +1,24 @@
/* 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/>.
*/
$labelColor: rgba(255, 255, 255, 0.5);
$labelFontSize: 0.75rem;
.label {
color: $labelColor;
font-size: $labelFontSize;
}

View File

@ -0,0 +1,40 @@
// 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 React, { Component, PropTypes } from 'react';
import styles from './label.css';
export default class Label extends Component {
static propTypes = {
className: PropTypes.string,
label: PropTypes.node
}
render () {
const { className, label } = this.props;
if (!label) {
return null;
}
return (
<label className={ [styles.label, className].join(' ') }>
{ label }
</label>
);
}
}

View File

@ -15,18 +15,23 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.spaced {
margin: 0.25em 0;
}
.container {
.label {
}
.typeContainer {
display: flex;
flex-direction: column;
.radioButton {
margin: 0.25em 0;
}
.desc {
font-size: 0.8em;
margin-bottom: 0.5em;
color: #ccc;
z-index: 2;
.radioLabel {
display: flex;
flex-direction: column;
.description {
font-size: 0.8em;
margin-bottom: 0.5em;
color: #ccc;
z-index: 2;
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
@ -18,10 +18,14 @@ import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
import React, { Component, PropTypes } from 'react';
import { arrayOrObjectProptype } from '~/util/proptypes';
import Label from '../Label';
import styles from './radioButtons.css';
export default class RadioButtons extends Component {
static propTypes = {
className: PropTypes.string,
label: PropTypes.node,
name: PropTypes.string,
onChange: PropTypes.func.isRequired,
value: PropTypes.any,
@ -34,10 +38,10 @@ export default class RadioButtons extends Component {
};
render () {
const { value, values } = this.props;
const { className, label, value, values } = this.props;
const index = Number.isNaN(parseInt(value))
? values.findIndex((val) => val.key === value)
? values.findIndex((_value) => _value.key === value)
: parseInt(value);
const selectedValue = typeof value !== 'object'
? values[index]
@ -45,12 +49,19 @@ export default class RadioButtons extends Component {
const key = this.getKey(selectedValue, index);
return (
<RadioButtonGroup
name={ name }
onChange={ this.onChange }
valueSelected={ key } >
{ this.renderContent() }
</RadioButtonGroup>
<div className={ [styles.container, className].join(' ') }>
<Label
className={ styles.label }
label={ label }
/>
<RadioButtonGroup
name={ name }
onChange={ this.onChange }
valueSelected={ key }
>
{ this.renderContent() }
</RadioButtonGroup>
</div>
);
}
@ -66,19 +77,20 @@ export default class RadioButtons extends Component {
return (
<RadioButton
className={ styles.spaced }
className={ styles.radioButton }
key={ index }
label={
<div className={ styles.typeContainer }>
<div className={ styles.radioLabel }>
<span>{ label }</span>
{
description
? <span className={ styles.desc }>{ description }</span>
? <span className={ styles.description }>{ description }</span>
: null
}
</div>
}
value={ key } />
value={ key }
/>
);
});
}
@ -95,7 +107,7 @@ export default class RadioButtons extends Component {
onChange = (event, index) => {
const { onChange, values } = this.props;
const value = values[index] || values.find((v) => v.key === index);
const value = values[index] || values.find((value) => value.key === index);
onChange(value, index);
}

View File

@ -16,25 +16,31 @@
import AddressSelect from './AddressSelect';
import FormWrap from './FormWrap';
import TypedInput from './TypedInput';
import Input from './Input';
import InputAddress from './InputAddress';
import InputAddressSelect from './InputAddressSelect';
import InputChip from './InputChip';
import InputDate from './InputDate';
import InputInline from './InputInline';
import Select from './Select';
import InputTime from './InputTime';
import Label from './Label';
import RadioButtons from './RadioButtons';
import Select from './Select';
import TypedInput from './TypedInput';
export default from './form';
export {
AddressSelect,
FormWrap,
TypedInput,
Input,
InputAddress,
InputAddressSelect,
InputChip,
InputDate,
InputInline,
InputTime,
Label,
RadioButtons,
Select,
RadioButtons
TypedInput
};

View File

@ -16,6 +16,46 @@
*/
.container {
display: flex;
flex-direction: column;
}
.conditionContainer {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
margin-bottom: 1.5em;
.input {
flex: 0 1 50%;
}
}
.conditionRadio {
display: flex;
flex-direction: column;
margin-bottom: 1em;
&>label {
margin-bottom: 0.5em;
}
&>div {
display: flex;
flex-direction: row;
&>div {
width: auto !important;
label {
padding-right: 1.5em;
white-space: nowrap;
}
}
}
}
.graphContainer {
display: flex;
flex-wrap: wrap;
position: relative;

View File

@ -1,4 +1,4 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
@ -17,13 +17,44 @@
import BigNumber from 'bignumber.js';
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import Input from '../Form/Input';
import { Input, InputDate, InputTime, RadioButtons } from '../Form';
import GasPriceSelector from '../GasPriceSelector';
import Store from './store';
import Store, { CONDITIONS } from './store';
import styles from './gasPriceEditor.css';
const CONDITION_VALUES = [
{
label: (
<FormattedMessage
id='txEditor.condition.none'
defaultMessage='No conditions'
/>
),
key: CONDITIONS.NONE
},
{
label: (
<FormattedMessage
id='txEditor.condition.blocknumber'
defaultMessage='Send after BlockNumber'
/>
),
key: CONDITIONS.BLOCK
},
{
label: (
<FormattedMessage
id='txEditor.condition.datetime'
defaultMessage='Send after Date & Time'
/>
),
key: CONDITIONS.TIME
}
];
@observer
export default class GasPriceEditor extends Component {
static contextTypes = {
@ -41,7 +72,7 @@ export default class GasPriceEditor extends Component {
render () {
const { api } = this.context;
const { children, store } = this.props;
const { errorGas, errorPrice, errorTotal, estimated, gas, histogram, price, priceDefault, totalValue } = store;
const { conditionType, errorGas, errorPrice, errorTotal, estimated, gas, histogram, price, priceDefault, totalValue } = store;
const eth = api.util.fromWei(totalValue).toFormat();
const gasLabel = `gas (estimated: ${new BigNumber(estimated).toFormat()})`;
@ -49,43 +80,147 @@ export default class GasPriceEditor extends Component {
return (
<div className={ styles.container }>
<div className={ styles.graphColumn }>
<GasPriceSelector
histogram={ histogram }
onChange={ this.onEditGasPrice }
price={ price } />
<div className={ styles.gasPriceDesc }>
You can choose the gas price based on the distribution of recent included transaction gas prices. The lower the gas price is, the cheaper the transaction will be. The higher the gas price is, the faster it should get mined by the network.
<RadioButtons
className={ styles.conditionRadio }
label={
<FormattedMessage
id='txEditor.condition.label'
defaultMessage='Condition where transaction activates'
/>
}
onChange={ this.onChangeConditionType }
value={ conditionType }
values={ CONDITION_VALUES }
/>
{ this.renderConditions() }
<div className={ styles.graphContainer }>
<div className={ styles.graphColumn }>
<GasPriceSelector
histogram={ histogram }
onChange={ this.onEditGasPrice }
price={ price }
/>
<div className={ styles.gasPriceDesc }>
<FormattedMessage
id='txEditor.gas.info'
defaultMessage='You can choose the gas price based on the distribution of recent included transaction gas prices. The lower the gas price is, the cheaper the transaction will be. The higher the gas price is, the faster it should get mined by the network.'
/>
</div>
</div>
<div className={ styles.editColumn }>
<div className={ styles.row }>
<Input
error={ errorGas }
hint='the amount of gas to use for the transaction'
label={ gasLabel }
min={ 1 }
onChange={ this.onEditGas }
type='number'
value={ gas }
/>
<Input
error={ errorPrice }
hint='the price of gas to use for the transaction'
label={ priceLabel }
min={ 1 }
onChange={ this.onEditGasPrice }
type='number'
value={ price }
/>
</div>
<div className={ styles.row }>
<Input
disabled
error={ errorTotal }
hint='the total amount of the transaction'
label='total transaction amount'
value={ `${eth} ETH` }
/>
</div>
<div className={ styles.row }>
{ children }
</div>
</div>
</div>
</div>
);
}
<div className={ styles.editColumn }>
<div className={ styles.row }>
renderConditions () {
const { conditionType, condition, conditionBlockError } = this.props.store;
if (conditionType === CONDITIONS.NONE) {
return null;
}
if (conditionType === CONDITIONS.BLOCK) {
return (
<div className={ styles.conditionContainer }>
<div className={ styles.input }>
<Input
error={ errorGas }
hint='the amount of gas to use for the transaction'
label={ gasLabel }
onChange={ this.onEditGas }
value={ gas } />
<Input
error={ errorPrice }
hint='the price of gas to use for the transaction'
label={ priceLabel }
onChange={ this.onEditGasPrice }
value={ price } />
</div>
<div className={ styles.row }>
<Input
disabled
error={ errorTotal }
hint='the total amount of the transaction'
label='total transaction amount'
value={ `${eth} ETH` } />
</div>
<div className={ styles.row }>
{ children }
error={ conditionBlockError }
hint={
<FormattedMessage
id='txEditor.condition.block.hint'
defaultMessage='The minimum block to send from'
/>
}
label={
<FormattedMessage
id='txEditor.condition.block.label'
defaultMessage='Transaction send block'
/>
}
min={ 1 }
onChange={ this.onChangeConditionBlock }
type='number'
value={ condition.block }
/>
</div>
</div>
);
}
return (
<div className={ styles.conditionContainer }>
<div className={ styles.input }>
<InputDate
hint={
<FormattedMessage
id='txEditor.condition.date.hint'
defaultMessage='The minimum date to send from'
/>
}
label={
<FormattedMessage
id='txEditor.condition.date.label'
defaultMessage='Transaction send date'
/>
}
onChange={ this.onChangeConditionDateTime }
value={ condition.time }
/>
</div>
<div className={ styles.input }>
<InputTime
hint={
<FormattedMessage
id='txEditor.condition.time.hint'
defaultMessage='The minimum time to send from'
/>
}
label={
<FormattedMessage
id='txEditor.condition.time.label'
defaultMessage='Transaction send time'
/>
}
onChange={ this.onChangeConditionDateTime }
value={ condition.time }
/>
</div>
</div>
);
}
@ -103,4 +238,16 @@ export default class GasPriceEditor extends Component {
store.setPrice(price);
onChange && onChange('gasPrice', price);
}
onChangeConditionType = (conditionType) => {
this.props.store.setConditionType(conditionType.key);
}
onChangeConditionBlock = (event, blockNumber) => {
this.props.store.setConditionBlockNumber(blockNumber);
}
onChangeConditionDateTime = (event, datetime) => {
this.props.store.setConditionDateTime(datetime);
}
}

View File

@ -21,26 +21,64 @@ import sinon from 'sinon';
import GasPriceEditor from './';
const api = {
util: {
fromWei: (value) => new BigNumber(value)
}
};
let api;
let component;
let store;
const store = {
estimated: '123',
histogram: {},
priceDefault: '456',
totalValue: '789',
setGas: sinon.stub(),
setPrice: sinon.stub()
};
function createApi () {
api = {
eth: {
blockNumber: sinon.stub().resolves(new BigNumber(3))
},
util: {
fromWei: (value) => new BigNumber(value)
}
};
return api;
}
function createStore () {
createApi();
store = {
_api: api,
conditionType: 'none',
estimated: '123',
histogram: {},
priceDefault: '456',
totalValue: '789',
setGas: sinon.stub(),
setPrice: sinon.stub()
};
return store;
}
function render (props = {}) {
createStore();
component = shallow(
<GasPriceEditor
store={ store }
{ ...props }
/>,
{
context: {
api
}
}
);
return component;
}
describe('ui/GasPriceEditor', () => {
beforeEach(() => {
render();
});
it('renders', () => {
expect(shallow(
<GasPriceEditor store={ store } />,
{ context: { api } }
)).to.be.ok;
expect(component).to.be.ok;
});
});

View File

@ -20,7 +20,17 @@ import { action, computed, observable, transaction } from 'mobx';
import { ERRORS, validatePositiveNumber } from '~/util/validation';
import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/constants';
const CONDITIONS = {
NONE: 'none',
BLOCK: 'blockNumber',
TIME: 'timestamp'
};
export default class GasPriceEditor {
@observable blockNumber = 0;
@observable condition = {};
@observable conditionBlockError = null;
@observable conditionType = CONDITIONS.NONE;
@observable errorEstimated = null;
@observable errorGas = null;
@observable errorPrice = null;
@ -34,13 +44,23 @@ export default class GasPriceEditor {
@observable priceDefault;
@observable weiValue = '0';
constructor (api, { gas, gasLimit, gasPrice }) {
constructor (api, { gas, gasLimit, gasPrice, condition = null }) {
this._api = api;
this.gas = gas;
this.gasLimit = gasLimit;
this.price = gasPrice;
if (condition) {
if (condition.block) {
this.condition = { block: condition.block.toFixed(0) };
this.conditionType = CONDITIONS.BLOCK;
} else if (condition.time) {
this.condition = { time: condition.time };
this.conditionType = CONDITIONS.TIME;
}
}
if (api) {
this.loadDefaults();
}
@ -54,6 +74,39 @@ export default class GasPriceEditor {
}
}
@action setConditionType = (conditionType = CONDITIONS.NONE) => {
transaction(() => {
this.conditionBlockError = null;
this.conditionType = conditionType;
switch (conditionType) {
case CONDITIONS.BLOCK:
this.condition = Object.assign({}, this.condition, { block: this.blockNumber || 1 });
break;
case CONDITIONS.TIME:
this.condition = Object.assign({}, this.condition, { time: new Date() });
break;
case CONDITIONS.NONE:
default:
this.condition = {};
break;
}
});
}
@action setConditionBlockNumber = (block) => {
transaction(() => {
this.conditionBlockError = validatePositiveNumber(block).numberError;
this.condition = Object.assign({}, this.condition, { block });
});
}
@action setConditionDateTime = (time) => {
this.condition = Object.assign({}, this.condition, { time });
}
@action setEditing = (isEditing) => {
this.isEditing = isEditing;
}
@ -130,9 +183,10 @@ export default class GasPriceEditor {
bucket_bounds: [],
counts: []
})),
this._api.eth.gasPrice()
this._api.eth.gasPrice(),
this._api.eth.blockNumber()
])
.then(([histogram, _price]) => {
.then(([histogram, _price, blockNumber]) => {
transaction(() => {
const price = _price.toFixed(0);
@ -142,6 +196,7 @@ export default class GasPriceEditor {
this.setHistogram(histogram);
this.priceDefault = price;
this.blockNumber = blockNumber.toNumber();
});
})
.catch((error) => {
@ -150,13 +205,37 @@ export default class GasPriceEditor {
}
overrideTransaction = (transaction) => {
if (this.errorGas || this.errorPrice) {
if (this.errorGas || this.errorPrice || this.conditionBlockError) {
return transaction;
}
return Object.assign({}, transaction, {
const override = {
condition: this.condition,
gas: new BigNumber(this.gas || DEFAULT_GAS),
gasPrice: new BigNumber(this.price || DEFAULT_GASPRICE)
});
};
const result = Object.assign({}, transaction, override);
switch (this.conditionType) {
case CONDITIONS.BLOCK:
result.condition = { block: new BigNumber(this.condition.block || 0) };
break;
case CONDITIONS.TIME:
result.condition = { time: this.condition.time };
break;
case CONDITIONS.NONE:
default:
delete result.condition;
break;
}
return result;
}
}
export {
CONDITIONS
};

View File

@ -21,6 +21,7 @@ import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/consta
import { ERRORS } from '~/util/validation';
import GasPriceEditor from './gasPriceEditor';
import { CONDITIONS } from './store';
const { Store } = GasPriceEditor;
@ -31,18 +32,30 @@ const HISTOGRAM = {
counts: [3, 4]
};
const api = {
eth: {
gasPrice: sinon.stub().resolves(GASPRICE)
},
parity: {
gasPriceHistogram: sinon.stub().resolves(HISTOGRAM)
}
};
let api;
describe('ui/GasPriceEditor/store', () => {
// TODO: share with gasPriceEditor.spec.js
function createApi () {
api = {
eth: {
blockNumber: sinon.stub().resolves(new BigNumber(2)),
gasPrice: sinon.stub().resolves(GASPRICE)
},
parity: {
gasPriceHistogram: sinon.stub().resolves(HISTOGRAM)
}
};
return api;
}
describe('ui/GasPriceEditor/Store', () => {
let store = null;
beforeEach(() => {
createApi();
});
it('is available via GasPriceEditor.Store', () => {
expect(new Store(null, {})).to.be.ok;
});
@ -65,6 +78,7 @@ describe('ui/GasPriceEditor/store', () => {
describe('constructor (defaults) when histogram not available', () => {
const api = {
eth: {
blockNumber: sinon.stub().resolves(new BigNumber(2)),
gasPrice: sinon.stub().resolves(GASPRICE)
},
parity: {
@ -92,6 +106,67 @@ describe('ui/GasPriceEditor/store', () => {
store = new Store(null, { gasLimit: GASLIMIT });
});
describe('setConditionType', () => {
it('sets the actual type', () => {
store.setConditionType('testingType');
expect(store.conditionType).to.equal('testingType');
});
it('clears any block error on changing type', () => {
store.setConditionBlockNumber(-1);
expect(store.conditionBlockError).not.to.be.null;
store.setConditionType(CONDITIONS.BLOCK);
expect(store.conditionBlockError).to.be.null;
});
it('sets condition.block when type === CONDITIONS.BLOCK', () => {
store.setConditionType(CONDITIONS.BLOCK);
expect(store.condition.block).to.be.ok;
});
it('clears condition when type === CONDITIONS.NONE', () => {
store.setConditionType(CONDITIONS.BLOCK);
store.setConditionType(CONDITIONS.NONE);
expect(store.condition).to.deep.equal({});
});
it('sets condition.time when type === CONDITIONS.TIME', () => {
store.setConditionType(CONDITIONS.TIME);
expect(store.condition.time).to.be.ok;
});
});
describe('setConditionBlockNumber', () => {
beforeEach(() => {
store.setConditionBlockNumber('testingBlock');
});
it('sets the blockNumber', () => {
expect(store.condition.block).to.equal('testingBlock');
});
it('sets the error on invalid numbers', () => {
expect(store.conditionBlockError).not.to.be.null;
});
it('sets the error on negative numbers', () => {
store.setConditionBlockNumber(-1);
expect(store.conditionBlockError).not.to.be.null;
});
it('clears the error on positive numbers', () => {
store.setConditionBlockNumber(1000);
expect(store.conditionBlockError).to.be.null;
});
});
describe('setConditionDateTime', () => {
it('sets the datatime', () => {
store.setConditionDateTime('testingDateTime');
expect(store.condition.time).to.equal('testingDateTime');
});
});
describe('setEditing', () => {
it('sets the value', () => {
expect(store.isEditing).to.be.false;

View File

@ -21,17 +21,23 @@ import CloseIcon from 'material-ui/svg-icons/navigation/close';
import CompareIcon from 'material-ui/svg-icons/action/compare-arrows';
import ComputerIcon from 'material-ui/svg-icons/hardware/desktop-mac';
import ContractIcon from 'material-ui/svg-icons/action/code';
import CopyIcon from 'material-ui/svg-icons/content/content-copy';
import DashboardIcon from 'material-ui/svg-icons/action/dashboard';
import DeleteIcon from 'material-ui/svg-icons/action/delete';
import DoneIcon from 'material-ui/svg-icons/action/done-all';
import EditIcon from 'material-ui/svg-icons/content/create';
import FingerprintIcon from 'material-ui/svg-icons/action/fingerprint';
import LinkIcon from 'material-ui/svg-icons/content/link';
import LockedIcon from 'material-ui/svg-icons/action/lock';
import MoveIcon from 'material-ui/svg-icons/action/open-with';
import NextIcon from 'material-ui/svg-icons/navigation/arrow-forward';
import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back';
import SaveIcon from 'material-ui/svg-icons/content/save';
import SendIcon from 'material-ui/svg-icons/content/send';
import SnoozeIcon from 'material-ui/svg-icons/av/snooze';
import StarCircleIcon from 'material-ui/svg-icons/action/stars';
import StarIcon from 'material-ui/svg-icons/toggle/star';
import StarOutlineIcon from 'material-ui/svg-icons/toggle/star-border';
import VerifyIcon from 'material-ui/svg-icons/action/verified-user';
import VisibleIcon from 'material-ui/svg-icons/image/remove-red-eye';
import VpnIcon from 'material-ui/svg-icons/notification/vpn-lock';
@ -44,17 +50,23 @@ export {
CompareIcon,
ComputerIcon,
ContractIcon,
CopyIcon,
DashboardIcon,
DeleteIcon,
DoneIcon,
EditIcon,
FingerprintIcon,
LinkIcon,
LockedIcon,
MoveIcon,
NextIcon,
PrevIcon,
SaveIcon,
SendIcon,
SnoozeIcon,
StarIcon,
StarCircleIcon,
StarOutlineIcon,
VerifyIcon,
VisibleIcon,
VpnIcon

View File

@ -14,9 +14,10 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { CircularProgress } from 'material-ui';
import moment from 'moment';
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import CircularProgress from 'material-ui/CircularProgress';
import { TypedInput, InputAddress } from '../Form';
import MethodDecodingStore from './methodDecodingStore';
@ -128,15 +129,25 @@ class MethodDecoding extends Component {
renderMinBlock () {
const { historic, transaction } = this.props;
const { minBlock } = transaction;
const { condition } = transaction;
if (!minBlock || minBlock.eq(0)) {
if (!condition) {
return null;
}
return (
<span>, { historic ? 'Submitted' : 'Submission' } at block <span className={ styles.highlight }>#{ minBlock.toFormat(0) }</span></span>
);
if (condition.block && condition.block.gt(0)) {
return (
<span>, { historic ? 'Submitted' : 'Submission' } at block <span className={ styles.highlight }>#{ condition.block.toFormat(0) }</span></span>
);
}
if (condition.time) {
return (
<span>, { historic ? 'Submitted' : 'Submission' } at <span className={ styles.highlight }>{ moment(condition.time).format('LLLL') }</span></span>
);
}
return null;
}
renderAction () {

View File

@ -47,20 +47,6 @@
.title {
background: rgba(0, 0, 0, 0.25) !important;
padding: 1em;
margin-bottom: 0;
h3 {
margin: 0;
text-transform: uppercase;
}
.steps {
margin-bottom: -1em;
}
}
.waiting {
margin: 1em -1em -1em -1em;
}
.overlay {

View File

@ -22,7 +22,7 @@ import { connect } from 'react-redux';
import { nodeOrStringProptype } from '~/util/proptypes';
import Container from '../Container';
import Title from './Title';
import Title from '../Title';
const ACTIONS_STYLE = { borderStyle: 'none' };
const TITLE_STYLE = { borderStyle: 'none' };
@ -63,11 +63,14 @@ class Modal extends Component {
const contentStyle = muiTheme.parity.getBackgroundStyle(null, settings.backgroundSeed);
const header = (
<Title
activeStep={ current }
busy={ busy }
current={ current }
busySteps={ waiting }
className={ styles.title }
steps={ steps }
title={ title }
waiting={ waiting } />
waiting={ waiting }
/>
);
const classes = `${styles.dialog} ${className}`;

View File

@ -26,7 +26,12 @@ class ParityBackground extends Component {
backgroundSeed: PropTypes.string,
children: PropTypes.node,
className: PropTypes.string,
onClick: PropTypes.func
onClick: PropTypes.func,
style: PropTypes.object
};
static defaultProps = {
style: {}
};
state = {
@ -65,7 +70,11 @@ class ParityBackground extends Component {
render () {
const { children, className, onClick } = this.props;
const { style } = this.state;
const style = {
...this.state.style,
...this.props.style
};
return (
<div

View File

@ -15,30 +15,32 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
$left: 1.5em;
$right: $left;
$bottom: $left;
$top: 20vh;
$modalMargin: 1.5em;
$modalPadding: 1.5em;
$modalBackZ: 2500;
/* This should be the default case, the Portal used as a stand-alone modal */
$modalBottom: $modalMargin;
$modalLeft: $modalMargin;
$modalRight: $modalMargin;
$modalTop: $modalMargin;
$modalZ: 3500;
/* This is the case where popped-up over another modal, Portal or otherwise */
$popoverBottom: $modalMargin;
$popoverLeft: $modalMargin;
$popoverRight: $modalMargin;
$popoverTop: 20vh;
$popoverZ: 3600;
.backOverlay {
background-color: rgba(255, 255, 255, 0.35);
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(255, 255, 255, 0.25);
z-index: -10;
opacity: 0;
transform-origin: 100% 0;
transition-property: opacity, z-index;
transition-duration: 0.25s;
transition-timing-function: ease-out;
&.expanded {
opacity: 1;
z-index: 2500;
}
z-index: $modalBackZ;
}
.parityBackground {
@ -48,57 +50,78 @@ $top: 20vh;
left: 0;
right: 0;
opacity: 0.25;
z-index: -1;
}
.overlay {
display: flex;
position: fixed;
top: $top;
left: $left;
width: calc(100vw - $left - $right);
height: calc(100vh - $top - $bottom);
transform-origin: 100% 0;
transition-property: opacity, z-index;
transition-duration: 0.25s;
transition-timing-function: ease-out;
background-color: rgba(0, 0, 0, 1);
opacity: 0;
z-index: -10;
padding: 1em;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: $modalPadding;
position: fixed;
* {
min-width: 0;
}
&.expanded {
opacity: 1;
z-index: 3500;
}
}
.closeIcon {
position: absolute;
top: 0.5rem;
right: 1rem;
font-size: 4em;
z-index: 100;
transition-property: opacity;
transition-duration: 0.25s;
transition-timing-function: ease-out;
&, * {
height: 48px !important;
width: 48px !important;
}
&:hover {
cursor: pointer;
opacity: 0.5;
&.modal {
bottom: $modalBottom;
left: $modalLeft;
right: $modalRight;
top: $modalTop;
z-index: $modalZ;
}
&.popover {
left: $popoverLeft;
top: $popoverTop;
height: calc(100vh - $popoverTop - $popoverBottom);
width: calc(100vw - $popoverLeft - $popoverRight);
z-index: $popoverZ;
}
.buttonRow {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-end;
padding: $modalPadding 0 0 0;
button:not([disabled]) {
color: white !important;
svg {
fill: white !important;
}
}
}
.childContainer {
flex: 1;
overflow-x: hidden;
overflow-y: auto;
}
.closeIcon {
font-size: 4em;
position: absolute;
right: 1rem;
top: 0.5rem;
z-index: 100;
&, * {
height: 48px !important;
width: 48px !important;
}
&:hover {
cursor: pointer;
opacity: 0.5;
}
}
.titleRow {
margin-bottom: $modalPadding;
}
}

View File

@ -0,0 +1,121 @@
// 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 React, { Component } from 'react';
import { Button } from '~/ui';
import PlaygroundExample from '~/playground/playgroundExample';
import Modal from '../Modal';
import Portal from './portal';
export default class PortalExample extends Component {
state = {
open: []
};
render () {
const { open } = this.state;
return (
<div>
<PlaygroundExample name='Standard Portal'>
<div>
<button onClick={ this.handleOpen(0) }>Open</button>
<Portal
open={ open[0] || false }
onClose={ this.handleClose }
>
<p>This is the first portal</p>
</Portal>
</div>
</PlaygroundExample>
<PlaygroundExample name='Popover Portal'>
<div>
<button onClick={ this.handleOpen(1) }>Open</button>
<Portal
isChildModal
open={ open[1] || false }
onClose={ this.handleClose }
>
<p>This is the second portal</p>
</Portal>
</div>
</PlaygroundExample>
<PlaygroundExample name='Portal in Modal'>
<div>
<button onClick={ this.handleOpen(2) }>Open</button>
<Modal
title='Modal'
visible={ open[2] || false }
>
<button onClick={ this.handleOpen(3) }>Open</button>
<button onClick={ this.handleClose }>Close</button>
</Modal>
<Portal
isChildModal
open={ open[3] || false }
onClose={ this.handleClose }
>
<p>This is the second portal</p>
</Portal>
</div>
</PlaygroundExample>
<PlaygroundExample name='Portal with Buttons'>
<div>
<button onClick={ this.handleOpen(4) }>Open</button>
<Portal
activeStep={ 0 }
buttons={ [
<Button
key='close'
label='close'
onClick={ this.handleClose }
/>
] }
isChildModal
open={ open[4] || false }
onClose={ this.handleClose }
steps={ [ 'step 1', 'step 2' ] }
title='Portal with button'
>
<p>This is the fourth portal</p>
</Portal>
</div>
</PlaygroundExample>
</div>
);
}
handleOpen = (index) => {
return () => {
const { open } = this.state;
const nextOpen = open.slice();
nextOpen[index] = true;
this.setState({ open: nextOpen });
};
}
handleClose = () => {
this.setState({ open: [] });
}
}

View File

@ -14,13 +14,16 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import EventListener from 'react-event-listener';
import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import ReactPortal from 'react-portal';
import keycode from 'keycode';
import { nodeOrStringProptype } from '~/util/proptypes';
import { CloseIcon } from '~/ui/Icons';
import ParityBackground from '~/ui/ParityBackground';
import Title from '~/ui/Title';
import styles from './portal.css';
@ -29,101 +32,154 @@ export default class Portal extends Component {
static propTypes = {
onClose: PropTypes.func.isRequired,
open: PropTypes.bool.isRequired,
activeStep: PropTypes.number,
busy: PropTypes.bool,
busySteps: PropTypes.array,
buttons: PropTypes.array,
children: PropTypes.node,
className: PropTypes.string,
onKeyDown: PropTypes.func
hideClose: PropTypes.bool,
isChildModal: PropTypes.bool,
onKeyDown: PropTypes.func,
steps: PropTypes.array,
title: nodeOrStringProptype()
};
state = {
expanded: false
componentDidMount () {
this.setBodyOverflow(this.props.open);
}
componentWillReceiveProps (nextProps) {
if (this.props.open !== nextProps.open) {
const opening = nextProps.open;
const closing = !opening;
if (opening) {
return this.setState({ expanded: true });
}
if (closing) {
return this.setState({ expanded: false });
}
if (nextProps.open !== this.props.open) {
this.setBodyOverflow(nextProps.open);
}
}
componentWillUnmount () {
this.setBodyOverflow(false);
}
render () {
const { expanded } = this.state;
const { children, className } = this.props;
const { activeStep, busy, busySteps, children, className, isChildModal, open, steps, title } = this.props;
const classes = [ styles.overlay, className ];
const backClasses = [ styles.backOverlay ];
if (expanded) {
classes.push(styles.expanded);
backClasses.push(styles.expanded);
if (!open) {
return null;
}
return (
<ReactPortal isOpened onClose={ this.handleClose }>
<div className={ backClasses.join(' ') } onClick={ this.handleClose }>
<ReactPortal
isOpened
onClose={ this.handleClose }
>
<div
className={ styles.backOverlay }
onClick={ this.handleClose }
>
<div
className={ classes.join(' ') }
className={
[
styles.overlay,
isChildModal
? styles.popover
: styles.modal,
className
].join(' ')
}
onClick={ this.stopEvent }
onKeyDown={ this.handleKeyDown }
>
<EventListener
target='window'
onKeyUp={ this.handleKeyUp }
/>
<ParityBackground className={ styles.parityBackground } />
{ this.renderCloseIcon() }
{ children }
{ this.renderClose() }
<Title
activeStep={ activeStep }
busy={ busy }
busySteps={ busySteps }
className={ styles.titleRow }
steps={ steps }
title={ title }
/>
<div className={ styles.childContainer }>
{ children }
</div>
{ this.renderButtons() }
</div>
</div>
</ReactPortal>
);
}
renderCloseIcon () {
const { expanded } = this.state;
renderButtons () {
const { buttons } = this.props;
if (!expanded) {
if (!buttons) {
return null;
}
return (
<div className={ styles.closeIcon } onClick={ this.handleClose }>
<CloseIcon />
<div className={ styles.buttonRow }>
{ buttons }
</div>
);
}
renderClose () {
const { hideClose } = this.props;
if (hideClose) {
return null;
}
return (
<CloseIcon
className={ styles.closeIcon }
onClick={ this.handleClose }
/>
);
}
stopEvent = (event) => {
event.preventDefault();
event.stopPropagation();
}
handleClose = () => {
this.props.onClose();
const { hideClose, onClose } = this.props;
if (!hideClose) {
onClose();
}
}
handleKeyDown = (event) => {
const { onKeyDown } = this.props;
event.persist();
return onKeyDown
? onKeyDown(event)
: false;
}
handleKeyUp = (event) => {
const codeName = keycode(event);
switch (codeName) {
case 'esc':
event.preventDefault();
return this.handleClose();
default:
event.persist();
return this.props.onKeyDown(event);
}
}
handleDOMAction = (ref, method) => {
const refItem = typeof ref === 'string' ? this.refs[ref] : ref;
const element = ReactDOM.findDOMNode(refItem);
const element = ReactDOM.findDOMNode(
typeof ref === 'string'
? this.refs[ref]
: ref
);
if (!element || typeof element[method] !== 'function') {
console.warn('could not find', ref, 'or method', method);
@ -132,4 +188,12 @@ export default class Portal extends Component {
return element[method]();
}
setBodyOverflow (open) {
if (!this.props.isChildModal) {
document.body.style.overflow = open
? 'hidden'
: null;
}
}
}

View File

@ -0,0 +1,64 @@
// 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 Portal from './';
let component;
let onClose;
function render (props = {}) {
onClose = sinon.stub();
component = shallow(
<Portal
onClose={ onClose }
open
{ ...props }
/>
);
return component;
}
describe('ui/Portal', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
describe('title rendering', () => {
const TITLE = 'some test title';
let title;
beforeEach(() => {
title = render({ title: TITLE }).find('Title');
});
it('renders the specified title', () => {
expect(title).to.have.length(1);
});
it('renders the passed title', () => {
expect(title.props().title).to.equal(TITLE);
});
});
});

View File

@ -14,4 +14,4 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './summary';
export default from './qrCode';

View File

@ -0,0 +1,63 @@
// 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 React, { Component } from 'react';
import PlaygroundExample from '~/playground/playgroundExample';
import QrCode from './';
export default class QrCodeExample extends Component {
render () {
return (
<div>
<PlaygroundExample name='Simple QRCode'>
<QrCode
value='this is a test'
/>
</PlaygroundExample>
<PlaygroundExample name='Simple QRCode with margin'>
<QrCode
margin={ 10 }
value='this is a test'
/>
</PlaygroundExample>
<PlaygroundExample name='Ethereum Address QRCode'>
<QrCode
margin={ 10 }
value='0x8c30393085C8C3fb4C1fB16165d9fBac5D86E1D9'
/>
</PlaygroundExample>
<PlaygroundExample name='Bitcoin Address QRCode'>
<QrCode
margin={ 10 }
value='3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy'
/>
</PlaygroundExample>
<PlaygroundExample name='Big QRCode'>
<QrCode
size={ 10 }
value='this is a test'
/>
</PlaygroundExample>
</div>
);
}
}

Some files were not shown because too many files have changed in this diff Show More