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: vote modal with reason #427

Merged
merged 43 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
27dcca2
feat(ui): vote via a vote modal
wa0x6e Jun 23, 2024
9d446c9
Merge branch 'master' into feat-vote-modal-with-reason
wa0x6e Jun 23, 2024
d7d2a61
feat: add reason to vote
wa0x6e Jun 24, 2024
0af6587
fix: metadatauri should be an ipfs link instead of just cid
wa0x6e Jun 24, 2024
58e6b08
chore: output the pinned metadata
wa0x6e Jun 24, 2024
22cd8f0
fix: set metadataUri to all networks
wa0x6e Jun 25, 2024
58a0340
refactor: move getVotingPower logic into store
wa0x6e Jun 25, 2024
0eda7e1
fix: show voting power in vote modal
wa0x6e Jun 25, 2024
b7a851e
fix: open account modal if not logged
wa0x6e Jun 25, 2024
fda0d92
fix: fix modal not closing properly
wa0x6e Jun 25, 2024
b203e63
chore: style fix
wa0x6e Jun 25, 2024
dad57c4
refactor: rename variable to avoid confusion
wa0x6e Jun 25, 2024
c9a97c7
fix: pin the vote metadata inside network
wa0x6e Jun 25, 2024
594433c
fix: remove unecessary async
wa0x6e Jun 25, 2024
dd950b2
refactor: moving more properties to votingPower store
wa0x6e Jun 25, 2024
b19ccbc
fix(ui): style disabled primary button
wa0x6e Jun 25, 2024
46b4127
refactor: move votingPower formatting to helper
wa0x6e Jun 25, 2024
ee4879b
refactor: use vpStore to get vp in Editor
wa0x6e Jun 25, 2024
ed06cd7
fix: show message on insufficient or errored vp
wa0x6e Jun 26, 2024
5d0a0fc
refactoring: move votingpower error messages to component
wa0x6e Jun 27, 2024
767894a
fix: fix wrong error message
wa0x6e Jun 27, 2024
638c1f2
fix: allow votingPower refetch on error
wa0x6e Jun 27, 2024
5f11fec
feat: add votingPower composable
wa0x6e Jun 27, 2024
5b326fd
fix: DRY
wa0x6e Jun 27, 2024
628aab0
fix: fix ref
wa0x6e Jun 27, 2024
8d015d0
fix: fix typo
wa0x6e Jun 27, 2024
1127e52
fix: refresh voting power on user change
wa0x6e Jun 27, 2024
74baf13
fix: fix voting power not refreshing on modal open
wa0x6e Jun 27, 2024
150f184
fix: DRY user check
wa0x6e Jun 27, 2024
13f3fb9
fix: update vp status on reload
wa0x6e Jun 27, 2024
12b8957
fix: UI fix
wa0x6e Jun 27, 2024
7640993
fix: fix warning
wa0x6e Jun 27, 2024
3e6738d
chore: fix tests
wa0x6e Jun 27, 2024
98ace86
fix(ui): fix wrong loading icon on disabled primary button
wa0x6e Jun 27, 2024
733c3fe
fix: close the vote modal only on success
wa0x6e Jun 27, 2024
b4384fb
Update apps/ui/src/components/MessageVotingPower.vue
wa0x6e Jun 28, 2024
46dcbcc
Merge branch 'master' into feat-vote-modal-with-reason
wa0x6e Jun 28, 2024
664cea1
fix: fix tests
wa0x6e Jun 29, 2024
7304f06
fix: fix tests
wa0x6e Jun 29, 2024
64432a4
Merge branch 'master' into feat-vote-modal-with-reason
wa0x6e Jul 4, 2024
9da65b7
fix: fix typecheck error
wa0x6e Jul 11, 2024
9ef9dd2
chore: add changeset
wa0x6e Jul 14, 2024
3912bb7
fix: remove `reason` field from read type until implemented on the AP…
wa0x6e Jul 14, 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
47 changes: 21 additions & 26 deletions apps/ui/src/components/IndicatorVotingPower.vue
Original file line number Diff line number Diff line change
@@ -1,38 +1,33 @@
<script setup lang="ts">
import { _vp } from '@/helpers/utils';
import { NetworkID } from '@/types';
import { VotingPower, VotingPowerStatus } from '@/networks/types';
import { getFormattedVotingPower } from '@/helpers/utils';
import { utils } from '@snapshot-labs/sx';
import { evmNetworks } from '@/networks';
import type { NetworkID } from '@/types';
import type { VotingPower, VotingPowerStatus } from '@/networks/types';

