-
Notifications
You must be signed in to change notification settings - Fork 98
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
Changes from 6 commits
135455c
247fb89
f981549
a3d1899
14daec6
b5b8ae6
68d62bc
4833038
e3fd139
8e25681
ad77080
a00ffd4
0200053
3dfdf72
fcf8a74
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,7 +44,7 @@ export default { | |
computed: { | ||
...mapGetters(['candidates', 'filters', 'shoppingCart']), | ||
pageTitle () { | ||
return `Delegates (${this.candidatesNum} Candidates Selected)` | ||
return `Delegate (${this.candidatesNum} Candidates Selected)` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think "Candidates Selected" belongs in the page title. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's put it in the label for the Bond button instead. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think back arrows should always say "back" There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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—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" | ||
|
@@ -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> | ||
|
@@ -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: () => ({ | ||
|
@@ -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) | ||
|
@@ -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 | ||
|
@@ -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 } | ||
} | ||
}, | ||
|
@@ -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> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -91,15 +91,15 @@ export default { | |
this.sending = false | ||
this.$v.$reset() | ||
}, | ||
onSubmit () { | ||
async onSubmit () { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what does this have to do with bonding/unbonding? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 }], | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,7 +13,7 @@ export default ({ commit, node }) => { | |
} | ||
|
||
const emptyUser = { | ||
atoms: 0, | ||
atoms: 2097152, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can't we use real data here yet? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
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) | ||
} |
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() | ||
}) | ||
}) |
There was a problem hiding this comment.
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.Transactions, Proposals, Validators,
Delegates
(nouns) ... but i guess i see howDelegate
fits with send (verbs)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 theDelegates
page. bonding ATOMs is a type of transaction and should have a similar UX to sending.see also #223
There was a problem hiding this comment.
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.