|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181 |
- ---
- 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
-
- ```mermaid
- 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.
|