From 1548201551f36c1cb4383c536844b389a0bf98fd Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Fri, 3 Mar 2017 19:50:54 +0100 Subject: [PATCH] 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 --- .../CreateAccount/ChangeVault/changeVault.js | 51 ++++++++ .../ChangeVault/changeVault.spec.js | 100 ++++++++++++++++ .../modals/CreateAccount/ChangeVault/index.js | 17 +++ .../CreateAccount/NewAccount/newAccount.js | 8 +- .../CreateAccount/NewImport/newImport.js | 9 +- js/src/modals/CreateAccount/RawKey/rawKey.js | 8 +- .../RecoveryPhrase/recoveryPhrase.js | 8 +- js/src/modals/CreateAccount/createAccount.css | 1 + js/src/modals/CreateAccount/createAccount.js | 30 ++++- .../CreateAccount/createAccount.test.js | 4 +- js/src/modals/CreateAccount/store.js | 29 ++++- js/src/modals/CreateAccount/store.spec.js | 91 ++++++++++++--- js/src/modals/DeleteAccount/deleteAccount.js | 11 +- js/src/modals/EditMeta/editMeta.js | 62 ++-------- js/src/modals/EditMeta/store.js | 27 +++-- js/src/modals/EditMeta/store.spec.js | 72 +++++++++--- js/src/modals/VaultSelector/vaultSelector.js | 11 +- .../VaultSelector/vaultSelector.spec.js | 4 +- js/src/ui/Balance/balance.css | 2 +- js/src/ui/Certifications/certifications.css | 6 +- js/src/ui/Form/VaultSelect/index.js | 17 +++ js/src/ui/Form/VaultSelect/vaultSelect.js | 109 ++++++++++++++++++ .../ui/Form/VaultSelect/vaultSelect.spec.js | 90 +++++++++++++++ js/src/ui/Form/index.js | 1 + js/src/ui/SelectionList/selectionList.css | 1 + js/src/ui/VaultTag/index.js | 17 +++ js/src/ui/VaultTag/vaultTag.css | 48 ++++++++ js/src/ui/VaultTag/vaultTag.js | 45 ++++++++ js/src/ui/index.js | 3 +- js/src/views/Account/Header/header.css | 1 + js/src/views/Account/Header/header.js | 14 +-- js/src/views/Accounts/Summary/summary.js | 13 ++- js/src/views/Accounts/accounts.css | 8 +- js/src/views/Vaults/store.js | 2 + js/src/views/Vaults/store.spec.js | 36 ++++++ 35 files changed, 822 insertions(+), 134 deletions(-) create mode 100644 js/src/modals/CreateAccount/ChangeVault/changeVault.js create mode 100644 js/src/modals/CreateAccount/ChangeVault/changeVault.spec.js create mode 100644 js/src/modals/CreateAccount/ChangeVault/index.js create mode 100644 js/src/ui/Form/VaultSelect/index.js create mode 100644 js/src/ui/Form/VaultSelect/vaultSelect.js create mode 100644 js/src/ui/Form/VaultSelect/vaultSelect.spec.js create mode 100644 js/src/ui/VaultTag/index.js create mode 100644 js/src/ui/VaultTag/vaultTag.css create mode 100644 js/src/ui/VaultTag/vaultTag.js diff --git a/js/src/modals/CreateAccount/ChangeVault/changeVault.js b/js/src/modals/CreateAccount/ChangeVault/changeVault.js new file mode 100644 index 000000000..566fa402c --- /dev/null +++ b/js/src/modals/CreateAccount/ChangeVault/changeVault.js @@ -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 . + +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 ( + + ); + } + + onSelect = (vaultName) => { + const { store } = this.props; + + store.setVaultName(vaultName); + } +} diff --git a/js/src/modals/CreateAccount/ChangeVault/changeVault.spec.js b/js/src/modals/CreateAccount/ChangeVault/changeVault.spec.js new file mode 100644 index 000000000..a2fcb834b --- /dev/null +++ b/js/src/modals/CreateAccount/ChangeVault/changeVault.spec.js @@ -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 . + +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( + + ); + 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'); + }); + }); + }); +}); diff --git a/js/src/modals/CreateAccount/ChangeVault/index.js b/js/src/modals/CreateAccount/ChangeVault/index.js new file mode 100644 index 000000000..5eac8b21d --- /dev/null +++ b/js/src/modals/CreateAccount/ChangeVault/index.js @@ -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 . + +export default from './changeVault'; diff --git a/js/src/modals/CreateAccount/NewAccount/newAccount.js b/js/src/modals/CreateAccount/NewAccount/newAccount.js index 3bf34b27e..e0a2f8ba2 100644 --- a/js/src/modals/CreateAccount/NewAccount/newAccount.js +++ b/js/src/modals/CreateAccount/NewAccount/newAccount.js @@ -24,13 +24,15 @@ import { Form, Input, IdentityIcon } from '~/ui'; import PasswordStrength from '~/ui/Form/PasswordStrength'; import { RefreshIcon } from '~/ui/Icons'; +import ChangeVault from '../ChangeVault'; import styles from '../createAccount.css'; @observer export default class CreateAccount extends Component { static propTypes = { newError: PropTypes.func.isRequired, - store: PropTypes.object.isRequired + store: PropTypes.object.isRequired, + vaultStore: PropTypes.object } state = { @@ -123,6 +125,10 @@ export default class CreateAccount extends Component { + { this.renderIdentitySelector() } { this.renderIdentities() } diff --git a/js/src/modals/CreateAccount/NewImport/newImport.js b/js/src/modals/CreateAccount/NewImport/newImport.js index 121f0be57..e3d888c3f 100644 --- a/js/src/modals/CreateAccount/NewImport/newImport.js +++ b/js/src/modals/CreateAccount/NewImport/newImport.js @@ -20,12 +20,15 @@ import { FormattedMessage } from 'react-intl'; import { Form, FileSelect, Input } from '~/ui'; +import ChangeVault from '../ChangeVault'; import styles from '../createAccount.css'; @observer export default class NewImport extends Component { static propTypes = { - store: PropTypes.object.isRequired + store: PropTypes.object.isRequired, + vaultStore: PropTypes.object + } render () { @@ -88,6 +91,10 @@ export default class NewImport extends Component { /> + { this.renderFileSelector() } ); diff --git a/js/src/modals/CreateAccount/RawKey/rawKey.js b/js/src/modals/CreateAccount/RawKey/rawKey.js index ad96064bd..7f31cb066 100644 --- a/js/src/modals/CreateAccount/RawKey/rawKey.js +++ b/js/src/modals/CreateAccount/RawKey/rawKey.js @@ -21,6 +21,7 @@ import { FormattedMessage } from 'react-intl'; import { Form, Input } from '~/ui'; import PasswordStrength from '~/ui/Form/PasswordStrength'; +import ChangeVault from '../ChangeVault'; import styles from '../createAccount.css'; @observer @@ -30,7 +31,8 @@ export default class RawKey extends Component { } static propTypes = { - store: PropTypes.object.isRequired + store: PropTypes.object.isRequired, + vaultStore: PropTypes.object } render () { @@ -131,6 +133,10 @@ export default class RawKey extends Component { + ); } diff --git a/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.js b/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.js index 894daa767..1e49f821f 100644 --- a/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.js +++ b/js/src/modals/CreateAccount/RecoveryPhrase/recoveryPhrase.js @@ -22,12 +22,14 @@ import { Checkbox } from 'material-ui'; import { Form, Input } from '~/ui'; import PasswordStrength from '~/ui/Form/PasswordStrength'; +import ChangeVault from '../ChangeVault'; import styles from '../createAccount.css'; @observer export default class RecoveryPhrase extends Component { static propTypes = { - store: PropTypes.object.isRequired + store: PropTypes.object.isRequired, + vaultStore: PropTypes.object } render () { @@ -127,6 +129,10 @@ export default class RecoveryPhrase extends Component { + ); } @@ -132,18 +141,27 @@ class CreateAccount extends Component { if (createType === 'fromPhrase') { return ( - + ); } if (createType === 'fromRaw') { return ( - + ); } return ( - + ); case STAGE_INFO: @@ -266,7 +284,7 @@ class CreateAccount extends Component { this.store.setBusy(true); return this.store - .createAccount() + .createAccount(this.vaultStore) .then(() => { this.store.setBusy(false); this.store.nextStage(); diff --git a/js/src/modals/CreateAccount/createAccount.test.js b/js/src/modals/CreateAccount/createAccount.test.js index d66729aec..d4ba5a0a4 100644 --- a/js/src/modals/CreateAccount/createAccount.test.js +++ b/js/src/modals/CreateAccount/createAccount.test.js @@ -42,7 +42,9 @@ function createApi () { newAccountFromWallet: sinon.stub().resolves(ADDRESS), phraseToAddress: () => Promise.resolve(`${++counter}`), setAccountMeta: sinon.stub().resolves(), - setAccountName: sinon.stub().resolves() + setAccountName: sinon.stub().resolves(), + listVaults: sinon.stub().resolves([]), + listOpenedVaults: sinon.stub().resolves([]) } }; } diff --git a/js/src/modals/CreateAccount/store.js b/js/src/modals/CreateAccount/store.js index 7371e8df3..c76102d5b 100644 --- a/js/src/modals/CreateAccount/store.js +++ b/js/src/modals/CreateAccount/store.js @@ -44,6 +44,7 @@ export default class Store { @observable rawKey = ''; @observable rawKeyError = ERRORS.nokey; @observable stage = STAGE_SELECT_TYPE; + @observable vaultName = ''; @observable walletFile = ''; @observable walletFileError = ERRORS.noFile; @observable walletJson = ''; @@ -95,6 +96,7 @@ export default class Store { this.nameError = null; this.rawKey = ''; this.rawKeyError = null; + this.vaultName = ''; this.walletFile = ''; this.walletFileError = null; this.walletJson = ''; @@ -134,6 +136,10 @@ export default class Store { this.gethImported = gethImported; } + @action setVaultName = (vaultName) => { + this.vaultName = vaultName; + } + @action setWindowsPhrase = (isWindowsPhrase = false) => { this.isWindowsPhrase = isWindowsPhrase; } @@ -220,7 +226,28 @@ export default class Store { this.stage--; } - createAccount = () => { + createAccount = (vaultStore) => { + this.setBusy(true); + + return this + ._createAccount() + .then(() => { + if (vaultStore && this.vaultName && this.vaultName.length) { + return vaultStore.moveAccount(this.vaultName, this.address); + } + + return true; + }) + .then(() => { + this.setBusy(false); + }) + .catch((error) => { + this.setBusy(false); + throw error; + }); + } + + _createAccount = () => { switch (this.createType) { case 'fromGeth': return this.createAccountFromGeth(); diff --git a/js/src/modals/CreateAccount/store.spec.js b/js/src/modals/CreateAccount/store.spec.js index 67303fa21..833cb7ef5 100644 --- a/js/src/modals/CreateAccount/store.spec.js +++ b/js/src/modals/CreateAccount/store.spec.js @@ -22,8 +22,20 @@ import { ACCOUNTS, ADDRESS, GETH_ADDRESSES, createApi } from './createAccount.te let api; let store; +let vaultStore; + +function createVaultStore () { + vaultStore = { + moveAccount: sinon.stub().resolves(), + listVaults: sinon.stub().resolves() + }; + + return vaultStore; +} function createStore (loadGeth) { + createVaultStore(); + api = createApi(); store = new Store(api, ACCOUNTS, loadGeth); @@ -65,8 +77,9 @@ describe('modals/CreateAccount/Store', () => { describe('@action', () => { describe('clearErrors', () => { beforeEach(() => { - store.setName(''); - store.setPassword('123'); + store.setName('testing'); + store.setPassword('testing'); + store.setVaultName('testing'); store.setRawKey('test'); store.setWalletFile('test'); store.setWalletJson('test'); @@ -75,10 +88,13 @@ describe('modals/CreateAccount/Store', () => { it('clears all errors', () => { store.clearErrors(); + expect(store.name).to.equal(''); expect(store.nameError).to.be.null; + expect(store.password).to.equal(''); expect(store.passwordRepeatError).to.be.null; expect(store.rawKey).to.equal(''); expect(store.rawKeyError).to.be.null; + expect(store.vaultName).to.equal(''); expect(store.walletFile).to.equal(''); expect(store.walletFileError).to.be.null; expect(store.walletJson).to.equal(''); @@ -198,6 +214,13 @@ describe('modals/CreateAccount/Store', () => { }); }); + describe('setVaultName', () => { + it('sets the vault name', () => { + store.setVaultName('testVault'); + expect(store.vaultName).to.equal('testVault'); + }); + }); + describe('setWalletFile', () => { it('sets the filepath', () => { store.setWalletFile('testing'); @@ -384,12 +407,22 @@ describe('modals/CreateAccount/Store', () => { let createAccountFromWalletSpy; let createAccountFromPhraseSpy; let createAccountFromRawSpy; + let busySpy; beforeEach(() => { createAccountFromGethSpy = sinon.spy(store, 'createAccountFromGeth'); createAccountFromWalletSpy = sinon.spy(store, 'createAccountFromWallet'); createAccountFromPhraseSpy = sinon.spy(store, 'createAccountFromPhrase'); createAccountFromRawSpy = sinon.spy(store, 'createAccountFromRaw'); + busySpy = sinon.spy(store, 'setBusy'); + }); + + afterEach(() => { + store.createAccountFromGeth.restore(); + store.createAccountFromWallet.restore(); + store.createAccountFromPhrase.restore(); + store.createAccountFromRaw.restore(); + store.setBusy.restore(); }); it('throws error on invalid createType', () => { @@ -399,38 +432,68 @@ describe('modals/CreateAccount/Store', () => { it('calls createAccountFromGeth on createType === fromGeth', () => { store.setCreateType('fromGeth'); - store.createAccount(); - expect(createAccountFromGethSpy).to.have.been.called; + + return store.createAccount().then(() => { + expect(createAccountFromGethSpy).to.have.been.called; + }); }); it('calls createAccountFromWallet on createType === fromJSON', () => { store.setCreateType('fromJSON'); - store.createAccount(); - expect(createAccountFromWalletSpy).to.have.been.called; + + return store.createAccount().then(() => { + expect(createAccountFromWalletSpy).to.have.been.called; + }); }); it('calls createAccountFromPhrase on createType === fromNew', () => { store.setCreateType('fromNew'); - store.createAccount(); - expect(createAccountFromPhraseSpy).to.have.been.called; + + return store.createAccount().then(() => { + expect(createAccountFromPhraseSpy).to.have.been.called; + }); }); it('calls createAccountFromPhrase on createType === fromPhrase', () => { store.setCreateType('fromPhrase'); - store.createAccount(); - expect(createAccountFromPhraseSpy).to.have.been.called; + + return store.createAccount().then(() => { + expect(createAccountFromPhraseSpy).to.have.been.called; + }); }); it('calls createAccountFromWallet on createType === fromPresale', () => { store.setCreateType('fromPresale'); - store.createAccount(); - expect(createAccountFromWalletSpy).to.have.been.called; + + return store.createAccount().then(() => { + expect(createAccountFromWalletSpy).to.have.been.called; + }); }); it('calls createAccountFromRaw on createType === fromRaw', () => { store.setCreateType('fromRaw'); - store.createAccount(); - expect(createAccountFromRawSpy).to.have.been.called; + + return store.createAccount().then(() => { + expect(createAccountFromRawSpy).to.have.been.called; + }); + }); + + it('moves account to vault when vaultName set', () => { + store.setCreateType('fromNew'); + store.setVaultName('testing'); + + return store.createAccount(vaultStore).then(() => { + expect(vaultStore.moveAccount).to.have.been.calledWith('testing', ADDRESS); + }); + }); + + it('sets and rests the busy flag', () => { + store.setCreateType('fromNew'); + + return store.createAccount().then(() => { + expect(busySpy).to.have.been.calledWith(true); + expect(busySpy).to.have.been.calledWith(false); + }); }); describe('createAccountFromGeth', () => { diff --git a/js/src/modals/DeleteAccount/deleteAccount.js b/js/src/modals/DeleteAccount/deleteAccount.js index 8f47777c5..cd69d6bd9 100644 --- a/js/src/modals/DeleteAccount/deleteAccount.js +++ b/js/src/modals/DeleteAccount/deleteAccount.js @@ -37,25 +37,27 @@ class DeleteAccount extends Component { } state = { + isBusy: false, password: '' } render () { const { account } = this.props; - const { password } = this.state; + const { isBusy, password } = this.state; return ( } - visible >
{ + this.setState({ isBusy: true }); + if (result === true) { router.push('/accounts'); this.closeDeleteDialog(); @@ -128,6 +134,7 @@ class DeleteAccount extends Component { } }) .catch((error) => { + this.setState({ isBusy: false }); console.error('onDeleteConfirmed', error); newError(new Error(`Deletion failed: ${error.message}`)); }); diff --git a/js/src/modals/EditMeta/editMeta.js b/js/src/modals/EditMeta/editMeta.js index 71e222ca6..5d0c91dbe 100644 --- a/js/src/modals/EditMeta/editMeta.js +++ b/js/src/modals/EditMeta/editMeta.js @@ -21,11 +21,10 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { newError } from '~/redux/actions'; -import { Button, Form, Input, InputAddress, InputChip, Portal } from '~/ui'; +import { Button, Form, Input, InputChip, Portal, VaultSelect } from '~/ui'; import { CancelIcon, SaveIcon } from '~/ui/Icons'; import VaultStore from '~/views/Vaults/store'; -import VaultSelector from '../VaultSelector'; import Store from './store'; @observer @@ -48,11 +47,12 @@ class EditMeta extends Component { } render () { - const { description, name, nameError, tags } = this.store; + const { description, isBusy, name, nameError, tags } = this.store; return ( } > - { this.renderVaultSelector() }
- { this.renderVault() } + { this.renderVaultSelector() }
); @@ -163,7 +162,7 @@ class EditMeta extends Component { ); } - renderVault () { + renderVaultSelector () { const { isAccount, vaultName } = this.store; if (!isAccount) { @@ -171,40 +170,9 @@ class EditMeta extends Component { } return ( - - } - label={ - - } - onClick={ this.toggleVaultSelector } - value={ vaultName } - /> - ); - } - - renderVaultSelector () { - const { isAccount, isVaultSelectorOpen, vaultName } = this.store; - - if (!isAccount || !isVaultSelectorOpen) { - return null; - } - - return ( - ); @@ -215,21 +183,12 @@ class EditMeta extends Component { } onSave = () => { - const { address, isAccount, meta, vaultName } = this.store; - if (this.store.hasError) { return; } return this.store - .save() - .then(() => { - if (isAccount && (meta.vault !== vaultName)) { - return this.vaultStore.moveAccount(vaultName, address); - } - - return true; - }) + .save(this.vaultStore) .then(this.onClose) .catch((error) => { this.props.newError(error); @@ -238,11 +197,6 @@ class EditMeta extends Component { setVaultName = (vaultName) => { this.store.setVaultName(vaultName); - this.toggleVaultSelector(); - } - - toggleVaultSelector = () => { - this.store.toggleVaultSelector(); } } diff --git a/js/src/modals/EditMeta/store.js b/js/src/modals/EditMeta/store.js index 46951f095..da3d88cd7 100644 --- a/js/src/modals/EditMeta/store.js +++ b/js/src/modals/EditMeta/store.js @@ -21,7 +21,7 @@ import { validateName } from '~/util/validation'; export default class Store { @observable address = null; @observable isAccount = false; - @observable isVaultSelectorOpen = false; + @observable isBusy = false; @observable description = null; @observable meta = null; @observable name = null; @@ -73,6 +73,10 @@ export default class Store { this.passwordHint = passwordHint; } + @action setBusy = (isBusy) => { + this.isBusy = isBusy; + } + @action setTags = (tags) => { this.tags = tags.slice(); } @@ -81,11 +85,9 @@ export default class Store { this.vaultName = vaultName; } - @action setVaultSelectorOpen = (isOpen) => { - this.isVaultSelectorOpen = isOpen; - } + save (vaultStore) { + this.setBusy(true); - save () { const meta = { description: this.description, tags: this.tags.peek() @@ -100,13 +102,20 @@ export default class Store { this._api.parity.setAccountName(this.address, this.name), this._api.parity.setAccountMeta(this.address, Object.assign({}, this.meta, meta)) ]) + .then(() => { + if (vaultStore && this.isAccount && (this.meta.vault !== this.vaultName)) { + return vaultStore.moveAccount(this.vaultName, this.address); + } + + return true; + }) + .then(() => { + this.setBusy(false); + }) .catch((error) => { console.error('onSave', error); + this.setBusy(false); throw error; }); } - - toggleVaultSelector () { - this.setVaultSelectorOpen(!this.isVaultSelectorOpen); - } } diff --git a/js/src/modals/EditMeta/store.spec.js b/js/src/modals/EditMeta/store.spec.js index 4ff775718..a38da055f 100644 --- a/js/src/modals/EditMeta/store.spec.js +++ b/js/src/modals/EditMeta/store.spec.js @@ -14,14 +14,24 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +import sinon from 'sinon'; + import Store from './store'; import { ACCOUNT, ADDRESS, createApi } from './editMeta.test.js'; let api; let store; +let vaultStore; + +function createVaultStore () { + return { + moveAccount: sinon.stub().resolves(true) + }; +} function createStore (account) { api = createApi(); + vaultStore = createVaultStore(); store = new Store(api, account); @@ -108,6 +118,13 @@ describe('modals/EditMeta/Store', () => { createStore(ADDRESS); }); + describe('setBusy', () => { + it('sets the isBusy flag', () => { + store.setBusy('testing'); + expect(store.isBusy).to.equal('testing'); + }); + }); + describe('setDescription', () => { it('sets the description', () => { store.setDescription('description'); @@ -149,26 +166,56 @@ describe('modals/EditMeta/Store', () => { expect(store.vaultName).to.equal('testing'); }); }); - - describe('setVaultSelectorOpen', () => { - it('sets the state', () => { - store.setVaultSelectorOpen('testing'); - expect(store.isVaultSelectorOpen).to.equal('testing'); - }); - }); }); describe('operations', () => { describe('save', () => { beforeEach(() => { createStore(ACCOUNT); + sinon.spy(store, 'setBusy'); + }); + + afterEach(() => { + store.setBusy.restore(); + }); + + it('sets the busy flag, clearing it when done', () => { + return store.save().then(() => { + expect(store.setBusy).to.have.been.calledWith(true); + expect(store.setBusy).to.have.been.calledWith(false); + }); }); it('calls parity.setAccountName with the set value', () => { store.setName('test name'); - store.save(); - expect(api.parity.setAccountName).to.be.calledWith(ACCOUNT.address, 'test name'); + return store.save().then(() => { + expect(api.parity.setAccountName).to.be.calledWith(ACCOUNT.address, 'test name'); + }); + }); + + it('calls parity.setAccountMeta with the adjusted values', () => { + store.setDescription('some new description'); + store.setPasswordHint('some new passwordhint'); + store.setTags(['taga']); + + return store.save().then(() => { + expect(api.parity.setAccountMeta).to.have.been.calledWith( + ACCOUNT.address, Object.assign({}, ACCOUNT.meta, { + description: 'some new description', + passwordHint: 'some new passwordhint', + tags: ['taga'] + }) + ); + }); + }); + + it('moves vault account when applicable', () => { + store.setVaultName('testing'); + + return store.save(vaultStore).then(() => { + expect(vaultStore.moveAccount).to.have.been.calledWith('testing', ACCOUNT.address); + }); }); it('calls parity.setAccountMeta with the adjusted values', () => { @@ -185,11 +232,4 @@ describe('modals/EditMeta/Store', () => { }); }); }); - - describe('toggleVaultSelector', () => { - it('inverts the selector state', () => { - store.toggleVaultSelector(); - expect(store.isVaultSelectorOpen).to.be.true; - }); - }); }); diff --git a/js/src/modals/VaultSelector/vaultSelector.js b/js/src/modals/VaultSelector/vaultSelector.js index a2ae48294..4fceb5cb0 100644 --- a/js/src/modals/VaultSelector/vaultSelector.js +++ b/js/src/modals/VaultSelector/vaultSelector.js @@ -18,7 +18,9 @@ import { observer } from 'mobx-react'; import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; -import { Portal, SelectionList, VaultCard } from '~/ui'; +import Portal from '~/ui/Portal'; +import SelectionList from '~/ui/SelectionList'; +import VaultCard from '~/ui/VaultCard'; @observer export default class VaultSelector extends Component { @@ -48,10 +50,9 @@ export default class VaultSelector extends Component { } renderList () { - const { vaults } = this.props.vaultStore; - const openVaults = vaults.filter((vault) => vault.isOpen); + const { vaultsOpened } = this.props.vaultStore; - if (openVaults.length === 0) { + if (vaultsOpened.length === 0) { return ( . + +export default from './vaultSelect'; diff --git a/js/src/ui/Form/VaultSelect/vaultSelect.js b/js/src/ui/Form/VaultSelect/vaultSelect.js new file mode 100644 index 000000000..c93e530e1 --- /dev/null +++ b/js/src/ui/Form/VaultSelect/vaultSelect.js @@ -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 . + +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 ( +
+ { this.renderSelector() } + + } + label={ + + } + onClick={ this.openSelector } + value={ (value || '').toUpperCase() } + /> +
+ ); + } + + renderSelector () { + const { value } = this.props; + const { isOpen } = this.state; + + if (!isOpen) { + return null; + } + + return ( + + ); + } + + openSelector = () => { + this.setState({ + isOpen: true + }); + } + + closeSelector = () => { + this.setState({ + isOpen: false + }); + } + + onSelect = (vaultName) => { + this.props.onSelect(vaultName); + this.closeSelector(); + } +} diff --git a/js/src/ui/Form/VaultSelect/vaultSelect.spec.js b/js/src/ui/Form/VaultSelect/vaultSelect.spec.js new file mode 100644 index 000000000..a0d5ed583 --- /dev/null +++ b/js/src/ui/Form/VaultSelect/vaultSelect.spec.js @@ -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 . + +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( + + ); + 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'); + }); + }); + }); +}); diff --git a/js/src/ui/Form/index.js b/js/src/ui/Form/index.js index bb5516c89..8c8c7e1f2 100644 --- a/js/src/ui/Form/index.js +++ b/js/src/ui/Form/index.js @@ -29,5 +29,6 @@ export Label from './Label'; export RadioButtons from './RadioButtons'; export Select from './Select'; export TypedInput from './TypedInput'; +export VaultSelect from './VaultSelect'; export default from './form'; diff --git a/js/src/ui/SelectionList/selectionList.css b/js/src/ui/SelectionList/selectionList.css index 44f5a39b3..6a1a37eaf 100644 --- a/js/src/ui/SelectionList/selectionList.css +++ b/js/src/ui/SelectionList/selectionList.css @@ -17,6 +17,7 @@ .item { border: 2px solid transparent; + cursor: pointer; display: flex; flex: 1; height: 100%; diff --git a/js/src/ui/VaultTag/index.js b/js/src/ui/VaultTag/index.js new file mode 100644 index 000000000..af0419c99 --- /dev/null +++ b/js/src/ui/VaultTag/index.js @@ -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 . + +export default from './vaultTag'; diff --git a/js/src/ui/VaultTag/vaultTag.css b/js/src/ui/VaultTag/vaultTag.css new file mode 100644 index 000000000..acb139c6e --- /dev/null +++ b/js/src/ui/VaultTag/vaultTag.css @@ -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 . +*/ + +/* 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; + } +} diff --git a/js/src/ui/VaultTag/vaultTag.js b/js/src/ui/VaultTag/vaultTag.js new file mode 100644 index 000000000..303aaca61 --- /dev/null +++ b/js/src/ui/VaultTag/vaultTag.js @@ -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 . + +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 ( +
+
+ +
+ { vault } +
+
+
+ ); + } +} diff --git a/js/src/ui/index.js b/js/src/ui/index.js index ae1ce8451..d076986be 100644 --- a/js/src/ui/index.js +++ b/js/src/ui/index.js @@ -30,7 +30,7 @@ export DappCard from './DappCard'; export DappIcon from './DappIcon'; export Errors from './Errors'; export Features, { FEATURES, FeaturesStore } from './Features'; -export Form, { AddressSelect, DappUrlInput, FileSelect, FormWrap, Input, InputAddress, InputAddressSelect, InputChip, InputDate, InputInline, InputTime, Label, RadioButtons, Select, TypedInput } from './Form'; +export Form, { AddressSelect, DappUrlInput, FileSelect, FormWrap, Input, InputAddress, InputAddressSelect, InputChip, InputDate, InputInline, InputTime, Label, RadioButtons, Select, TypedInput, VaultSelect } from './Form'; export GasPriceEditor from './GasPriceEditor'; export GasPriceSelector from './GasPriceSelector'; export Icons from './Icons'; @@ -56,4 +56,5 @@ export Tooltips, { Tooltip } from './Tooltips'; export TxHash from './TxHash'; export TxList from './TxList'; export VaultCard from './VaultCard'; +export VaultTag from './VaultTag'; export Warning from './Warning'; diff --git a/js/src/views/Account/Header/header.css b/js/src/views/Account/Header/header.css index 62f072574..f894b7c49 100644 --- a/js/src/views/Account/Header/header.css +++ b/js/src/views/Account/Header/header.css @@ -66,6 +66,7 @@ .text { display: inline-block; opacity: 0.25; + text-transform: uppercase; } } diff --git a/js/src/views/Account/Header/header.js b/js/src/views/Account/Header/header.js index dc367d136..d3c4a9c69 100644 --- a/js/src/views/Account/Header/header.js +++ b/js/src/views/Account/Header/header.js @@ -17,7 +17,7 @@ import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; -import { Balance, Certifications, Container, CopyToClipboard, ContainerTitle, IdentityIcon, IdentityName, QrCode, Tags } from '~/ui'; +import { Balance, Certifications, Container, CopyToClipboard, ContainerTitle, IdentityIcon, IdentityName, QrCode, Tags, VaultTag } from '~/ui'; import styles from './header.css'; @@ -69,7 +69,6 @@ export default class Header extends Component { { address }
- { this.renderVault() } { this.renderUuid() }
{ meta.description } @@ -81,6 +80,7 @@ export default class Header extends Component { balance={ balance } /> + { this.renderVault() }
@@ -169,15 +169,7 @@ export default class Header extends Component { } return ( -
- -
- { meta.vault } -
-
+ ); } } diff --git a/js/src/views/Accounts/Summary/summary.js b/js/src/views/Accounts/Summary/summary.js index 3a4b282fa..78f4dd5c4 100644 --- a/js/src/views/Accounts/Summary/summary.js +++ b/js/src/views/Accounts/Summary/summary.js @@ -22,7 +22,7 @@ import { isEqual } from 'lodash'; import ReactTooltip from 'react-tooltip'; import { FormattedMessage } from 'react-intl'; -import { Balance, Container, ContainerTitle, CopyToClipboard, IdentityIcon, IdentityName, Tags } from '~/ui'; +import { Balance, Container, ContainerTitle, CopyToClipboard, IdentityIcon, IdentityName, Tags, VaultTag } from '~/ui'; import Certifications from '~/ui/Certifications'; import { arrayOrObjectProptype, nullableProptype } from '~/util/proptypes'; @@ -117,6 +117,7 @@ class Summary extends Component { { this.renderDescription(account.meta) } { this.renderOwners() } { this.renderCertifications() } + { this.renderVault(account.meta) }
} link={ this.getLink() } @@ -287,6 +288,16 @@ class Summary extends Component { /> ); } + + renderVault (meta) { + if (!meta || !meta.vault) { + return null; + } + + return ( + + ); + } } function mapStateToProps (state) { diff --git a/js/src/views/Accounts/accounts.css b/js/src/views/Accounts/accounts.css index de6ce3ab5..296ae6714 100644 --- a/js/src/views/Accounts/accounts.css +++ b/js/src/views/Accounts/accounts.css @@ -56,6 +56,10 @@ } } + .overlay { + margin-top: -3.25em; + } + .owners { display: flex; justify-content: center; @@ -68,10 +72,6 @@ } } - .overlay { - margin-top: -3.25em; - } - &:not(:hover) { .tags { display: none; diff --git a/js/src/views/Vaults/store.js b/js/src/views/Vaults/store.js index 75a52954d..2d4f4c2df 100644 --- a/js/src/views/Vaults/store.js +++ b/js/src/views/Vaults/store.js @@ -37,6 +37,7 @@ export default class Store { @observable selectedAccounts = {}; @observable vault = null; @observable vaults = []; + @observable vaultsOpened = []; @observable vaultNames = []; @observable vaultName = ''; @observable vaultNameError = ERRORS.noName; @@ -143,6 +144,7 @@ export default class Store { isOpen: openedVaults.includes(name) }; }); + this.vaultsOpened = this.vaults.filter((vault) => vault.isOpen); }); } diff --git a/js/src/views/Vaults/store.spec.js b/js/src/views/Vaults/store.spec.js index 9f971d383..863b853da 100644 --- a/js/src/views/Vaults/store.spec.js +++ b/js/src/views/Vaults/store.spec.js @@ -180,6 +180,12 @@ describe('modals/Vaults/Store', () => { { name: 'some', meta: 'metaSome', isOpen: false } ]); }); + + it('sets the opened vaults', () => { + expect(store.vaultsOpened.peek()).to.deep.equal([ + { name: 'TEST', meta: 'metaTest', isOpen: true } + ]); + }); }); describe('setVaultDescription', () => { @@ -553,6 +559,36 @@ describe('modals/Vaults/Store', () => { }); }); + describe('editVaultMeta', () => { + beforeEach(() => { + sinon.spy(store, 'setBusyMeta'); + + store.setVaultDescription('testDescription'); + store.setVaultName('testCreateName'); + store.setVaultPasswordHint('testCreateHint'); + store.setVaultTags('testTags'); + + return store.editVaultMeta(); + }); + + afterEach(() => { + store.setBusyMeta.restore(); + }); + + it('sets and resets the busy flag', () => { + expect(store.setBusyMeta).to.have.been.calledWith(true); + expect(store.isBusyMeta).to.be.false; + }); + + it('calls into parity_setVaultMeta', () => { + expect(api.parity.setVaultMeta).to.have.been.calledWith('testCreateName', { + description: 'testDescription', + passwordHint: 'testCreateHint', + tags: 'testTags' + }); + }); + }); + describe('editVaultPassword', () => { beforeEach(() => { sinon.spy(store, 'setBusyMeta');