NWP-101: let ops choose export columns and scope - #179
Open
dmytromanannykov wants to merge 1 commit into
Open
Conversation
Adds an options dialog to the payments Export button: pick which columns are included (card last-four off by default) and the scope (current filter vs all payments, with the row count shown before download). - src/lib/csv.ts: DEFAULT_EXPORT_COLUMNS, parseExportColumns() (the column allowlist/serializer), exportLabel(), exportFilename() gains an optional scope/status segment - src/app/api/payments/export/route.ts: validates columns and scope server-side, 400s on an empty column set, reuses the one query builder for scope=all - src/data/queries.ts: countPayments() helper - src/app/payments/export-dialog.tsx: new Drawer-based dialog - src/app/payments/page.tsx: wires the dialog in, computes counts - src/lib/csv.test.ts: 16 new cases covering the serializer, defaults, labels, and filenames npm test: 42/42 passing. Verified end-to-end via curl against the export route and a UI click-through (see PR description). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ticket
Closes NWP-101
What changed
Ops can now pick which columns go into a payments export and how much of the
table it covers, instead of always getting every column for the current page
of filters. The Export button opens a dialog: check the columns you want
(card last-four is unchecked by default), choose current filter or all
payments as the scope, see the row count before you download, and the file
comes down as
payments-<scope-or-status>-<date>.csv. Unchecking everycolumn disables Download instead of letting you produce an empty file.
How I verified it
npm test— 42 passing (26 existing + 16 new), all insrc/lib/csv.test.tsnpx tsc --noEmit— cleannpm run lint— no warnings or errorsRan
npm run devand exercisedGET /api/payments/exportdirectly with curl:columns, filename
payments-disputed-<date>.csvpayments-all-<date>.csvcolumns=(empty) →400 {"message":"Select at least one column to export."}columns=id,evil,amount→ unknown column silently dropped, headerid,amountcolumnsparam at all (legacy link) → falls back to the default set, nolast4columns=id,last4→ last4 populated for card paymentsClicked through the dialog on
/payments?status=disputedin the browser:opened it, confirmed every column checked except Card last four and
"Current filter" selected by default with the count matching the table
footer, unchecked every column and confirmed Download disabled with a
message, re-checked one and confirmed it re-enabled, switched to "All
payments" and watched the count update, and confirmed Escape closes the
dialog and returns focus to the Export button.
npm testpassesNew behavior is covered by a test
Checked it in the browser
Acceptance criteria
the default, and the row count is visible before download.
scope=allit'spayments-<segment>-<date>.csv(e.g.payments-disputed-2026-08-13.csv). One case is narrower than theliteral wording: a current-filter export with no status filter
(e.g. only a merchant or search filter active) still produces
payments-<date>.csvwith no segment, matching today's filename andkeeping it short — merchant/search aren't encoded into the name. Flagged
below.
way out, with currency in its own column. (Already true of
toCsv;unchanged by this PR, still covered by existing + new tests.)
empty file.
Bugs fixed along the way
None found outside the ticket's scope. Noted but not touched:
sortPayments'amount sort in
src/data/queries.tscomparesString(amount)lexicallyrather than numerically — pre-existing, the export inherits whatever order
the table shows by design, so I left it alone.
Notes for the reviewer
GET+ browser download (an<a href>, not afetch), so the CSV is still built entirely server-side through the onequery builder (
filterPayments/sortPayments).scope=allisn't a secondfilter path — it's the same call with
{status: "all"}and the otherfilters dropped.
src/lib/csv.ts(
parseExportColumns,DEFAULT_EXPORT_COLUMNS,exportLabel) so the"column serializer" the ticket's DoD asks for is directly unit-testable;
the route calls them rather than reading
searchParamsitself.Dialogcomponent in this codebase (despite what.claude/rules/components.mdsays) — onlyDrawer.tsx. I reused thatrather than building a new modal primitive. Column checkboxes and the
scope radios are native
<input>elements in labeled<fieldset>s; therewas no existing Checkbox/Radio component to reach for either.
page.tsx(viacountPayments, a new small helper next toqueryPayments) and passed tothe dialog as props — this app has no client-side
fetchanywhere, and Ikept it that way rather than introducing one for the count.
jsdom, no route-handler test harness): the Download-disabled DOM behavior,
the live row-count display, and the emitted
content-dispositionheader.All three are covered by the curl and browser checks above instead.