Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix bonding/unbonding ui #218

Merged
merged 15 commits into from
Dec 13, 2017
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/src/renderer/components/common/AppMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ menu.app-menu
part(title='Governance' v-if="config.devMode")
list-item(to="/proposals" exact @click.native="close" title="Proposals")
part(title='Stake' v-if="config.devMode")
list-item(to="/staking" exact @click.native="close" title="Delegates")
list-item(to="/staking" exact @click.native="close" title="Delegate")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hahaha. we keep switching this back and forth. the reason i changed it to Delegates with an 's' is for a couple reasons.

  1. Transactions, Proposals, Validators, Delegates (nouns) ... but i guess i see how Delegate fits with send (verbs)

  2. when you click the link Delegates what you see is a list of delegates (noun), not a screen for delegating (verb).

this highlights a bigger issue that you brought up in #128 and that i've been thinking about — ideally, our sidebar would use consistent language. actions (verbs) should take place within the context of important nouns (Balances, Transactions, Proposals, Delegates, Validators). your idea to include send as a modal on balances makes a ton of sense to me for this reason. also, why Delegate, the action, makes sense as a feature modal of the Delegates page. bonding ATOMs is a type of transaction and should have a similar UX to sending.

see also #223

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read your reasoning -- let's change it to Delegates.

