[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

@@ -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

@@ -0,0 +1,41 @@
/* 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/>.
*/
.container {
height: 100%;
position: relative;
}
.image {
left: 1.5em;
position: absolute;
top: 1.5em;
}
.description {
margin-left: 72px;
}
.title {
mragin-bottom: 0.5em;
}
.author, .version {
font-size: 0.75em;
opacity: 0.5;
margin-top: 0.5em;
}

View File

@@ -0,0 +1,98 @@
// 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 { Link } from 'react-router';
import Container, { Title as ContainerTitle } from '~/ui/Container';
import DappIcon from '~/ui/DappIcon';
import Tags from '~/ui/Tags';
import styles from './dappCard.css';
export default class DappCard extends Component {
static propTypes = {
app: PropTypes.object.isRequired,
children: PropTypes.node,
className: PropTypes.string,
onClick: PropTypes.func,
showLink: PropTypes.bool,
showTags: PropTypes.bool
};
static defaultProps = {
showLink: false,
showTags: false
};
render () {
const { app, children, className, onClick, showLink, showTags } = this.props;
if (!app) {
return null;
}
return (
<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={
showLink
? this.renderLink(app)
: app.name
}
byline={ app.description }
/>
<div className={ styles.author }>
{ app.author }, v{ app.version }
</div>
{ children }
</div>
</Container>
);
}
renderLink (app) {
return (
<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);
});
});
});

17
js/src/ui/QrCode/index.js Normal file
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 './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>
);
}
}

View File

@@ -0,0 +1,83 @@
// 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/>.
// https://github.com/cmanzana/qrcode-npm packaging the standard
// https://github.com/kazuhikoarase/qrcode-generator
import { qrcode } from 'qrcode-npm';
import React, { Component, PropTypes } from 'react';
const QROPTS = {
CODE_TYPE: 4,
ERROR_LEVEL: 'M'
};
export default class QrCode extends Component {
static propTypes = {
className: PropTypes.string,
margin: PropTypes.number,
size: PropTypes.number,
value: PropTypes.string.isRequired
};
static defaultProps = {
margin: 2,
size: 4
};
state = {
image: null
};
componentWillMount () {
this.generateCode(this.props);
}
componentWillReceiveProps (nextProps) {
const hasChanged = nextProps.value !== this.props.value ||
nextProps.size !== this.props.size ||
nextProps.margin !== this.props.margin;
if (hasChanged) {
this.generateCode(nextProps);
}
}
render () {
const { className } = this.props;
const { image } = this.state;
return (
<div
className={ className }
dangerouslySetInnerHTML={ {
__html: image
} }
/>
);
}
generateCode (props) {
const { margin, size, value } = props;
const qr = qrcode(QROPTS.CODE_TYPE, QROPTS.ERROR_LEVEL);
qr.addData(value);
qr.make();
this.setState({
image: qr.createImgTag(size, margin)
});
}
}

View File

