Skip to content

Commit

Permalink
feat: add reason when voting (#511)
Browse files Browse the repository at this point in the history
* feat: add reason when voting

* fix: update voting power fetching

* fix: do not pin reason when empty

* chore: lint fix

* Update apps/ui/src/views/Proposal.vue

Co-authored-by: Wiktor Tkaczyński <[email protected]>

* fix(ui): improve disabled button UI

* Update apps/ui/src/components/ProposalsListItem.vue

Co-authored-by: Wiktor Tkaczyński <[email protected]>

* fix: improve typing

* refactor: use same VotingPowerItem type

* refactor: improve chaining

* refactor: use optional chaining

* chore: fix formatting

* chore: fix missing ;

* refactor: improve code readability

* fix: fix to match updated name

* fix: use web3.account to detect logged in user

There's not need to check for authInitiated, as we don't support login and logout while in this page

* fix: fix PUBLISH button always stuck on loading state for guest user

* fix: always use the full VotingPowerItem type

* fix(ux): improve button layout on small screen

* refactor: avoid deprecated validateForm usage

* fix(ui): decrease margin size

* fix(ui): improve error message

* fix: fix variable not reactive

* chore: update tests to include metadataUri

* fix: fix metadataUri being ignored from voteHash

* fix: check that voting power is successful, in case threshold is 0

* refactor: save propose and vote potential in vp store

* chore: remove console output

* fix: remove unecessary template wrapper

* Update apps/ui/src/views/Space/Proposals.vue

Co-authored-by: Wiktor Tkaczyński <[email protected]>

---------

Co-authored-by: Wiktor Tkaczyński <[email protected]>
  • Loading branch information
wa0x6e and Sekhmet authored Aug 7, 2024
1 parent ead1ebe commit 9e4e6ea
Show file tree
Hide file tree
Showing 35 changed files with 645 additions and 269 deletions.
5 changes: 5 additions & 0 deletions .changeset/breezy-owls-clean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@snapshot-labs/sx": patch
---

support submitting a reason when voting
38 changes: 12 additions & 26 deletions apps/ui/src/components/IndicatorVotingPower.vue
Original file line number Diff line number Diff line change
@@ -1,40 +1,29 @@
<script setup lang="ts">
import { _vp } from '@/helpers/utils';
import { getFormattedVotingPower } from '@/helpers/utils';
import { evmNetworks } from '@/networks';
import { VotingPower, VotingPowerStatus } from '@/networks/types';
import { VotingPowerItem } from '@/stores/votingPowers';
import { NetworkID } from '@/types';
const props = defineProps<{
networkId: NetworkID;
status: VotingPowerStatus;
votingPowerSymbol: string;
votingPowers: VotingPower[];
votingPower?: VotingPowerItem;
}>();
defineEmits<{
(e: 'getVotingPower');
(e: 'fetchVotingPower');
}>();
const { web3 } = useWeb3();
const modalOpen = ref(false);
const votingPower = computed(() =>
props.votingPowers.reduce((acc, b) => acc + b.value, 0n)
const formattedVotingPower = computed(() =>
getFormattedVotingPower(props.votingPower)
);
const decimals = computed(() =>
Math.max(...props.votingPowers.map(votingPower => votingPower.decimals), 0)
);
const formattedVotingPower = computed(() => {
const value = _vp(Number(votingPower.value) / 10 ** decimals.value);
if (props.votingPowerSymbol) {
return `${value} ${props.votingPowerSymbol}`;
}
return value;
});
const loading = computed(() => props.status === 'loading');
const loading = computed(
() => !props.votingPower || props.votingPower.status === 'loading'
);
function handleModalOpen() {
modalOpen.value = true;
Expand Down Expand Up @@ -63,7 +52,7 @@ function handleModalOpen() {
>
<IH-lightning-bolt class="inline-block -ml-1" />
<IH-exclamation
v-if="props.status === 'error'"
v-if="props.votingPower?.status === 'error'"
class="inline-block ml-1 text-rose-500"
/>
<span v-else class="ml-1">{{ formattedVotingPower }}</span>
Expand All @@ -74,12 +63,9 @@ function handleModalOpen() {
<ModalVotingPower
:open="modalOpen"
:network-id="networkId"
:voting-power-symbol="votingPowerSymbol"
:voting-powers="props.votingPowers"
:voting-power-status="status"
:final-decimals="decimals"
:voting-power="props.votingPower"
@close="modalOpen = false"
@get-voting-power="$emit('getVotingPower')"
@fetch-voting-power="$emit('fetchVotingPower')"
/>
</teleport>
</div>
Expand Down
45 changes: 45 additions & 0 deletions apps/ui/src/components/MessageVotingPower.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<script setup lang="ts">
import { VotingPowerItem } from '@/stores/votingPowers';
defineProps<{
votingPower: VotingPowerItem;
action?: 'vote' | 'propose';
}>();
defineEmits<{
(e: 'fetchVotingPower');
}>();
</script>

<template>
<div
v-if="votingPower.status === 'error'"
class="flex flex-col gap-3 items-start"
v-bind="$attrs"
>
<UiAlert type="error">
There was an error fetching your voting power.
</UiAlert>
<UiButton
type="button"
class="flex items-center gap-2"
@click="$emit('fetchVotingPower')"
>
<IH-refresh />Retry
</UiButton>
</div>
<UiAlert
v-else-if="action === 'vote' && !votingPower.canVote"
type="error"
v-bind="$attrs"
>
You do not have enough voting power to vote.
</UiAlert>
<UiAlert
v-else-if="action === 'propose' && !votingPower.canPropose"
type="error"
v-bind="$attrs"
>
You do not have enough voting power to create proposal in this space.
</UiAlert>
</template>
168 changes: 168 additions & 0 deletions apps/ui/src/components/Modal/Vote.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
<script setup lang="ts">
import { getChoiceText, getFormattedVotingPower } from '@/helpers/utils';
import { getValidator } from '@/helpers/validation';
import { Choice, Proposal } from '@/types';
const REASON_DEFINITION = {
title: 'Reason',
type: 'string',
format: 'long',
examples: ['Share you reason (optional)'],
maxLength: 1000
};
const props = defineProps<{
proposal: Proposal;
choice: Choice | null;
open: boolean;
}>();
const emit = defineEmits<{
(e: 'close');
(e: 'voted');
}>();
const { vote } = useActions();
const { web3 } = useWeb3();
const {
votingPower,
fetch: fetchVotingPower,
reset: resetVotingPower
} = useVotingPower();
const loading = ref(false);
const form = ref<Record<string, string>>({ reason: '' });
const formErrors = ref({} as Record<string, any>);
const formValidated = ref(false);
const formValidator = getValidator({
$async: true,
type: 'object',
title: 'Reason',
additionalProperties: false,
required: [],
properties: {
reason: REASON_DEFINITION
}
});
const formattedVotingPower = computed(() =>
getFormattedVotingPower(votingPower.value)
);
const canSubmit = computed(
() =>
formValidated &&
!!props.choice &&
Object.keys(formErrors.value).length === 0 &&
votingPower.value?.canVote
);
async function handleSubmit() {
loading.value = true;
if (!props.choice) return;
try {
await vote(props.proposal, props.choice, form.value.reason);
emit('voted');
emit('close');
} finally {
loading.value = false;
}
}
function handleFetchVotingPower() {
fetchVotingPower(props.proposal);
}
watch(
[() => props.open, () => web3.value.account],
([open, toAccount], [, fromAccount]) => {
if (fromAccount && toAccount && fromAccount !== toAccount) {
resetVotingPower();
}
if (open) handleFetchVotingPower();
},
{ immediate: true }
);
watchEffect(async () => {
formValidated.value = false;
formErrors.value = await formValidator.validateAsync(form.value);
formValidated.value = true;
});
</script>

<template>
<UiModal :open="open" @close="$emit('close')">
<template #header>
<h3>Cast your vote</h3>
</template>
<div class="m-4 mb-3 flex flex-col space-y-3">
<MessageVotingPower
v-if="votingPower"
:voting-power="votingPower"
action="vote"
@fetch-voting-power="handleFetchVotingPower"
/>
<dl>
<dt class="text-sm leading-5">Choice</dt>
<dd class="text-skin-heading text-[20px] leading-6">
<span
v-if="choice"
class="test-skin-heading font-semibold"
v-text="getChoiceText(proposal.choices, choice)"
/>
<div v-else class="flex gap-1 text-skin-danger items-center">
<IH-exclamation-circle />
No choice selected
</div>
</dd>
<dt class="text-sm leading-5 mt-3">Voting power</dt>
<dd v-if="!votingPower || votingPower.status === 'loading'">
<UiLoading />
</dd>
<dd
v-else-if="votingPower.status === 'success'"
class="font-semibold text-skin-heading text-[20px] leading-6"
v-text="formattedVotingPower"
/>
<dd
v-else-if="votingPower.status === 'error'"
class="font-semibold text-skin-heading text-[20px] leading-6"
v-text="formattedVotingPower"
/>
</dl>
<div class="s-box">
<UiForm
v-model="form"
:error="formErrors"
:definition="{ properties: { reason: REASON_DEFINITION } }"
/>
</div>
</div>

<template #footer>
<div class="flex flex-col xs:flex-row gap-3">
<UiButton
class="w-full order-last xs:order-none"
@click="$emit('close')"
>
Cancel
</UiButton>
<UiButton
primary
class="w-full"
:disabled="!canSubmit"
:loading="loading"
@click="handleSubmit"
>
Confirm
</UiButton>
</div>
</template>
</UiModal>
</template>
49 changes: 22 additions & 27 deletions apps/ui/src/components/Modal/VotingPower.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,18 @@
import { _n, shorten } from '@/helpers/utils';
import { addressValidator as isValidAddress } from '@/helpers/validation';
import { getNetwork } from '@/networks';
import { VotingPower, VotingPowerStatus } from '@/networks/types';
import { VotingPowerItem } from '@/stores/votingPowers';
import { NetworkID } from '@/types';
const props = defineProps<{
open: boolean;
networkId: NetworkID;
votingPowerSymbol: string;
votingPowers: VotingPower[];
votingPowerStatus: VotingPowerStatus;
finalDecimals: number;
votingPower?: VotingPowerItem;
}>();
defineEmits<{
(e: 'close');
(e: 'getVotingPower');
(e: 'fetchVotingPower');
}>();
const network = computed(() => getNetwork(props.networkId));
Expand All @@ -25,8 +22,9 @@ const baseNetwork = computed(() =>
? getNetwork(network.value.baseNetworkId)
: network.value
);
const loading = computed(() => props.votingPowerStatus === 'loading');
const error = computed(() => props.votingPowerStatus === 'error');
const loading = computed(
() => !props.votingPower || props.votingPower.status === 'loading'
);
</script>

<template>
Expand All @@ -35,21 +33,14 @@ const error = computed(() => props.votingPowerStatus === 'error');
<h3>Your voting power</h3>
</template>
<UiLoading v-if="loading" class="p-4 block text-center" />
<div v-else>
<div v-if="error" class="p-4 flex flex-col gap-3 items-start">
<UiAlert type="error"
>There was an error fetching your voting power.</UiAlert
>
<UiButton
type="button"
class="flex items-center gap-2"
@click="$emit('getVotingPower')"
>
<IH-refresh />Retry
</UiButton>
</div>
<div v-else-if="votingPower">
<MessageVotingPower
class="p-4"
:voting-power="votingPower"
@fetch-voting-power="$emit('fetchVotingPower')"
/>
<div
v-for="(strategy, i) in votingPowers"
v-for="(strategy, i) in votingPower.votingPowers"
:key="i"
class="py-3 px-4 border-b last:border-b-0"
>
Expand All @@ -67,12 +58,16 @@ const error = computed(() => props.votingPowerStatus === 'error');
/>
<div class="text-skin-link shrink-0">
{{
_n(Number(strategy.value) / 10 ** finalDecimals, 'compact', {
maximumFractionDigits: 2,
formatDust: true
})
_n(
Number(strategy.value) / 10 ** votingPower.decimals,
'compact',
{
maximumFractionDigits: 2,
formatDust: true
}
)
}}
{{ votingPowerSymbol }}
{{ votingPower.symbol }}
</div>
</div>
<div class="flex justify-between">
Expand Down
2 changes: 0 additions & 2 deletions apps/ui/src/components/ProposalVoteApproval.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { Choice, Proposal } from '@/types';
type ApprovalChoice = number[];
const props = defineProps<{
sendingType: Choice | null;
proposal: Proposal;
defaultChoice?: Choice;
}>();
Expand Down Expand Up @@ -45,7 +44,6 @@ function toggleSelectedChoice(choice: number) {
<UiButton
primary
class="!h-[48px] w-full"
:loading="!!sendingType"
@click="emit('vote', selectedChoices)"
>
Vote
Expand Down
Loading

0 comments on commit 9e4e6ea

Please sign in to comment.