The frontend component of PuffPastry, meant to interface with the backend, which in turn interfaces with the smart contract.
Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 
 

14 KiB

sessionId
session-261005-184808-15gg

Requirements

Overview & Goals

Provide immediate, clear visual feedback to users regarding their previous voting decisions on any visited proposal and its associated amendments. By leveraging ProposalService.getUserVotes, the application eliminates cognitive friction, prevents redundant vote attempts, and enhances user confidence by highlighting previously selected voting choices across proposal and amendment action controls.

Scope

  • In Scope:
    • Fetching user vote records on proposal load using ProposalService.getUserVotes(proposalId).
    • Handling authenticated and unauthenticated states gracefully without blocking proposal content.
    • Updating ProposalVotingCard.vue to highlight the user’s previous proposal vote (Approve, Reject, or Abstain) with active colored styling and accessible state.
    • Updating AmendmentCarousel.vue to color the quick-action approval (Confirm) and disapproval (Cancel) icons according to the user’s vote on each amendment.
    • Synchronizing vote indicators immediately upon casting a new proposal or amendment vote.
  • Out of Scope:
    • Modifying backend OpenAPI endpoints or database schemas.
    • Adding vote retraction/deletion workflows beyond standard vote casting.

User Stories

  • As a governance participant, I want to see my existing votes highlighted when I visit a proposal so that I immediately know whether I have already participated and what position I took.
  • As a voter evaluating amendments, I want the approval/disapproval icons on each amendment card to reflect my past votes so that I can quickly review my stance without opening each amendment individually.

Functional Requirements

  • Data Retrieval:
    • When navigating to a proposal, ProposalDetail.vue must call ProposalService.getUserVotes(proposalId) to fetch all proposal and amendment votes cast by the current user.
    • Unauthenticated requests (e.g. 401 responses) or users with no recorded votes must be handled gracefully without throwing errors or breaking page rendering.
  • Proposal Voting Card States:
    • If the user voted VoteChoice.YES (yes / Positive), the “Approve” button and icon must display active emerald styling.
    • If the user voted VoteChoice.NO (no / Negative), the “Reject” button and icon must display active rose styling.
    • If the user voted VoteChoice.ABSTAIN (abstain / Abstain), the “Abstain” button and icon must display active amber styling.
    • If the user has not voted, all buttons retain their default neutral resting styling with hover accents.
  • Amendment Quick-Action States:
    • For each proposed amendment, if the user voted in favor (inFavor === true), the Confirm button/icon is colored emerald.
    • If the user voted against (inFavor === false), the Cancel button/icon is colored rose.
    • If unvoted, both icons retain neutral slate styling (text-slate-400).
  • Reactive Synchronization:
    • Casting a vote on either the proposal or an amendment must promptly re-fetch or optimistically update the vote state so the visual indicators reflect the change without requiring a page reload.

Non-Functional Requirements

  • Accessibility: Include aria-pressed="true" or descriptive aria-label / title attributes on active buttons to inform screen readers of the user’s recorded stance.
  • Efficiency: Fetch proposal and amendment votes in a single network request (getUserVotes) to avoid N+1 queries.

Technical Design

Current Implementation

  • ProposalService.getUserVotes(proposalId: number): CancelablePromise<UserProposalVotes> is available in src/generated/typescript/services/ProposalService.ts and returns:
    • proposalVote?: ProposalVote | null containing choice: VoteChoice ('yes' | 'no' | 'abstain').
    • amendmentVotes: Array<UserAmendmentVote> containing amendmentId: number and vote?: AmendmentVote | null with inFavor: boolean.
  • src/views/ProposalView.vue loads proposal and passes it to src/components/ProposalDetail.vue.
  • ProposalDetail.vue renders:
    • <ProposalVotingCard :proposal-id="proposal.id" @vote="recordVote" />
    • <AmendmentCarousel :proposal-id="proposal.id" />
  • Currently, neither component fetches getUserVotes or accepts user vote props; buttons in ProposalVotingCard.vue and AmendmentCarousel.vue display static unselected styling regardless of user voting history.

Key Decisions

  1. Centralized Vote State in ProposalDetail.vue:
    • Decision: Fetch getUserVotes in ProposalDetail.vue and pass relevant slice data as props down to ProposalVotingCard.vue and AmendmentCarousel.vue.
    • Rationale: Prevents duplicate API requests while ensuring both components remain pure presentational/reactive consumers synchronized to the single source of truth.
  2. Resilient Unauthenticated Handling:
    • Decision: Wrap ProposalService.getUserVotes in a try/catch block; if 401 or network error occurs, silently default userVotes to null.
    • Rationale: Allows guest users or users without wallet connection to browse proposals seamlessly without console spam or UI disruption.
  3. Semantic Tailwind Color Tokens:
    • Decision: Use Tailwind emerald (positive/approve), rose (negative/reject), and amber (abstain) classes with subtle background and border tints for active states.
    • Rationale: Matches existing design tokens across Chip.vue, AmendmentCarousel.vue, and VotingControls.vue while delivering high-contrast clarity.