@@ -0,0 +1,108 @@
// 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 QrCode from './';
const DEFAULT_PROPS = {
margin: 1,
size: 4,
value: 'someTestValue'
};
let component;
let instance;
function render (props = {}) {
component = shallow(
<QrCode
{ ...DEFAULT_PROPS }
{ ...props }
/>
);
instance = component.instance();
return component;
}
describe('ui/QrCode', () => {
beforeEach(() => {
render();
sinon.spy(instance, 'generateCode');
});
afterEach(() => {
instance.generateCode.restore();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
describe('lifecycle', () => {
describe('componentWillMount', () => {
it('generates the image on mount', () => {
instance.componentWillMount();
expect(instance.generateCode).to.have.been.calledWith(DEFAULT_PROPS);
});
});
describe('componentWillReceiveProps', () => {
it('does not re-generate when no props changed', () => {
instance.componentWillReceiveProps(DEFAULT_PROPS);
expect(instance.generateCode).not.to.have.been.called;
});
it('does not re-generate when className changed', () => {
const nextProps = Object.assign({}, DEFAULT_PROPS, { className: 'test' });
instance.componentWillReceiveProps(nextProps);
expect(instance.generateCode).not.to.have.been.called;
});
it('does not re-generate when additional property changed', () => {
const nextProps = Object.assign({}, DEFAULT_PROPS, { something: 'test' });
instance.componentWillReceiveProps(nextProps);
expect(instance.generateCode).not.to.have.been.called;
});
it('does re-generate when value changed', () => {
const nextProps = Object.assign({}, DEFAULT_PROPS, { value: 'somethingElse' });
instance.componentWillReceiveProps(nextProps);
expect(instance.generateCode).to.have.been.calledWith(nextProps);
});
it('does re-generate when size changed', () => {
const nextProps = Object.assign({}, DEFAULT_PROPS, { size: 10 });
instance.componentWillReceiveProps(nextProps);
expect(instance.generateCode).to.have.been.calledWith(nextProps);
});
it('does re-generate when margin changed', () => {
const nextProps = Object.assign({}, DEFAULT_PROPS, { margin: 10 });
instance.componentWillReceiveProps(nextProps);
expect(instance.generateCode).to.have.been.calledWith(nextProps);
});
});
});
});

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 './sectionList';

View File

@@ -0,0 +1,84 @@
/* 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/>.
*/
.section {
overflow-x: hidden;
position: relative;
.overlay {
background: rgba(0, 0, 0, 0.85);
bottom: 0;
left: 0;
padding: 1.5em;
position: absolute;
right: 0;
top: 0;
z-index: 199;
}
.row {
display: flex;
justify-content: center;
overflow-x: hidden;
/* TODO: As per JS comments, the flex-base could be adjusted in the future to allow for */
/* case where <> 3 columns are required should the need arrise from a UI pov. */
.item {
box-sizing: border-box;
cursor: pointer;
display: flex;
flex: 0 1 33.33%;
opacity: 0.75;
overflow-x: hidden;
padding: 0.25em;
transition: all 0.75s cubic-bezier(0.23, 1, 0.32, 1);
/* TODO: The hover and no-hover states can be improved to not "just appear" */
&:not(:hover) {
& [data-hover="hide"] {
}
& [data-hover="show"] {
display: none;
}
}
&:hover {
opacity: 1;
z-index: 100;
& [data-hover="hide"] {
display: none;
}
& [data-hover="show"] {
}
}
&.stretch-on:hover {
flex: 0 0 50%;
}
&.stretch-off:hover {
}
}
}
}
.section+.section {
margin-top: 1em;
}

View File

@@ -0,0 +1,94 @@
// 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 SectionList from './';
const ITEM_STYLE = {
backgroundColor: 'rgba(0, 0, 0, 0.75)',
padding: '1em'
};
const items = [
{ name: 'Jack', desc: 'Item number 1' },
{ name: 'Paul', desc: 'Item number 2' },
{ name: 'Matt', desc: 'Item number 3' },
{ name: 'Titi', desc: 'Item number 4' }
];
export default class SectionListExample extends Component {
state = {
showOverlay: true
};
render () {
return (
<div>
<PlaygroundExample name='Simple Usage'>
{ this.renderSimple() }
</PlaygroundExample>
<PlaygroundExample name='With Overlay'>
{ this.renderWithOverlay() }
</PlaygroundExample>
</div>
);
}
renderSimple () {
return (
<SectionList
items={ items }
renderItem={ this.renderItem }
/>
);
}
renderWithOverlay () {
const { showOverlay } = this.state;
const overlay = (
<div>
<p>Overlay</p>
<button onClick={ this.hideOverlay }>hide</button>
</div>
);
return (
<SectionList
items={ items }
overlay={ showOverlay ? overlay : null }
renderItem={ this.renderItem }
/>
);
}
renderItem (item, index) {
const { desc, name } = item;
return (
<div style={ ITEM_STYLE }>
<h3>{ name }</h3>
<h3 data-hover='show'>{ desc }</h3>
</div>
);
}
hideOverlay = () => {
this.setState({ showOverlay: false });
}
}

View File

@@ -0,0 +1,103 @@
// 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 { chunkArray } from '~/util/array';
import { arrayOrObjectProptype, nodeOrStringProptype } from '~/util/proptypes';
import styles from './sectionList.css';
// TODO: We probably want this to be passed via props - additional work required in that case to
// support the styling for both the hover and no-hover CSS for the pre/post sizes. Future work only
// if/when required.
const ITEMS_PER_ROW = 3;
export default class SectionList extends Component {
static propTypes = {
className: PropTypes.string,
items: arrayOrObjectProptype().isRequired,
renderItem: PropTypes.func.isRequired,
noStretch: PropTypes.bool,
overlay: nodeOrStringProptype()
};
static defaultProps = {
noStretch: false
};
render () {
const { className, items } = this.props;
if (!items || !items.length) {
return null;
}
return (
<section className={ [styles.section, className].join(' ') }>
{ this.renderOverlay() }
{ chunkArray(items, ITEMS_PER_ROW).map(this.renderRow) }
</section>
);
}
renderOverlay () {
const { overlay } = this.props;
if (!overlay) {
return null;
}
return (
<div className={ styles.overlay }>
{ overlay }
</div>
);
}
renderRow = (row, index) => {
return (
<div
className={ styles.row }
key={ `row_${index}` }
>
{ row.map(this.renderItem) }
</div>
);
}
renderItem = (item, index) => {
const { noStretch, renderItem } = this.props;
// NOTE: Any children that is to be showed or hidden (depending on hover state)
// should have the data-hover="show|hide" attributes. For the current implementation
// this does the trick, however there may be a case for adding a hover attribute
// to an item (mouseEnter/mouseLeave events) and then adjusting the styling with
// :root[hover]/:root:not[hover] for the tragetted elements. Currently it is a
// CSS-only solution to let the browser do all the work via selectors.
return (
<div
className={ [
styles.item,
styles[`stretch-${noStretch ? 'off' : 'on'}`]
].join(' ') }
key={ `item_${index}` }
>
{ renderItem(item, index) }
</div>
);
}
}

View File

@@ -0,0 +1,103 @@
// 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 SectionList from './';
const ITEMS = ['itemA', 'itemB', 'itemC', 'itemD', 'itemE'];
let component;
let instance;
let renderItem;
function render (props = {}) {
renderItem = sinon.stub();
component = shallow(
<SectionList
className='testClass'
items={ ITEMS }
renderItem={ renderItem }
section='testSection'
/>
);
instance = component.instance();
return component;
}
describe('SectionList', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
it('adds className as specified', () => {
expect(component.hasClass('testClass')).to.be.true;
});
describe('instance methods', () => {
describe('renderRow', () => {
let row;
beforeEach(() => {
sinon.stub(instance, 'renderItem');
row = instance.renderRow(['testA', 'testB']);
});
afterEach(() => {
instance.renderItem.restore();
});
it('renders a row', () => {
expect(row).to.be.ok;
});
it('adds a key for the row', () => {
expect(row.key).to.be.ok;
});
it('calls renderItem for the items', () => {
expect(instance.renderItem).to.have.been.calledTwice;
});
});
describe('renderItem', () => {
let item;
beforeEach(() => {
item = instance.renderItem('testItem', 50);
});
it('renders an item', () => {
expect(item).to.be.ok;
});
it('adds a key for the item', () => {
expect(item.key).to.be.ok;
});
it('calls the external renderer', () => {
expect(renderItem).to.have.been.calledWith('testItem', 50);
});
});
});
});

View File

@@ -28,14 +28,21 @@ export default class Tags extends Component {
}
render () {
return (<div className={ styles.tags }>
{ this.renderTags() }
</div>);
const { tags } = this.props;
if (!tags || tags.length === 0) {
return null;
}
return (
<div className={ styles.tags }>
{ this.renderTags() }
</div>
);
}
renderTags () {
const { handleAddSearchToken, setRefs } = this.props;
const tags = this.props.tags || [];
const { handleAddSearchToken, setRefs, tags } = this.props;
const tagClasses = handleAddSearchToken
? [ styles.tag, styles.tagClickable ]
@@ -47,14 +54,14 @@ export default class Tags extends Component {
return tags
.sort()
.map((tag, idx) => {
.map((tag, index) => {
const onClick = handleAddSearchToken
? () => handleAddSearchToken(tag)
: null;
return (
<div
key={ idx }
key={ `tag_${index}` }
className={ tagClasses.join(' ') }
onClick={ onClick }
ref={ setRef }

26
js/src/ui/Title/title.css Normal file
View File

@@ -0,0 +1,26 @@
/* 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/>.
*/
.title {
.steps {
margin: -0.5em 0 -1em 0;
}
.waiting {
margin: 1em -1em -1em -1em;
}
}

View File

@@ -14,35 +14,49 @@
// 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 { LinearProgress } from 'material-ui';
import { Step, Stepper, StepLabel } from 'material-ui/Stepper';
import React, { Component, PropTypes } from 'react';
// TODO: It would make sense (going forward) to replace all uses of
// ContainerTitle with this component. In that case the styles for the
// h3 (title) can be pulled from there. (As it stands the duplication
// between the 2 has been removed, but as a short-term DRY only)
import { Title as ContainerTitle } from '~/ui/Container';
import { nodeOrStringProptype } from '~/util/proptypes';
import styles from '../modal.css';
import styles from './title.css';
export default class Title extends Component {
static propTypes = {
activeStep: PropTypes.number,
busy: PropTypes.bool,
current: PropTypes.number,
busySteps: PropTypes.array,
className: PropTypes.string,
steps: PropTypes.array,
waiting: PropTypes.array,
title: nodeOrStringProptype()
}
render () {
const { current, steps, title } = this.props;
const { activeStep, className, steps, title } = this.props;
if (!title && !steps) {
return null;
}
return (
<div className={ styles.title }>
<h3>
{
<div
className={
[styles.title, className].join(' ')
}
>
<ContainerTitle
title={
steps
? steps[current]
? steps[activeStep || 0]
: title
}
</h3>
/>
{ this.renderSteps() }
{ this.renderWaiting() }
</div>
@@ -50,7 +64,7 @@ export default class Title extends Component {
}
renderSteps () {
const { current, steps } = this.props;
const { activeStep, steps } = this.props;
if (!steps) {
return;
@@ -58,8 +72,7 @@ export default class Title extends Component {
return (
<div className={ styles.steps }>
<Stepper
activeStep={ current }>
<Stepper activeStep={ activeStep }>
{ this.renderTimeline() }
</Stepper>
</div>
@@ -82,8 +95,8 @@ export default class Title extends Component {
}
renderWaiting () {
const { current, busy, waiting } = this.props;
const isWaiting = busy || (waiting || []).includes(current);
const { activeStep, busy, busySteps } = this.props;
const isWaiting = busy || (busySteps || []).includes(activeStep);
if (!isWaiting) {
return null;

View File

@@ -14,6 +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 AccountCard from './AccountCard';
import Actionbar from './Actionbar';
import ActionbarExport from './Actionbar/Export';
import ActionbarImport from './Actionbar/Import';
@@ -28,9 +29,12 @@ import ConfirmDialog from './ConfirmDialog';
import Container, { Title as ContainerTitle } from './Container';
import ContextProvider from './ContextProvider';
import CopyToClipboard from './CopyToClipboard';
import CurrencySymbol from './CurrencySymbol';
import DappCard from './DappCard';
import DappIcon from './DappIcon';
import Editor from './Editor';
import Errors from './Errors';
import Form, { AddressSelect, FormWrap, TypedInput, Input, InputAddress, InputAddressSelect, InputChip, InputInline, Select, RadioButtons } from './Form';
import Form, { AddressSelect, FormWrap, Input, InputAddress, InputAddressSelect, InputChip, InputDate, InputInline, InputTime, Label, RadioButtons, Select, TypedInput } from './Form';
import GasPriceEditor from './GasPriceEditor';
import GasPriceSelector from './GasPriceSelector';
import Icons from './Icons';
@@ -44,15 +48,20 @@ import muiTheme from './Theme';
import Page from './Page';
import ParityBackground from './ParityBackground';
import PasswordStrength from './Form/PasswordStrength';
import Portal from './Portal';
import QrCode from './QrCode';
import SectionList from './SectionList';
import ShortenedHash from './ShortenedHash';
import SignerIcon from './SignerIcon';
import Tags from './Tags';
import Title from './Title';
import Tooltips, { Tooltip } from './Tooltips';
import TxHash from './TxHash';
import TxList from './TxList';
import Warning from './Warning';
export {
AccountCard,
Actionbar,
ActionbarExport,
ActionbarImport,
@@ -69,6 +78,9 @@ export {
ContainerTitle,
ContextProvider,
CopyToClipboard,
CurrencySymbol,
DappIcon,
DappCard,
Editor,
Errors,
Form,
@@ -80,9 +92,12 @@ export {
InputAddress,
InputAddressSelect,
InputChip,
InputDate,
InputInline,
InputTime,
IdentityIcon,
IdentityName,
Label,
LanguageSelector,
Loading,
MethodDecoding,
@@ -93,11 +108,15 @@ export {
Page,
ParityBackground,
PasswordStrength,
Portal,
QrCode,
RadioButtons,
ShortenedHash,
Select,
ShortenedHash,
SectionList,
SignerIcon,
Tags,
Title,
Tooltip,
Tooltips,
TxHash,