Skip to content

Commit ac4ff18

Browse files
committed
fix inflight votes sorting [ci]
Signed-off-by: Paweł Perek <pawel.perek@digitalasset.com>
1 parent 93690e3 commit ac4ff18

File tree

2 files changed

+69
-202
lines changed

2 files changed

+69
-202
lines changed

apps/sv/frontend/src/__tests__/governance/governance-sorting.test.tsx

Lines changed: 53 additions & 188 deletions
Original file line numberDiff line numberDiff line change
@@ -14,64 +14,29 @@ import { ProposalListingData } from '../../utils/types';
1414

1515
describe('Governance Page Sorting', () => {
1616
describe('Action Required Section', () => {
17-
test('should render items sorted by voting closes date ascending (closest deadline first)', () => {
17+
test('should sort by voting closes date ascending (closest deadline first)', () => {
1818
const unsortedRequests: ActionRequiredData[] = [
1919
{
20-
actionName: 'Action C - Latest deadline',
20+
actionName: 'Action C - Latest',
2121
contractId: 'c' as ContractId<VoteRequest>,
2222
votingCloses: '2025-01-25 12:00',
2323
createdAt: '2025-01-10 12:00',
2424
requester: 'sv1',
2525
},
2626
{
27-
actionName: 'Action A - Earliest deadline',
27+
actionName: 'Action A - Earliest',
2828
contractId: 'a' as ContractId<VoteRequest>,
29-
votingCloses: '2025-01-15 12:00',
29+
votingCloses: '2025-01-15 10:00',
3030
createdAt: '2025-01-10 12:00',
3131
requester: 'sv1',
3232
},
3333
{
34-
actionName: 'Action B - Middle deadline',
35-
contractId: 'b' as ContractId<VoteRequest>,
36-
votingCloses: '2025-01-20 12:00',
37-
createdAt: '2025-01-10 12:00',
38-
requester: 'sv1',
39-
},
40-
];
41-
42-
render(
43-
<MemoryRouter>
44-
<ActionRequiredSection actionRequiredRequests={unsortedRequests} />
45-
</MemoryRouter>
46-
);
47-
48-
const cards = screen.getAllByTestId('action-required-card');
49-
expect(cards).toHaveLength(3);
50-
51-
const actionNames = cards.map(
52-
card => card.querySelector('[data-testid="action-required-action-content"]')?.textContent
53-
);
54-
expect(actionNames[0]).toBe('Action A - Earliest deadline');
55-
expect(actionNames[1]).toBe('Action B - Middle deadline');
56-
expect(actionNames[2]).toBe('Action C - Latest deadline');
57-
});
58-
59-
test('should handle same-day deadlines with different times', () => {
60-
const unsortedRequests: ActionRequiredData[] = [
61-
{
62-
actionName: 'Action B - Later time',
34+
actionName: 'Action B - Middle',
6335
contractId: 'b' as ContractId<VoteRequest>,
6436
votingCloses: '2025-01-15 18:00',
6537
createdAt: '2025-01-10 12:00',
6638
requester: 'sv1',
6739
},
68-
{
69-
actionName: 'Action A - Earlier time',
70-
contractId: 'a' as ContractId<VoteRequest>,
71-
votingCloses: '2025-01-15 10:00',
72-
createdAt: '2025-01-10 12:00',
73-
requester: 'sv1',
74-
},
7540
];
7641

7742
render(
@@ -84,137 +49,66 @@ describe('Governance Page Sorting', () => {
8449
const actionNames = cards.map(
8550
card => card.querySelector('[data-testid="action-required-action-content"]')?.textContent
8651
);
87-
expect(actionNames[0]).toBe('Action A - Earlier time');
88-
expect(actionNames[1]).toBe('Action B - Later time');
52+
53+
expect(actionNames).toEqual([
54+
'Action A - Earliest',
55+
'Action B - Middle',
56+
'Action C - Latest',
57+
]);
8958
});
9059
});
9160

9261
describe('Inflight Votes Section', () => {
9362
const baseData: Omit<
9463
ProposalListingData,
95-
'actionName' | 'contractId' | 'voteTakesEffect' | 'votingThresholdDeadline'
64+
'actionName' | 'contractId' | 'voteTakesEffect' | 'votingThresholdDeadline' | 'voteStats'
9665
> = {
9766
yourVote: 'accepted',
9867
status: 'In Progress',
99-
voteStats: { accepted: 5, rejected: 2, 'no-vote': 1 },
10068
acceptanceThreshold: BigInt(11),
10169
};
10270

103-
test('should render items sorted by effective date ascending (closest first)', () => {
71+
test('should sort with Threshold items first (by votes desc, then deadline asc), then dated items by effective date asc', () => {
10472
const unsortedRequests: ProposalListingData[] = [
10573
{
10674
...baseData,
107-
actionName: 'Action C - Latest effective',
108-
contractId: 'c' as ContractId<VoteRequest>,
75+
actionName: 'Dated - Later',
76+
contractId: 'd2' as ContractId<VoteRequest>,
10977
voteTakesEffect: '2025-01-30 12:00',
11078
votingThresholdDeadline: '2025-01-25 12:00',
79+
voteStats: { accepted: 5, rejected: 2, 'no-vote': 1 },
11180
},
11281
{
11382
...baseData,
114-
actionName: 'Action A - Earliest effective',
115-
contractId: 'a' as ContractId<VoteRequest>,
116-
voteTakesEffect: '2025-01-20 12:00',
117-
votingThresholdDeadline: '2025-01-15 12:00',
118-
},
119-
{
120-
...baseData,
121-
actionName: 'Action B - Middle effective',
122-
contractId: 'b' as ContractId<VoteRequest>,
123-
voteTakesEffect: '2025-01-25 12:00',
124-
votingThresholdDeadline: '2025-01-20 12:00',
125-
},
126-
];
127-
128-
render(
129-
<MemoryRouter>
130-
<ProposalListingSection
131-
sectionTitle="Inflight Votes"
132-
data={unsortedRequests}
133-
noDataMessage="No data"
134-
uniqueId="inflight-votes"
135-
showVoteStats
136-
showAcceptanceThreshold
137-
showThresholdDeadline
138-
sortOrder="effectiveAtAsc"
139-
/>
140-
</MemoryRouter>
141-
);
142-
143-
const rows = screen.getAllByTestId('inflight-votes-row');
144-
expect(rows).toHaveLength(3);
145-
146-
const actionNames = rows.map(
147-
row => row.querySelector('[data-testid="inflight-votes-row-action-name"]')?.textContent
148-
);
149-
expect(actionNames[0]).toBe('Action A - Earliest effective');
150-
expect(actionNames[1]).toBe('Action B - Middle effective');
151-
expect(actionNames[2]).toBe('Action C - Latest effective');
152-
});
153-
154-
test('should sort Threshold items by their voting deadline alongside dated items', () => {
155-
const unsortedRequests: ProposalListingData[] = [
156-
{
157-
...baseData,
158-
actionName: 'Threshold Action',
159-
contractId: 't' as ContractId<VoteRequest>,
83+
actionName: 'Threshold - 5 votes, later deadline',
84+
contractId: 't2' as ContractId<VoteRequest>,
16085
voteTakesEffect: 'Threshold',
161-
votingThresholdDeadline: '2025-01-15 12:00',
162-
},
163-
{
164-
...baseData,
165-
actionName: 'Dated Action',
166-
contractId: 'd' as ContractId<VoteRequest>,
167-
voteTakesEffect: '2025-01-30 12:00',
16886
votingThresholdDeadline: '2025-01-25 12:00',
87+
voteStats: { accepted: 3, rejected: 2, 'no-vote': 3 },
16988
},
170-
];
171-
172-
render(
173-
<MemoryRouter>
174-
<ProposalListingSection
175-
sectionTitle="Inflight Votes"
176-
data={unsortedRequests}
177-
noDataMessage="No data"
178-
uniqueId="inflight-votes"
179-
showVoteStats
180-
showAcceptanceThreshold
181-
showThresholdDeadline
182-
sortOrder="effectiveAtAsc"
183-
/>
184-
</MemoryRouter>
185-
);
186-
187-
const rows = screen.getAllByTestId('inflight-votes-row');
188-
const actionNames = rows.map(
189-
row => row.querySelector('[data-testid="inflight-votes-row-action-name"]')?.textContent
190-
);
191-
192-
expect(actionNames[0]).toBe('Threshold Action');
193-
expect(actionNames[1]).toBe('Dated Action');
194-
});
195-
196-
test('should sort multiple Threshold items by voting deadline', () => {
197-
const unsortedRequests: ProposalListingData[] = [
19889
{
19990
...baseData,
200-
actionName: 'Threshold C - Latest deadline',
201-
contractId: 'c' as ContractId<VoteRequest>,
202-
voteTakesEffect: 'Threshold',
203-
votingThresholdDeadline: '2025-01-25 12:00',
91+
actionName: 'Dated - Earlier',
92+
contractId: 'd1' as ContractId<VoteRequest>,
93+
voteTakesEffect: '2025-01-20 12:00',
94+
votingThresholdDeadline: '2025-01-15 12:00',
95+
voteStats: { accepted: 5, rejected: 2, 'no-vote': 1 },
20496
},
20597
{
20698
...baseData,
207-
actionName: 'Threshold A - Earliest deadline',
208-
contractId: 'a' as ContractId<VoteRequest>,
99+
actionName: 'Threshold - 10 votes',
100+
contractId: 't1' as ContractId<VoteRequest>,
209101
voteTakesEffect: 'Threshold',
210-
votingThresholdDeadline: '2025-01-15 12:00',
102+
votingThresholdDeadline: '2025-01-30 12:00',
103+
voteStats: { accepted: 7, rejected: 3, 'no-vote': 0 },
211104
},
212105
{
213106
...baseData,
214-
actionName: 'Threshold B - Middle deadline',
215-
contractId: 'b' as ContractId<VoteRequest>,
107+
actionName: 'Threshold - 5 votes, earlier deadline',
108+
contractId: 't3' as ContractId<VoteRequest>,
216109
voteTakesEffect: 'Threshold',
217-
votingThresholdDeadline: '2025-01-20 12:00',
110+
votingThresholdDeadline: '2025-01-15 12:00',
111+
voteStats: { accepted: 4, rejected: 1, 'no-vote': 3 },
218112
},
219113
];
220114

@@ -238,9 +132,13 @@ describe('Governance Page Sorting', () => {
238132
row => row.querySelector('[data-testid="inflight-votes-row-action-name"]')?.textContent
239133
);
240134

241-
expect(actionNames[0]).toBe('Threshold A - Earliest deadline');
242-
expect(actionNames[1]).toBe('Threshold B - Middle deadline');
243-
expect(actionNames[2]).toBe('Threshold C - Latest deadline');
135+
expect(actionNames).toEqual([
136+
'Threshold - 10 votes', // Threshold first, most votes
137+
'Threshold - 5 votes, earlier deadline', // Same 5 votes, earlier deadline wins
138+
'Threshold - 5 votes, later deadline', // Same 5 votes, later deadline
139+
'Dated - Earlier', // Dated items sorted by effective date asc
140+
'Dated - Later',
141+
]);
244142
});
245143
});
246144

@@ -255,7 +153,7 @@ describe('Governance Page Sorting', () => {
255153
acceptanceThreshold: BigInt(11),
256154
};
257155

258-
test('should render items sorted by effective date descending (most recent first)', () => {
156+
test('should sort by effective date descending (most recent first)', () => {
259157
const unsortedRequests: ProposalListingData[] = [
260158
{
261159
...baseData,
@@ -268,58 +166,21 @@ describe('Governance Page Sorting', () => {
268166
...baseData,
269167
actionName: 'Action C - Most recent',
270168
contractId: 'c' as ContractId<VoteRequest>,
271-
voteTakesEffect: '2025-01-20 12:00',
169+
voteTakesEffect: '2025-01-20 18:00',
272170
votingThresholdDeadline: '2025-01-15 12:00',
273171
},
274172
{
275173
...baseData,
276-
actionName: 'Action B - Middle',
277-
contractId: 'b' as ContractId<VoteRequest>,
278-
voteTakesEffect: '2025-01-15 12:00',
279-
votingThresholdDeadline: '2025-01-10 12:00',
280-
},
281-
];
282-
283-
render(
284-
<MemoryRouter>
285-
<ProposalListingSection
286-
sectionTitle="Vote History"
287-
data={unsortedRequests}
288-
noDataMessage="No data"
289-
uniqueId="vote-history"
290-
showStatus
291-
showVoteStats
292-
showAcceptanceThreshold
293-
sortOrder="effectiveAtDesc"
294-
/>
295-
</MemoryRouter>
296-
);
297-
298-
const rows = screen.getAllByTestId('vote-history-row');
299-
expect(rows).toHaveLength(3);
300-
301-
const actionNames = rows.map(
302-
row => row.querySelector('[data-testid="vote-history-row-action-name"]')?.textContent
303-
);
304-
expect(actionNames[0]).toBe('Action C - Most recent');
305-
expect(actionNames[1]).toBe('Action B - Middle');
306-
expect(actionNames[2]).toBe('Action A - Oldest');
307-
});
308-
309-
test('should handle same-day effective dates with different times', () => {
310-
const unsortedRequests: ProposalListingData[] = [
311-
{
312-
...baseData,
313-
actionName: 'Action A - Earlier time',
314-
contractId: 'a' as ContractId<VoteRequest>,
315-
voteTakesEffect: '2025-01-15 10:00',
316-
votingThresholdDeadline: '2025-01-10 12:00',
174+
actionName: 'Action D - Same day, earlier time',
175+
contractId: 'd' as ContractId<VoteRequest>,
176+
voteTakesEffect: '2025-01-20 10:00',
177+
votingThresholdDeadline: '2025-01-15 12:00',
317178
},
318179
{
319180
...baseData,
320-
actionName: 'Action B - Later time',
181+
actionName: 'Action B - Middle',
321182
contractId: 'b' as ContractId<VoteRequest>,
322-
voteTakesEffect: '2025-01-15 18:00',
183+
voteTakesEffect: '2025-01-15 12:00',
323184
votingThresholdDeadline: '2025-01-10 12:00',
324185
},
325186
];
@@ -344,8 +205,12 @@ describe('Governance Page Sorting', () => {
344205
row => row.querySelector('[data-testid="vote-history-row-action-name"]')?.textContent
345206
);
346207

347-
expect(actionNames[0]).toBe('Action B - Later time');
348-
expect(actionNames[1]).toBe('Action A - Earlier time');
208+
expect(actionNames).toEqual([
209+
'Action C - Most recent',
210+
'Action D - Same day, earlier time',
211+
'Action B - Middle',
212+
'Action A - Oldest',
213+
]);
349214
});
350215
});
351216
});

apps/sv/frontend/src/components/governance/ProposalListingSection.tsx

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,27 +39,29 @@ const getColumnsCount = (alwaysShown: number, ...sometimesShown: (boolean | unde
3939
alwaysShown +
4040
sometimesShown.reduce((columnsCount, isShown) => columnsCount + (isShown ? 1 : 0), 0);
4141

42+
const getTotalVotes = (item: ProposalListingData): number =>
43+
item.voteStats['accepted'] + item.voteStats['rejected'];
44+
45+
const getEffectiveDate = (item: ProposalListingData): dayjs.Dayjs =>
46+
item.voteTakesEffect === 'Threshold' ? dayjs(0) : dayjs(item.voteTakesEffect);
47+
48+
// Using stable sort: chain sorts from least to most significant criterion
4249
const sortProposals = (
4350
data: ProposalListingData[],
4451
sortOrder?: ProposalSortOrder
4552
): ProposalListingData[] => {
4653
if (!sortOrder) return data;
4754

55+
if (sortOrder === 'effectiveAtDesc') {
56+
return data.toSorted((a, b) => dayjs(b.voteTakesEffect).diff(dayjs(a.voteTakesEffect)));
57+
}
58+
59+
// For effectiveAtAsc (Inflight Votes):
60+
// Threshold items first (by votes desc, then deadline asc), then dated items (by effective date asc)
4861
return data
49-
.map(item => ({
50-
item,
51-
effectiveDate: dayjs(
52-
item.voteTakesEffect === 'Threshold' ? item.votingThresholdDeadline : item.voteTakesEffect
53-
),
54-
}))
55-
.toSorted((a, b) => {
56-
if (sortOrder === 'effectiveAtAsc') {
57-
return a.effectiveDate.isBefore(b.effectiveDate) ? -1 : 1;
58-
} else {
59-
return a.effectiveDate.isAfter(b.effectiveDate) ? -1 : 1;
60-
}
61-
})
62-
.map(({ item }) => item);
62+
.toSorted((a, b) => dayjs(a.votingThresholdDeadline).diff(dayjs(b.votingThresholdDeadline)))
63+
.toSorted((a, b) => getTotalVotes(b) - getTotalVotes(a))
64+
.toSorted((a, b) => getEffectiveDate(a).diff(getEffectiveDate(b)));
6365
};
6466

6567
export const ProposalListingSection: React.FC<ProposalListingSectionProps> = props => {

0 commit comments

Comments
 (0)