From 570e6f32b08733847c97d1eb81265a2b68225dc0 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Fri, 24 Feb 2017 18:05:04 +0100 Subject: [PATCH] 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 --- js/src/modals/EditMeta/editMeta.js | 76 ++++- js/src/modals/EditMeta/editMeta.test.js | 4 +- js/src/modals/EditMeta/store.js | 17 +- js/src/modals/EditMeta/store.spec.js | 65 ++-- js/src/modals/VaultCreate/vaultCreate.css | 36 ++- js/src/modals/VaultCreate/vaultCreate.js | 2 +- js/src/modals/VaultMeta/index.js | 17 + js/src/modals/VaultMeta/vaultMeta.js | 298 ++++++++++++++++++ js/src/modals/VaultMeta/vaultMeta.spec.js | 171 ++++++++++ js/src/modals/VaultSelector/index.js | 17 + js/src/modals/VaultSelector/vaultSelector.js | 99 ++++++ .../VaultSelector/vaultSelector.spec.js | 169 ++++++++++ js/src/modals/index.js | 2 + js/src/ui/Form/InputAddress/inputAddress.js | 5 +- js/src/ui/VaultCard/Layout/layout.js | 4 +- js/src/ui/VaultCard/vaultCard.js | 54 +++- js/src/views/Account/Header/header.css | 10 + js/src/views/Account/Header/header.js | 22 ++ js/src/views/Vaults/store.js | 108 ++++++- js/src/views/Vaults/store.spec.js | 136 +++++++- js/src/views/Vaults/vaults.js | 32 +- js/src/views/Vaults/vaults.spec.js | 18 +- js/src/views/Vaults/vaults.test.js | 3 +- 23 files changed, 1298 insertions(+), 67 deletions(-) create mode 100644 js/src/modals/VaultMeta/index.js create mode 100644 js/src/modals/VaultMeta/vaultMeta.js create mode 100644 js/src/modals/VaultMeta/vaultMeta.spec.js create mode 100644 js/src/modals/VaultSelector/index.js create mode 100644 js/src/modals/VaultSelector/vaultSelector.js create mode 100644 js/src/modals/VaultSelector/vaultSelector.spec.js diff --git a/js/src/modals/EditMeta/editMeta.js b/js/src/modals/EditMeta/editMeta.js index 182560c16..71e222ca6 100644 --- a/js/src/modals/EditMeta/editMeta.js +++ b/js/src/modals/EditMeta/editMeta.js @@ -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() }
+ { this.renderVault() }
); @@ -154,22 +163,87 @@ class EditMeta extends Component { ); } + renderVault () { + const { isAccount, vaultName } = this.store; + + if (!isAccount) { + return null; + } + + return ( + + } + label={ + + } + onClick={ this.toggleVaultSelector } + value={ vaultName } + /> + ); + } + + renderVaultSelector () { + const { isAccount, isVaultSelectorOpen, vaultName } = this.store; + + if (!isAccount || !isVaultSelectorOpen) { + return null; + } + + return ( + + ); + } + 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) { diff --git a/js/src/modals/EditMeta/editMeta.test.js b/js/src/modals/EditMeta/editMeta.test.js index 8193ca63e..c0cbf83a1 100644 --- a/js/src/modals/EditMeta/editMeta.test.js +++ b/js/src/modals/EditMeta/editMeta.test.js @@ -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([]) } }; } diff --git a/js/src/modals/EditMeta/store.js b/js/src/modals/EditMeta/store.js index bddb49a9d..46951f095 100644 --- a/js/src/modals/EditMeta/store.js +++ b/js/src/modals/EditMeta/store.js @@ -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); + } } diff --git a/js/src/modals/EditMeta/store.spec.js b/js/src/modals/EditMeta/store.spec.js index 983357836..4ff775718 100644 --- a/js/src/modals/EditMeta/store.spec.js +++ b/js/src/modals/EditMeta/store.spec.js @@ -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; }); }); }); diff --git a/js/src/modals/VaultCreate/vaultCreate.css b/js/src/modals/VaultCreate/vaultCreate.css index fcd2e26a3..125d26217 100644 --- a/js/src/modals/VaultCreate/vaultCreate.css +++ b/js/src/modals/VaultCreate/vaultCreate.css @@ -15,24 +15,30 @@ /* along with Parity. If not, see . */ -.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; +} diff --git a/js/src/modals/VaultCreate/vaultCreate.js b/js/src/modals/VaultCreate/vaultCreate.js index ba27e1a9c..c472b28d0 100644 --- a/js/src/modals/VaultCreate/vaultCreate.js +++ b/js/src/modals/VaultCreate/vaultCreate.js @@ -80,7 +80,7 @@ class VaultCreate extends Component { /> } > -
+
. + +export default from './vaultMeta'; diff --git a/js/src/modals/VaultMeta/vaultMeta.js b/js/src/modals/VaultMeta/vaultMeta.js new file mode 100644 index 000000000..5089af642 --- /dev/null +++ b/js/src/modals/VaultMeta/vaultMeta.js @@ -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 . + +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 ( + } + key='close' + label={ + + } + onClick={ this.onClose } + />, +
); diff --git a/js/src/ui/VaultCard/vaultCard.js b/js/src/ui/VaultCard/vaultCard.js index e567ed6af..62dae2049 100644 --- a/js/src/ui/VaultCard/vaultCard.js +++ b/js/src/ui/VaultCard/vaultCard.js @@ -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 } > -
-
- + { this.renderButtons() } + + { children } + ); } 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 { ); } + + renderButtons () { + const { buttons, hideButtons, vault } = this.props; + const { isOpen } = vault; + + if (hideButtons) { + return null; + } + + return ( +
+
+ ); + } } diff --git a/js/src/views/Account/Header/header.css b/js/src/views/Account/Header/header.css index 6709851bd..62f072574 100644 --- a/js/src/views/Account/Header/header.css +++ b/js/src/views/Account/Header/header.css @@ -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; } diff --git a/js/src/views/Account/Header/header.js b/js/src/views/Account/Header/header.js index 93e129360..e92f545a1 100644 --- a/js/src/views/Account/Header/header.js +++ b/js/src/views/Account/Header/header.js @@ -65,6 +65,7 @@ export default class Header extends Component {
{ address }
+ { this.renderVault() } { this.renderUuid() }
{ meta.description } @@ -154,4 +155,25 @@ export default class Header extends Component {
); } + + renderVault () { + const { account } = this.props; + const { meta } = account; + + if (!meta || !meta.vault) { + return null; + } + + return ( +
+ +
+ { meta.vault } +
+
+ ); + } } diff --git a/js/src/views/Vaults/store.js b/js/src/views/Vaults/store.js index db2d1c705..75a52954d 100644 --- a/js/src/views/Vaults/store.js +++ b/js/src/views/Vaults/store.js @@ -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; }); } diff --git a/js/src/views/Vaults/store.spec.js b/js/src/views/Vaults/store.spec.js index 53c228776..9f971d383 100644 --- a/js/src/views/Vaults/store.spec.js +++ b/js/src/views/Vaults/store.spec.js @@ -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'); + }); + }); }); }); diff --git a/js/src/views/Vaults/vaults.js b/js/src/views/Vaults/vaults.js index 314f990e2..79870e566 100644 --- a/js/src/views/Vaults/vaults.js +++ b/js/src/views/Vaults/vaults.js @@ -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 { + { this.renderList() } @@ -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 } />, +