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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -42,7 +42,9 @@ function createApi () {
newAccountFromWallet: sinon.stub().resolves(ADDRESS), newAccountFromWallet: sinon.stub().resolves(ADDRESS),
phraseToAddress: () => Promise.resolve(`${++counter}`), phraseToAddress: () => Promise.resolve(`${++counter}`),
setAccountMeta: sinon.stub().resolves(), 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 rawKey = '';
@observable rawKeyError = ERRORS.nokey; @observable rawKeyError = ERRORS.nokey;
@observable stage = STAGE_SELECT_TYPE; @observable stage = STAGE_SELECT_TYPE;
@observable vaultName = '';
@observable walletFile = ''; @observable walletFile = '';
@observable walletFileError = ERRORS.noFile; @observable walletFileError = ERRORS.noFile;
@observable walletJson = ''; @observable walletJson = '';
@ -95,6 +96,7 @@ export default class Store {
this.nameError = null; this.nameError = null;
this.rawKey = ''; this.rawKey = '';
this.rawKeyError = null; this.rawKeyError = null;
this.vaultName = '';
this.walletFile = ''; this.walletFile = '';
this.walletFileError = null; this.walletFileError = null;
this.walletJson = ''; this.walletJson = '';
@ -134,6 +136,10 @@ export default class Store {
this.gethImported = gethImported; this.gethImported = gethImported;
} }
@action setVaultName = (vaultName) => {
this.vaultName = vaultName;
}
@action setWindowsPhrase = (isWindowsPhrase = false) => { @action setWindowsPhrase = (isWindowsPhrase = false) => {
this.isWindowsPhrase = isWindowsPhrase; this.isWindowsPhrase = isWindowsPhrase;
} }
@ -220,7 +226,28 @@ export default class Store {
this.stage--; 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) { switch (this.createType) {
case 'fromGeth': case 'fromGeth':
return this.createAccountFromGeth(); return this.createAccountFromGeth();

View File

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

View File

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

View File

@ -21,11 +21,10 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { newError } from '~/redux/actions'; 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 { CancelIcon, SaveIcon } from '~/ui/Icons';
import VaultStore from '~/views/Vaults/store'; import VaultStore from '~/views/Vaults/store';
import VaultSelector from '../VaultSelector';
import Store from './store'; import Store from './store';
@observer @observer
@ -48,11 +47,12 @@ class EditMeta extends Component {
} }
render () { render () {
const { description, name, nameError, tags } = this.store; const { description, isBusy, name, nameError, tags } = this.store;
return ( return (
<Portal <Portal
buttons={ this.renderActions() } buttons={ this.renderActions() }
busy={ isBusy }
onClose={ this.onClose } onClose={ this.onClose }
open open
title={ title={
@ -62,7 +62,6 @@ class EditMeta extends Component {
/> />
} }
> >
{ this.renderVaultSelector() }
<Form> <Form>
<Input <Input
autoFocus autoFocus
@ -110,7 +109,7 @@ class EditMeta extends Component {
onTokensChange={ this.store.setTags } onTokensChange={ this.store.setTags }
tokens={ tags.slice() } tokens={ tags.slice() }
/> />
{ this.renderVault() } { this.renderVaultSelector() }
</Form> </Form>
</Portal> </Portal>
); );
@ -163,7 +162,7 @@ class EditMeta extends Component {
); );
} }
renderVault () { renderVaultSelector () {
const { isAccount, vaultName } = this.store; const { isAccount, vaultName } = this.store;
if (!isAccount) { if (!isAccount) {
@ -171,40 +170,9 @@ class EditMeta extends Component {
} }
return ( return (
<InputAddress <VaultSelect
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 }
onSelect={ this.setVaultName } onSelect={ this.setVaultName }
selected={ vaultName } value={ vaultName }
vaultStore={ this.vaultStore } vaultStore={ this.vaultStore }
/> />
); );
@ -215,21 +183,12 @@ class EditMeta extends Component {
} }
onSave = () => { onSave = () => {
const { address, isAccount, meta, vaultName } = this.store;
if (this.store.hasError) { if (this.store.hasError) {
return; return;
} }
return this.store return this.store
.save() .save(this.vaultStore)
.then(() => {
if (isAccount && (meta.vault !== vaultName)) {
return this.vaultStore.moveAccount(vaultName, address);
}
return true;
})
.then(this.onClose) .then(this.onClose)
.catch((error) => { .catch((error) => {
this.props.newError(error); this.props.newError(error);
@ -238,11 +197,6 @@ class EditMeta extends Component {
setVaultName = (vaultName) => { setVaultName = (vaultName) => {
this.store.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 { export default class Store {
@observable address = null; @observable address = null;
@observable isAccount = false; @observable isAccount = false;
@observable isVaultSelectorOpen = false; @observable isBusy = false;
@observable description = null; @observable description = null;
@observable meta = null; @observable meta = null;
@observable name = null; @observable name = null;
@ -73,6 +73,10 @@ export default class Store {
this.passwordHint = passwordHint; this.passwordHint = passwordHint;
} }
@action setBusy = (isBusy) => {
this.isBusy = isBusy;
}
@action setTags = (tags) => { @action setTags = (tags) => {
this.tags = tags.slice(); this.tags = tags.slice();
} }
@ -81,11 +85,9 @@ export default class Store {
this.vaultName = vaultName; this.vaultName = vaultName;
} }
@action setVaultSelectorOpen = (isOpen) => { save (vaultStore) {
this.isVaultSelectorOpen = isOpen; this.setBusy(true);
}
save () {
const meta = { const meta = {
description: this.description, description: this.description,
tags: this.tags.peek() tags: this.tags.peek()
@ -100,13 +102,20 @@ export default class Store {
this._api.parity.setAccountName(this.address, this.name), this._api.parity.setAccountName(this.address, this.name),
this._api.parity.setAccountMeta(this.address, Object.assign({}, this.meta, meta)) 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) => { .catch((error) => {
console.error('onSave', error); console.error('onSave', error);
this.setBusy(false);
throw error; 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 // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import sinon from 'sinon';
import Store from './store'; import Store from './store';
import { ACCOUNT, ADDRESS, createApi } from './editMeta.test.js'; import { ACCOUNT, ADDRESS, createApi } from './editMeta.test.js';
let api; let api;
let store; let store;
let vaultStore;
function createVaultStore () {
return {
moveAccount: sinon.stub().resolves(true)
};
}
function createStore (account) { function createStore (account) {
api = createApi(); api = createApi();
vaultStore = createVaultStore();
store = new Store(api, account); store = new Store(api, account);
@ -108,6 +118,13 @@ describe('modals/EditMeta/Store', () => {
createStore(ADDRESS); createStore(ADDRESS);
}); });
describe('setBusy', () => {
it('sets the isBusy flag', () => {
store.setBusy('testing');
expect(store.isBusy).to.equal('testing');
});
});
describe('setDescription', () => { describe('setDescription', () => {
it('sets the description', () => { it('sets the description', () => {
store.setDescription('description'); store.setDescription('description');
@ -149,27 +166,57 @@ describe('modals/EditMeta/Store', () => {
expect(store.vaultName).to.equal('testing'); expect(store.vaultName).to.equal('testing');
}); });
}); });
describe('setVaultSelectorOpen', () => {
it('sets the state', () => {
store.setVaultSelectorOpen('testing');
expect(store.isVaultSelectorOpen).to.equal('testing');
});
});
}); });
describe('operations', () => { describe('operations', () => {
describe('save', () => { describe('save', () => {
beforeEach(() => { beforeEach(() => {
createStore(ACCOUNT); 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', () => { it('calls parity.setAccountName with the set value', () => {
store.setName('test name'); store.setName('test name');
store.save();
return store.save().then(() => {
expect(api.parity.setAccountName).to.be.calledWith(ACCOUNT.address, 'test name'); 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', () => { it('calls parity.setAccountMeta with the adjusted values', () => {
store.setDescription('some new description'); store.setDescription('some new description');
@ -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 React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl'; 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 @observer
export default class VaultSelector extends Component { export default class VaultSelector extends Component {
@ -48,10 +50,9 @@ export default class VaultSelector extends Component {
} }
renderList () { renderList () {
const { vaults } = this.props.vaultStore; const { vaultsOpened } = this.props.vaultStore;
const openVaults = vaults.filter((vault) => vault.isOpen);
if (openVaults.length === 0) { if (vaultsOpened.length === 0) {
return ( return (
<FormattedMessage <FormattedMessage
id='vaults.selector.noneAvailable' id='vaults.selector.noneAvailable'
@ -62,7 +63,7 @@ export default class VaultSelector extends Component {
return ( return (
<SelectionList <SelectionList
items={ openVaults } items={ vaultsOpened }
isChecked={ this.isSelected } isChecked={ this.isSelected }
noStretch noStretch
onSelectClick={ this.onSelect } onSelectClick={ this.onSelect }

View File

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

View File

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

View File

@ -16,7 +16,7 @@
*/ */
.certifications { .certifications {
margin-top: 1em; margin-top: 0.75em;
} }
.certification, .certification,
@ -43,7 +43,7 @@
background-color: rgba(255, 255, 255, 0.07); background-color: rgba(255, 255, 255, 0.07);
margin-right: 0.5em; margin-right: 0.5em;
margin-top: 1em; margin-top: 1em;
padding: 0.3em 0.6em 0.2em 2.6em; padding: 0.3em 0.6em 0.2em 3em;
text-transform: uppercase; text-transform: uppercase;
&:last-child { &:last-child {
@ -52,7 +52,7 @@
.icon { .icon {
position: absolute; position: absolute;
top: -.25em; top: -0.25em;
left: 0; 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 RadioButtons from './RadioButtons';
export Select from './Select'; export Select from './Select';
export TypedInput from './TypedInput'; export TypedInput from './TypedInput';
export VaultSelect from './VaultSelect';
export default from './form'; export default from './form';

View File

@ -17,6 +17,7 @@
.item { .item {
border: 2px solid transparent; border: 2px solid transparent;
cursor: pointer;
display: flex; display: flex;
flex: 1; flex: 1;
height: 100%; 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 DappIcon from './DappIcon';
export Errors from './Errors'; export Errors from './Errors';
export Features, { FEATURES, FeaturesStore } from './Features'; 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 GasPriceEditor from './GasPriceEditor';
export GasPriceSelector from './GasPriceSelector'; export GasPriceSelector from './GasPriceSelector';
export Icons from './Icons'; export Icons from './Icons';
@ -56,4 +56,5 @@ export Tooltips, { Tooltip } from './Tooltips';
export TxHash from './TxHash'; export TxHash from './TxHash';
export TxList from './TxList'; export TxList from './TxList';
export VaultCard from './VaultCard'; export VaultCard from './VaultCard';
export VaultTag from './VaultTag';
export Warning from './Warning'; export Warning from './Warning';

View File

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

View File

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

View File

@ -22,7 +22,7 @@ import { isEqual } from 'lodash';
import ReactTooltip from 'react-tooltip'; import ReactTooltip from 'react-tooltip';
import { FormattedMessage } from 'react-intl'; 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 Certifications from '~/ui/Certifications';
import { arrayOrObjectProptype, nullableProptype } from '~/util/proptypes'; import { arrayOrObjectProptype, nullableProptype } from '~/util/proptypes';
@ -117,6 +117,7 @@ class Summary extends Component {
{ this.renderDescription(account.meta) } { this.renderDescription(account.meta) }
{ this.renderOwners() } { this.renderOwners() }
{ this.renderCertifications() } { this.renderCertifications() }
{ this.renderVault(account.meta) }
</div> </div>
} }
link={ this.getLink() } 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) { function mapStateToProps (state) {

View File

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

View File

@ -37,6 +37,7 @@ export default class Store {
@observable selectedAccounts = {}; @observable selectedAccounts = {};
@observable vault = null; @observable vault = null;
@observable vaults = []; @observable vaults = [];
@observable vaultsOpened = [];
@observable vaultNames = []; @observable vaultNames = [];
@observable vaultName = ''; @observable vaultName = '';
@observable vaultNameError = ERRORS.noName; @observable vaultNameError = ERRORS.noName;
@ -143,6 +144,7 @@ export default class Store {
isOpen: openedVaults.includes(name) 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 } { 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', () => { 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', () => { describe('editVaultPassword', () => {
beforeEach(() => { beforeEach(() => {
sinon.spy(store, 'setBusyMeta'); sinon.spy(store, 'setBusyMeta');