The frontend component of PuffPastry, meant to interface with the backend, which in turn interfaces with the smart contract.
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

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.