Select elections in the table and archive them in one go - #1504
Conversation
Addresses Equal-Vote#820's mass-processing request, archive half only. /manage lists every election you officiate, and the only way to tidy it is to open each one and archive it from its admin page. This adds row selection to the table and an Actions menu in the toolbar, matching the pattern the issue's ElectionBuddy example shows (select rows, choose from Actions) rather than a right-click menu, which has no touch story. EnhancedTable already carried the skeleton of MUI's demo -- a `selected` state destructured without its setter, a toolbar taking `numSelected` and highlighting for it, row ids named `...-checkbox-N` -- so this mostly reconnects what was already there: - selection is opt-in via `bulkActions` + `getRowId`, so the five other tables sharing this component (public archive, elections you voted in, invitations, ...) are untouched - the header checkbox selects the current page, which is what the issue asks for and what `visibleRows` already computes - the checkbox cell stops click propagation: the whole row is a link to the election, so without it ticking a box navigates away - a selected row that a filter later hides is dropped from the selection, so an action can never reach a row the user can't see No backend change. The client loops over the existing per-election POST /Election/:id/archive in batches of 5, which keeps that endpoint's permission check, its expected_update_date concurrency guard, and its history entry -- all of which hang off `router.param('id', ...)` and would have to be reimplemented inside a bulk endpoint. The trade is that a bulk run isn't atomic, so partial results are reported instead of rolled back; re-running is harmless because the backend rejects an already-archived election. Archiving is one-way (nothing moves an election out of 'archived'), so the confirm dialog names the count and says so. Rows you don't own are filtered out client-side rather than sent and 401'd -- canEditElectionState is owner/system_admin, but this table also lists elections where you're only an admin or auditor. Verified against a local stack (postgres + keycloak + both dev servers) with 11 seeded elections: archived 3 of them, list went 11 -> 8, each one got a new head version with state='archived' and its predecessor demoted, select-all-on-page ticked 8/8, ticking a box didn't navigate while clicking the row still did, and selecting an already-archived row was refused client-side without a request. tsc --noEmit passes.
✅ Deploy Preview for bettervoting ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Warning Review limit reached
Next review available in: 34 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
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. Comment |
Description
/managelists every election you officiate, and the only way to tidy it up today is to open each one and archive it from its admin page. After a lot of testing that adds up. This adds row selection to the table plus an Actions menu in the toolbar, so you can archive a batch in one pass.Why an Actions menu and not right-click (which #820 asks for): right-click has no touch story and is unusual on the web, and the ElectionBuddy example quoted in the issue isn't right-click either — it's "select the checkboxes, then choose Delete from the Actions menu". The MUI table this is built on already ships that pattern, so it came nearly free.
This is the archive half only. Notes on delete at the bottom.
What's in it
EnhancedTablewas already carrying the skeleton of MUI's own EnhancedTable demo with the selection parts removed — aselectedstate destructured without its setter, a toolbar that still takesnumSelectedand still has the highlight styling for it, row ids still namedenhanced-table-checkbox-N. Most of this reconnects what was there:bulkActions+getRowIdprops. The other tables sharing this component (Open Elections, Public Archive, Elections You Voted In, Invitations, Query Tool, Voter Rolls, Election History, Upload Elections) render exactly as before.visibleRows, which the component already computes.<TableRow>is a link to the election, so without this, ticking a box navigates away from the list.No backend change
The client loops over the existing
POST /Election/:id/archivein batches of 5. That endpoint's permission check, itsexpected_update_dateoptimistic-concurrency guard and its history entry all hang offelectionsRouter.param('id', ...), so a new bulk endpoint taking an array of ids would get none of them and would have to reimplement all three. Reusing the per-election route keeps each archive a normal, fully-audited write — the DB shows each one getting a newheadversion withstate='archived'and its predecessor demoted, exactly as a single archive does.The trade-off is that a bulk run isn't atomic. Partial results are reported rather than rolled back, and re-running is harmless because the backend rejects an already-archived election.
Two smaller behaviours worth flagging for review:
canEditElectionStateis[system_admin, owner], but this table also lists elections where you're only an admin, auditor or credentialer. Those are reported as skipped.archived(setOpenStaterefuses any state that isn'topen/closed), so the confirm dialog names the count and says the action can't be undone. If an unarchive ever lands, a bulk archive becomes considerably less scary — happy to follow up with one if that's wanted.All new strings are in
en.yamlunderbulk_actions, with_one/_otherplurals.One open question for reviewers. The toolbar is contextual — the Actions button only appears once a row is ticked, which is what MUI's own demo does and what Gmail-style tables do. The first person to try this build looked at the resting page and said "missing actions", then found it a moment later by ticking a box. So there's a real discoverability cost. The alternative is to always render the button and disable it until something is selected. It's a two-line change; say the word and I'll switch it.
Screenshots / Videos (frontend only)
Desktop
Three selected — the toolbar swaps the title for the count, the Actions button and a clear-selection ✕, and the header checkbox goes indeterminate:
The Actions menu:
Confirmation, naming the count:
After — the three are gone from the default view (the State filter already defaults to
archived: false), selection cleared, one summary snackbar:Mobile (320px)
The toolbar fits without overflowing (measured: 288px content in a 288px box, no horizontal page scroll):
How it was tested
Local stack (postgres + keycloak + both dev servers), 11 seeded elections, driven by Playwright:
headrow withstate='archived', predecessor demoted tohead=falsetsc --noEmitcleanRelated Issues
Part of #820.
On the mass-delete half of that issue — left out deliberately, and I think it needs a backend decision first.
DELETE /Election/:idexists and is owner/system_admin gated, but nothing in the frontend calls it, anddeleteElectionControllercallsElectionsModel.delete(), which removes rows fromelectionDBonly. The transactionaldeleteAllElectionData()right below it inModels/Elections.ts— the one that also clearsballotDBandelectionRollDB— currently has no callers anywhere in the repo. So wiring a mass-delete button to today's endpoint would orphan ballots and voter rolls in bulk. Happy to open a separate issue or PR for that if you'd like it fixed; the table-side work here is method-agnostic and aDeleteentry would drop straight into the same Actions menu once the backend is settled.