Proposed Changes

1. src/components/ProposalDetail.vue

  • Import UserProposalVotes, UserAmendmentVote, VoteChoice from generated typescript models.
  • Add reactive state const userVotes = ref<UserProposalVotes | null>(null);.
  • Implement fetchUserVotes(proposalId: number):
    • Invoke ProposalService.getUserVotes(proposalId).
    • Store response in userVotes.value.
    • On error (401/404/network), reset userVotes.value = null.
  • Call fetchUserVotes in onMounted and within the watch(() => props.proposal?.id) handler.
  • Pass current-vote="userVotes?.proposalVote?.choice" to <ProposalVotingCard>.
  • Pass :amendment-votes="userVotes?.amendmentVotes" to <AmendmentCarousel>.
  • Update recordVote to await voteProposal and immediately call fetchUserVotes(props.proposal.id).

2. src/components/ProposalVotingCard.vue

  • Accept prop currentVote?: VoteChoice | 'yes' | 'no' | 'abstain' | null.
  • Compute active helper booleans:
    • isApproved = computed(() => props.currentVote === VoteChoice.YES || props.currentVote === 'yes')
    • isRejected = computed(() => props.currentVote === VoteChoice.NO || props.currentVote === 'no')
    • isAbstained = computed(() => props.currentVote === VoteChoice.ABSTAIN || props.currentVote === 'abstain')
  • Apply conditional classes:
    • Approve button: :class="isApproved ? 'border-emerald-600 bg-emerald-50 text-emerald-700 ring-1 ring-emerald-500' : 'border-slate-300 bg-white text-slate-700 hover:border-green-600 hover:bg-green-600 hover:text-white'"
    • Reject button: :class="isRejected ? 'border-rose-600 bg-rose-50 text-rose-700 ring-1 ring-rose-500' : 'border-slate-300 bg-white text-slate-700 hover:border-red-600 hover:bg-red-600 hover:text-white'"
    • Abstain button: :class="isAbstained ? 'border-amber-500 bg-amber-50 text-amber-700 ring-1 ring-amber-500' : 'border-slate-300 bg-white text-slate-700 hover:border-amber-500 hover:bg-amber-500 hover:text-slate-950'"
  • Pass aria-pressed based on active vote status.

3. src/components/AmendmentCarousel.vue

  • Add prop amendmentVotes?: UserAmendmentVote[].
  • Create a computed lookup map or helper function getAmendmentVote(amendmentId: number): boolean | null | undefined:
    • Returns true if vote?.inFavor === true, false if vote?.inFavor === false, or null/undefined if unvoted.
  • In the template for proposed amendments:
    • Highlight Confirm button with text-emerald-600 bg-emerald-50 ring-1 ring-emerald-400 when getAmendmentVote(amendment.id) === true.
    • Highlight Cancel button with text-rose-600 bg-rose-50 ring-1 ring-rose-400 when getAmendmentVote(amendment.id) === false.
  • Emit a voted event when confirm or reject completes so parent ProposalDetail can refresh getUserVotes.

Architecture Diagram

graph TD
    PV[ProposalView.vue] --> PD[ProposalDetail.vue]
    PD -->|1. getUserVotes proposalId| PS[ProposalService API]
    PS -->|UserProposalVotes| PD
    PD -->|currentVote| PVC[ProposalVotingCard.vue]
    PD -->|amendmentVotes| AC[AmendmentCarousel.vue]
    PVC -->|@vote -> voteProposal + refresh| PD
    AC -->|@voted -> voteAmendment + refresh| PD

File Structure

  • src/components/ProposalDetail.vue (Modified): Adds user votes fetching, state management, and props dispatch.
  • src/components/ProposalVotingCard.vue (Modified): Receives active proposal vote prop and renders highlight styles for Approve, Reject, and Abstain.
  • src/components/AmendmentCarousel.vue (Modified): Receives amendment votes prop, applies active color styles on Confirm/Cancel icons, and notifies parent on vote.

Risks & Mitigations

  • Unauthenticated / Expired Tokens: getUserVotes will return 401 when tokens are missing or invalid. Mitigation: Catch 401 errors gracefully and treat user votes as empty without throwing alerts.
  • Race Condition on Voting: Multiple quick clicks on vote actions. Mitigation: Await API resolution and update state reactivity to ensure visual indicators match persisted server state.

Testing

