Skip to content

feat(watchlist): bulk edit and remove - #12

Merged
danielrpof merged 65 commits into
mainfrom
gsd/phase-26-bulk-edit-remove
Oct 19, 2026
Merged

danielrpof merged 65 commits into
mainfrom
gsd/phase-26-bulk-edit-remove

Conversation

@danielrpof

Copy link
Copy Markdown
Owner

Summary

  • Multi-select on the watchlist with a bulk action bar: add tag, remove tag (with Undo), per-type Preferences delta, and remove.
  • Server: single-statement bulk delete, bulk add/remove tag with cap-skip reporting, bulk preferences. One request per bulk action.
  • UI: Shift+click range select (auto-repeat fix included), select-all under a filter, selection announcements, Esc to clear, bar and toast clearance at 375px/1280px.
  • Embedded SPA bundle rebuilt.
  • Bumps golang.org/x/text to v0.41.0 to clear CVE-2026-56851 (trivy-fs).

Verification

  • Phase verification 165/165; human UAT 9/9 pass.
  • CI green on an identical push (incl. -race integration job and trivy-fs).

Known advisories

  • Remove-tag Undo re-adds by the tag name captured client-side, so it can duplicate after a rename.
  • No committed test forces the bulk DELETE to fail mid-statement (checked manually, atomic).

- BulkDeleteWatchlist sqlc query and Service.BulkRemove
- POST /watchlist/bulk/delete behind the auth gate, WatchlistBulkStore seam
- end-to-end test against real Postgres
…otency

- artists, artist_tags links and unnotified events survive a bulk remove
- the note goes with the row
- 401/403 gate, 503 without the option, literal-path precedence
- id and body bounds, dedupe, empty arrays, no error leak on 500
- ordered FOR NO KEY UPDATE pre-lock, then count-guarded insert
- cap skips carry the artist name, gone ids are reported, all-capped rolls back
- POST /watchlist/bulk/tags/add behind the gate and CSRF check
…an-tag rollback

- 9 and 10 link adjacency, already-tagged at cap counts as applied
- reversed id lists across goroutines never hit 40P01
- a bulk add waiting on an uncommitted 10th link skips that artist as cap
… contract

- ADR records the ordered pre-lock and the all-capped rollback
- tests pin 401/403, 503, literal-path precedence, validation, error map and skip wire shape
- bulkRemoveWatchlist, formatNameList, formatAlreadyRemoved
- leading Select checkbox on each card, sticky selection bar
- route-hosted destructive ConfirmDialog, close-then-clear on success
…s shown

- pure effectiveSelection/selectAllState/toggleAllVisible helpers
- vendored Checkbox renders indeterminate with a Minus glyph
- heading-row Select all visible; hidden selections stay inert
- unit cases for effectiveSelection, selectAllState, toggleAllVisible
- route cases: hidden inert, uncheck keeps hidden, empty view, one card, Clear
- applyRange spans only the visible ids
- anchor resets on search, filter, sort change and empty selection
- BulkUpdateWatchlistPreferences locks targets, rebuilds each axis in canonical order, updates only changed rows
- Service.BulkUpdatePreferences validates via normalizeSet; conflict and empty deltas are rejected
- PATCH /watchlist/bulk/preferences with end-to-end test
… concurrency

- per-type delta keeps untouched types, canonical order, no-op rows keep updated_at
- validation sentinels and empty-ids ordering
- held-lock cases for a concurrent axis write and a concurrent delete
- error-code mapping, over-post and old-shape refusal, id bounds with zero store calls
- 401/403 gate, 503 without option, literal-path precedence, skip wire shape
- ConfirmDialog.finalFocus accepts base-ui's full union
- fallback chain: Select all visible, Clear filters, page heading
- cancel, Esc and failure return focus to the bar's Remove
- status region reads selection start, select-all and clear
- filter announcement appends the visible selected count
- Esc inside the bar clears the whole selection
- bar publishes --bulk-bar-clearance on the root while mounted
- Toaster offsets, html scroll-padding-bottom and page padding use it
- BulkDetachTag and GetTagByID statements; tags.Service.BulkDetach
- POST /watchlist/bulk/tags/remove reports applied, carried and gone ids
- end-to-end test over HTTP against Postgres
- vendor popover (hand-written; shadcn CLI tried to pnpm add a bogus cn package)
- bulkUpdatePreferences, tri-state helpers, bar popover editor, route handler
- tracer: mute one type on a mixed selection sends exactly one changed type
- initialTriStates, cycleTriState, buildAxisDelta cases
- popover focus, dismissal, pending lock, failure retention, one-surface rules
…ckiness

