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

feat: add reason when voting #511

Merged
merged 39 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
bb47e8d
feat: add reason when voting
wa0x6e Jul 20, 2024
4a933a4
fix: update voting power fetching
wa0x6e Jul 20, 2024
2d6bfcc
fix: do not pin reason when empty
wa0x6e Jul 20, 2024
fe998e4
Merge branch 'master' into feat-vote-reason-modal
wa0x6e Jul 25, 2024
3635482
chore: lint fix
wa0x6e Jul 25, 2024
079eff0
Merge branch 'master' into feat-vote-reason-modal
wa0x6e Jul 30, 2024
7b918cf
Merge branch 'master' into feat-vote-reason-modal
wa0x6e Jul 31, 2024
bbae392
Update apps/ui/src/views/Proposal.vue
wa0x6e Aug 1, 2024
ab2bcba
Merge branch 'master' into feat-vote-reason-modal
wa0x6e Aug 1, 2024
599ea9e
Merge branch 'feat-vote-reason-modal' of https://github.com/snapshot-…
wa0x6e Aug 1, 2024
ae3f4b0
fix(ui): improve disabled button UI
wa0x6e Aug 1, 2024
2b6c70d
Update apps/ui/src/components/ProposalsListItem.vue
wa0x6e Aug 1, 2024
8460f1c
Merge branch 'feat-vote-reason-modal' of https://github.com/snapshot-…
wa0x6e Aug 1, 2024
d0832a6
Merge branch 'master' into feat-vote-reason-modal
wa0x6e Aug 2, 2024
7f57ace
fix: improve typing
wa0x6e Aug 2, 2024
b4768f7
refactor: use same VotingPowerItem type
wa0x6e Aug 2, 2024
60d0edd
refactor: improve chaining
wa0x6e Aug 2, 2024
5416daa
refactor: use optional chaining
wa0x6e Aug 2, 2024
c48188b
chore: fix formatting
wa0x6e Aug 2, 2024
0eb92d1
chore: fix missing ;
wa0x6e Aug 2, 2024
5373837
refactor: improve code readability
wa0x6e Aug 2, 2024
f708b8c
fix: fix to match updated name
wa0x6e Aug 2, 2024
e741ffd
fix: use web3.account to detect logged in user
wa0x6e Aug 2, 2024
b805f30
fix: fix PUBLISH button always stuck on loading state for guest user
wa0x6e Aug 2, 2024
098d278
fix: always use the full VotingPowerItem type
wa0x6e Aug 2, 2024
0d2c4a0
fix(ux): improve button layout on small screen
wa0x6e Aug 2, 2024
81cb6ad
refactor: avoid deprecated validateForm usage
wa0x6e Aug 5, 2024
936b125
fix(ui): decrease margin size
wa0x6e Aug 5, 2024
9d29854
Merge branch 'master' into feat-vote-reason-modal
wa0x6e Aug 5, 2024
29cd1bb
fix(ui): improve error message
wa0x6e Aug 5, 2024
5287a1e
fix: fix variable not reactive
wa0x6e Aug 5, 2024
4c6d306
chore: update tests to include metadataUri
wa0x6e Aug 5, 2024
0292d23
fix: fix metadataUri being ignored from voteHash
wa0x6e Aug 5, 2024
af1e616
fix: check that voting power is successful, in case threshold is 0
wa0x6e Aug 5, 2024
a2f8d67
refactor: save propose and vote potential in vp store
wa0x6e Aug 5, 2024
4362cee
chore: remove console output
wa0x6e Aug 5, 2024
583004b
fix: remove unecessary template wrapper
wa0x6e Aug 5, 2024
a19627f
Update apps/ui/src/views/Space/Proposals.vue
wa0x6e Aug 7, 2024
7f78422
Merge branch 'master' into feat-vote-reason-modal
wa0x6e Aug 7, 2024
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
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
Loading