Validation Approach

Verify the functionality by loading proposals in authenticated and unauthenticated states, testing all vote positions on both proposal and amendment components, and confirming real-time UI updates upon casting votes.

Key Scenarios

  1. Proposal Vote Highlighting:
    • Navigate to a proposal where the user previously voted “Approve” -> Verify the Approve button in ProposalVotingCard is highlighted in emerald with active border/background.
    • Navigate to a proposal where the user previously voted “Reject” -> Verify the Reject button is highlighted in rose.
    • Navigate to a proposal where the user previously voted “Abstain” -> Verify the Abstain button is highlighted in amber.
    • Navigate to an unvoted proposal -> Verify all 3 buttons are rendered in their neutral resting state.
  2. Amendment Vote Highlighting:
    • For an amendment where the user voted in favor (inFavor: true) -> Verify the checkmark (Confirm) icon is highlighted in emerald.
    • For an amendment where the user voted against (inFavor: false) -> Verify the cross (Cancel) icon is highlighted in rose.
    • For unvoted amendments -> Verify both icons render in neutral slate color (text-slate-400).
  3. Dynamic Voting State Updates:
    • Click “Approve” on an unvoted proposal -> Verify the Approve button immediately shifts to active highlighted state upon API confirmation.
    • Click “Confirm” on a proposed amendment -> Verify the checkmark icon immediately transitions to the active emerald state.
  4. Guest / Unauthenticated Experience:
    • View proposals with no token in localStorage -> Verify the proposal details, comments, and amendment carousel render properly with default unselected action buttons and no console runtime errors.

Edge Cases

  • No Amendments on Proposal: Verify amendmentVotes handles empty arrays safely without runtime errors.
  • Token Expiry: When token expires mid-session, API failures are caught cleanly, leaving icons in fallback neutral states.

Delivery Steps

✓ Step 1: Integrate user vote retrieval and state management in ProposalDetail

ProposalDetail fetches and manages user vote records for the active proposal and its amendments, maintaining synchronized reactive state.

  • Implement fetchUserVotes in src/components/ProposalDetail.vue using ProposalService.getUserVotes(props.proposal.id).
  • Gracefully handle unauthorized (401), not found (404), or network error states when a user is unauthenticated or has no recorded votes.
  • Add reactive state userVotes typed as UserProposalVotes | null to track active proposal and amendment votes.
  • Trigger fetchUserVotes on component mount and watch proposal.id changes to keep user vote state updated when navigating proposals.
  • Pass the proposal vote selection (userVotes?.proposalVote?.choice) to ProposalVotingCard and the amendment vote mapping to AmendmentCarousel.

✓ Step 2: Render active vote indicators in ProposalVotingCard

ProposalVotingCard visually indicates the user’s previously cast vote with distinct color and container styling for Approve, Reject, and Abstain options.

  • Add a currentVote prop to src/components/ProposalVotingCard.vue accepting VoteChoice | 'Positive' | 'Negative' | 'Abstain' | null | undefined.
  • Update button styles to reflect active selection states:
    • Approve: Highlight with emerald border/background (border-emerald-500 bg-emerald-50 text-emerald-700) and emerald Confirm icon.
    • Reject: Highlight with rose border/background (border-rose-500 bg-rose-50 text-rose-700) and rose Cancel icon.
    • Abstain: Highlight with amber border/background (border-amber-500 bg-amber-50 text-amber-700) and amber Abstain icon.
  • Add aria-pressed attributes to buttons for assistive technology feedback.
  • Update recordVote in ProposalDetail.vue to refresh or optimistically update user vote state immediately after casting a vote.

✓ Step 3: Highlight previous votes in AmendmentCarousel and synchronize state

AmendmentCarousel displays colored approval and disapproval quick-action icons corresponding to the user’s recorded amendment votes.

  • Update src/components/AmendmentCarousel.vue to accept amendment vote data (e.g. userVotes?: UserAmendmentVote[] or a vote lookup map) from ProposalDetail.vue.
  • Derive helper methods/computed maps to resolve a user’s vote (inFavor: true, inFavor: false, or unvoted) for each amendment ID.
  • Update the quick-action button styles in proposed amendment cards:
    • Approve (in favor): Render active emerald styling (text-emerald-600 bg-emerald-50 border border-emerald-200) on the Confirm icon button.
    • Reject (against): Render active rose styling (text-rose-600 bg-rose-50 border border-rose-200) on the Cancel icon button.
    • Unvoted: Maintain neutral resting styling (text-slate-400 hover:text-emerald-500 / hover:text-rose-500).
  • Update confirm and reject voting handlers in AmendmentCarousel.vue to emit vote events or trigger vote refreshes to ensure immediate UI synchronization.