part(title='Monitor' v-if="config.devMode")
list-item(to="/blockchain" exact @click.native="close" title="Blockchain")
list-item(to="/validators" exact @click.native="close" title="Validators"
Expand Down
2 changes: 1 addition & 1 deletion app/src/renderer/components/common/NiFormStruct.vue
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export default {
> div
padding 1rem 1rem 1rem - px
display flex
justify-content flex-end
justify-content space-between
align-items center

> *:not(:last-child)
Expand Down
2 changes: 1 addition & 1 deletion app/src/renderer/components/staking/PageCandidates.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default {
computed: {
...mapGetters(['candidates', 'filters', 'shoppingCart']),
pageTitle () {
return `Delegates (${this.candidatesNum} Candidates Selected)`
return `Delegate (${this.candidatesNum} Candidates Selected)`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think "Candidates Selected" belongs in the page title.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's put it in the label for the Bond button instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, let's keep this here until we design a new component to put this in.

},
filteredCandidates () {
let query = this.filters.candidates.search.query
Expand Down
148 changes: 62 additions & 86 deletions app/src/renderer/components/staking/PageDelegate.vue
Original file line number Diff line number Diff line change
@@ -1,37 +1,33 @@
<template lang="pug">
page(:title="pageTitle")
page.page-delegate(title="Delegate Atoms")
div(slot="menu"): tool-bar
router-link(to='/staking')
i.material-icons arrow_back
.label Change Candidates
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think back arrows should always say "back"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.


form-struct(:submit="onSubmit")
form-group(:error="$v.fields.reservedAtoms.$error")
Label Reserved Atoms
field-group
h1 {{unallocatedAtoms}}
small &nbsp;ATOM not delegated.

form-msg: div Reserved Atoms will be held by you and remain unbonded
.reserved-atoms(v-if="unallocatedAtoms == this.user.atoms")
| You have #[.reserved-atoms__number {{ unallocatedAtoms }}] unbonded atoms. Begin the delegation process by allocating some of your atoms to these validator candidates.
.reserved-atoms(v-else-if="unallocatedAtoms === 0")
| You are allocating #[.reserved-atoms__number ALL {{ allocatedAtoms }}] atoms to these validator candidates. We suggest reserving some atoms for personal use&mdash;are you sure you wish to proceed? #[a(@click="resetAlloc") (start over?)]
.reserved-atoms(v-else-if="unallocatedAtoms < 0")
| #[.reserved-atoms__number.reserved-atoms__number--error You are allocating {{ unallocatedAtoms * -1 }} more atoms than exist in your balance.] Please reduce the number of atoms you are allocating to these validator candidates. #[a(@click="resetAlloc") (start over?)]
.reserved-atoms(v-else)
| You are reserving #[.reserved-atoms__number {{ unallocatedAtoms }}] ({{ unallocatedAtomsPct }}) atoms. You are allocating #[.reserved-atoms__number {{ allocatedAtoms }}] ({{ allocatedAtomsPct }}) atoms to these validator candidates. #[a(@click="resetAlloc") (start over?)]

form-struct(:submit="onSubmit")
form-group(v-for='(candidate, index) in fields.candidates' key='candidate.id'
:error="$v.fields.candidates.$each[index].$error")
Label {{ candidate.candidate.keybaseID }}
Label {{ candidate.candidate.keybaseID }} ({{ percentAtoms(candidate.atoms) }})
field-group
field(
theme="cosmos"
type="number"
step="any"
placeholder="Atoms"
v-model.number="candidate.atoms")
field-addon Atoms
.percentage {{ percentAtoms(candidate.atoms) }}
btn(type="button" theme="cosmos" value="Max"
field-addon Atoms
// btn(type="button" value="Max"
@click.native="fillAtoms(candidate.id)")
// btn(type="button" theme="cosmos" value="Clear"
@click.native="clearAtoms(candidate.id)")
btn(type="button" theme="cosmos" value="Remove"
@click.native="rm(candidate.id)")
btn(type="button" icon="clear" @click.native="rm(candidate.id)")
form-msg(name="Atoms" type="required"
v-if="!$v.fields.candidates.$each[index].atoms.required")
form-msg(name="Atoms" type="numeric"
Expand All @@ -40,9 +36,8 @@ page(:title="pageTitle")
v-if="!$v.fields.candidates.$each[index].atoms.between")

div(slot="footer")
div
btn(theme="cosmos" icon="balance-scale" value="Equalize" type="button" @click.native="equalAlloc")
btn(theme="cosmos" icon="check" value="Set Allocation")
btn(icon="drag_handle" value="Equalize" type="button" @click.native="equalAlloc")
btn(icon="check" value="Delegate")
</template>

<script>
Expand Down Expand Up @@ -72,28 +67,27 @@ export default {
},
computed: {
...mapGetters(['shoppingCart', 'user']),
pageTitle () {
let title = 'Delegate Atoms '
if (this.unallocatedAtoms > 0) {
title += this.unallocatedAtoms + ', ' + this.unallocatedAtomsPercent
} else {
title += '(DONE)'
}
return title
},
unreservedAtoms () {
return this.user.atoms - this.fields.reservedAtoms
},
unallocatedAtoms () {
let value = this.unreservedAtoms

// reduce unallocated atoms by assigned atoms
// reduce unreserved atoms by allocated atoms
this.fields.candidates.forEach(f => (value -= f.atoms))

return value
},
unallocatedAtomsPercent () {
unallocatedAtomsPct () {
return Math.round(this.unallocatedAtoms / this.user.atoms * 100 * 100) / 100 + '%'
},
allocatedAtoms () {
let value = 0
this.fields.candidates.forEach(f => (value += f.atoms))
return value
},
allocatedAtomsPct () {
return Math.round(this.allocatedAtoms / this.user.atoms * 100 * 100) / 100 + '%'
}
},
data: () => ({
Expand All @@ -108,7 +102,6 @@ export default {
methods: {
fillAtoms (candidateId) {
if (candidateId === 'unreserved') {
console.log('filling reserved atoms')
this.fields.reservedAtoms += this.unallocatedAtoms
} else {
let candidate = this.fields.candidates.find(c => c.id === candidateId)
Expand All @@ -126,9 +119,8 @@ export default {
}
},
equalAlloc () {
console.log(this.fields)
this.equalize = true
this.resetCandidates()
this.resetAlloc()
let atoms = this.unreservedAtoms
let candidates = this.fields.candidates.length
let remainderAtoms = atoms % candidates
Expand All @@ -140,63 +132,64 @@ export default {
for (let i = 0; i < remainderAtoms; i++) {
this.fields.candidates[i].atoms += 1
}

this.$store.commit('notify', { title: 'Equal Allocation',
body: 'You have split your atoms equally among all of these validator candidates.'
})
},
percentAtoms (assignedAtoms) {
return Math.round(assignedAtoms / this.user.atoms * 100 * 100) / 100 + '%'
percentAtoms (allocatedAtoms) {
return Math.round(allocatedAtoms / this.user.atoms * 100 * 100) / 100 + '%'
},
onSubmit () {
if (this.unallocatedAtoms === this.user.atoms) {
this.$store.commit('notifyWarn', { title: 'Unallocated Atoms',
this.$store.commit('notifyError', { title: 'Unallocated Atoms',
body: 'You haven\'t allocated any atoms yet.' })
return
} else if (this.unallocatedAtoms < 0) {
this.$store.commit('notifyWarn', { title: 'Too Many Allocated Atoms',
this.$store.commit('notifyError', { title: 'Too Many Allocated Atoms',
body: `You've allocated ${this.unallocatedAtoms * -1} more atoms than you have.`})
return
}
this.$v.$touch()
if (!this.$v.$error) {
this.$store.commit('activateDelegation')
this.$store.dispatch('submitDelegation', this.fields)
this.$store.commit('notify',
{ title: 'Atom Allocation Set',
body: 'You have successfully set your atom allocation. You can change it up until the end of the game.' })
} else {
console.log('onSubmit error', this.$v.$error)
this.$store.commit('notify', { title: 'Atoms Delegated',
body: 'You have successfully delegated your atoms. You can change your delegation after the unbonding period (30 days).' })
}
},
resetCandidates () {
resetAlloc () {
this.fields.candidates = []
this.shoppingCart.map(c => this.fields.candidates.push(Object.assign({}, c)))
},
getShoppingCartItem (candidateId) {
return this.shoppingCart.find(c => c.candidateId === candidateId)
},
leaveIfNoCandidates (count) {
if (count < 1) {
this.$store.commit('notify', {
leaveIfEmpty (count) {
if (count === 0) {
this.$store.commit('notifyError', {
title: 'No Candidates Selected',
body: 'Choose one or more candidates before proceeding to delegate atoms.'
})
this.$router.push('/staking')
}
},
rm (candidateId) {
this.$store.commit('removeFromCart', candidateId)
this.resetCandidates()
let confirm = window.confirm('Are you sure you want to remove this validator candidate?')
if (confirm) {
this.$store.commit('removeFromCart', candidateId)
this.resetAlloc()
}
}
},
mounted () {
this.resetCandidates()
this.leaveIfNoCandidates(this.shoppingCart.length)
this.leaveIfEmpty(this.shoppingCart.length)
this.resetAlloc()
if (this.user.delegationActive) {
this.fields = JSON.parse(JSON.stringify(this.user.delegation))
}
},
watch: {
shoppingCart (newVal) {
this.leaveIfNoCandidates(newVal.length)
// this.resetCandidates()
this.leaveIfEmpty(newVal.length)
// this.resetAlloc()
if (this.equalize) { this.equalAlloc }
}
},
Expand Down Expand Up @@ -226,36 +219,19 @@ export default {
</script>

<style lang="stylus">
@import '~variables'

.page-delegate
.cards
margin 1rem 0

.ni-form
.ni-field-group
.ni-btn
margin-left 1rem
.ni-form-footer
.left
.ni-btn
margin-right 1rem
&:last-child
margin 0
@require '~variables'

h1
font-size h1
.reserved-atoms
padding 1rem
background app-fg
margin 0 0 1rem
color dim

small
font-size xs
.reserved-atoms__number
display inline
color bright
font-weight 500

.percentage
border px solid bc
border-left none
height 2rem
width 3.625rem
display flex
align-items center
justify-content center
color dim
.reserved-atoms__number--error
color danger
</style>
4 changes: 2 additions & 2 deletions app/src/renderer/components/wallet/PageSend.vue
Original file line number Diff line number Diff line change
Expand Up @@ -91,15 +91,15 @@ export default {
this.sending = false
this.$v.$reset()
},
onSubmit () {
async onSubmit () {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does this have to do with bonding/unbonding?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was me improving some tests while fixing the tests. Should have maybe gone into another PR.

this.$v.$touch()
if (this.$v.$error) return

this.sending = true
let amount = +this.fields.amount
let address = this.fields.address
let denom = this.fields.denom
this.walletSend({
await this.walletSend({
fees: { denom, amount: 0 },
to: address,
amount: [{ denom, amount }],
Expand Down
2 changes: 1 addition & 1 deletion app/src/renderer/vuex/modules/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default ({ commit, node }) => {
}

const emptyUser = {
atoms: 0,
atoms: 2097152,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't we use real data here yet?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not yet. We can put it in mockServer.js though, @mappum ?

nominationActive: false,
nomination: JSON.parse(JSON.stringify(emptyNomination)),
delegationActive: false,
Expand Down
14 changes: 14 additions & 0 deletions test/unit/helpers/node_mock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = {
generateKey: () => ({key: '123'}),
queryAccount: () => null,
queryNonce: () => '123',
buildSend: (args) => {
if (args.to.addr.indexOf('fail') !== -1) return Promise.reject('Failed on purpose')
return Promise.resolve(null)
},
postTx: () => Promise.resolve({
check_tx: { code: 0 },
deliver_tx: { code: 0 }
}),
sign: () => Promise.resolve(null)
}
6 changes: 1 addition & 5 deletions test/unit/specs/App.spec.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
jest.mock('renderer/node.js', () => () => ({
generateKey: () => ({key: '123'}),
queryAccount: () => null,
queryNonce: () => '123'
}))
jest.mock('renderer/node.js', () => () => require('../helpers/node_mock'))

// needs to be mocked as it imports the native supercop module and this fails in jest
jest.mock('app/node_modules/tendermint-crypto', () => {})
Expand Down
4 changes: 1 addition & 3 deletions test/unit/specs/PageBalances.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import PageBalances from 'renderer/components/wallet/PageBalances'

const filters = require('renderer/vuex/modules/filters').default({})
const wallet = require('renderer/vuex/modules/wallet').default({
node: {
queryAccount () {}
}
node: require('../helpers/node_mock')
})

const localVue = createLocalVue()
Expand Down
38 changes: 38 additions & 0 deletions test/unit/specs/PageDelegate.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Vuex from 'vuex'
import VueRouter from 'vue-router'
import { mount, createLocalVue } from 'vue-test-utils'
import htmlBeautify from 'html-beautify'
import PageDelegate from 'renderer/components/staking/PageDelegate'

const user = require('renderer/vuex/modules/user').default({})
const shoppingCart = require('renderer/vuex/modules/shoppingCart').default({})
const notifications = require('renderer/vuex/modules/notifications').default({})

const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(VueRouter)

describe('PageDelegate', () => {
let wrapper, store, router

beforeEach(() => {
store = new Vuex.Store({
modules: { user, shoppingCart, notifications },
getters: {
shoppingCart: () => shoppingCart.state.candidates,
user: () => user.state
}
})
router = new VueRouter({})
wrapper = mount(PageDelegate, {
localVue,
store,
router
})
store.commit = jest.fn()
})

it('has the expected html structure', () => {
expect(htmlBeautify(wrapper.html())).toMatchSnapshot()
})
})
Loading