The frontend component of PuffPastry, meant to interface with the backend, which in turn interfaces with the smart contract.
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

highlight-user-proposal-and-amendment-votes.md 14 KiB

​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. ---
  2. sessionId: session-261005-184808-15gg
  3. ---
  4. # Requirements
  5. ### Overview & Goals
  6. 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.
  7. ### Scope
  8. - **In Scope**:
  9. - Fetching user vote records on proposal load using `ProposalService.getUserVotes(proposalId)`.
  10. - Handling authenticated and unauthenticated states gracefully without blocking proposal content.
  11. - Updating `ProposalVotingCard.vue` to highlight the user's previous proposal vote (`Approve`, `Reject`, or `Abstain`) with active colored styling and accessible state.
  12. - Updating `AmendmentCarousel.vue` to color the quick-action approval (`Confirm`) and disapproval (`Cancel`) icons according to the user's vote on each amendment.
  13. - Synchronizing vote indicators immediately upon casting a new proposal or amendment vote.
  14. - **Out of Scope**:
  15. - Modifying backend OpenAPI endpoints or database schemas.
  16. - Adding vote retraction/deletion workflows beyond standard vote casting.
  17. ### User Stories
  18. - **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.
  19. - **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.
  20. ### Functional Requirements
  21. - **Data Retrieval**:
  22. - When navigating to a proposal, `ProposalDetail.vue` must call `ProposalService.getUserVotes(proposalId)` to fetch all proposal and amendment votes cast by the current user.
  23. - Unauthenticated requests (e.g. 401 responses) or users with no recorded votes must be handled gracefully without throwing errors or breaking page rendering.
  24. - **Proposal Voting Card States**:
  25. - If the user voted `VoteChoice.YES` (`yes` / `Positive`), the "Approve" button and icon must display active emerald styling.
  26. - If the user voted `VoteChoice.NO` (`no` / `Negative`), the "Reject" button and icon must display active rose styling.
  27. - If the user voted `VoteChoice.ABSTAIN` (`abstain` / `Abstain`), the "Abstain" button and icon must display active amber styling.
  28. - If the user has not voted, all buttons retain their default neutral resting styling with hover accents.
  29. - **Amendment Quick-Action States**:
  30. - For each proposed amendment, if the user voted in favor (`inFavor === true`), the `Confirm` button/icon is colored emerald.
  31. - If the user voted against (`inFavor === false`), the `Cancel` button/icon is colored rose.
  32. - If unvoted, both icons retain neutral slate styling (`text-slate-400`).
  33. - **Reactive Synchronization**:
  34. - 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.
  35. ### Non-Functional Requirements
  36. - **Accessibility**: Include `aria-pressed="true"` or descriptive `aria-label` / `title` attributes on active buttons to inform screen readers of the user's recorded stance.
  37. - **Efficiency**: Fetch proposal and amendment votes in a single network request (`getUserVotes`) to avoid N+1 queries.
  38. # Technical Design
  39. ### Current Implementation
  40. - `ProposalService.getUserVotes(proposalId: number): CancelablePromise<UserProposalVotes>` is available in `src/generated/typescript/services/ProposalService.ts` and returns:
  41. - `proposalVote?: ProposalVote | null` containing `choice: VoteChoice` (`'yes' | 'no' | 'abstain'`).
  42. - `amendmentVotes: Array<UserAmendmentVote>` containing `amendmentId: number` and `vote?: AmendmentVote | null` with `inFavor: boolean`.
  43. - `src/views/ProposalView.vue` loads `proposal` and passes it to `src/components/ProposalDetail.vue`.
  44. - `ProposalDetail.vue` renders:
  45. - `<ProposalVotingCard :proposal-id="proposal.id" @vote="recordVote" />`
  46. - `<AmendmentCarousel :proposal-id="proposal.id" />`
  47. - 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.
  48. ### Key Decisions
  49. 1. **Centralized Vote State in `ProposalDetail.vue`**:
  50. - *Decision*: Fetch `getUserVotes` in `ProposalDetail.vue` and pass relevant slice data as props down to `ProposalVotingCard.vue` and `AmendmentCarousel.vue`.
  51. - *Rationale*: Prevents duplicate API requests while ensuring both components remain pure presentational/reactive consumers synchronized to the single source of truth.
  52. 2. **Resilient Unauthenticated Handling**:
  53. - *Decision*: Wrap `ProposalService.getUserVotes` in a try/catch block; if 401 or network error occurs, silently default `userVotes` to `null`.
  54. - *Rationale*: Allows guest users or users without wallet connection to browse proposals seamlessly without console spam or UI disruption.
  55. 3. **Semantic Tailwind Color Tokens**:
  56. - *Decision*: Use Tailwind `emerald` (positive/approve), `rose` (negative/reject), and `amber` (abstain) classes with subtle background and border tints for active states.
  57. - *Rationale*: Matches existing design tokens across `Chip.vue`, `AmendmentCarousel.vue`, and `VotingControls.vue` while delivering high-contrast clarity.
  58. ### Proposed Changes
  59. #### 1. `src/components/ProposalDetail.vue`
  60. - Import `UserProposalVotes`, `UserAmendmentVote`, `VoteChoice` from generated typescript models.
  61. - Add reactive state `const userVotes = ref<UserProposalVotes | null>(null);`.
  62. - Implement `fetchUserVotes(proposalId: number)`:
  63. - Invoke `ProposalService.getUserVotes(proposalId)`.
  64. - Store response in `userVotes.value`.
  65. - On error (401/404/network), reset `userVotes.value = null`.
  66. - Call `fetchUserVotes` in `onMounted` and within the `watch(() => props.proposal?.id)` handler.
  67. - Pass `current-vote="userVotes?.proposalVote?.choice"` to `<ProposalVotingCard>`.
  68. - Pass `:amendment-votes="userVotes?.amendmentVotes"` to `<AmendmentCarousel>`.
  69. - Update `recordVote` to await `voteProposal` and immediately call `fetchUserVotes(props.proposal.id)`.
  70. #### 2. `src/components/ProposalVotingCard.vue`
  71. - Accept prop `currentVote?: VoteChoice | 'yes' | 'no' | 'abstain' | null`.
  72. - Compute active helper booleans:
  73. - `isApproved = computed(() => props.currentVote === VoteChoice.YES || props.currentVote === 'yes')`
  74. - `isRejected = computed(() => props.currentVote === VoteChoice.NO || props.currentVote === 'no')`
  75. - `isAbstained = computed(() => props.currentVote === VoteChoice.ABSTAIN || props.currentVote === 'abstain')`
  76. - Apply conditional classes:
  77. - 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'"`
  78. - 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'"`
  79. - 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'"`
  80. - Pass `aria-pressed` based on active vote status.
  81. #### 3. `src/components/AmendmentCarousel.vue`
  82. - Add prop `amendmentVotes?: UserAmendmentVote[]`.
  83. - Create a computed lookup map or helper function `getAmendmentVote(amendmentId: number): boolean | null | undefined`:
  84. - Returns `true` if `vote?.inFavor === true`, `false` if `vote?.inFavor === false`, or `null`/`undefined` if unvoted.
  85. - In the template for proposed amendments:
  86. - Highlight Confirm button with `text-emerald-600 bg-emerald-50 ring-1 ring-emerald-400` when `getAmendmentVote(amendment.id) === true`.
  87. - Highlight Cancel button with `text-rose-600 bg-rose-50 ring-1 ring-rose-400` when `getAmendmentVote(amendment.id) === false`.
  88. - Emit a `voted` event when `confirm` or `reject` completes so parent `ProposalDetail` can refresh `getUserVotes`.
  89. ### Architecture Diagram
  90. ```mermaid
  91. graph TD
  92. PV[ProposalView.vue] --> PD[ProposalDetail.vue]
  93. PD -->|1. getUserVotes proposalId| PS[ProposalService API]
  94. PS -->|UserProposalVotes| PD
  95. PD -->|currentVote| PVC[ProposalVotingCard.vue]
  96. PD -->|amendmentVotes| AC[AmendmentCarousel.vue]
  97. PVC -->|@vote -> voteProposal + refresh| PD
  98. AC -->|@voted -> voteAmendment + refresh| PD
  99. ```
  100. ### File Structure
  101. - `src/components/ProposalDetail.vue` *(Modified)*: Adds user votes fetching, state management, and props dispatch.
  102. - `src/components/ProposalVotingCard.vue` *(Modified)*: Receives active proposal vote prop and renders highlight styles for Approve, Reject, and Abstain.
  103. - `src/components/AmendmentCarousel.vue` *(Modified)*: Receives amendment votes prop, applies active color styles on Confirm/Cancel icons, and notifies parent on vote.
  104. ### Risks & Mitigations
  105. - **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.
  106. - **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.
  107. # Testing
  108. ### Validation Approach
  109. 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.
  110. ### Key Scenarios
  111. 1. **Proposal Vote Highlighting**:
  112. - Navigate to a proposal where the user previously voted "Approve" -> Verify the Approve button in `ProposalVotingCard` is highlighted in emerald with active border/background.
  113. - Navigate to a proposal where the user previously voted "Reject" -> Verify the Reject button is highlighted in rose.
  114. - Navigate to a proposal where the user previously voted "Abstain" -> Verify the Abstain button is highlighted in amber.
  115. - Navigate to an unvoted proposal -> Verify all 3 buttons are rendered in their neutral resting state.
  116. 2. **Amendment Vote Highlighting**:
  117. - For an amendment where the user voted in favor (`inFavor: true`) -> Verify the checkmark (`Confirm`) icon is highlighted in emerald.
  118. - For an amendment where the user voted against (`inFavor: false`) -> Verify the cross (`Cancel`) icon is highlighted in rose.
  119. - For unvoted amendments -> Verify both icons render in neutral slate color (`text-slate-400`).
  120. 3. **Dynamic Voting State Updates**:
  121. - Click "Approve" on an unvoted proposal -> Verify the Approve button immediately shifts to active highlighted state upon API confirmation.
  122. - Click "Confirm" on a proposed amendment -> Verify the checkmark icon immediately transitions to the active emerald state.
  123. 4. **Guest / Unauthenticated Experience**:
  124. - 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.
  125. ### Edge Cases
  126. - **No Amendments on Proposal**: Verify `amendmentVotes` handles empty arrays safely without runtime errors.
  127. - **Token Expiry**: When token expires mid-session, API failures are caught cleanly, leaving icons in fallback neutral states.
  128. # Delivery Steps
  129. ### ✓ Step 1: Integrate user vote retrieval and state management in ProposalDetail
  130. ProposalDetail fetches and manages user vote records for the active proposal and its amendments, maintaining synchronized reactive state.
  131. - Implement `fetchUserVotes` in `src/components/ProposalDetail.vue` using `ProposalService.getUserVotes(props.proposal.id)`.
  132. - Gracefully handle unauthorized (401), not found (404), or network error states when a user is unauthenticated or has no recorded votes.
  133. - Add reactive state `userVotes` typed as `UserProposalVotes | null` to track active proposal and amendment votes.
  134. - Trigger `fetchUserVotes` on component mount and watch `proposal.id` changes to keep user vote state updated when navigating proposals.
  135. - Pass the proposal vote selection (`userVotes?.proposalVote?.choice`) to `ProposalVotingCard` and the amendment vote mapping to `AmendmentCarousel`.
  136. ### ✓ Step 2: Render active vote indicators in ProposalVotingCard
  137. ProposalVotingCard visually indicates the user's previously cast vote with distinct color and container styling for Approve, Reject, and Abstain options.
  138. - Add a `currentVote` prop to `src/components/ProposalVotingCard.vue` accepting `VoteChoice | 'Positive' | 'Negative' | 'Abstain' | null | undefined`.
  139. - Update button styles to reflect active selection states:
  140. - **Approve**: Highlight with emerald border/background (`border-emerald-500 bg-emerald-50 text-emerald-700`) and emerald `Confirm` icon.
  141. - **Reject**: Highlight with rose border/background (`border-rose-500 bg-rose-50 text-rose-700`) and rose `Cancel` icon.
  142. - **Abstain**: Highlight with amber border/background (`border-amber-500 bg-amber-50 text-amber-700`) and amber `Abstain` icon.
  143. - Add `aria-pressed` attributes to buttons for assistive technology feedback.
  144. - Update `recordVote` in `ProposalDetail.vue` to refresh or optimistically update user vote state immediately after casting a vote.
  145. ### ✓ Step 3: Highlight previous votes in AmendmentCarousel and synchronize state
  146. AmendmentCarousel displays colored approval and disapproval quick-action icons corresponding to the user's recorded amendment votes.
  147. - Update `src/components/AmendmentCarousel.vue` to accept amendment vote data (e.g. `userVotes?: UserAmendmentVote[]` or a vote lookup map) from `ProposalDetail.vue`.
  148. - Derive helper methods/computed maps to resolve a user's vote (`inFavor: true`, `inFavor: false`, or unvoted) for each amendment ID.
  149. - Update the quick-action button styles in proposed amendment cards:
  150. - **Approve (in favor)**: Render active emerald styling (`text-emerald-600 bg-emerald-50 border border-emerald-200`) on the `Confirm` icon button.
  151. - **Reject (against)**: Render active rose styling (`text-rose-600 bg-rose-50 border border-rose-200`) on the `Cancel` icon button.
  152. - **Unvoted**: Maintain neutral resting styling (`text-slate-400 hover:text-emerald-500` / `hover:text-rose-500`).
  153. - Update `confirm` and `reject` voting handlers in `AmendmentCarousel.vue` to emit vote events or trigger vote refreshes to ensure immediate UI synchronization.