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:
@@ -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)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user