From 1fa0c8fca5efbe2239636a85e46279155cd70f64 Mon Sep 17 00:00:00 2001 From: Peolite1 Date: Mon, 31 Aug 2026 01:24:08 +0100 Subject: [PATCH] feat: show individual approver weights in ApprovalBar tooltip (#394) --- frontend/src/components/ApprovalBar.tsx | 73 ++++++++++------------- frontend/src/components/ProposalCard.tsx | 4 ++ frontend/src/hooks/useContract.ts | 13 +++- frontend/src/lib/contract.ts | 11 ++++ frontend/src/pages/ProposalDetailPage.tsx | 10 +++- frontend/src/types/accord.ts | 1 + 6 files changed, 66 insertions(+), 46 deletions(-) diff --git a/frontend/src/components/ApprovalBar.tsx b/frontend/src/components/ApprovalBar.tsx index f289de3a..d2f358be 100644 --- a/frontend/src/components/ApprovalBar.tsx +++ b/frontend/src/components/ApprovalBar.tsx @@ -1,57 +1,50 @@ import React from "react"; export type ApprovalBarProps = { - approvalWeight?: number; // current accumulated approval weight - quorumWeight?: number; // required quorum weight - totalWeight?: number; // total voting power + approvalWeight?: number; + quorumWeight?: number; + totalWeight?: number; approvals?: number; threshold?: number; approverAddresses?: string[]; + approverWeights?: Record; label?: string; }; export const ApprovalBar = React.memo(function ApprovalBar({ - approvalWeight: rawApprovalWeight, - quorumWeight: rawQuorumWeight, - totalWeight: rawTotalWeight, - approvals, - threshold, - label, + approvals = 0, + threshold = 0, + approverAddresses = [], + approverWeights = {}, }: ApprovalBarProps) { - const approvalWeight = rawApprovalWeight ?? approvals ?? 0; - const quorumWeight = rawQuorumWeight ?? threshold ?? 0; - const totalWeight = rawTotalWeight ?? quorumWeight; - - const percentOfQuorum = quorumWeight > 0 ? Math.min((approvalWeight / quorumWeight) * 100, 100) : 0; - const quorumTickPct = totalWeight > 0 ? Math.min((quorumWeight / totalWeight) * 100, 100) : 0; - - const ariaLabel = - label ?? - `Approval weight ${approvalWeight} of required quorum ${quorumWeight}. ${Math.round(percentOfQuorum)} percent of quorum achieved.`; - return ( -
-
- {/* Filled progress representing approval towards quorum (clamped to 100%) */} -
- - {/* Quorum tick positioned relative to total voting power */} - {totalWeight > 0 && ( -
- )} -
+
+
+ {Array.from({ length: threshold }).map((_, i) => { + const isApproved = i < approvals; + + let tooltipTitle = undefined; + if (isApproved && approverAddresses[i]) { + const addr = approverAddresses[i]; + const weight = approverWeights[addr]; + const weightStr = weight !== undefined ? ` ยท weight ${weight}` : ""; + tooltipTitle = `${addr.slice(0, 6)}...${addr.slice(-4)}${weightStr}`; + } -
- {approvalWeight} / {quorumWeight} weight + return ( +
+ ); + })}
+ + {approvals}/{threshold} +
); }); diff --git a/frontend/src/components/ProposalCard.tsx b/frontend/src/components/ProposalCard.tsx index f3ecf948..8afdbc8e 100644 --- a/frontend/src/components/ProposalCard.tsx +++ b/frontend/src/components/ProposalCard.tsx @@ -360,6 +360,10 @@ export function ProposalCard({ bar reflects the original approval requirement even if owner weights change after the proposal is created. */} { const approverAddresses = await getApprovers(p.id); + const approverWeights: Record = {}; + await Promise.all( + approverAddresses.map(async (addr) => { + approverWeights[addr] = await getApproverWeight(addr); + }) + ); if (!walletAddress) { - return { ...p, userHasApproved: false, approverAddresses }; + return { ...p, userHasApproved: false, approverAddresses, approverWeights }; } try { const approved = await hasApproved(walletAddress, p.id); - return { ...p, userHasApproved: approved, approverAddresses }; + return { ...p, userHasApproved: approved, approverAddresses, approverWeights }; } catch (err) { console.error(`Failed to fetch approval for ${p.id}`, err); - return { ...p, userHasApproved: false, approverAddresses }; + return { ...p, userHasApproved: false, approverAddresses, approverWeights }; } }) ); diff --git a/frontend/src/lib/contract.ts b/frontend/src/lib/contract.ts index 6f6b9273..c9d08c6b 100644 --- a/frontend/src/lib/contract.ts +++ b/frontend/src/lib/contract.ts @@ -250,6 +250,17 @@ export async function getActiveDelegations(): Promise { } } +export async function getApproverWeight(owner: string): Promise { + try { + const val = await simulateView("get_owner_weight", [ + nativeToScVal(owner, { type: "address" }), + ]); + return Number(scValToNative(val)); + } catch { + return 0; // Safe fallback when address is not a current owner + } +} + export async function getOwners(): Promise { const val = await simulateView("get_owners"); return scValToNative(val) as string[]; diff --git a/frontend/src/pages/ProposalDetailPage.tsx b/frontend/src/pages/ProposalDetailPage.tsx index 1651924c..49985475 100644 --- a/frontend/src/pages/ProposalDetailPage.tsx +++ b/frontend/src/pages/ProposalDetailPage.tsx @@ -146,9 +146,13 @@ export function ProposalDetailPage({
acc + (weights[addr] ?? 0), 0)} - quorumWeight={quorumWeight || proposal.threshold} - totalWeight={totalWeight} + approvals={proposal.approvals} + threshold={proposal.threshold} + approverAddresses={proposal.approverAddresses} + approverWeights={proposal.approverWeights} + approvalWeight={(proposal.approverAddresses || []).reduce((acc, addr) => acc + (weights[addr] ?? 0), 0)} + quorumWeight={quorumWeight || proposal.threshold} + totalWeight={totalWeight} />
diff --git a/frontend/src/types/accord.ts b/frontend/src/types/accord.ts index f208cdb3..4cf7cce5 100644 --- a/frontend/src/types/accord.ts +++ b/frontend/src/types/accord.ts @@ -30,6 +30,7 @@ export type Proposal = { proposer: string; userHasApproved: boolean; approverAddresses: string[]; + approverWeights?: Record; executedAt?: string | null; };