const props = defineProps<{
networkId: NetworkID;
status: VotingPowerStatus;
votingPowerSymbol: string;
votingPowers: VotingPower[];
votingPower?: {
totalVotingPower: bigint;
votingPowers: VotingPower[];
status: VotingPowerStatus;
symbol: string;
decimals: number;
error: utils.errors.VotingPowerDetailsError | null;
};
}>();

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 decimals = computed(() =>
Math.max(...props.votingPowers.map(votingPower => votingPower.decimals), 0)
);
const formattedVotingPower = computed(() => {
const value = _vp(Number(votingPower.value) / 10 ** decimals.value);
const formattedVotingPower = computed(() => getFormattedVotingPower(props.votingPower));

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 All @@ -57,7 +52,10 @@ function handleModalOpen() {
@click="handleModalOpen"
>
<IH-lightning-bolt class="inline-block -ml-1" />
<IH-exclamation v-if="props.status === 'error'" class="inline-block ml-1 text-rose-500" />
<IH-exclamation
v-if="props.votingPower && props.votingPower.status === 'error'"
class="inline-block ml-1 text-rose-500"
/>
<span v-else class="ml-1">{{ formattedVotingPower }}</span>
</UiButton>
</UiTooltip>
Expand All @@ -66,12 +64,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
47 changes: 47 additions & 0 deletions apps/ui/src/components/MessageVotingPower.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script setup lang="ts">
const props = defineProps<{
minVotingPower?: bigint;
minProposalThreshold?: bigint;
votingPower: {
status: string;
totalVotingPower: bigint;
};
}>();

defineEmits<{
(e: 'fetchVotingPower');
}>();

const insufficientVoteVp = computed(
() =>
props.votingPower.status === 'success' &&
props.minVotingPower !== undefined &&
props.votingPower.totalVotingPower <= props.minVotingPower
);

const insufficientProposeVp = computed(
() =>
props.votingPower.status === 'success' &&
props.minProposalThreshold !== undefined &&
props.votingPower.totalVotingPower < props.minProposalThreshold
);
</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="insufficientVoteVp" type="error">
You do not have enough voting power to vote.
</UiAlert>
<UiAlert v-else-if="insufficientProposeVp" type="error">
You do not have enough voting power to create proposal in this space.
</UiAlert>
</template>
143 changes: 143 additions & 0 deletions apps/ui/src/components/Modal/Vote.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<script setup lang="ts">
import { getChoiceText, getFormattedVotingPower } from '@/helpers/utils';
import { validateForm } from '@/helpers/validation';
import type { Choice, Proposal } from '@/types';

const definition = {
type: 'object',
title: 'Vote',
additionalProperties: false,
required: [],
properties: {
reason: {
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,
hasVoteVp
} = useVotingPower();

const loading = ref(false);
const form = ref<Record<string, string>>({ reason: '' });

const formErrors = computed(() =>
validateForm(definition, form.value, { skipEmptyOptionalFields: true })
);
const formattedVotingPower = computed(() => getFormattedVotingPower(votingPower.value));

const canSubmit = computed(
() => !!props.choice && Object.keys(formErrors.value).length === 0 && hasVoteVp.value
);

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 }
);
</script>

<template>
<UiModal :open="open" @close="$emit('close')">
<template #header>
<h3>Cast your vote</h3>
</template>

<div class="m-4 flex flex-col space-y-3">
<MessageVotingPower
v-if="votingPower"
:voting-power="votingPower"
:min-voting-power="0n"
@fetch-voting-power="handleFetchVotingPower"
/>
<dl>
<dt class="text-sm leading-5">Choice</dt>
<dd class="font-semibold text-skin-heading text-[20px] leading-6">
<span
v-if="choice"
class="test-skin-heading"
v-text="getChoiceText(proposal.choices, choice)"
/>
<span v-else class="text-skin-danger"> No choice selected </span>
</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="definition" />
</div>
</div>

<template #footer>
<div class="flex space-x-3">
<UiButton class="w-full" @click="$emit('close')"> Cancel </UiButton>
<UiButton
primary
class="w-full"
:disabled="!canSubmit"
:loading="loading"
@click="handleSubmit"
>
Confirm
</UiButton>
</div>
</template>
</UiModal>
</template>
37 changes: 20 additions & 17 deletions apps/ui/src/components/Modal/VotingPower.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { utils } from '@snapshot-labs/sx';
import { getNetwork } from '@/networks';
import { _n, shorten } from '@/helpers/utils';
import { addressValidator as isValidAddress } from '@/helpers/validation';
Expand All @@ -8,23 +9,26 @@ import { VotingPower, VotingPowerStatus } from '@/networks/types';
const props = defineProps<{
open: boolean;
networkId: NetworkID;
votingPowerSymbol: string;
votingPowers: VotingPower[];
votingPowerStatus: VotingPowerStatus;
finalDecimals: number;
votingPower?: {
symbol: string;
totalVotingPower: bigint;
decimals: number;
votingPowers: VotingPower[];
status: VotingPowerStatus;
error: utils.errors.VotingPowerDetailsError | null;
};
}>();

defineEmits<{
(e: 'close');
(e: 'getVotingPower');
(e: 'fetchVotingPower');
}>();

const network = computed(() => getNetwork(props.networkId));
const baseNetwork = computed(() =>
network.value.baseNetworkId ? 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 @@ -33,15 +37,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 @@ -57,12 +60,12 @@ const error = computed(() => props.votingPowerStatus === 'error');
/>
<div class="text-skin-link shrink-0">
{{
_n(Number(strategy.value) / 10 ** finalDecimals, 'compact', {
_n(Number(strategy.value) / 10 ** votingPower.decimals, 'compact', {
maximumFractionDigits: 2,
formatDust: true
})
}}
{{ votingPowerSymbol }}
{{ votingPower.symbol }}
</div>
</div>
<div class="flex justify-between">
Expand Down
8 changes: 1 addition & 7 deletions apps/ui/src/components/ProposalVoteApproval.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { Choice, Proposal } from '@/types';

defineProps<{
sendingType: Choice | null;
proposal: Proposal;
}>();

Expand Down Expand Up @@ -37,12 +36,7 @@ function toggleSelectedChoice(choice: number) {
<IH-check v-if="selectedChoices.includes(index + 1)" class="shrink-0" />
</UiButton>
</div>
<UiButton
primary
class="!h-[48px] w-full"
:loading="!!sendingType"
@click="emit('vote', selectedChoices)"
>
<UiButton primary class="!h-[48px] w-full" @click="emit('vote', selectedChoices)">
Vote
</UiButton>
</div>
Expand Down
Loading
Loading