Vault Management UI (round 3) (#4652)

* Render Dapps via SectionList

* Initial rendering of accounts via SectionList

* Width vars

* Allow classNames in certifications & tags

* Overlay of info on hover

* Adjust hover balances

* Large owner icons (align with vaults)

* Consistent block mined at message

* Attach ParityBackground to html

* Adjust page padding to align

* Lint fixes

* Link to different types of addresses

* Make content parts clickable only (a within a)

* Force Chrome hardware acceleration

* Trust the vendors... don't go crazy with transform :)

* Use faster & default transitions

* Add VaultMeta edit dialog

* Updated (WIP)

* Meta & password edit completed

* Added SelectionList component for selections

* Use SelectionList in DappPermisions

* AddDapps uses SelectionList

* Fix AccountCard to consistent height

* Display type icons in creation dialog

* Complimentary colours

* Convert Signer defaults to SelectionList

* Fix Geth import - actually pass addresses through

* Work from addresses returned via RPC

* Display actual addresses imported (not selected)

* Update tests to cover bug fixed

* Prettyfy Geth import

* Description on selection actions

* SelectionList as entry point

* Update failing tests

* Subtle selection border

* Styling updates for account details

* Add ModalBox summary

* AddAddress updated

* Display account vault information

* Allow invalid addresses to display icons (e.g. vaults)

* Display vault on edit meta

* Convert VaultAccounts to SelectionList

* Allow editing of Vault in meta

* Add tests for SectionList component

* Add tests for ModalBox component

* Add tests for VaultSelector component

* Add vaultsOpened in store

* Add ~/ui/Form/VaultSelect

* WIP

* Fix failing tests

* Move account to vault when selected

* Fix circular build deps

* EditMeta uses Form/VaultSelect

* Vault move into meta store (alignment)

* Re-apply stretch fix

* Display vault in account summary

* Add busy indicators to relevant modals

* Auto-focus description field (aligns with #4657)

* Remove extra container (double scrolling)

* Remove unused container style

* Apply scroll fixes from lates commit in #4621

* Remove unneeded logs

* Remove extra div, fixing ParityBar overflow

* Make dapp iframe background white

* Stop event propgation on tag click

* ChangeVault component (re-usable)

* Use ChangeVault component

* Pass vaultStores in

* Icon highlight colour

* Tag-ify vault name display

* ChangeVault location

* Bothced merge, selector rendering twice

* Value can be undefined (no vault)

* Close selector on Select bug

* Fix toggle botched merge

* Update tests

* Add Vault Tags to Account Header
This commit is contained in:
Jaco Greeff 2017-03-03 19:50:54 +01:00 committed by Gav Wood
parent cb118f1936
commit 1548201551
35 changed files with 822 additions and 134 deletions

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 { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { VaultSelect } from '~/ui';
@observer
export default class ChangeVault extends Component {
static propTypes = {
store: PropTypes.object.isRequired,
vaultStore: PropTypes.object
}
render () {
const { store, vaultStore } = this.props;
const { vaultName } = store;
if (!vaultStore || vaultStore.vaultsOpened.length === 0) {
return null;
}
return (
<VaultSelect
onSelect={ this.onSelect }
value={ vaultName }
vaultStore={ vaultStore }
/>
);
}
onSelect = (vaultName) => {
const { store } = this.props;
store.setVaultName(vaultName);
}
}

View File

@ -0,0 +1,100 @@
// 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 ChangeVault from './';
let component;
let instance;
let store;
let vaultStore;
function createStore () {
store = {
setVaultName: sinon.stub(),
vaultName: 'testing'
};
return store;
}
function createVaultStore () {
vaultStore = {
vaultsOpened: ['testing']
};
return vaultStore;
}
function render () {
component = shallow(
<ChangeVault
store={ createStore() }
vaultStore={ createVaultStore() }
/>
);
instance = component.instance();
return component;
}
describe('modals/CreateAccount/ChangeVault', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
describe('components', () => {
describe('VaultSelect', () => {
let select;
beforeEach(() => {
select = component.find('VaultSelect');
});
it('renders', () => {
expect(select.get(0)).to.be.ok;
});
it('passes onSelect as instance method', () => {
expect(select.props().onSelect).to.equal(instance.onSelect);
});
it('passes the value', () => {
expect(select.props().value).to.equal('testing');
});
it('passes the vaultStore', () => {
expect(select.props().vaultStore).to.equal(vaultStore);
});
});
});
describe('instance methods', () => {
describe('onSelect', () => {
it('calls into store setVaultName', () => {
instance.onSelect('newName');
expect(store.setVaultName).to.have.been.calledWith('newName');
});
});
});
});

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

View File

@ -24,13 +24,15 @@ import { Form, Input, IdentityIcon } from '~/ui';
import PasswordStrength from '~/ui/Form/PasswordStrength';
import { RefreshIcon } from '~/ui/Icons';
import ChangeVault from '../ChangeVault';
import styles from '../createAccount.css';
@observer
export default class CreateAccount extends Component {
static propTypes = {
newError: PropTypes.func.isRequired,
store: PropTypes.object.isRequired
store: PropTypes.object.isRequired,
vaultStore: PropTypes.object
}
state = {
@ -123,6 +125,10 @@ export default class CreateAccount extends Component {
</div>
</div>
<PasswordStrength input={ password } />
<ChangeVault
store={ this.props.store }
vaultStore={ this.props.vaultStore }
/>
{ this.renderIdentitySelector() }
{ this.renderIdentities() }
</Form>

View File

@ -20,12 +20,15 @@ import { FormattedMessage } from 'react-intl';
import { Form, FileSelect, Input } from '~/ui';
import ChangeVault from '../ChangeVault';
import styles from '../createAccount.css';
@observer
export default class NewImport extends Component {
static propTypes = {
store: PropTypes.object.isRequired
store: PropTypes.object.isRequired,
vaultStore: PropTypes.object
}
render () {
@ -88,6 +91,10 @@ export default class NewImport extends Component {
/>
</div>
</div>
<ChangeVault
store={ this.props.store }
vaultStore={ this.props.vaultStore }
/>
{ this.renderFileSelector() }
</Form>
);

View File

@ -21,6 +21,7 @@ import { FormattedMessage } from 'react-intl';
import { Form, Input } from '~/ui';
import PasswordStrength from '~/ui/Form/PasswordStrength';
import ChangeVault from '../ChangeVault';
import styles from '../createAccount.css';
@observer
@ -30,7 +31,8 @@ export default class RawKey extends Component {
}
static propTypes = {
store: PropTypes.object.isRequired
store: PropTypes.object.isRequired,
vaultStore: PropTypes.object
}
render () {
@ -131,6 +133,10 @@ export default class RawKey extends Component {
</div>
</div>
<PasswordStrength input={ password } />
<ChangeVault
store={ this.props.store }
vaultStore={ this.props.vaultStore }
/>
</Form>
);
}

View File

@ -22,12 +22,14 @@ import { Checkbox } from 'material-ui';
import { Form, Input } from '~/ui';
import PasswordStrength from '~/ui/Form/PasswordStrength';
import ChangeVault from '../ChangeVault';
import styles from '../createAccount.css';
@observer
export default class RecoveryPhrase extends Component {
static propTypes = {
store: PropTypes.object.isRequired
store: PropTypes.object.isRequired,
vaultStore: PropTypes.object
}
render () {
@ -127,6 +129,10 @@ export default class RecoveryPhrase extends Component {
</div>
</div>
<PasswordStrength input={ password } />
<ChangeVault
store={ this.props.store }
vaultStore={ this.props.vaultStore }
/>
<Checkbox
checked={ isWindowsPhrase }
className={ styles.checkbox }

View File

@ -109,6 +109,7 @@
display: flex;
.icon {
color: rgb(167, 151, 0) !important;
flex: 0 0 56px;
height: 56px !important;
margin-right: 0.75em;

View File

@ -20,11 +20,13 @@ import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import ParityLogo from '~/../assets/images/parity-logo-black-no-text.svg';
import { createIdentityImg } from '~/api/util/identity';
import { newError } from '~/redux/actions';
import { Button, ModalBox, Portal } from '~/ui';
import { CancelIcon, CheckIcon, DoneIcon, NextIcon, PrevIcon, PrintIcon } from '~/ui/Icons';
import ParityLogo from '~/../assets/images/parity-logo-black-no-text.svg';
import VaultStore from '~/views/Vaults/store';
import AccountDetails from './AccountDetails';
import AccountDetailsGeth from './AccountDetailsGeth';
@ -82,13 +84,19 @@ class CreateAccount extends Component {
}
store = new Store(this.context.api, this.props.accounts);
vaultStore = VaultStore.get(this.context.api);
componentWillMount () {
return this.vaultStore.loadVaults();
}
render () {
const { createType, stage } = this.store;
const { isBusy, createType, stage } = this.store;
return (
<Portal
buttons={ this.renderDialogActions() }
busy={ isBusy }
activeStep={ stage }
onClose={ this.onClose }
open
@ -120,6 +128,7 @@ class CreateAccount extends Component {
<NewAccount
newError={ this.props.newError }
store={ this.store }
vaultStore={ this.vaultStore }
/>
);
}
@ -132,18 +141,27 @@ class CreateAccount extends Component {
if (createType === 'fromPhrase') {
return (
<RecoveryPhrase store={ this.store } />
<RecoveryPhrase
store={ this.store }
vaultStore={ this.vaultStore }
/>
);
}
if (createType === 'fromRaw') {
return (
<RawKey store={ this.store } />
<RawKey
store={ this.store }
vaultStore={ this.vaultStore }
/>
);
}
return (
<NewImport store={ this.store } />
<NewImport
store={ this.store }
vaultStore={ this.vaultStore }
/>
);
case STAGE_INFO:
@ -266,7 +284,7 @@ class CreateAccount extends Component {
this.store.setBusy(true);
return this.store
.createAccount()
.createAccount(this.vaultStore)
.then(() => {
this.store.setBusy(false);
this.store.nextStage();

View File

@ -42,7 +42,9 @@ function createApi () {
newAccountFromWallet: sinon.stub().resolves(ADDRESS),
phraseToAddress: () => Promise.resolve(`${++counter}`),
setAccountMeta: sinon.stub().resolves(),
setAccountName: sinon.stub().resolves()
setAccountName: sinon.stub().resolves(),
listVaults: sinon.stub().resolves([]),
listOpenedVaults: sinon.stub().resolves([])
}
};
}

View File

@ -44,6 +44,7 @@ export default class Store {
@observable rawKey = '';
@observable rawKeyError = ERRORS.nokey;
@observable stage = STAGE_SELECT_TYPE;
@observable vaultName = '';
@observable walletFile = '';
@observable walletFileError = ERRORS.noFile;
@observable walletJson = '';
@ -95,6 +96,7 @@ export default class Store {
this.nameError = null;
this.rawKey = '';
this.rawKeyError = null;
this.vaultName = '';
this.walletFile = '';
this.walletFileError = null;
this.walletJson = '';
@ -134,6 +136,10 @@ export default class Store {
this.gethImported = gethImported;
}
@action setVaultName = (vaultName) => {
this.vaultName = vaultName;
}
@action setWindowsPhrase = (isWindowsPhrase = false) => {
this.isWindowsPhrase = isWindowsPhrase;
}
@ -220,7 +226,28 @@ export default class Store {
this.stage--;
}
createAccount = () => {
createAccount = (vaultStore) => {
this.setBusy(true);
return this
._createAccount()
.then(() => {
if (vaultStore && this.vaultName && this.vaultName.length) {
return vaultStore.moveAccount(this.vaultName, this.address);
}
return true;
})
.then(() => {
this.setBusy(false);
})
.catch((error) => {
this.setBusy(false);
throw error;
});
}
_createAccount = () => {
switch (this.createType) {
case 'fromGeth':
return this.createAccountFromGeth();

View File

@ -22,8 +22,20 @@ import { ACCOUNTS, ADDRESS, GETH_ADDRESSES, createApi } from './createAccount.te
let api;
let store;
let vaultStore;
function createVaultStore () {
vaultStore = {
moveAccount: sinon.stub().resolves(),
listVaults: sinon.stub().resolves()
};
return vaultStore;
}
function createStore (loadGeth) {
createVaultStore();
api = createApi();
store = new Store(api, ACCOUNTS, loadGeth);
@ -65,8 +77,9 @@ describe('modals/CreateAccount/Store', () => {
describe('@action', () => {
describe('clearErrors', () => {
beforeEach(() => {
store.setName('');
store.setPassword('123');
store.setName('testing');
store.setPassword('testing');
store.setVaultName('testing');
store.setRawKey('test');
store.setWalletFile('test');
store.setWalletJson('test');
@ -75,10 +88,13 @@ describe('modals/CreateAccount/Store', () => {
it('clears all errors', () => {
store.clearErrors();
expect(store.name).to.equal('');
expect(store.nameError).to.be.null;
expect(store.password).to.equal('');
expect(store.passwordRepeatError).to.be.null;
expect(store.rawKey).to.equal('');
expect(store.rawKeyError).to.be.null;
expect(store.vaultName).to.equal('');
expect(store.walletFile).to.equal('');
expect(store.walletFileError).to.be.null;
expect(store.walletJson).to.equal('');
@ -198,6 +214,13 @@ describe('modals/CreateAccount/Store', () => {
});
});
describe('setVaultName', () => {
it('sets the vault name', () => {
store.setVaultName('testVault');
expect(store.vaultName).to.equal('testVault');
});
});
describe('setWalletFile', () => {
it('sets the filepath', () => {
store.setWalletFile('testing');
@ -384,12 +407,22 @@ describe('modals/CreateAccount/Store', () => {
let createAccountFromWalletSpy;
let createAccountFromPhraseSpy;
let createAccountFromRawSpy;
let busySpy;
beforeEach(() => {
createAccountFromGethSpy = sinon.spy(store, 'createAccountFromGeth');
createAccountFromWalletSpy = sinon.spy(store, 'createAccountFromWallet');
createAccountFromPhraseSpy = sinon.spy(store, 'createAccountFromPhrase');
createAccountFromRawSpy = sinon.spy(store, 'createAccountFromRaw');
busySpy = sinon.spy(store, 'setBusy');
});
afterEach(() => {
store.createAccountFromGeth.restore();
store.createAccountFromWallet.restore();
store.createAccountFromPhrase.restore();
store.createAccountFromRaw.restore();
store.setBusy.restore();
});
it('throws error on invalid createType', () => {
@ -399,38 +432,68 @@ describe('modals/CreateAccount/Store', () => {
it('calls createAccountFromGeth on createType === fromGeth', () => {
store.setCreateType('fromGeth');
store.createAccount();
expect(createAccountFromGethSpy).to.have.been.called;
return store.createAccount().then(() => {
expect(createAccountFromGethSpy).to.have.been.called;
});
});
it('calls createAccountFromWallet on createType === fromJSON', () => {
store.setCreateType('fromJSON');
store.createAccount();
expect(createAccountFromWalletSpy).to.have.been.called;
return store.createAccount().then(() => {
expect(createAccountFromWalletSpy).to.have.been.called;
});
});
it('calls createAccountFromPhrase on createType === fromNew', () => {
store.setCreateType('fromNew');
store.createAccount();
expect(createAccountFromPhraseSpy).to.have.been.called;
return store.createAccount().then(() => {
expect(createAccountFromPhraseSpy).to.have.been.called;
});
});
it('calls createAccountFromPhrase on createType === fromPhrase', () => {
store.setCreateType('fromPhrase');
store.createAccount();
expect(createAccountFromPhraseSpy).to.have.been.called;
return store.createAccount().then(() => {
expect(createAccountFromPhraseSpy).to.have.been.called;
});
});
it('calls createAccountFromWallet on createType === fromPresale', () => {
store.setCreateType('fromPresale');
store.createAccount();
expect(createAccountFromWalletSpy).to.have.been.called;
return store.createAccount().then(() => {
expect(createAccountFromWalletSpy).to.have.been.called;
});
});
it('calls createAccountFromRaw on createType === fromRaw', () => {
store.setCreateType('fromRaw');
store.createAccount();
expect(createAccountFromRawSpy).to.have.been.called;
return store.createAccount().then(() => {
expect(createAccountFromRawSpy).to.have.been.called;
});
});
it('moves account to vault when vaultName set', () => {
store.setCreateType('fromNew');
store.setVaultName('testing');
return store.createAccount(vaultStore).then(() => {
expect(vaultStore.moveAccount).to.have.been.calledWith('testing', ADDRESS);
});
});
it('sets and rests the busy flag', () => {
store.setCreateType('fromNew');
return store.createAccount().then(() => {
expect(busySpy).to.have.been.calledWith(true);
expect(busySpy).to.have.been.calledWith(false);
});
});
describe('createAccountFromGeth', () => {

View File

@ -37,25 +37,27 @@ class DeleteAccount extends Component {
}
state = {
isBusy: false,
password: ''
}
render () {
const { account } = this.props;
const { password } = this.state;
const { isBusy, password } = this.state;
return (
<ConfirmDialog
busy={ isBusy }
className={ styles.body }
onConfirm={ this.onDeleteConfirmed }
onDeny={ this.closeDeleteDialog }
open
title={
<FormattedMessage
id='deleteAccount.title'
defaultMessage='confirm removal'
/>
}
visible
>
<div className={ styles.hero }>
<FormattedMessage
@ -117,9 +119,13 @@ class DeleteAccount extends Component {
const { account, newError } = this.props;
const { password } = this.state;
this.setState({ isBusy: true });
return api.parity
.killAccount(account.address, password)
.then((result) => {
this.setState({ isBusy: true });
if (result === true) {
router.push('/accounts');
this.closeDeleteDialog();
@ -128,6 +134,7 @@ class DeleteAccount extends Component {
}
})
.catch((error) => {
this.setState({ isBusy: false });
console.error('onDeleteConfirmed', error);
newError(new Error(`Deletion failed: ${error.message}`));
});

View File

@ -21,11 +21,10 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { newError } from '~/redux/actions';
import { Button, Form, Input, InputAddress, InputChip, Portal } from '~/ui';
import { Button, Form, Input, InputChip, Portal, VaultSelect } from '~/ui';
import { CancelIcon, SaveIcon } from '~/ui/Icons';
import VaultStore from '~/views/Vaults/store';
import VaultSelector from '../VaultSelector';
import Store from './store';
@observer
@ -48,11 +47,12 @@ class EditMeta extends Component {
}
render () {
const { description, name, nameError, tags } = this.store;
const { description, isBusy, name, nameError, tags } = this.store;
return (
<Portal
buttons={ this.renderActions() }
busy={ isBusy }
onClose={ this.onClose }
open
title={
@ -62,7 +62,6 @@ class EditMeta extends Component {
/>
}
>
{ this.renderVaultSelector() }
<Form>
<Input
autoFocus
@ -110,7 +109,7 @@ class EditMeta extends Component {
onTokensChange={ this.store.setTags }
tokens={ tags.slice() }
/>
{ this.renderVault() }
{ this.renderVaultSelector() }
</Form>
</Portal>
);
@ -163,7 +162,7 @@ class EditMeta extends Component {
);
}
renderVault () {
renderVaultSelector () {
const { isAccount, vaultName } = this.store;
if (!isAccount) {
@ -171,40 +170,9 @@ class EditMeta extends Component {
}
return (
<InputAddress
allowCopy={ false }
allowInvalid
readOnly
hint={
<FormattedMessage
id='editMeta.vault.hint'
defaultMessage='the vault this account is attached to'
/>
}
label={
<FormattedMessage
id='editMeta.vault.label'
defaultMessage='associated vault'
/>
}
onClick={ this.toggleVaultSelector }
value={ vaultName }
/>
);
}
renderVaultSelector () {
const { isAccount, isVaultSelectorOpen, vaultName } = this.store;
if (!isAccount || !isVaultSelectorOpen) {
return null;
}
return (
<VaultSelector
onClose={ this.toggleVaultSelector }
<VaultSelect
onSelect={ this.setVaultName }
selected={ vaultName }
value={ vaultName }
vaultStore={ this.vaultStore }
/>
);
@ -215,21 +183,12 @@ class EditMeta extends Component {
}
onSave = () => {
const { address, isAccount, meta, vaultName } = this.store;
if (this.store.hasError) {
return;
}
return this.store
.save()
.then(() => {
if (isAccount && (meta.vault !== vaultName)) {
return this.vaultStore.moveAccount(vaultName, address);
}
return true;
})
.save(this.vaultStore)
.then(this.onClose)
.catch((error) => {
this.props.newError(error);
@ -238,11 +197,6 @@ class EditMeta extends Component {
setVaultName = (vaultName) => {
this.store.setVaultName(vaultName);
this.toggleVaultSelector();
}
toggleVaultSelector = () => {
this.store.toggleVaultSelector();
}
}

View File

@ -21,7 +21,7 @@ import { validateName } from '~/util/validation';
export default class Store {
@observable address = null;
@observable isAccount = false;
@observable isVaultSelectorOpen = false;
@observable isBusy = false;
@observable description = null;
@observable meta = null;
@observable name = null;
@ -73,6 +73,10 @@ export default class Store {
this.passwordHint = passwordHint;
}
@action setBusy = (isBusy) => {
this.isBusy = isBusy;
}
@action setTags = (tags) => {
this.tags = tags.slice();
}
@ -81,11 +85,9 @@ export default class Store {
this.vaultName = vaultName;
}
@action setVaultSelectorOpen = (isOpen) => {
this.isVaultSelectorOpen = isOpen;
}
save (vaultStore) {
this.setBusy(true);
save () {
const meta = {
description: this.description,
tags: this.tags.peek()
@ -100,13 +102,20 @@ export default class Store {
this._api.parity.setAccountName(this.address, this.name),
this._api.parity.setAccountMeta(this.address, Object.assign({}, this.meta, meta))
])
.then(() => {
if (vaultStore && this.isAccount && (this.meta.vault !== this.vaultName)) {
return vaultStore.moveAccount(this.vaultName, this.address);
}
return true;
})
.then(() => {
this.setBusy(false);
})
.catch((error) => {
console.error('onSave', error);
this.setBusy(false);
throw error;
});
}
toggleVaultSelector () {
this.setVaultSelectorOpen(!this.isVaultSelectorOpen);
}
}

View File

@ -14,14 +14,24 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import sinon from 'sinon';
import Store from './store';
import { ACCOUNT, ADDRESS, createApi } from './editMeta.test.js';
let api;
let store;
let vaultStore;
function createVaultStore () {
return {
moveAccount: sinon.stub().resolves(true)
};
}
function createStore (account) {
api = createApi();
vaultStore = createVaultStore();
store = new Store(api, account);
@ -108,6 +118,13 @@ describe('modals/EditMeta/Store', () => {
createStore(ADDRESS);
});
describe('setBusy', () => {
it('sets the isBusy flag', () => {
store.setBusy('testing');
expect(store.isBusy).to.equal('testing');
});
});
describe('setDescription', () => {
it('sets the description', () => {
store.setDescription('description');
@ -149,26 +166,56 @@ describe('modals/EditMeta/Store', () => {
expect(store.vaultName).to.equal('testing');
});
});
describe('setVaultSelectorOpen', () => {
it('sets the state', () => {
store.setVaultSelectorOpen('testing');
expect(store.isVaultSelectorOpen).to.equal('testing');
});
});
});
describe('operations', () => {
describe('save', () => {
beforeEach(() => {
createStore(ACCOUNT);
sinon.spy(store, 'setBusy');
});
afterEach(() => {
store.setBusy.restore();
});
it('sets the busy flag, clearing it when done', () => {
return store.save().then(() => {
expect(store.setBusy).to.have.been.calledWith(true);
expect(store.setBusy).to.have.been.calledWith(false);
});
});
it('calls parity.setAccountName with the set value', () => {
store.setName('test name');
store.save();
expect(api.parity.setAccountName).to.be.calledWith(ACCOUNT.address, 'test name');
return store.save().then(() => {
expect(api.parity.setAccountName).to.be.calledWith(ACCOUNT.address, 'test name');
});
});
it('calls parity.setAccountMeta with the adjusted values', () => {
store.setDescription('some new description');
store.setPasswordHint('some new passwordhint');
store.setTags(['taga']);
return store.save().then(() => {
expect(api.parity.setAccountMeta).to.have.been.calledWith(
ACCOUNT.address, Object.assign({}, ACCOUNT.meta, {
description: 'some new description',
passwordHint: 'some new passwordhint',
tags: ['taga']
})
);
});
});
it('moves vault account when applicable', () => {
store.setVaultName('testing');
return store.save(vaultStore).then(() => {
expect(vaultStore.moveAccount).to.have.been.calledWith('testing', ACCOUNT.address);
});
});
it('calls parity.setAccountMeta with the adjusted values', () => {
@ -185,11 +232,4 @@ describe('modals/EditMeta/Store', () => {
});
});
});
describe('toggleVaultSelector', () => {
it('inverts the selector state', () => {
store.toggleVaultSelector();
expect(store.isVaultSelectorOpen).to.be.true;
});
});
});

View File

@ -18,7 +18,9 @@ import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { Portal, SelectionList, VaultCard } from '~/ui';
import Portal from '~/ui/Portal';
import SelectionList from '~/ui/SelectionList';
import VaultCard from '~/ui/VaultCard';
@observer
export default class VaultSelector extends Component {
@ -48,10 +50,9 @@ export default class VaultSelector extends Component {
}
renderList () {
const { vaults } = this.props.vaultStore;
const openVaults = vaults.filter((vault) => vault.isOpen);
const { vaultsOpened } = this.props.vaultStore;
if (openVaults.length === 0) {
if (vaultsOpened.length === 0) {
return (
<FormattedMessage
id='vaults.selector.noneAvailable'
@ -62,7 +63,7 @@ export default class VaultSelector extends Component {
return (
<SelectionList
items={ openVaults }
items={ vaultsOpened }
isChecked={ this.isSelected }
noStretch
onSelectClick={ this.onSelect }

View File

@ -28,6 +28,7 @@ const VAULTS_CLOSED = [
{ name: 'C' },
{ name: 'D' }
];
const VAULTS_ALL = VAULTS_OPENED.concat(VAULTS_CLOSED);
let component;
let instance;
@ -37,7 +38,8 @@ let vaultStore;
function createVaultStore () {
vaultStore = {
vaults: VAULTS_OPENED.concat(VAULTS_CLOSED)
vaults: VAULTS_ALL,
vaultsOpened: VAULTS_OPENED
};
return vaultStore;

View File

@ -18,7 +18,7 @@
.balances {
display: flex;
flex-wrap: wrap;
margin: 1em 0 0 0;
margin: 0.75em 0 0 0;
vertical-align: top;
}

View File

@ -16,7 +16,7 @@
*/
.certifications {
margin-top: 1em;
margin-top: 0.75em;
}
.certification,
@ -43,7 +43,7 @@
background-color: rgba(255, 255, 255, 0.07);
margin-right: 0.5em;
margin-top: 1em;
padding: 0.3em 0.6em 0.2em 2.6em;
padding: 0.3em 0.6em 0.2em 3em;
text-transform: uppercase;
&:last-child {
@ -52,7 +52,7 @@
.icon {
position: absolute;
top: -.25em;
top: -0.25em;
left: 0;
}
}

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

View File

@ -0,0 +1,109 @@
// 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 { FormattedMessage } from 'react-intl';
import VaultSelector from '~/modals/VaultSelector';
import VaultStore from '~/views/Vaults/store';
import InputAddress from '../InputAddress';
export default class VaultSelect extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
onSelect: PropTypes.func.isRequired,
value: PropTypes.string,
vaultStore: PropTypes.object
};
state = {
isOpen: false
};
vaultStore = this.props.vaultStore || VaultStore.get(this.context.api);
componentWillMount () {
return this.vaultStore.loadVaults();
}
render () {
const { value } = this.props;
return (
<div>
{ this.renderSelector() }
<InputAddress
allowCopy={ false }
allowInvalid
disabled
hint={
<FormattedMessage
id='ui.vaultSelect.hint'
defaultMessage='the vault this account is attached to'
/>
}
label={
<FormattedMessage
id='ui.vaultSelect.label'
defaultMessage='associated vault'
/>
}
onClick={ this.openSelector }
value={ (value || '').toUpperCase() }
/>
</div>
);
}
renderSelector () {
const { value } = this.props;
const { isOpen } = this.state;
if (!isOpen) {
return null;
}
return (
<VaultSelector
onClose={ this.closeSelector }
onSelect={ this.onSelect }
selected={ value }
vaultStore={ this.vaultStore }
/>
);
}
openSelector = () => {
this.setState({
isOpen: true
});
}
closeSelector = () => {
this.setState({
isOpen: false
});
}
onSelect = (vaultName) => {
this.props.onSelect(vaultName);
this.closeSelector();
}
}

View File

@ -0,0 +1,90 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import VaultSelect from './';
let component;
let instance;
let onSelect;
let vaultStore;
function createVaultStore () {
vaultStore = {
loadVaults: sinon.stub().resolves(true)
};
return vaultStore;
}
function render () {
onSelect = sinon.stub();
component = shallow(
<VaultSelect
onSelect={ onSelect }
value='initialValue'
vaultStore={ createVaultStore() }
/>
);
instance = component.instance();
return component;
}
describe('ui/Form/VaultSelect', () => {
beforeEach(() => {
render();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
describe('components', () => {
describe('InputAddress', () => {
let input;
beforeEach(() => {
input = component.find('Connect(InputAddress)');
});
it('renders', () => {
expect(input.get(0)).to.be.ok;
});
it('passes value from props', () => {
expect(input.props().value).to.equal('INITIALVALUE');
});
it('passes instance openSelector to onClick', () => {
expect(input.props().onClick).to.equal(instance.openSelector);
});
});
});
describe('instance methods', () => {
describe('onSelect', () => {
it('calls into props', () => {
instance.onSelect('testing');
expect(onSelect).to.have.been.calledWith('testing');
});
});
});
});

View File

@ -29,5 +29,6 @@ export Label from './Label';
export RadioButtons from './RadioButtons';
export Select from './Select';
export TypedInput from './TypedInput';
export VaultSelect from './VaultSelect';
export default from './form';

View File

@ -17,6 +17,7 @@
.item {
border: 2px solid transparent;
cursor: pointer;
display: flex;
flex: 1;
height: 100%;

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

View File

@ -0,0 +1,48 @@
/* 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/>.
*/
/* TODO: These tag styles are shared with Balances & Certifications - should be made into
/* a component that can take a list of tags and render them in the correct format
*/
.vault {
display: flex;
flex-wrap: wrap;
margin: 0.75em 0 0;
vertical-align: top;
.vaultBody {
margin: 0.75em 0.5em 0 0;
background: rgba(255, 255, 255, 0.07);
border-radius: 16px;
max-height: 24px;
max-width: 100%;
display: flex;
align-items: center;
}
img {
height: 32px !important;
margin: -4px 1em 0 0;
width: 32px !important;
}
.text {
margin: 0 0.5em 0 0;
text-transform: uppercase;
white-space: nowrap;
}
}

View File

@ -0,0 +1,45 @@
// 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 IdentityIcon from '~/ui/IdentityIcon';
import styles from './vaultTag.css';
export default class VaultTag extends Component {
static propTypes = {
vault: PropTypes.string.isRequired
};
render () {
const { vault } = this.props;
return (
<div className={ styles.vault }>
<div className={ styles.vaultBody }>
<IdentityIcon
address={ vault }
inline
/>
<div className={ styles.text }>
{ vault }
</div>
</div>
</div>
);
}
}

View File

@ -30,7 +30,7 @@ export DappCard from './DappCard';
export DappIcon from './DappIcon';
export Errors from './Errors';
export Features, { FEATURES, FeaturesStore } from './Features';
export Form, { AddressSelect, DappUrlInput, FileSelect, FormWrap, Input, InputAddress, InputAddressSelect, InputChip, InputDate, InputInline, InputTime, Label, RadioButtons, Select, TypedInput } from './Form';
export Form, { AddressSelect, DappUrlInput, FileSelect, FormWrap, Input, InputAddress, InputAddressSelect, InputChip, InputDate, InputInline, InputTime, Label, RadioButtons, Select, TypedInput, VaultSelect } from './Form';
export GasPriceEditor from './GasPriceEditor';
export GasPriceSelector from './GasPriceSelector';
export Icons from './Icons';
@ -56,4 +56,5 @@ export Tooltips, { Tooltip } from './Tooltips';
export TxHash from './TxHash';
export TxList from './TxList';
export VaultCard from './VaultCard';
export VaultTag from './VaultTag';
export Warning from './Warning';

View File

@ -66,6 +66,7 @@
.text {
display: inline-block;
opacity: 0.25;
text-transform: uppercase;
}
}

View File

@ -17,7 +17,7 @@
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { Balance, Certifications, Container, CopyToClipboard, ContainerTitle, IdentityIcon, IdentityName, QrCode, Tags } from '~/ui';
import { Balance, Certifications, Container, CopyToClipboard, ContainerTitle, IdentityIcon, IdentityName, QrCode, Tags, VaultTag } from '~/ui';
import styles from './header.css';
@ -69,7 +69,6 @@ export default class Header extends Component {
{ address }
</div>
</div>
{ this.renderVault() }
{ this.renderUuid() }
<div className={ styles.infoline }>
{ meta.description }
@ -81,6 +80,7 @@ export default class Header extends Component {
balance={ balance }
/>
<Certifications address={ address } />
{ this.renderVault() }
</div>
</div>
<div className={ styles.tags }>
@ -169,15 +169,7 @@ export default class Header extends Component {
}
return (
<div className={ styles.vault }>
<IdentityIcon
address={ meta.vault }
inline
/>
<div className={ styles.text }>
{ meta.vault }
</div>
</div>
<VaultTag vault={ meta.vault } />
);
}
}

View File

@ -22,7 +22,7 @@ import { isEqual } from 'lodash';
import ReactTooltip from 'react-tooltip';
import { FormattedMessage } from 'react-intl';
import { Balance, Container, ContainerTitle, CopyToClipboard, IdentityIcon, IdentityName, Tags } from '~/ui';
import { Balance, Container, ContainerTitle, CopyToClipboard, IdentityIcon, IdentityName, Tags, VaultTag } from '~/ui';
import Certifications from '~/ui/Certifications';
import { arrayOrObjectProptype, nullableProptype } from '~/util/proptypes';
@ -117,6 +117,7 @@ class Summary extends Component {
{ this.renderDescription(account.meta) }
{ this.renderOwners() }
{ this.renderCertifications() }
{ this.renderVault(account.meta) }
</div>
}
link={ this.getLink() }
@ -287,6 +288,16 @@ class Summary extends Component {
/>
);
}
renderVault (meta) {
if (!meta || !meta.vault) {
return null;
}
return (
<VaultTag vault={ meta.vault } />
);
}
}
function mapStateToProps (state) {

View File

@ -56,6 +56,10 @@
}
}
.overlay {
margin-top: -3.25em;
}
.owners {
display: flex;
justify-content: center;
@ -68,10 +72,6 @@
}
}
.overlay {
margin-top: -3.25em;
}
&:not(:hover) {
.tags {
display: none;

View File

@ -37,6 +37,7 @@ export default class Store {
@observable selectedAccounts = {};
@observable vault = null;
@observable vaults = [];
@observable vaultsOpened = [];
@observable vaultNames = [];
@observable vaultName = '';
@observable vaultNameError = ERRORS.noName;
@ -143,6 +144,7 @@ export default class Store {
isOpen: openedVaults.includes(name)
};
});
this.vaultsOpened = this.vaults.filter((vault) => vault.isOpen);
});
}

View File

@ -180,6 +180,12 @@ describe('modals/Vaults/Store', () => {
{ name: 'some', meta: 'metaSome', isOpen: false }
]);
});
it('sets the opened vaults', () => {
expect(store.vaultsOpened.peek()).to.deep.equal([
{ name: 'TEST', meta: 'metaTest', isOpen: true }
]);
});
});
describe('setVaultDescription', () => {
@ -553,6 +559,36 @@ describe('modals/Vaults/Store', () => {
});
});
describe('editVaultMeta', () => {
beforeEach(() => {
sinon.spy(store, 'setBusyMeta');
store.setVaultDescription('testDescription');
store.setVaultName('testCreateName');
store.setVaultPasswordHint('testCreateHint');
store.setVaultTags('testTags');
return store.editVaultMeta();
});
afterEach(() => {
store.setBusyMeta.restore();
});
it('sets and resets the busy flag', () => {
expect(store.setBusyMeta).to.have.been.calledWith(true);
expect(store.isBusyMeta).to.be.false;
});
it('calls into parity_setVaultMeta', () => {
expect(api.parity.setVaultMeta).to.have.been.calledWith('testCreateName', {
description: 'testDescription',
passwordHint: 'testCreateHint',
tags: 'testTags'
});
});
});
describe('editVaultPassword', () => {
beforeEach(() => {
sinon.spy(store, 'setBusyMeta');