Skip to content

Commit d95669d

Browse files
authored
VxPrint: Hide language in ballot print reporting for single language elections (#7650)
* print/frontend: Hide language in the Report tab ui for single language elections * libs/utils: Migrate language helpers from print/frontend * libs/ui: In Ballots Printed Report, hide languages for single language elections * PR feedback: Use constants, update comment for clarity
1 parent 563926c commit d95669d

File tree

9 files changed

+182
-66
lines changed

9 files changed

+182
-66
lines changed

apps/print/backend/src/util/sort.ts

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,5 @@
1-
import { LanguageCode, BallotPrintCount } from '@votingworks/types';
2-
3-
function sortLanguages(
4-
languageA: LanguageCode,
5-
languageB: LanguageCode
6-
): number {
7-
const languageOrder: LanguageCode[] = [
8-
LanguageCode.ENGLISH,
9-
LanguageCode.SPANISH,
10-
LanguageCode.CHINESE_SIMPLIFIED,
11-
LanguageCode.CHINESE_TRADITIONAL,
12-
];
13-
const indexA = languageOrder.indexOf(languageA);
14-
const indexB = languageOrder.indexOf(languageB);
15-
return indexA - indexB;
16-
}
1+
import { BallotPrintCount } from '@votingworks/types';
2+
import { languageSort } from '@votingworks/utils';
173

184
// sortBallotPrintCounts sort order: totalCount, precinctOrSplitName, partyName, languageCode
195
export function sortBallotPrintCounts(
@@ -41,7 +27,7 @@ export function sortBallotPrintCounts(
4127
}
4228
}
4329

44-
return sortLanguages(
30+
return languageSort(
4531
ballotPrintCountA.languageCode,
4632
ballotPrintCountB.languageCode
4733
);

apps/print/frontend/src/components/print_all_button.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,12 @@ import {
1010
} from '@votingworks/ui';
1111
import { BallotType, LanguageCode } from '@votingworks/types';
1212
import { assertDefined } from '@votingworks/basics';
13-
import { format } from '@votingworks/utils';
13+
import { format, getLanguageOptions } from '@votingworks/utils';
1414
import {
1515
getDistinctBallotStylesCount,
1616
getElectionRecord,
1717
printAllBallotStyles,
1818
} from '../api';
19-
import { getLanguageOptions } from '../utils';
2019

2120
const DEFAULT_PROGRESS_MODAL_DELAY_SECONDS = 3;
2221

apps/print/frontend/src/screens/print_screen.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from '@votingworks/ui';
1414
import { assertDefined } from '@votingworks/basics';
1515

16+
import { getLanguageOptions } from '@votingworks/utils';
1617
import { ExpandedSelect } from '../components/expanded_select';
1718
import { TitleBar } from '../components/title_bar';
1819
import { PrintAllButton } from '../components/print_all_button';
@@ -22,7 +23,7 @@ import {
2223
getPrecinctSelection,
2324
printBallot,
2425
} from '../api';
25-
import { getLanguageOptions, getPartyOptions } from '../utils';
26+
import { getPartyOptions } from '../utils';
2627

2728
const DEFAULT_PROGRESS_MODAL_DELAY_SECONDS = 3;
2829

apps/print/frontend/src/screens/report_screen.tsx

Lines changed: 88 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
Loading,
1515
} from '@votingworks/ui';
1616
import { hasSplits } from '@votingworks/types';
17-
import { format } from '@votingworks/utils';
17+
import { format, getLanguageOptions } from '@votingworks/utils';
1818
import { assert } from '@votingworks/basics';
1919

2020
import {
@@ -80,6 +80,43 @@ const TableRow = styled.tr`
8080
}
8181
`;
8282

83+
interface ColumnWidths {
84+
precinctName: number;
85+
attribute: number;
86+
count: number;
87+
rightPadding: number;
88+
}
89+
90+
type AttributeColumnCount = 0 | 1 | 2;
91+
92+
// Column width percentages for different numbers of attribute columns.
93+
// precinctName -> precinct name column, adjusts based on number of attribute columns
94+
// attribute -> optional attribute columns (Party, Language)
95+
// count -> three count columns (Total, Precinct, Absentee), fixed width
96+
// rightPadding -> extra padding to make space for the scrollbar, fixed width
97+
const COUNT_COLUMN_WIDTH = 10; // Each count column is 10% of the table's width
98+
const RIGHT_PADDING_WIDTH = 2; // Right padding to accommodate scrollbar
99+
const COLUMN_WIDTH_MAP: Record<AttributeColumnCount, ColumnWidths> = {
100+
0: {
101+
precinctName: 68, // 100% - COUNT_COLUMN_WIDTH * 3 - RIGHT_PADDING_WIDTH = 68
102+
attribute: 0,
103+
count: COUNT_COLUMN_WIDTH,
104+
rightPadding: RIGHT_PADDING_WIDTH,
105+
},
106+
1: {
107+
precinctName: 38, // 100 - attributeWidth - COUNT_COLUMN_WIDTH * 3 - RIGHT_PADDING_WIDTH = 38
108+
attribute: 30, // Measured by eye
109+
count: COUNT_COLUMN_WIDTH,
110+
rightPadding: RIGHT_PADDING_WIDTH,
111+
},
112+
2: {
113+
precinctName: 30, // 100 - attributeWidth * 2 - COUNT_COLUMN_WIDTH * 3 - RIGHT_PADDING_WIDTH = 38
114+
attribute: 19, // Measured by eye
115+
count: COUNT_COLUMN_WIDTH,
116+
rightPadding: RIGHT_PADDING_WIDTH,
117+
},
118+
};
119+
83120
export function ReportScreen(): JSX.Element | null {
84121
const getBallotPrintCountsQuery = getBallotPrintCounts.useQuery();
85122
const getElectionRecordQuery = getElectionRecord.useQuery();
@@ -112,10 +149,15 @@ export function ReportScreen(): JSX.Element | null {
112149
}
113150

114151
assert(election !== undefined);
115-
const ballotPrintCounts = getBallotPrintCountsQuery.data;
116152
const hasParties = election.type === 'primary';
153+
const showLanguage = getLanguageOptions(election).length > 1;
154+
const ballotPrintCounts = getBallotPrintCountsQuery.data;
117155
const { printer } = getDeviceStatusesQuery.data;
118156

157+
const attributeColumnCount = ((hasParties ? 1 : 0) +
158+
(showLanguage ? 1 : 0)) as AttributeColumnCount;
159+
const columnWidths = COLUMN_WIDTH_MAP[attributeColumnCount];
160+
119161
return (
120162
<Container>
121163
<TitleBar
@@ -175,16 +217,28 @@ export function ReportScreen(): JSX.Element | null {
175217
<Table style={{ tableLayout: 'fixed', width: '100%' }}>
176218
<thead>
177219
<TableRow>
178-
<TH style={{ width: hasParties ? '25%' : '35%' }}>
220+
<TH style={{ width: `${columnWidths.precinctName}%` }}>
179221
{electionHasSplits
180222
? 'Precinct / Split Name'
181223
: 'Precinct Name'}
182224
</TH>
183-
{hasParties && <TH style={{ width: '19%' }}>Party</TH>}
184-
<TH style={{ width: hasParties ? '20%' : '25%' }}>Language</TH>
185-
<TH style={{ width: '12%' }}>Total</TH>
186-
<TH style={{ width: '12%' }}>Precinct</TH>
187-
<TH style={{ width: '12%' }}>Absentee</TH>
225+
{hasParties && (
226+
<TH style={{ width: `${columnWidths.attribute}%` }}>Party</TH>
227+
)}
228+
{showLanguage && (
229+
<TH style={{ width: `${columnWidths.attribute}%` }}>
230+
Language
231+
</TH>
232+
)}
233+
<TH style={{ width: `${columnWidths.count}%` }}>Total</TH>
234+
<TH style={{ width: `${columnWidths.count}%` }}>Precinct</TH>
235+
<TH
236+
style={{
237+
width: `${columnWidths.count + columnWidths.rightPadding}%`,
238+
}}
239+
>
240+
Absentee
241+
</TH>
188242
</TableRow>
189243
</thead>
190244
</Table>
@@ -204,21 +258,37 @@ export function ReportScreen(): JSX.Element | null {
204258
<TableRow
205259
key={`${counts.ballotStyleId}-${counts.precinctOrSplitName}`}
206260
>
207-
<TD style={{ width: hasParties ? '25%' : '35%' }}>
261+
<TD style={{ width: `${columnWidths.precinctName}%` }}>
208262
{counts.precinctOrSplitName}
209263
</TD>
210264
{hasParties && (
211-
<TD style={{ width: '19%' }}>{counts.partyName}</TD>
265+
<TD style={{ width: `${columnWidths.attribute}%` }}>
266+
{counts.partyName}
267+
</TD>
212268
)}
213-
<TD style={{ width: hasParties ? '20%' : '25%' }}>
214-
{format.languageDisplayName({
215-
languageCode: counts.languageCode,
216-
displayLanguageCode: 'en',
217-
})}
269+
{showLanguage && (
270+
<TD style={{ width: `${columnWidths.attribute}%` }}>
271+
{format.languageDisplayName({
272+
languageCode: counts.languageCode,
273+
displayLanguageCode: 'en',
274+
})}
275+
</TD>
276+
)}
277+
<TD style={{ width: `${columnWidths.count}%` }}>
278+
{counts.totalCount}
279+
</TD>
280+
<TD style={{ width: `${columnWidths.count}%` }}>
281+
{counts.precinctCount}
282+
</TD>
283+
<TD
284+
style={{
285+
width: `${
286+
columnWidths.count + columnWidths.rightPadding
287+
}%`,
288+
}}
289+
>
290+
{counts.absenteeCount}
218291
</TD>
219-
<TD style={{ width: '12%' }}>{counts.totalCount}</TD>
220-
<TD style={{ width: '12%' }}>{counts.precinctCount}</TD>
221-
<TD style={{ width: '12%' }}>{counts.absenteeCount}</TD>
222292
</TableRow>
223293
))}
224294
</tbody>

apps/print/frontend/src/utils.ts

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,5 @@
11
import { find } from '@votingworks/basics';
2-
import { Election, LanguageCode, Party } from '@votingworks/types';
3-
4-
function sortLanguages(
5-
languageA: LanguageCode,
6-
languageB: LanguageCode
7-
): number {
8-
const languageOrder: LanguageCode[] = [
9-
LanguageCode.ENGLISH,
10-
LanguageCode.SPANISH,
11-
LanguageCode.CHINESE_SIMPLIFIED,
12-
LanguageCode.CHINESE_TRADITIONAL,
13-
];
14-
const indexA = languageOrder.indexOf(languageA);
15-
const indexB = languageOrder.indexOf(languageB);
16-
return indexA - indexB;
17-
}
18-
19-
export function getLanguageOptions(election: Election): LanguageCode[] {
20-
return Array.from(
21-
new Set(
22-
election.ballotStyles.flatMap((bs) => bs.languages as LanguageCode[])
23-
)
24-
)
25-
.filter((lang) => lang !== undefined)
26-
.sort(sortLanguages);
27-
}
2+
import { Election, Party } from '@votingworks/types';
283

294
export function getPartyOptions(election: Election): Party[] {
305
if (election.type !== 'primary') {

libs/ui/src/reports/ballots_printed_report.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
hasSplits,
66
BallotPrintCount,
77
} from '@votingworks/types';
8-
import { format } from '@votingworks/utils';
8+
import { format, getLanguageOptions } from '@votingworks/utils';
99
import { throwIllegalValue } from '@votingworks/basics';
1010

1111
import { PrintedReport, reportColors, printedReportThemeFn } from './layout';
@@ -264,6 +264,7 @@ function BallotsPrintedTable({
264264
}): JSX.Element {
265265
const { election } = electionDefinition;
266266
const hasPrecinctSplits = election.precincts.some((p) => hasSplits(p));
267+
const hasMultipleLanguages = getLanguageOptions(election).length > 1;
267268

268269
const columns: Column[] = [];
269270

@@ -276,7 +277,9 @@ function BallotsPrintedTable({
276277
columns.push({ type: 'attribute', id: 'party' });
277278
}
278279

279-
columns.push({ type: 'attribute', id: 'language' });
280+
if (hasMultipleLanguages) {
281+
columns.push({ type: 'attribute', id: 'language' });
282+
}
280283

281284
columns.push({ type: 'filler', id: 'center' });
282285

libs/utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export * from './filenames';
1515
export * from './file_reading';
1616
export * from './hmpb';
1717
export * from './json_stream';
18+
export * from './languages';
1819
export * from './multi_language_mock';
1920
export * from './mutex';
2021
export * from './perf';

libs/utils/src/languages.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { expect, test } from 'vitest';
2+
import { LanguageCode, Election } from '@votingworks/types';
3+
import { languageSort, getLanguageOptions } from './languages';
4+
5+
test('languageSort', () => {
6+
const languages = [
7+
LanguageCode.CHINESE_TRADITIONAL,
8+
LanguageCode.SPANISH,
9+
LanguageCode.ENGLISH,
10+
LanguageCode.CHINESE_SIMPLIFIED,
11+
];
12+
13+
const sorted = languages.toSorted(languageSort);
14+
15+
expect(sorted).toEqual([
16+
LanguageCode.ENGLISH,
17+
LanguageCode.SPANISH,
18+
LanguageCode.CHINESE_SIMPLIFIED,
19+
LanguageCode.CHINESE_TRADITIONAL,
20+
]);
21+
});
22+
23+
test('getLanguageOptions', () => {
24+
const election: Pick<Election, 'ballotStyles'> = {
25+
ballotStyles: [
26+
{
27+
id: 'bs1',
28+
precincts: ['p1'],
29+
languages: [LanguageCode.SPANISH, LanguageCode.ENGLISH],
30+
groupId: 'g1',
31+
districts: ['d1'],
32+
},
33+
{
34+
id: 'bs2',
35+
precincts: ['p2'],
36+
languages: [LanguageCode.CHINESE_SIMPLIFIED, LanguageCode.ENGLISH],
37+
groupId: 'g2',
38+
districts: ['d2'],
39+
},
40+
{
41+
id: 'bs3',
42+
precincts: ['p3'],
43+
languages: [LanguageCode.ENGLISH],
44+
groupId: 'g1',
45+
districts: ['d1'],
46+
},
47+
],
48+
};
49+
50+
expect(getLanguageOptions(election as Election)).toEqual([
51+
LanguageCode.ENGLISH,
52+
LanguageCode.SPANISH,
53+
LanguageCode.CHINESE_SIMPLIFIED,
54+
]);
55+
});

libs/utils/src/languages.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { LanguageCode, Election } from '@votingworks/types';
2+
3+
export function languageSort(
4+
languageA: LanguageCode,
5+
languageB: LanguageCode
6+
): number {
7+
const languageOrder: LanguageCode[] = [
8+
LanguageCode.ENGLISH,
9+
LanguageCode.SPANISH,
10+
LanguageCode.CHINESE_SIMPLIFIED,
11+
LanguageCode.CHINESE_TRADITIONAL,
12+
];
13+
const indexA = languageOrder.indexOf(languageA);
14+
const indexB = languageOrder.indexOf(languageB);
15+
return indexA - indexB;
16+
}
17+
18+
export function getLanguageOptions(election: Election): LanguageCode[] {
19+
return [
20+
...new Set(
21+
election.ballotStyles.flatMap((bs) => bs.languages as LanguageCode[])
22+
),
23+
]
24+
.filter((lang) => lang !== undefined)
25+
.sort(languageSort);
26+
}

0 commit comments

Comments
 (0)