fix(admin): drag-and-drop application reorder, fix broken swap (BUNYIP-473) #468

Merged
longjacksonle merged 1 commit from fix/BUNYIP-473-app-reorder-dnd into main 2026-08-04 21:00:34 +02:00

Fixes BUNYIP-473: the admin Applications list reorder does nothing and the list jumps. Replaces the broken up/down control with drag-and-drop plus a keyboard path.

Root cause (confirmed, as the reporter suspected both bugs share one)

  • Arrows do nothing: apps created through the admin form never set sort_order, so they all take the column default 0 (ApplicationRepository::create; migration 20260316000022 only ranked the rows that existed then). The list orders by (sort_order, display_name), and the control called swap_sort_order, which swaps the two rows' sort_order values. Swapping 0 with 0 writes the same value to both rows, so nothing moves and the display_name tiebreaker keeps the order identical.
  • List jumps: the control was a full-page form POST that redirected to /admin/applications, reloading at the top. The web handler also discarded the API result (let _ = ...), so a failure was silent.

Fix: explicit positions + one drag handle

  • ApplicationRepository::set_order sets each id's sort_order from its index in an ordered list (one unnest ... WITH ORDINALITY statement), so positions stay distinct and the equal-value no-op cannot recur. create() appends new apps at MAX(sort_order)+1, and a new migration densifies existing rows to distinct sequential positions preserving their current order. swap_sort_order / SwapApplicationOrderRequest are removed.
  • New endpoints replace the pairwise swap: PUT /v1/admin/applications/reorder (registered before the /{app_id} routes so "reorder" is never parsed as an id) and the bunyip-web POST /admin/applications/reorder proxy, which returns a bare status (no redirect) because it is called by fetch.
  • Each row gets a single grab-cursor drag handle (grip icon) and is draggable. assets/js/app-reorder.js moves rows live on drag (the moving row is its own drop indicator) and, for keyboard users, moves the focused row on ArrowUp/ArrowDown; on drop or key-move it POSTs the new id order. Nothing navigates, so the list never scroll-jumps. Ships as a first-party data-*-wired module, so script-src stays 'self' (BUNYIP-424).

Acceptance criteria

  • Dragging a row reorders it and persists across reload: yes (set_order + fetch).
  • Reordering never scrolls/jumps: yes (fetch, no navigation; live in-place move).
  • Keyboard reorder + persist: yes (ArrowUp/ArrowDown on the focused handle).
  • One clear affordance, not two chevrons: yes (single grip handle).
  • New apps get a distinct position; existing apps densified: yes (create MAX+1 + backfill migration).

Tests / verification

  • Unit: the row renders one drag handle and data-reorder-* markup, no swap-order/chevron controls, and keeps the toggles + Edit link; the reorder request DTO parses its ordered id list.
  • just check-container green (fmt, clippy -D warnings, 178 web tests) in the pinned image. Stylesheet rebuilt for the new cursor-grab / focus:ring-* utilities. Screenshot of the new list below.
  • Migration is a new file (immutability check safe); no .sqlx regen (runtime queries only).

Note: native HTML5 drag-and-drop is desktop-pointer; the ArrowUp/ArrowDown handle path is the keyboard-accessible equivalent.

🤖 Generated with Claude Code

https://claude.ai/code/session_018TXaT3P192nDsZzbzHETb9