- gone-skip toast, failure retry with the same delta, too-many-ids backstop
- single Deluxe delta in one request, muted-filter stickiness
- bulkUpdatePreferences wire shape
- bulkRemoveTag wrapper and removableTags union with carrier counts
- Remove tag button and single-value picker with N-of-M counts
- handleBulkRemoveTag: one request, sticky applied ids, Undo toast
- undoRemoveTag re-adds by name to applied ids only (restore copy)
- Undo re-adds to applied ids only and leaves a newer selection alone
- restore, cap-warning, all-at-cap and failure toasts verbatim
- 404 tag_not_found refreshes and keeps the selection
- bulk artist removal still offers no Undo
- removableTags union, counts, collator order and id ties
- picker label, filtering, empty states, class contract, Esc and Cancel focus
- bar control order and one-surface rule in remove-tag mode
- bulkRemoveTag wire shape
- Refresh internal/webassets/build/client from web/ so a Go-only build serves
  selection, the action bar, bulk pickers, Preferences and remove confirmation
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Coverage

Area Coverage Δ vs main Gate Status
Backend 90.58% +0.39pp 80% ✅
Frontend 97.65% +0.49pp 70% ✅

head e8ec5b8 · 2026-10-19T19:43:38Z
baseline: main@5c0d214

## Summary

Stacked on #12. Behavior-preserving cleanup of the phase 26 bulk UI: the
route and the action bar each did too many jobs.

```text
web/app/
├── routes/watchlist.tsx                 1065 -> 711 lines, wiring only
├── components/watchlist/
│   ├── BulkActionBar.tsx                585 -> 330, tag modes + button row
│   └── BulkPreferencesPopover.tsx       new, owns tri-state, delta, Apply focus
└── lib/
    ├── useBulkSelection.ts              new, selection, remove confirm, handlers, Undo, focus
    ├── bulkOutcomeMessages.ts           new, pure toast data
    ├── preferenceOptions.ts             new, option lists shared with the card
    └── useBarClearance.ts               new, publishes --bulk-bar-clearance
```

```diff
 Watchlist (route)
-  selectedIds, anchorRef, removeConfirm, 4 handlers, toast copy, Undo, focus refs
+  useBulkSelection({ entries, visible, ... }) -> { effective, bar, confirmDialog, selectAll, refs }
```

- `preferenceOptions.ts` replaces the bar's hand-synced copy of the
card's option lists; `PreferencesDelta` is defined once in `api.ts`.
- Toast copy is returned as data (`{ level, title, description?,
duration? }`); only the hook calls `sonner`.
- Opaque comment tags (`U1`, `T-26-45`, `grill Q9`) removed from touched
files.
- `CONTEXT.md` gains **Bulk selection**, **Effective selection**, **Bulk
outcome**.
- `.graphifyignore` now excludes tests, sqlc output, `web/package.json`
and per-plan logs; graph rebuilt (4313 -> 1784 nodes, hubs are source,
not `testing.T`).

## Evidence

- **Before:** 782 web tests pass.
**After:** 793 pass (11 new table tests for `bulkOutcomeMessages`).
`watchlist.bulk.test.tsx` and `BulkActionBar.test.tsx` pass unedited.

```text
addTagToast(add([], [cap Kid, gone 3]), ...)
  -> { level: "warning",
       title: "Couldn't add “trap” — every selected artist already has 10 tags.",
       description: "Kid. 1 artist was already removed from your watchlist.",
       duration: 8000 }
```

`prettier --check` and `tsc --noEmit` clean. Go gates not run (no Go
changes).

## Merge Danger

**Door:** two-way

**Blast Radius:** watchlist bulk actions

Frontend-only module moves. The embedded SPA bundle under
`internal/webassets` is not rebuilt here, so the shipped bundle still
matches #12 until the next build.
@danielrpof
danielrpof merged commit 877439c into main Oct 19, 2026
26 checks passed
@danielrpof
danielrpof deleted the gsd/phase-26-bulk-edit-remove branch October 19, 2026 19:45
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.

1 participant