Vault Management UI (round 2) (#4631)
* 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 * Convert Signer defaults to SelectionList * Subtle selection border * 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 VaultSelector component * Auto-focus description field (aligns with #4657) * Apply scroll fixes from lates commit in #4621 * Remove unneeded logs * Remove extra div, fixing ParityBar overflow * Disable save if password don't match * s/disabled/readOnly/ * string -> bool
This commit is contained in:
parent
9ff427caaf
commit
570e6f32b0
@ -21,9 +21,11 @@ import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
|
||||
import { newError } from '~/redux/actions';
|
||||
import { Button, Form, Input, InputChip, Portal } from '~/ui';
|
||||
import { Button, Form, Input, InputAddress, InputChip, Portal } from '~/ui';
|
||||
import { CancelIcon, SaveIcon } from '~/ui/Icons';
|
||||
import VaultStore from '~/views/Vaults/store';
|
||||
|
||||
import VaultSelector from '../VaultSelector';
|
||||
import Store from './store';
|
||||
|
||||
@observer
|
||||
@ -39,6 +41,11 @@ class EditMeta extends Component {
|
||||
}
|
||||
|
||||
store = new Store(this.context.api, this.props.account);
|
||||
vaultStore = VaultStore.get(this.context.api);
|
||||
|
||||
componentWillMount () {
|
||||
this.vaultStore.loadVaults();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { description, name, nameError, tags } = this.store;
|
||||
@ -55,6 +62,7 @@ class EditMeta extends Component {
|
||||
/>
|
||||
}
|
||||
>
|
||||
{ this.renderVaultSelector() }
|
||||
<Form>
|
||||
<Input
|
||||
autoFocus
|
||||
@ -102,6 +110,7 @@ class EditMeta extends Component {
|
||||
onTokensChange={ this.store.setTags }
|
||||
tokens={ tags.slice() }
|
||||
/>
|
||||
{ this.renderVault() }
|
||||
</Form>
|
||||
</Portal>
|
||||
);
|
||||
@ -154,22 +163,87 @@ class EditMeta extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderVault () {
|
||||
const { isAccount, vaultName } = this.store;
|
||||
|
||||
if (!isAccount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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 }
|
||||
onSelect={ this.setVaultName }
|
||||
selected={ vaultName }
|
||||
vaultStore={ this.vaultStore }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
onClose = () => {
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
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;
|
||||
})
|
||||
.then(this.onClose)
|
||||
.catch((error) => {
|
||||
this.props.newError(error);
|
||||
});
|
||||
}
|
||||
|
||||
setVaultName = (vaultName) => {
|
||||
this.store.setVaultName(vaultName);
|
||||
this.toggleVaultSelector();
|
||||
}
|
||||
|
||||
toggleVaultSelector = () => {
|
||||
this.store.toggleVaultSelector();
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
|
@ -45,7 +45,9 @@ function createApi () {
|
||||
return {
|
||||
parity: {
|
||||
setAccountName: sinon.stub().resolves(),
|
||||
setAccountMeta: sinon.stub().resolves()
|
||||
setAccountMeta: sinon.stub().resolves(),
|
||||
listVaults: sinon.stub().resolves([]),
|
||||
listOpenedVaults: sinon.stub().resolves([])
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -21,12 +21,14 @@ import { validateName } from '~/util/validation';
|
||||
export default class Store {
|
||||
@observable address = null;
|
||||
@observable isAccount = false;
|
||||
@observable isVaultSelectorOpen = false;
|
||||
@observable description = null;
|
||||
@observable meta = null;
|
||||
@observable name = null;
|
||||
@observable nameError = null;
|
||||
@observable passwordHint = null;
|
||||
@observable tags = null;
|
||||
@observable vaultName = null;
|
||||
|
||||
constructor (api, account) {
|
||||
const { address, name, meta, uuid } = account;
|
||||
@ -34,14 +36,15 @@ export default class Store {
|
||||
this._api = api;
|
||||
|
||||
transaction(() => {
|
||||
this.isAccount = !!uuid;
|
||||
this.address = address;
|
||||
this.meta = meta || {};
|
||||
this.name = name || '';
|
||||
this.isAccount = !!uuid;
|
||||
|
||||
this.description = this.meta.description || '';
|
||||
this.passwordHint = this.meta.passwordHint || '';
|
||||
this.tags = this.meta.tags && this.meta.tags.peek() || [];
|
||||
this.vaultName = this.meta.vault;
|
||||
});
|
||||
}
|
||||
|
||||
@ -74,6 +77,14 @@ export default class Store {
|
||||
this.tags = tags.slice();
|
||||
}
|
||||
|
||||
@action setVaultName = (vaultName) => {
|
||||
this.vaultName = vaultName;
|
||||
}
|
||||
|
||||
@action setVaultSelectorOpen = (isOpen) => {
|
||||
this.isVaultSelectorOpen = isOpen;
|
||||
}
|
||||
|
||||
save () {
|
||||
const meta = {
|
||||
description: this.description,
|
||||
@ -94,4 +105,8 @@ export default class Store {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
toggleVaultSelector () {
|
||||
this.setVaultSelectorOpen(!this.isVaultSelectorOpen);
|
||||
}
|
||||
}
|
||||
|
@ -142,31 +142,54 @@ describe('modals/EditMeta/Store', () => {
|
||||
expect(store.tags.peek()).to.deep.equal(['taga', 'tagb']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setVaultName', () => {
|
||||
it('sets the name', () => {
|
||||
store.setVaultName('testing');
|
||||
expect(store.vaultName).to.equal('testing');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setVaultSelectorOpen', () => {
|
||||
it('sets the state', () => {
|
||||
store.setVaultSelectorOpen('testing');
|
||||
expect(store.isVaultSelectorOpen).to.equal('testing');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('save', () => {
|
||||
beforeEach(() => {
|
||||
createStore(ACCOUNT);
|
||||
describe('operations', () => {
|
||||
describe('save', () => {
|
||||
beforeEach(() => {
|
||||
createStore(ACCOUNT);
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
it('calls parity.setAccountMeta with the adjusted values', () => {
|
||||
store.setDescription('some new description');
|
||||
store.setPasswordHint('some new passwordhint');
|
||||
store.setTags(['taga']);
|
||||
store.save();
|
||||
|
||||
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('calls parity.setAccountName with the set value', () => {
|
||||
store.setName('test name');
|
||||
store.save();
|
||||
|
||||
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']);
|
||||
store.save();
|
||||
|
||||
expect(api.parity.setAccountMeta).to.have.been.calledWith(ACCOUNT.address, Object.assign({}, ACCOUNT.meta, {
|
||||
description: 'some new description',
|
||||
passwordHint: 'some new passwordhint',
|
||||
tags: ['taga']
|
||||
}));
|
||||
describe('toggleVaultSelector', () => {
|
||||
it('inverts the selector state', () => {
|
||||
store.toggleVaultSelector();
|
||||
expect(store.isVaultSelectorOpen).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -15,24 +15,30 @@
|
||||
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.body {
|
||||
/* TODO: These styles are shared with CreateAccount - DRY up */
|
||||
.passwords {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
/* TODO: These styles are shared with CreateAccount - DRY up */
|
||||
.passwords {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.password {
|
||||
box-sizing: border-box;
|
||||
flex: 0 1 50%;
|
||||
width: 50%;
|
||||
.password {
|
||||
box-sizing: border-box;
|
||||
flex: 0 1 50%;
|
||||
width: 50%;
|
||||
|
||||
&:nth-child(odd) {
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
&:nth-child(odd) {
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
&:nth-child(even) {
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
&:nth-child(even) {
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.group+.group {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ class VaultCreate extends Component {
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={ styles.body }>
|
||||
<div>
|
||||
<Input
|
||||
error={ vaultNameError }
|
||||
hint={
|
||||
|
17
js/src/modals/VaultMeta/index.js
Normal file
17
js/src/modals/VaultMeta/index.js
Normal 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 './vaultMeta';
|
298
js/src/modals/VaultMeta/vaultMeta.js
Normal file
298
js/src/modals/VaultMeta/vaultMeta.js
Normal file
@ -0,0 +1,298 @@
|
||||
// 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 { Checkbox } from 'material-ui';
|
||||
import { observer } from 'mobx-react';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
|
||||
import { newError } from '~/redux/actions';
|
||||
import { Button, Form, Input, Portal, VaultCard } from '~/ui';
|
||||
import PasswordStrength from '~/ui/Form/PasswordStrength';
|
||||
import { CheckIcon, CloseIcon } from '~/ui/Icons';
|
||||
|
||||
import styles from '../VaultCreate/vaultCreate.css';
|
||||
|
||||
@observer
|
||||
class VaultMeta extends Component {
|
||||
static propTypes = {
|
||||
newError: PropTypes.func.isRequired,
|
||||
vaultStore: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
state = {
|
||||
passwordEdit: false
|
||||
};
|
||||
|
||||
render () {
|
||||
const { isBusyMeta, isModalMetaOpen, vault, vaultDescription, vaultPassword, vaultPasswordRepeat, vaultPasswordRepeatError, vaultPasswordOld, vaultPasswordHint } = this.props.vaultStore;
|
||||
const { passwordEdit } = this.state;
|
||||
|
||||
if (!isModalMetaOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal
|
||||
busy={ isBusyMeta }
|
||||
buttons={ [
|
||||
<Button
|
||||
disabled={ isBusyMeta }
|
||||
icon={ <CloseIcon /> }
|
||||
key='close'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='vaults.editMeta.button.close'
|
||||
defaultMessage='close'
|
||||
/>
|
||||
}
|
||||
onClick={ this.onClose }
|
||||
/>,
|
||||
<Button
|
||||
disabled={ isBusyMeta || !!vaultPasswordRepeatError }
|
||||
icon={ <CheckIcon /> }
|
||||
key='vault'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='vaults.editMeta.button.save'
|
||||
defaultMessage='save'
|
||||
/>
|
||||
}
|
||||
onClick={ this.onExecute }
|
||||
/>
|
||||
] }
|
||||
onClose={ this.onClose }
|
||||
open
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='vaults.editMeta.title'
|
||||
defaultMessage='Edit Vault Metadata'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<VaultCard.Layout vault={ vault }>
|
||||
<Form>
|
||||
<div className={ styles.group }>
|
||||
<Input
|
||||
autoFocus
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='vaults.editMeta.description.hint'
|
||||
defaultMessage='the description for this vault'
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='vaults.editMeta.description.label'
|
||||
defaultMessage='vault description'
|
||||
/>
|
||||
}
|
||||
onChange={ this.onEditDescription }
|
||||
value={ vaultDescription }
|
||||
/>
|
||||
<Input
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='vaults.editMeta.passwordHint.hint'
|
||||
defaultMessage='your password hint for this vault'
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='vaults.editMeta.passwordHint.label'
|
||||
defaultMessage='password hint'
|
||||
/>
|
||||
}
|
||||
onChange={ this.onEditPasswordHint }
|
||||
value={ vaultPasswordHint }
|
||||
/>
|
||||
</div>
|
||||
<div className={ styles.group }>
|
||||
<Checkbox
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='vaults.editMeta.allowPassword'
|
||||
defaultMessage='Change vault password'
|
||||
/>
|
||||
}
|
||||
checked={ passwordEdit }
|
||||
onCheck={ this.onTogglePassword }
|
||||
/>
|
||||
<div className={ [styles.passwords, passwordEdit ? null : styles.disabled].join(' ') }>
|
||||
<div className={ styles.password }>
|
||||
<Input
|
||||
disabled={ !passwordEdit }
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='vaults.editMeta.currentPassword.hint'
|
||||
defaultMessage='your current vault password'
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='vaults.editMeta.currentPassword.label'
|
||||
defaultMessage='current password'
|
||||
/>
|
||||
}
|
||||
onChange={ this.onEditPasswordCurrent }
|
||||
type='password'
|
||||
value={ vaultPasswordOld }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={ [styles.passwords, passwordEdit ? null : styles.disabled].join(' ') }>
|
||||
<div className={ styles.password }>
|
||||
<Input
|
||||
disabled={ !passwordEdit }
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='vaults.editMeta.password.hint'
|
||||
defaultMessage='a strong, unique password'
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='vaults.editMeta.password.label'
|
||||
defaultMessage='new password'
|
||||
/>
|
||||
}
|
||||
onChange={ this.onEditPassword }
|
||||
type='password'
|
||||
value={ vaultPassword }
|
||||
/>
|
||||
</div>
|
||||
<div className={ styles.password }>
|
||||
<Input
|
||||
disabled={ !passwordEdit }
|
||||
error={ vaultPasswordRepeatError }
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='vaults.editMeta.password2.hint'
|
||||
defaultMessage='verify your new password'
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='vaults.editMeta.password2.label'
|
||||
defaultMessage='new password (repeat)'
|
||||
/>
|
||||
}
|
||||
onChange={ this.onEditPasswordRepeat }
|
||||
type='password'
|
||||
value={ vaultPasswordRepeat }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={ passwordEdit ? null : styles.disabled }>
|
||||
<PasswordStrength input={ vaultPassword } />
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</VaultCard.Layout>
|
||||
</Portal>
|
||||
);
|
||||
|
||||
// <InputChip
|
||||
// addOnBlur
|
||||
// hint={
|
||||
// <FormattedMessage
|
||||
// id='vaults.editMeta.tags.hint'
|
||||
// defaultMessage='press <Enter> to add a tag'
|
||||
// />
|
||||
// }
|
||||
// label={
|
||||
// <FormattedMessage
|
||||
// id='vaults.editMeta.tags.label'
|
||||
// defaultMessage='(optional) tags'
|
||||
// />
|
||||
// }
|
||||
// onTokensChange={ this.onEditTags }
|
||||
// tokens={ vaultTags.slice() }
|
||||
// />
|
||||
}
|
||||
|
||||
onEditDescription = (event, description) => {
|
||||
this.props.vaultStore.setVaultDescription(description);
|
||||
}
|
||||
|
||||
onEditPasswordCurrent = (event, password) => {
|
||||
this.props.vaultStore.setVaultPasswordOld(password);
|
||||
}
|
||||
|
||||
onEditPassword = (event, password) => {
|
||||
this.props.vaultStore.setVaultPassword(password);
|
||||
}
|
||||
|
||||
onEditPasswordHint = (event, hint) => {
|
||||
this.props.vaultStore.setVaultPasswordHint(hint);
|
||||
}
|
||||
|
||||
onEditPasswordRepeat = (event, password) => {
|
||||
this.props.vaultStore.setVaultPasswordRepeat(password);
|
||||
}
|
||||
|
||||
onEditTags = (tags) => {
|
||||
this.props.vaultStore.setVaultTags(tags);
|
||||
}
|
||||
|
||||
onTogglePassword = () => {
|
||||
this.setState({
|
||||
passwordEdit: !this.state.passwordEdit
|
||||
});
|
||||
}
|
||||
|
||||
onExecute = () => {
|
||||
const { vaultPasswordRepeatError } = this.props.vaultStore;
|
||||
const { passwordEdit } = this.state;
|
||||
|
||||
if (vaultPasswordRepeatError) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Promise
|
||||
.all([
|
||||
passwordEdit
|
||||
? this.props.vaultStore.editVaultPassword()
|
||||
: true
|
||||
])
|
||||
.then(() => {
|
||||
return this.props.vaultStore.editVaultMeta();
|
||||
})
|
||||
.catch(this.props.newError)
|
||||
.then(this.onClose);
|
||||
}
|
||||
|
||||
onClose = () => {
|
||||
this.setState({
|
||||
passwordEdit: false
|
||||
});
|
||||
|
||||
this.props.vaultStore.closeMetaModal();
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return bindActionCreators({
|
||||
newError
|
||||
}, dispatch);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(VaultMeta);
|
171
js/src/modals/VaultMeta/vaultMeta.spec.js
Normal file
171
js/src/modals/VaultMeta/vaultMeta.spec.js
Normal file
@ -0,0 +1,171 @@
|
||||
// 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 VaultMeta from './';
|
||||
|
||||
const VAULT = {
|
||||
name: 'testVault'
|
||||
};
|
||||
|
||||
let component;
|
||||
let instance;
|
||||
let reduxStore;
|
||||
let vaultStore;
|
||||
|
||||
function createReduxStore () {
|
||||
reduxStore = {
|
||||
dispatch: sinon.stub(),
|
||||
subscribe: sinon.stub(),
|
||||
getState: () => {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
return reduxStore;
|
||||
}
|
||||
|
||||
function createVaultStore () {
|
||||
vaultStore = {
|
||||
isBusyMeta: false,
|
||||
isModalMetaOpen: true,
|
||||
vault: VAULT,
|
||||
vaultDescription: '',
|
||||
vaultTags: [],
|
||||
vaultName: VAULT.name,
|
||||
vaults: [VAULT],
|
||||
closeMetaModal: sinon.stub(),
|
||||
editVaultMeta: sinon.stub().resolves(true),
|
||||
editVaultPassword: sinon.stub().resolves(true),
|
||||
setVaultDescription: sinon.stub(),
|
||||
setVaultPassword: sinon.stub(),
|
||||
setVaultPasswordRepeat: sinon.stub(),
|
||||
setVaultPasswordHint: sinon.stub(),
|
||||
setVaultPasswordOld: sinon.stub(),
|
||||
setVaultTags: sinon.stub()
|
||||
};
|
||||
|
||||
return vaultStore;
|
||||
}
|
||||
|
||||
function render (props = {}) {
|
||||
component = shallow(
|
||||
<VaultMeta vaultStore={ createVaultStore() } />,
|
||||
{
|
||||
context: {
|
||||
store: createReduxStore()
|
||||
}
|
||||
}
|
||||
).find('VaultMeta').shallow();
|
||||
instance = component.instance();
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
describe('modals/VaultMeta', () => {
|
||||
beforeEach(() => {
|
||||
render();
|
||||
});
|
||||
|
||||
it('renders defaults', () => {
|
||||
expect(component).to.be.ok;
|
||||
});
|
||||
|
||||
describe('event methods', () => {
|
||||
describe('onEditDescription', () => {
|
||||
beforeEach(() => {
|
||||
instance.onEditDescription(null, 'testing');
|
||||
});
|
||||
|
||||
it('calls into setVaultDescription', () => {
|
||||
expect(vaultStore.setVaultDescription).to.have.been.calledWith('testing');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onEditPassword', () => {
|
||||
beforeEach(() => {
|
||||
instance.onEditPassword(null, 'testPassword');
|
||||
});
|
||||
|
||||
it('calls setVaultPassword', () => {
|
||||
expect(vaultStore.setVaultPassword).to.have.been.calledWith('testPassword');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onEditPasswordHint', () => {
|
||||
beforeEach(() => {
|
||||
instance.onEditPasswordHint(null, 'testPasswordHint');
|
||||
});
|
||||
|
||||
it('calls setVaultPasswordHint', () => {
|
||||
expect(vaultStore.setVaultPasswordHint).to.have.been.calledWith('testPasswordHint');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onEditPasswordCurrent', () => {
|
||||
beforeEach(() => {
|
||||
instance.onEditPasswordCurrent(null, 'testPasswordOld');
|
||||
});
|
||||
|
||||
it('calls setVaultPasswordHint', () => {
|
||||
expect(vaultStore.setVaultPasswordOld).to.have.been.calledWith('testPasswordOld');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onEditPasswordRepeat', () => {
|
||||
beforeEach(() => {
|
||||
instance.onEditPasswordRepeat(null, 'testPassword');
|
||||
});
|
||||
|
||||
it('calls setVaultPasswordRepeat', () => {
|
||||
expect(vaultStore.setVaultPasswordRepeat).to.have.been.calledWith('testPassword');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onEditTags', () => {
|
||||
beforeEach(() => {
|
||||
instance.onEditTags('testing');
|
||||
});
|
||||
|
||||
it('calls into setVaultTags', () => {
|
||||
expect(vaultStore.setVaultTags).to.have.been.calledWith('testing');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onClose', () => {
|
||||
beforeEach(() => {
|
||||
instance.onClose();
|
||||
});
|
||||
|
||||
it('calls into closeMetaModal', () => {
|
||||
expect(vaultStore.closeMetaModal).to.have.been.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('onExecute', () => {
|
||||
beforeEach(() => {
|
||||
return instance.onExecute();
|
||||
});
|
||||
|
||||
it('calls into editVaultMeta', () => {
|
||||
expect(vaultStore.editVaultMeta).to.have.been.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
17
js/src/modals/VaultSelector/index.js
Normal file
17
js/src/modals/VaultSelector/index.js
Normal 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 './vaultSelector';
|
99
js/src/modals/VaultSelector/vaultSelector.js
Normal file
99
js/src/modals/VaultSelector/vaultSelector.js
Normal file
@ -0,0 +1,99 @@
|
||||
// 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 { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Portal, SelectionList, VaultCard } from '~/ui';
|
||||
|
||||
@observer
|
||||
export default class VaultSelector extends Component {
|
||||
static propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
selected: PropTypes.string,
|
||||
vaultStore: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
render () {
|
||||
return (
|
||||
<Portal
|
||||
isChildModal
|
||||
onClose={ this.onClose }
|
||||
open
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='vaults.selector.title'
|
||||
defaultMessage='Select Account Vault'
|
||||
/>
|
||||
}
|
||||
>
|
||||
{ this.renderList() }
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
|
||||
renderList () {
|
||||
const { vaults } = this.props.vaultStore;
|
||||
const openVaults = vaults.filter((vault) => vault.isOpen);
|
||||
|
||||
if (openVaults.length === 0) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='vaults.selector.noneAvailable'
|
||||
defaultMessage='There are currently no vaults opened and available for selection. Create and open some first before attempting to select a vault for an account move.'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectionList
|
||||
items={ openVaults }
|
||||
isChecked={ this.isSelected }
|
||||
noStretch
|
||||
onSelectClick={ this.onSelect }
|
||||
renderItem={ this.renderVault }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderVault = (vault) => {
|
||||
return (
|
||||
<VaultCard
|
||||
hideAccounts
|
||||
hideButtons
|
||||
vault={ vault }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
isSelected = (vault) => {
|
||||
return this.props.selected === vault.name;
|
||||
}
|
||||
|
||||
onSelect = (vault) => {
|
||||
this.props.onSelect(
|
||||
this.props.selected === vault.name
|
||||
? ''
|
||||
: vault.name
|
||||
);
|
||||
}
|
||||
|
||||
onClose = () => {
|
||||
this.props.onClose();
|
||||
}
|
||||
}
|
169
js/src/modals/VaultSelector/vaultSelector.spec.js
Normal file
169
js/src/modals/VaultSelector/vaultSelector.spec.js
Normal file
@ -0,0 +1,169 @@
|
||||
// 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 VaultSelector from './';
|
||||
|
||||
const VAULTS_OPENED = [
|
||||
{ name: 'A', isOpen: true },
|
||||
{ name: 'B', isOpen: true }
|
||||
];
|
||||
const VAULTS_CLOSED = [
|
||||
{ name: 'C' },
|
||||
{ name: 'D' }
|
||||
];
|
||||
|
||||
let component;
|
||||
let instance;
|
||||
let onClose;
|
||||
let onSelect;
|
||||
let vaultStore;
|
||||
|
||||
function createVaultStore () {
|
||||
vaultStore = {
|
||||
vaults: VAULTS_OPENED.concat(VAULTS_CLOSED)
|
||||
};
|
||||
|
||||
return vaultStore;
|
||||
}
|
||||
|
||||
function render () {
|
||||
onClose = sinon.stub();
|
||||
onSelect = sinon.stub();
|
||||
|
||||
component = shallow(
|
||||
<VaultSelector
|
||||
onClose={ onClose }
|
||||
onSelect={ onSelect }
|
||||
selected='firstValue'
|
||||
vaultStore={ createVaultStore() }
|
||||
/>
|
||||
);
|
||||
instance = component.instance();
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
describe('ui/VaultSelector', () => {
|
||||
beforeEach(() => {
|
||||
render();
|
||||
});
|
||||
|
||||
it('renders defaults', () => {
|
||||
expect(component).to.be.ok;
|
||||
});
|
||||
|
||||
describe('components', () => {
|
||||
describe('Portal', () => {
|
||||
let portal;
|
||||
|
||||
beforeEach(() => {
|
||||
portal = component.find('Portal');
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
expect(portal.get(0)).to.be.ok;
|
||||
});
|
||||
|
||||
it('opens as a child modal', () => {
|
||||
expect(portal.props().isChildModal).to.be.true;
|
||||
});
|
||||
|
||||
it('passes the instance onClose', () => {
|
||||
expect(portal.props().onClose).to.equal(instance.onClose);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SelectionList', () => {
|
||||
let list;
|
||||
|
||||
beforeEach(() => {
|
||||
list = component.find('SelectionList');
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
expect(list.get(0)).to.be.ok;
|
||||
});
|
||||
|
||||
it('passes the open vaults', () => {
|
||||
expect(list.props().items).to.deep.equal(VAULTS_OPENED);
|
||||
});
|
||||
|
||||
it('passes internal renderItem', () => {
|
||||
expect(list.props().renderItem).to.equal(instance.renderVault);
|
||||
});
|
||||
|
||||
it('passes internal isChecked', () => {
|
||||
expect(list.props().isChecked).to.equal(instance.isSelected);
|
||||
});
|
||||
|
||||
it('passes internal onSelectClick', () => {
|
||||
expect(list.props().onSelectClick).to.equal(instance.onSelect);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('instance methods', () => {
|
||||
describe('renderVault', () => {
|
||||
let card;
|
||||
|
||||
beforeEach(() => {
|
||||
card = instance.renderVault({ name: 'testVault' });
|
||||
});
|
||||
|
||||
it('renders VaultCard', () => {
|
||||
expect(card).to.be.ok;
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSelected', () => {
|
||||
it('returns true when vault name matches', () => {
|
||||
expect(instance.isSelected({ name: 'firstValue' })).to.be.true;
|
||||
});
|
||||
|
||||
it('returns false when vault name does not match', () => {
|
||||
expect(instance.isSelected({ name: 'testValue' })).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('onSelect', () => {
|
||||
it('calls into props onSelect', () => {
|
||||
instance.onSelect({ name: 'testing' });
|
||||
expect(onSelect).to.have.been.called;
|
||||
});
|
||||
|
||||
it('passes name when new selection made', () => {
|
||||
instance.onSelect({ name: 'newValue' });
|
||||
expect(onSelect).to.have.been.calledWith('newValue');
|
||||
});
|
||||
|
||||
it('passes empty name when current selection made', () => {
|
||||
instance.onSelect({ name: 'firstValue' });
|
||||
expect(onSelect).to.have.been.calledWith('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onClose', () => {
|
||||
it('calls props onClose', () => {
|
||||
instance.onClose();
|
||||
expect(onClose).to.have.been.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -34,6 +34,8 @@ export UpgradeParity from './UpgradeParity';
|
||||
export VaultAccounts from './VaultAccounts';
|
||||
export VaultCreate from './VaultCreate';
|
||||
export VaultLock from './VaultLock';
|
||||
export VaultMeta from './VaultMeta';
|
||||
export VaultSelector from './VaultSelector';
|
||||
export VaultUnlock from './VaultUnlock';
|
||||
export Verification from './Verification';
|
||||
export WalletSettings from './WalletSettings';
|
||||
|
@ -31,6 +31,7 @@ class InputAddress extends Component {
|
||||
accountsInfo: PropTypes.object,
|
||||
allowCopy: PropTypes.bool,
|
||||
autoFocus: PropTypes.bool,
|
||||
allowInvalid: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
error: PropTypes.string,
|
||||
@ -112,9 +113,9 @@ class InputAddress extends Component {
|
||||
}
|
||||
|
||||
renderIcon () {
|
||||
const { value, disabled, label, allowCopy, hideUnderline, readOnly } = this.props;
|
||||
const { allowInvalid, value, disabled, label, allowCopy, hideUnderline, readOnly } = this.props;
|
||||
|
||||
if (!value || !value.length || !util.isAddressValid(value)) {
|
||||
if (!value || !value.length || (!util.isAddressValid(value) && !allowInvalid)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -23,12 +23,13 @@ import styles from './layout.css';
|
||||
|
||||
export default class Layout extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
vault: PropTypes.object.isRequired,
|
||||
withBorder: PropTypes.bool
|
||||
};
|
||||
|
||||
render () {
|
||||
const { vault, withBorder } = this.props;
|
||||
const { children, vault, withBorder } = this.props;
|
||||
const { isOpen, meta, name } = vault;
|
||||
|
||||
return (
|
||||
@ -59,6 +60,7 @@ export default class Layout extends Component {
|
||||
byline={ meta.description }
|
||||
title={ name }
|
||||
/>
|
||||
{ children }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -30,13 +30,16 @@ export default class VaultCard extends Component {
|
||||
static propTypes = {
|
||||
accounts: PropTypes.array,
|
||||
buttons: PropTypes.array,
|
||||
children: PropTypes.node,
|
||||
hideAccounts: PropTypes.bool,
|
||||
hideButtons: PropTypes.bool,
|
||||
vault: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static Layout = Layout;
|
||||
|
||||
render () {
|
||||
const { buttons, vault } = this.props;
|
||||
const { children, vault } = this.props;
|
||||
const { isOpen } = vault;
|
||||
|
||||
return (
|
||||
@ -48,26 +51,20 @@ export default class VaultCard extends Component {
|
||||
: null
|
||||
}
|
||||
>
|
||||
<div className={ styles.buttons }>
|
||||
<Button
|
||||
className={ styles.status }
|
||||
disabled
|
||||
icon={
|
||||
isOpen
|
||||
? <UnlockedIcon />
|
||||
: <LockedIcon />
|
||||
}
|
||||
key='status'
|
||||
/>
|
||||
{ buttons }
|
||||
</div>
|
||||
<Layout vault={ vault } />
|
||||
{ this.renderButtons() }
|
||||
<Layout vault={ vault }>
|
||||
{ children }
|
||||
</Layout>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
renderAccounts () {
|
||||
const { accounts } = this.props;
|
||||
const { accounts, hideAccounts } = this.props;
|
||||
|
||||
if (hideAccounts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!accounts || !accounts.length) {
|
||||
return (
|
||||
@ -101,4 +98,29 @@ export default class VaultCard extends Component {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderButtons () {
|
||||
const { buttons, hideButtons, vault } = this.props;
|
||||
const { isOpen } = vault;
|
||||
|
||||
if (hideButtons) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ styles.buttons }>
|
||||
<Button
|
||||
className={ styles.status }
|
||||
disabled
|
||||
icon={
|
||||
isOpen
|
||||
? <UnlockedIcon />
|
||||
: <LockedIcon />
|
||||
}
|
||||
key='status'
|
||||
/>
|
||||
{ buttons }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,7 @@
|
||||
.addressline,
|
||||
.infoline,
|
||||
.uuidline,
|
||||
.vault,
|
||||
.title {
|
||||
margin-left: 72px;
|
||||
}
|
||||
@ -59,6 +60,15 @@
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.vault {
|
||||
line-height: 32px;
|
||||
|
||||
.text {
|
||||
display: inline-block;
|
||||
opacity: 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
.addressline {
|
||||
display: flex;
|
||||
}
|
||||
|
@ -65,6 +65,7 @@ export default class Header extends Component {
|
||||
<CopyToClipboard data={ address } />
|
||||
<div className={ styles.address }>{ address }</div>
|
||||
</div>
|
||||
{ this.renderVault() }
|
||||
{ this.renderUuid() }
|
||||
<div className={ styles.infoline }>
|
||||
{ meta.description }
|
||||
@ -154,4 +155,25 @@ export default class Header extends Component {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderVault () {
|
||||
const { account } = this.props;
|
||||
const { meta } = account;
|
||||
|
||||
if (!meta || !meta.vault) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ styles.vault }>
|
||||
<IdentityIcon
|
||||
address={ meta.vault }
|
||||
inline
|
||||
/>
|
||||
<div className={ styles.text }>
|
||||
{ meta.vault }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -27,10 +27,12 @@ export default class Store {
|
||||
@observable isBusyCreate = false;
|
||||
@observable isBusyLoad = false;
|
||||
@observable isBusyLock = false;
|
||||
@observable isBusyMeta = false;
|
||||
@observable isBusyUnlock = false;
|
||||
@observable isModalAccountsOpen = false;
|
||||
@observable isModalCreateOpen = false;
|
||||
@observable isModalLockOpen = false;
|
||||
@observable isModalMetaOpen = false;
|
||||
@observable isModalUnlockOpen = false;
|
||||
@observable selectedAccounts = {};
|
||||
@observable vault = null;
|
||||
@ -41,7 +43,9 @@ export default class Store {
|
||||
@observable vaultDescription = '';
|
||||
@observable vaultPassword = '';
|
||||
@observable vaultPasswordHint = '';
|
||||
@observable vaultPasswordOld = '';
|
||||
@observable vaultPasswordRepeat = '';
|
||||
@observable vaultTags = [];
|
||||
|
||||
constructor (api) {
|
||||
this._api = api;
|
||||
@ -59,7 +63,9 @@ export default class Store {
|
||||
this.setVaultDescription('');
|
||||
this.setVaultPassword('');
|
||||
this.setVaultPasswordHint('');
|
||||
this.setVaultPasswordOld('');
|
||||
this.setVaultPasswordRepeat('');
|
||||
this.setVaultTags([]);
|
||||
});
|
||||
}
|
||||
|
||||
@ -79,6 +85,10 @@ export default class Store {
|
||||
this.isBusyLock = isBusy;
|
||||
}
|
||||
|
||||
@action setBusyMeta = (isBusy) => {
|
||||
this.isBusyMeta = isBusy;
|
||||
}
|
||||
|
||||
@action setBusyUnlock = (isBusy) => {
|
||||
this.isBusyUnlock = isBusy;
|
||||
}
|
||||
@ -104,6 +114,13 @@ export default class Store {
|
||||
});
|
||||
}
|
||||
|
||||
@action setModalMetaOpen = (isOpen) => {
|
||||
transaction(() => {
|
||||
this.setBusyMeta(false);
|
||||
this.isModalMetaOpen = isOpen;
|
||||
});
|
||||
}
|
||||
|
||||
@action setModalUnlockOpen = (isOpen) => {
|
||||
transaction(() => {
|
||||
this.setBusyUnlock(false);
|
||||
@ -161,10 +178,18 @@ export default class Store {
|
||||
this.vaultPasswordHint = hint;
|
||||
}
|
||||
|
||||
@action setVaultPasswordOld = (password) => {
|
||||
this.vaultPasswordOld = password;
|
||||
}
|
||||
|
||||
@action setVaultPasswordRepeat = (password) => {
|
||||
this.vaultPasswordRepeat = password;
|
||||
}
|
||||
|
||||
@action setVaultTags = (tags) => {
|
||||
this.vaultTags = tags;
|
||||
}
|
||||
|
||||
@action toggleSelectedAccount = (address) => {
|
||||
this.setSelectedAccounts(Object.assign({}, this.selectedAccounts, {
|
||||
[address]: !this.selectedAccounts[address] })
|
||||
@ -183,6 +208,10 @@ export default class Store {
|
||||
this.setModalLockOpen(false);
|
||||
}
|
||||
|
||||
closeMetaModal () {
|
||||
this.setModalMetaOpen(false);
|
||||
}
|
||||
|
||||
closeUnlockModal () {
|
||||
this.setModalUnlockOpen(false);
|
||||
}
|
||||
@ -209,6 +238,20 @@ export default class Store {
|
||||
});
|
||||
}
|
||||
|
||||
openMetaModal (name) {
|
||||
transaction(() => {
|
||||
this.clearVaultFields();
|
||||
this.setVaultName(name);
|
||||
|
||||
if (this.vault && this.vault.meta) {
|
||||
this.setVaultDescription(this.vault.meta.description);
|
||||
this.setVaultPasswordHint(this.vault.meta.passwordHint);
|
||||
}
|
||||
|
||||
this.setModalMetaOpen(true);
|
||||
});
|
||||
}
|
||||
|
||||
openUnlockModal (name) {
|
||||
transaction(() => {
|
||||
this.setVaultName(name);
|
||||
@ -268,7 +311,8 @@ export default class Store {
|
||||
.then(() => {
|
||||
return this._api.parity.setVaultMeta(this.vaultName, {
|
||||
description: this.vaultDescription,
|
||||
passwordHint: this.vaultPasswordHint
|
||||
passwordHint: this.vaultPasswordHint,
|
||||
tags: this.vaultTags
|
||||
});
|
||||
})
|
||||
.then(this.loadVaults)
|
||||
@ -282,6 +326,48 @@ export default class Store {
|
||||
});
|
||||
}
|
||||
|
||||
editVaultMeta () {
|
||||
this.setBusyMeta(true);
|
||||
|
||||
return this._api.parity
|
||||
.setVaultMeta(this.vaultName, {
|
||||
description: this.vaultDescription,
|
||||
passwordHint: this.vaultPasswordHint,
|
||||
tags: this.vaultTags
|
||||
})
|
||||
.then(this.loadVaults)
|
||||
.then(() => {
|
||||
this.setBusyMeta(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('editVaultMeta', error);
|
||||
this.setBusyMeta(false);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
editVaultPassword () {
|
||||
this.setBusyMeta(true);
|
||||
|
||||
return this._api.parity
|
||||
.closeVault(this.vaultName)
|
||||
.then(() => {
|
||||
return this._api.parity.openVault(this.vaultName, this.vaultPasswordOld);
|
||||
})
|
||||
.then(() => {
|
||||
return this._api.parity.changeVaultPassword(this.vaultName, this.vaultPassword);
|
||||
})
|
||||
.then(() => {
|
||||
this.setBusyMeta(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('editVaultPassword', error);
|
||||
this.loadVaults();
|
||||
this.setBusyMeta(false);
|
||||
throw new Error('Unable to change the vault password');
|
||||
});
|
||||
}
|
||||
|
||||
openVault () {
|
||||
this.setBusyUnlock(true);
|
||||
|
||||
@ -307,8 +393,28 @@ export default class Store {
|
||||
outAccounts.map((address) => this._api.parity.changeVault(address, ''))
|
||||
])
|
||||
.then(this.loadVaults)
|
||||
.then(() => {
|
||||
this.setBusyAccounts(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('moveAccounts', error);
|
||||
this.setBusyAccounts(false);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
moveAccount (vaultName, address) {
|
||||
this.setBusyAccounts(true);
|
||||
|
||||
return this._api.parity
|
||||
.changeVault(address, vaultName)
|
||||
.then(this.loadVaults)
|
||||
.then(() => {
|
||||
this.setBusyAccounts(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('moveAccount', error);
|
||||
this.setBusyAccounts(false);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
@ -44,6 +44,8 @@ describe('modals/Vaults/Store', () => {
|
||||
store.setVaultPassword('blah');
|
||||
store.setVaultPasswordRepeat('bleh');
|
||||
store.setVaultPasswordHint('hint');
|
||||
store.setVaultPasswordOld('old');
|
||||
store.setVaultTags('tags');
|
||||
|
||||
store.clearVaultFields();
|
||||
});
|
||||
@ -55,6 +57,8 @@ describe('modals/Vaults/Store', () => {
|
||||
expect(store.vaultPassword).to.equal('');
|
||||
expect(store.vaultPasswordRepeat).to.equal('');
|
||||
expect(store.vaultPasswordHint).to.equal('');
|
||||
expect(store.vaultPasswordOld).to.equal('');
|
||||
expect(store.vaultTags.length).to.equal(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -90,6 +94,14 @@ describe('modals/Vaults/Store', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('setBusyMeta', () => {
|
||||
it('sets the flag', () => {
|
||||
store.setBusyMeta('busy');
|
||||
|
||||
expect(store.isBusyMeta).to.equal('busy');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setBusyUnlock', () => {
|
||||
it('sets the flag', () => {
|
||||
store.setBusyUnlock('busy');
|
||||
@ -122,6 +134,14 @@ describe('modals/Vaults/Store', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('setModalMetaOpen', () => {
|
||||
it('sets the flag', () => {
|
||||
store.setModalMetaOpen('opened');
|
||||
|
||||
expect(store.isModalMetaOpen).to.equal('opened');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setModalUnlockOpen', () => {
|
||||
beforeEach(() => {
|
||||
store.setVaultPassword('testing');
|
||||
@ -233,6 +253,14 @@ describe('modals/Vaults/Store', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('setVaultTags', () => {
|
||||
it('sets the tags', () => {
|
||||
store.setVaultTags('test');
|
||||
|
||||
expect(store.vaultTags).to.equal('test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleSelectedAccount', () => {
|
||||
beforeEach(() => {
|
||||
store.toggleSelectedAccount('123');
|
||||
@ -301,6 +329,17 @@ describe('modals/Vaults/Store', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('closeMetaModal', () => {
|
||||
beforeEach(() => {
|
||||
store.setModalMetaOpen(true);
|
||||
store.closeMetaModal();
|
||||
});
|
||||
|
||||
it('sets the opened state to false', () => {
|
||||
expect(store.isModalMetaOpen).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('closeUnlockModal', () => {
|
||||
beforeEach(() => {
|
||||
store.setModalUnlockOpen(true);
|
||||
@ -364,6 +403,20 @@ describe('modals/Vaults/Store', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('openMetaModal', () => {
|
||||
beforeEach(() => {
|
||||
store.openMetaModal('testing');
|
||||
});
|
||||
|
||||
it('sets the opened state to true', () => {
|
||||
expect(store.isModalMetaOpen).to.be.true;
|
||||
});
|
||||
|
||||
it('stores the name', () => {
|
||||
expect(store.vaultName).to.equal('testing');
|
||||
});
|
||||
});
|
||||
|
||||
describe('openUnlockModal', () => {
|
||||
beforeEach(() => {
|
||||
store.openUnlockModal('testing');
|
||||
@ -443,6 +496,7 @@ describe('modals/Vaults/Store', () => {
|
||||
store.setVaultPassword('testCreatePassword');
|
||||
store.setVaultPasswordRepeat('testCreatePassword');
|
||||
store.setVaultPasswordHint('testCreateHint');
|
||||
store.setVaultTags('testTags');
|
||||
|
||||
return store.createVault();
|
||||
});
|
||||
@ -463,11 +517,71 @@ describe('modals/Vaults/Store', () => {
|
||||
it('calls into parity_setVaultMeta', () => {
|
||||
expect(api.parity.setVaultMeta).to.have.been.calledWith('testCreateName', {
|
||||
description: 'testDescription',
|
||||
passwordHint: 'testCreateHint'
|
||||
passwordHint: 'testCreateHint',
|
||||
tags: 'testTags'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
store.setVaultName('testName');
|
||||
store.setVaultPasswordOld('oldPassword');
|
||||
store.setVaultPassword('newPassword');
|
||||
|
||||
return store.editVaultPassword();
|
||||
});
|
||||
|
||||
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_openVault', () => {
|
||||
expect(api.parity.openVault).to.have.been.calledWith('testName', 'oldPassword');
|
||||
});
|
||||
|
||||
it('calls into parity_changeVaultPassword', () => {
|
||||
expect(api.parity.changeVaultPassword).to.have.been.calledWith('testName', 'newPassword');
|
||||
});
|
||||
});
|
||||
|
||||
describe('openVault', () => {
|
||||
beforeEach(() => {
|
||||
sinon.spy(store, 'setBusyUnlock');
|
||||
@ -512,5 +626,25 @@ describe('modals/Vaults/Store', () => {
|
||||
expect(api.parity.changeVault).to.have.been.calledWith('C', '');
|
||||
});
|
||||
});
|
||||
|
||||
describe('moveAccount', () => {
|
||||
beforeEach(() => {
|
||||
sinon.spy(store, 'setBusyAccounts');
|
||||
|
||||
return store.moveAccount('testVault', 'A');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
store.setBusyAccounts.restore();
|
||||
});
|
||||
|
||||
it('sets the busy flag', () => {
|
||||
expect(store.setBusyAccounts).to.have.been.calledWith(true);
|
||||
});
|
||||
|
||||
it('calls into parity_changeVault', () => {
|
||||
expect(api.parity.changeVault).to.have.been.calledWith('A', 'testVault');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -19,9 +19,9 @@ import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { VaultAccounts, VaultCreate, VaultLock, VaultUnlock } from '~/modals';
|
||||
import { VaultAccounts, VaultCreate, VaultLock, VaultMeta, VaultUnlock } from '~/modals';
|
||||
import { Button, Container, Page, SectionList, VaultCard } from '~/ui';
|
||||
import { AccountsIcon, AddIcon, LockedIcon, UnlockedIcon } from '~/ui/Icons';
|
||||
import { AccountsIcon, AddIcon, EditIcon, LockedIcon, UnlockedIcon } from '~/ui/Icons';
|
||||
|
||||
import Store from './store';
|
||||
import styles from './vaults.css';
|
||||
@ -70,6 +70,7 @@ class Vaults extends Component {
|
||||
<VaultAccounts vaultStore={ this.vaultStore } />
|
||||
<VaultCreate vaultStore={ this.vaultStore } />
|
||||
<VaultLock vaultStore={ this.vaultStore } />
|
||||
<VaultMeta vaultStore={ this.vaultStore } />
|
||||
<VaultUnlock vaultStore={ this.vaultStore } />
|
||||
{ this.renderList() }
|
||||
</Page>
|
||||
@ -109,6 +110,10 @@ class Vaults extends Component {
|
||||
this.onOpenAccounts(name);
|
||||
return false;
|
||||
};
|
||||
const onClickEdit = () => {
|
||||
this.onOpenEdit(name);
|
||||
return false;
|
||||
};
|
||||
const onClickOpen = () => {
|
||||
isOpen
|
||||
? this.onOpenLockVault(name)
|
||||
@ -133,13 +138,24 @@ class Vaults extends Component {
|
||||
}
|
||||
onClick={ onClickAccounts }
|
||||
/>,
|
||||
<Button
|
||||
icon={ <EditIcon /> }
|
||||
key='edit'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='vaults.button.edit'
|
||||
defaultMessage='edit'
|
||||
/>
|
||||
}
|
||||
onClick={ onClickEdit }
|
||||
/>,
|
||||
<Button
|
||||
icon={ <LockedIcon /> }
|
||||
key='close'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='vaults.button.close'
|
||||
defaultMessage='close vault'
|
||||
defaultMessage='close'
|
||||
/>
|
||||
}
|
||||
onClick={ onClickOpen }
|
||||
@ -152,7 +168,7 @@ class Vaults extends Component {
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='vaults.button.open'
|
||||
defaultMessage='open vault'
|
||||
defaultMessage='open'
|
||||
/>
|
||||
}
|
||||
onClick={ onClickOpen }
|
||||
@ -172,10 +188,18 @@ class Vaults extends Component {
|
||||
this.vaultStore.openCreateModal();
|
||||
}
|
||||
|
||||
onOpenEdit = (name) => {
|
||||
this.vaultStore.openMetaModal(name);
|
||||
}
|
||||
|
||||
onOpenLockVault = (name) => {
|
||||
this.vaultStore.openLockModal(name);
|
||||
}
|
||||
|
||||
onOpenMeta = (name) => {
|
||||
this.vaultStore.openMetaModal(name);
|
||||
}
|
||||
|
||||
onOpenUnlockVault = (name) => {
|
||||
this.vaultStore.openUnlockModal(name);
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ function render (props = {}) {
|
||||
return component;
|
||||
}
|
||||
|
||||
describe('modals/Vaults', () => {
|
||||
describe('views/Vaults', () => {
|
||||
beforeEach(() => {
|
||||
render();
|
||||
});
|
||||
@ -150,6 +150,22 @@ describe('modals/Vaults', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('onOpenEdit', () => {
|
||||
beforeEach(() => {
|
||||
sinon.spy(instance.vaultStore, 'openMetaModal');
|
||||
|
||||
instance.onOpenEdit('testing');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
instance.vaultStore.openMetaModal.restore();
|
||||
});
|
||||
|
||||
it('calls into vaultStore.openMetaModal', () => {
|
||||
expect(instance.vaultStore.openMetaModal).to.have.been.calledWith('testing');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onOpenLockVault', () => {
|
||||
beforeEach(() => {
|
||||
sinon.spy(instance.vaultStore, 'openLockModal');
|
||||
|
@ -63,7 +63,8 @@ export function createApi () {
|
||||
getVaultMeta: sinon.stub().resolves(TEST_VAULTS_META),
|
||||
newVault: sinon.stub().resolves(true),
|
||||
openVault: sinon.stub().resolves(true),
|
||||
setVaultMeta: sinon.stub().resolves(true)
|
||||
setVaultMeta: sinon.stub().resolves(true),
|
||||
changeVaultPassword: sinon.stub().resolves(true)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user