Skip to content

Fix CSV ballot export: escape fields and download directly via Blob#1428

Open
jacksonloper wants to merge 2 commits into
mainfrom
masiarek/csv-escaping-fix
Open

Fix CSV ballot export: escape fields and download directly via Blob#1428
jacksonloper wants to merge 2 commits into
mainfrom
masiarek/csv-escaping-fix

Conversation

@jacksonloper

@jacksonloper jacksonloper commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Description

Extracts the CSV bug fixes from #1419 (authored by @masiarek — the commit carries his authorship) without the new JSON v2 export format or the Raw/Official menu split, plus a fix for the Download menu collapsing on first open.

Bug 1 — corrupt CSV when a field contains a comma. The CSV download rendered through a hidden <CSVLink> with enclosingCharacter={''}, which disabled quoting entirely: any candidate name, race title, or precinct containing a comma (or quote/newline) shifted every subsequent column. Verified end-to-end on the old code: a candidate named Smith, John splits into two header columns, so every later score sits under the wrong candidate. Fields are now escaped per RFC 4180 (quoted only when needed).

Bug 2 — first "Download CSV" click downloads nothing. The old path set React state and then synchronously clicked the hidden link — which doesn't exist until the next render. Verified end-to-end: the first click produces no download, and a second click delivers whatever the first click staged. The CSV string is now built directly and downloaded via a Blob, the same approach the JSON download already used.

Bug 3 — the Download menu collapses while ballots load. The component rendered two separate MenuButtons (loading vs. loaded). The first click opens the loading menu and starts the ballots fetch; when the ballots arrive, that subtree unmounts and the open menu vanishes, forcing a second click on the button. Now a single MenuButton stays mounted and only its items swap, so the menu opened by the first click stays up and CSV/JSON appear in place. Net effect with Bug 2's fix: one click on Download, one click on the format, one correct file — previously it took four clicks and produced a corrupt file.

Output is otherwise unchanged: same columns, same filename, blanks (not-scored) stay blank, and the JSON download still emits the existing { Election, Ballots, Results } shape. Removes the now-unused react-csv dependency.

Screenshots / Videos (frontend only)

No visible page change — the Download menu offers the same items (CSV / JSON); it now stays open while ballots load, and the downloaded CSV bytes differ when fields need escaping.

Related Issues

🤖 Generated with Claude Code

The CSV download rendered through a hidden <CSVLink> with
enclosingCharacter={''}, which disabled quoting entirely — any candidate
name, race title, or precinct containing a comma (or quote/newline)
shifted every subsequent column and corrupted the file. It also relied
on setting React state and synchronously clicking the hidden link, so a
click downloaded the previous render's data.

Build the CSV string directly with RFC-4180 field escaping (quote only
when needed) and download it via a Blob, the same approach as the JSON
export. Output is otherwise unchanged: same columns, same filename, and
a blank (not-scored) cell stays blank. Removes the now-unused react-csv
dependency.

Extracted from #1419.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@netlify

netlify Bot commented Jul 9, 2026

Copy link
Copy Markdown

Deploy Preview for bettervoting ready!

Name Link
🔨 Latest commit 3ad4053
🔍 Latest deploy log https://app.netlify.com/projects/bettervoting/deploys/6a5002bd037aab00084ad76f
😎 Deploy Preview https://deploy-preview-1428--bettervoting.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The react-csv dependency was removed from packages/frontend/package.json. BallotDataExport.tsx was refactored to remove the useState/CSVLink-based CSV generation flow, replacing it with a new triggerDownload helper that creates a Blob, object URL, and temporary anchor element to initiate downloads. downloadCSV now synchronously builds CSV headers and rows, applies RFC-4180-style escaping via a new csvField function, and includes IRV/STV rank columns. downloadJson was simplified to use the shared triggerDownload helper. The rendered menu UI retains its "Download CSV" and "Download JSON" options without the hidden CSVLink element.

Poem

A rabbit hops with CSV cheer,
No hidden link to click round here!
Blob and anchor, quick and neat,
Escaping quotes and commas sweet,
JSON too, now trim and light —
Download complete, hop out of sight! 🐇📄

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly matches the main change: CSV export escaping and Blob-based direct download.
Description check ✅ Passed The description includes all required sections and clearly explains the fixes, screenshots note, and issue reference.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch masiarek/csv-escaping-fix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jacksonloper jacksonloper marked this pull request as draft July 9, 2026 19:57
The component rendered two separate MenuButtons — one for the loading
state, one for the loaded state. The first Download click starts the
ballots fetch and opens the loading menu; when the ballots arrive, that
subtree unmounts and the open menu collapses, forcing a second click.

Render one MenuButton and swap only its items, so the menu opened by the
first click stays up and the CSV/JSON items appear in place.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jacksonloper jacksonloper marked this pull request as ready for review July 9, 2026 20:22
@jacksonloper jacksonloper requested a review from ArendPeter July 9, 2026 20:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants