Skip to content

Commit

Permalink
feat: support custom label for basic voting (#868)
Browse files Browse the repository at this point in the history
* feat: support custom label for basic voting

* feat: display custom choice labels

* feat: use custom choices in vote modal

* feat: show choice icon on proposal edit

* feat: use custom choices for onchain proposals

* feat: prevent deleting non-optional choices

* feat: prevent adding more choices

* feat: show delete button when possible

* feat: use custom plcaeholder for basic choices

* feat: handle proposals without abstain

---------

Co-authored-by: Wiktor Tkaczyński <[email protected]>
  • Loading branch information
bonustrack and Sekhmet authored Oct 25, 2024
1 parent a1555a9 commit 8114aa1
Show file tree
Hide file tree
Showing 12 changed files with 101 additions and 32 deletions.
56 changes: 49 additions & 7 deletions apps/ui/src/components/EditorChoices.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<script setup lang="ts">
import Draggable from 'vuedraggable';
import { BASIC_CHOICES } from '@/helpers/constants';
import { Draft } from '@/types';
const proposal = defineModel<Draft>({ required: true });
const props = defineProps<{
minimumBasicChoices: number;
error?: string;
definition: any;
}>();
Expand All @@ -18,6 +20,10 @@ function handleAddChoice() {
}
function handlePressEnter(index: number) {
if (proposal.value.type === 'basic' && proposal.value.choices.length === 3) {
return;
}
if (!choices.value[index + 1]) return handleAddChoice();
nextTick(() => choices.value[index + 1].focus());
Expand All @@ -27,12 +33,33 @@ function handlePressDelete(event: KeyboardEvent, index: number) {
if (proposal.value.choices[index] === '') {
event.preventDefault();
if (index !== 0) {
const canDelete =
proposal.value.type === 'basic'
? proposal.value.choices.length > props.minimumBasicChoices
: true;
if (canDelete && index !== 0) {
proposal.value.choices.splice(index, 1);
nextTick(() => choices.value[index - 1].focus());
}
}
}
function getPlaceholderText(index: number) {
if (proposal.value.type === 'basic' && index < 3) {
return BASIC_CHOICES[index];
}
return `Choice ${index + 1}`;
}
function shouldHaveDeleteButton(index: number) {
if (proposal.value.type === 'basic') {
return index > props.minimumBasicChoices - 1;
}
return proposal.value.choices.length > 1;
}
</script>

<template>
Expand Down Expand Up @@ -62,24 +89,39 @@ function handlePressDelete(event: KeyboardEvent, index: number) {
>
<IC-drag />
</div>
<div v-else class="mt-1.5">
<div
class="shrink-0 rounded-full choice-bg inline-block size-[18px]"
:class="`_${index + 1}`"
>
<IH-check
v-if="index === 0"
class="text-white size-[14px] mt-0.5 ml-0.5"
/>
<IH-x
v-else-if="index === 1"
class="text-white size-[14px] mt-0.5 ml-0.5"
/>
<IH-minus-sm
v-else-if="index === 2"
class="text-white size-[14px] mt-0.5 ml-0.5"
/>
</div>
</div>
<div class="grow">
<input
:ref="el => (choices[index] = el)"
v-model.trim="proposal.choices[index]"
type="text"
:maxLength="definition.items[0].maxLength"
class="w-full h-[40px] py-[10px] bg-transparent text-skin-heading"
:class="{
'!cursor-not-allowed ml-1': proposal.type === 'basic'
}"
:placeholder="`Choice ${index + 1}`"
:disabled="proposal.type === 'basic'"
:placeholder="getPlaceholderText(index)"
@keyup.enter="handlePressEnter(index)"
@keydown.delete="e => handlePressDelete(e, index)"
/>
</div>
<UiButton
v-if="proposal.choices.length > 1 && proposal.type !== 'basic'"
v-if="shouldHaveDeleteButton(index)"
class="!border-0 !rounded-l-none !rounded-r-lg !bg-transparent !size-[40px] !px-0 !text-skin-text shrink-0"
@click="proposal.choices.splice(index, 1)"
>
Expand Down
8 changes: 1 addition & 7 deletions apps/ui/src/components/ProposalResults.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,6 @@ const props = withDefaults(
}
);
const LABELS = {
0: 'For',
1: 'Against',
2: 'Abstain'
};
const displayAllChoices = ref(false);
const totalProgress = computed(() => quorumProgress(props.proposal));
Expand Down Expand Up @@ -209,7 +203,7 @@ const otherResultsSummary = computed(() => {
<div
v-for="result in results"
:key="result.choice"
:title="LABELS[result.choice - 1]"
:title="props.proposal.choices[result.choice - 1]"
class="choice-bg float-left h-full"
:style="{
width: `${quorumChoiceProgress(props.proposal.quorum_type, result, totalProgress).toFixed(3)}%`
Expand Down
31 changes: 25 additions & 6 deletions apps/ui/src/components/ProposalVote.vue
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,31 @@ const isEditable = computed(() => {
<IH-lock-closed class="size-[16px] shrink-0" />
<span class="truncate">Encrypted choice</span>
</div>
<div
v-else
class="grow truncate"
:class="{ 'text-skin-text': !isEditable }"
v-text="getChoiceText(proposal.choices, currentVote.choice)"
/>
<div v-else class="flex items-center gap-2">
<div
v-if="proposal.type === 'basic'"
class="shrink-0 rounded-full choice-bg inline-block size-[18px]"
:class="`_${currentVote.choice}`"
>
<IH-check
v-if="currentVote.choice === 1"
class="text-white size-[14px] mt-0.5 ml-0.5"
/>
<IH-x
v-else-if="currentVote.choice === 2"
class="text-white size-[14px] mt-0.5 ml-0.5"
/>
<IH-minus-sm
v-else-if="currentVote.choice === 3"
class="text-white size-[14px] mt-0.5 ml-0.5"
/>
</div>
<div
class="grow truncate"
:class="{ 'text-skin-text': !isEditable }"
v-text="getChoiceText(proposal.choices, currentVote.choice)"
/>
</div>
<IH-pencil v-if="isEditable" class="shrink-0" />
</UiButton>
</slot>
Expand Down
7 changes: 4 additions & 3 deletions apps/ui/src/components/ProposalVoteBasic.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Choice } from '@/types';
withDefaults(
defineProps<{
size?: number;
choices: string[];
}>(),
{ size: 48 }
);
Expand All @@ -15,7 +16,7 @@ const emit = defineEmits<{

<template>
<div class="flex space-x-2">
<UiTooltip title="For">
<UiTooltip :title="choices[0]">
<UiButton
class="!text-skin-success !border-skin-success !px-0"
:class="{
Expand All @@ -27,7 +28,7 @@ const emit = defineEmits<{
<IH-check class="inline-block" />
</UiButton>
</UiTooltip>
<UiTooltip title="Against">
<UiTooltip :title="choices[1]">
<UiButton
class="!text-skin-danger !border-skin-danger !px-0"
:class="{
Expand All @@ -39,7 +40,7 @@ const emit = defineEmits<{
<IH-x class="inline-block" />
</UiButton>
</UiTooltip>
<UiTooltip title="Abstain">
<UiTooltip v-if="choices.length === 3" :title="choices[2]">
<UiButton
class="!text-gray-500 !border-gray-500 !px-0"
:class="{
Expand Down
1 change: 1 addition & 0 deletions apps/ui/src/components/ProposalsListItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const handleVoteClick = (choice: Choice) => {
</template>
<ProposalVoteBasic
v-if="proposal.type === 'basic'"
:choices="proposal.choices"
:size="40"
class="py-2"
@vote="handleVoteClick"
Expand Down
6 changes: 3 additions & 3 deletions apps/ui/src/composables/useEditor.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { BASIC_CHOICES } from '@/helpers/constants';
import { lsGet, lsSet, omit } from '@/helpers/utils';
import { clone, lsGet, lsSet, omit } from '@/helpers/utils';
import { Draft, Drafts, VoteType } from '@/types';

const PREFERRED_VOTE_TYPE = 'single-choice';
const PREFERRED_VOTE_TYPE = 'basic';

const storedProposals = lsGet('proposals', {});
const processedProposals = Object.fromEntries(
Expand Down Expand Up @@ -117,7 +117,7 @@ export function useEditor() {
await setSpacesVoteType([spaceId]);

const type = payload?.type || spaceVoteType.get(spaceId)!;
const choices = type === 'basic' ? BASIC_CHOICES : Array(2).fill('');
const choices = type === 'basic' ? clone(BASIC_CHOICES) : Array(2).fill('');

const id = draftKey ?? generateId();
const key = `${spaceId}:${id}`;
Expand Down
10 changes: 8 additions & 2 deletions apps/ui/src/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,8 +538,14 @@ export function getChoiceWeight(

export function getChoiceText(availableChoices: string[], choice: Choice) {
if (typeof choice === 'string') {
return ['for', 'against', 'abstain'].includes(choice)
? choice.charAt(0).toUpperCase() + choice.slice(1)
const basicChoices = {
for: 0,
against: 1,
abstain: 2
};

return basicChoices[choice] !== undefined
? availableChoices[basicChoices[choice]]
: 'Invalid choice';
}

Expand Down
4 changes: 2 additions & 2 deletions apps/ui/src/networks/common/graphqlApi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
createHttpLink,
InMemoryCache
} from '@apollo/client/core';
import { BASIC_CHOICES, CHAIN_IDS } from '@/helpers/constants';
import { CHAIN_IDS } from '@/helpers/constants';
import { getNames } from '@/helpers/stamp';
import { clone, compareAddresses } from '@/helpers/utils';
import {
Expand Down Expand Up @@ -258,7 +258,7 @@ function formatProposal(
},
metadata_uri: proposal.metadata.id,
type: 'basic',
choices: BASIC_CHOICES,
choices: proposal.metadata.choices,
labels: proposal.metadata.labels,
scores: [proposal.scores_1, proposal.scores_2, proposal.scores_3],
title: proposal.metadata.title ?? '',
Expand Down
1 change: 1 addition & 0 deletions apps/ui/src/networks/common/graphqlApi/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ const PROPOSAL_FRAGMENT = gql`
body
discussion
execution
choices
labels
}
start
Expand Down
1 change: 1 addition & 0 deletions apps/ui/src/networks/common/graphqlApi/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export type ApiProposal = {
body: string | null;
discussion: string | null;
execution: string | null;
choices: string[];
labels: string[];
};
space: {
Expand Down
1 change: 1 addition & 0 deletions apps/ui/src/views/Proposal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ watchEffect(() => {
>
<ProposalVoteBasic
v-if="proposal.type === 'basic'"
:choices="proposal.choices"
@vote="handleVoteClick"
/>
<ProposalVoteSingleChoice
Expand Down
7 changes: 5 additions & 2 deletions apps/ui/src/views/Space/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ const bodyDefinition = computed(() => ({
const choicesDefinition = computed(() => ({
type: 'array',
title: 'Choices',
minItems: 1,
minItems: offchainNetworks.includes(props.space.network) ? 2 : 3,
maxItems: MAX_CHOICES[props.space.turbo ? 'turbo' : 'default'],
items: [{ type: 'string', minLength: 1, maxLength: 32 }],
additionalItems: { type: 'string', maxLength: 32 }
Expand All @@ -141,7 +141,7 @@ const formErrors = computed(() => {
title: proposal.value.title,
body: proposal.value.body,
discussion: proposal.value.discussion,
choices: proposal.value.choices
choices: proposal.value.choices.filter(choice => !!choice)
},
{
skipEmptyOptionalFields: true
Expand Down Expand Up @@ -498,6 +498,9 @@ watchEffect(() => {
/>
<EditorChoices
v-model="proposal"
:minimum-basic-choices="
offchainNetworks.includes(space.network) ? 2 : 3
"
:definition="choicesDefinition"
:error="
proposal.choices.length > choicesDefinition.maxItems
Expand Down

0 comments on commit 8114aa1

Please sign in to comment.