Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions src/hooks/governance/useGovernanceCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* proposals / votes / payloads; user & protocol state is read on-chain in
* separate hooks.
*/
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
import { keepPreviousData, useInfiniteQuery, useQuery } from '@tanstack/react-query';
import { Contract } from 'ethers';
import {
adaptCacheProposalToDetail,
Expand Down Expand Up @@ -96,12 +96,19 @@ const useEnsNames = (proposalId: number, addresses: string[]) => {
// Proposals — list / search / detail
// ============================================

/** Paginated proposals list, adapted to the canonical list item type. */
export const useGovernanceProposals = () =>
/**
* Paginated proposals list, adapted to the canonical list item type. `stateFilter` is a cache
* state (`executed`, `active`, …) applied server-side, so every page holds matching proposals.
*/
export const useGovernanceProposals = (stateFilter?: string) =>
useInfiniteQuery({
queryKey: queryKeysFactory.governanceCacheProposals(),
queryKey: queryKeysFactory.governanceCacheProposals(stateFilter),
queryFn: async ({ pageParam = 0 }) => {
const proposals = await getProposals(PROPOSALS_PAGE_SIZE, pageParam * PROPOSALS_PAGE_SIZE);
const proposals = await getProposals(
PROPOSALS_PAGE_SIZE,
pageParam * PROPOSALS_PAGE_SIZE,
stateFilter
);
return { proposals: proposals.map(adaptCacheProposalToListItem) };
},
initialPageParam: 0,
Expand All @@ -121,6 +128,7 @@ export const useGovernanceProposalsSearch = (query: string) => {
return results.map(adaptCacheProposalToListItem);
},
enabled: query.trim() !== '',
placeholderData: keepPreviousData,
...CACHE_QUERY_OPTIONS,
});
return { results: (data ?? []) as ProposalListItem[], loading: isFetching };
Expand Down
12 changes: 5 additions & 7 deletions src/modules/governance/ProposalsV3List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import { GOVERNANCE_PAGE } from 'src/utils/events';
import { figVars } from 'src/utils/figmaColors';
import { motion } from 'src/utils/motion';

import { badgeToCacheState } from './adapters';
import { ProposalListHeader } from './ProposalListHeader';
import { StateBadge, stringToState } from './StateBadge';
import { ProposalBadgeState, StateBadge } from './StateBadge';
import { ProposalListItem } from './types';
import { VoteBar } from './VoteBar';

Expand Down Expand Up @@ -162,7 +163,8 @@ const ProposalListSkeleton = () => {

export const ProposalsV3List = () => {
const [proposalFilter, setProposalFilter] = useState<string>('all');
const filterState = stringToState(proposalFilter);
// The dropdown's values are the badge labels; the backend filters on cache states.
const filterBadge = proposalFilter === 'all' ? undefined : (proposalFilter as ProposalBadgeState);

const [searchTerm, setSearchTerm] = useState<string>('');

Expand All @@ -174,7 +176,7 @@ export const ProposalsV3List = () => {
isFetching: loadingProposals,
fetchNextPage,
hasNextPage,
} = useGovernanceProposals();
} = useGovernanceProposals(filterBadge && badgeToCacheState(filterBadge));

let listItems: ProposalListItem[] = [];

Expand All @@ -184,10 +186,6 @@ export const ProposalsV3List = () => {
data.pages.forEach((page) => listItems.push(...page.proposals));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Search results ignore the selected proposal state filter
medium · bug
The diff removes the client-side filter listItems.filter((proposal) => proposal.badgeState === filterState). That filter ran on listItems after the list was built, so it probably also filtered search results. Now only useGovernanceProposals gets the state filter. useGovernanceProposalsSearch(query) still takes no state argument. A user who picks a state such as "Executed" and then types a search term will see proposals in every state. The dropdown still shows the selected state, so the results do not match the visible filter. The search branch is not in the diff context, so this finding assumes that search results also go into listItems.
Why this severity: The bug occurs only when a user combines search with a state filter. The user then sees wrong results, but no data is lost.
Suggested fix: Pass the cache state to useGovernanceProposalsSearch and include it in that query key, if the backend search endpoint supports a state filter. Otherwise, keep a client-side filter on the search results only.
Review harness · reply here to accept or reject this finding

}

if (proposalFilter !== 'all') {
listItems = listItems.filter((proposal) => proposal.badgeState === filterState);
}

return (
<Paper variant="table" sx={{ overflow: 'hidden' }}>
<ProposalListHeader
Expand Down
25 changes: 25 additions & 0 deletions src/modules/governance/adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,31 @@ import {
VoteProposalData,
} from './types';

// ============================================
// Badge -> cache state mapping (the inverse, for the server-side state filter)
// ============================================

export function badgeToCacheState(badge: ProposalBadgeState): string {
switch (badge) {
case ProposalBadgeState.Created:
return 'created';
case ProposalBadgeState.OpenForVoting:
return 'active';
case ProposalBadgeState.Passed:
return 'queued';
case ProposalBadgeState.Executed:
return 'executed';
case ProposalBadgeState.Failed:
return 'failed';
case ProposalBadgeState.Cancelled:
return 'cancelled';
case ProposalBadgeState.Expired:
return 'expired';
case ProposalBadgeState.PartiallyExecuted:
return 'partially_executed';
}
}

// ============================================
// Cache state -> badge mapping (replaces mapStateToBadge duplicated in 3 cache files)
// ============================================
Expand Down
Loading