Fixes [BUNYIP-473](https://niceguyit.myjetbrains.com/youtrack/issue/BUNYIP-473): the admin Applications list reorder does nothing and the list jumps. Replaces the broken up/down control with drag-and-drop plus a keyboard path. ## Root cause (confirmed, as the reporter suspected both bugs share one) - Arrows do nothing: apps created through the admin form never set `sort_order`, so they all take the column default `0` (`ApplicationRepository::create`; migration `20260316000022` only ranked the rows that existed then). The list orders by `(sort_order, display_name)`, and the control called `swap_sort_order`, which swaps the two rows' `sort_order` values. Swapping `0` with `0` writes the same value to both rows, so nothing moves and the `display_name` tiebreaker keeps the order identical. - List jumps: the control was a full-page form POST that redirected to `/admin/applications`, reloading at the top. The web handler also discarded the API result (`let _ = ...`), so a failure was silent. ## Fix: explicit positions + one drag handle - `ApplicationRepository::set_order` sets each id's `sort_order` from its index in an ordered list (one `unnest ... WITH ORDINALITY` statement), so positions stay distinct and the equal-value no-op cannot recur. `create()` appends new apps at `MAX(sort_order)+1`, and a new migration densifies existing rows to distinct sequential positions preserving their current order. `swap_sort_order` / `SwapApplicationOrderRequest` are removed. - New endpoints replace the pairwise swap: `PUT /v1/admin/applications/reorder` (registered before the `/{app_id}` routes so "reorder" is never parsed as an id) and the bunyip-web `POST /admin/applications/reorder` proxy, which returns a bare status (no redirect) because it is called by `fetch`. - Each row gets a single grab-cursor **drag handle** (grip icon) and is `draggable`. `assets/js/app-reorder.js` moves rows live on drag (the moving row is its own drop indicator) and, for keyboard users, moves the focused row on **ArrowUp/ArrowDown**; on drop or key-move it POSTs the new id order. Nothing navigates, so the list never scroll-jumps. Ships as a first-party `data-*`-wired module, so `script-src` stays `'self'` (BUNYIP-424). ## Acceptance criteria - Dragging a row reorders it and persists across reload: yes (`set_order` + fetch). - Reordering never scrolls/jumps: yes (fetch, no navigation; live in-place move). - Keyboard reorder + persist: yes (ArrowUp/ArrowDown on the focused handle). - One clear affordance, not two chevrons: yes (single grip handle). - New apps get a distinct position; existing apps densified: yes (`create` MAX+1 + backfill migration). ## Tests / verification - Unit: the row renders one drag handle and `data-reorder-*` markup, no `swap-order`/chevron controls, and keeps the toggles + Edit link; the reorder request DTO parses its ordered id list. - `just check-container` green (fmt, clippy `-D warnings`, 178 web tests) in the pinned image. Stylesheet rebuilt for the new `cursor-grab` / `focus:ring-*` utilities. Screenshot of the new list below. - Migration is a new file (immutability check safe); no `.sqlx` regen (runtime queries only). Note: native HTML5 drag-and-drop is desktop-pointer; the ArrowUp/ArrowDown handle path is the keyboard-accessible equivalent. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_018TXaT3P192nDsZzbzHETb9
fix(admin): drag-and-drop application reorder, fix broken swap (BUNYIP-473)
Some checks failed
E2E PR gate / Install + reachability (no deployment secrets) (pull_request) Successful in 17s
Check / fmt + clippy + build + tests (pull_request) Has been cancelled
f5d6a03542
The admin Applications list reorder was broken two ways, with one root cause plus a UI-mechanism problem.

Root cause of "the arrows do nothing": applications created through the admin form never set sort_order, so they all took the column default 0 (ApplicationRepository::create; the 20260316000022 migration only ranked the rows that existed then). The list orders by (sort_order, display_name) and the up/down control called swap_sort_order, which swapped the two rows' sort_order values. Swapping 0 with 0 writes the same value to both rows, so nothing moved and the display_name tiebreaker kept the order identical. Root cause of "the list jumps": the control was a full-page form POST that redirected to /admin/applications, reloading at the top; the web handler also discarded the API result, so a failure was silent.

Both are fixed by ordering on explicit positions instead of swapping values, and by replacing the two stacked chevrons with one drag handle:

- ApplicationRepository::set_order assigns each id's sort_order from its index in a supplied ordered list (one unnest WITH ORDINALITY statement), so positions stay distinct and the equal-value no-op cannot recur. create() now appends new apps at MAX(sort_order)+1, and a new migration densifies existing rows to distinct sequential positions preserving their current order. swap_sort_order and SwapApplicationOrderRequest are removed.
- New endpoints replace the per-row swap: PUT /v1/admin/applications/reorder (registered before the /{app_id} routes so "reorder" is never parsed as an id) and the bunyip-web POST /admin/applications/reorder proxy, which returns a bare status (no redirect) since it is called by fetch.
- Each row now carries a single grab-cursor drag handle (grip icon) and is draggable. assets/js/app-reorder.js moves rows live on drag (the moving row is its own drop indicator) and, for keyboard users, moves the focused row on ArrowUp/ArrowDown; on drop or key-move it POSTs the new id order. Nothing navigates, so the list never scroll-jumps. The behaviour ships as a first-party data-* wired module, so script-src stays 'self' (BUNYIP-424).

Tests: the row renders one drag handle and no swap-order/chevron markup while keeping the toggles and Edit link; the reorder request DTO parses its ordered id list. Verified with just check-container (fmt, clippy -D warnings, workspace tests) and a screenshot of the new list. The tracked stylesheet is rebuilt for the new cursor-grab / focus-ring utilities.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018TXaT3P192nDsZzbzHETb9
longjacksonle force-pushed fix/BUNYIP-473-app-reorder-dnd from f5d6a03542
Some checks failed
E2E PR gate / Install + reachability (no deployment secrets) (pull_request) Successful in 17s
Check / fmt + clippy + build + tests (pull_request) Has been cancelled
to 7b8736d132
All checks were successful
E2E PR gate / Install + reachability (no deployment secrets) (pull_request) Successful in 19s
Check / fmt + clippy + build + tests (pull_request) Successful in 4m22s
Create release / Create release from merged PR (pull_request) Has been skipped
2026-08-04 20:56:09 +02:00
Compare
longjacksonle scheduled this pull request to auto merge when all checks succeed 2026-08-04 20:56:27 +02:00
longjacksonle deleted branch fix/BUNYIP-473-app-reorder-dnd 2026-08-04 21:00:34 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!468
No description provided.