feat(sidebar): drag-and-drop category + room reorder (LC-79 phase 2) #131
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/lc-79-phase2-drag-and-drop"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
LC-79 phase 2: drag-and-drop reorder for sidebar categories and the rooms inside them. Closes LC-79 phase 2.
What's draggable
[data-category-list]->PATCH /sidebar/categories/positions).PATCH /sidebar/categories/{cat_id}/positions).Uncategorized rooms are intentionally not draggable in this phase: the existing "Move to category" / "Remove from category" dropdown from phase 1b already covers those transitions without the cross-list drag complexity. Future phase can promote the All-rooms list to a drop target too.
DnD implementation
Native HTML5 DnD in a new
server/assets/sidebar_dnd.js(~80 lines, no library). Event delegation offdocumentso HTMX-driven sidebar swaps don't break the handlers. On drop the JS reads the new DOM order, postsids=a,b,cto the list'sdata-positions-url, replaces#sidebarwith the server's rebuilt fragment, and reprocesses HTMX attributes on the new tree.Endpoints
PATCH /sidebar/categories/positions->set_category_positions(UPDATEspositionper id in a transaction, scoped to the calling user so a spoofed id from a different user is a no-op).PATCH /sidebar/categories/{cat_id}/positions->set_room_positionshandles three shapes in one call: same-category reorder, cross-category drag (room currently in a different category gets upserted), and newly categorizing (room previously uncategorized lands here). Membership-checks every room id viais_room_accessiblefirst so the endpoint cannot disclose private-room existence.Body shape
Comma-separated single field (
ids=3,1,2) rather than the repeated-key form (ids[]=...). The latter is not supported by axum's default Form extractor (returns 422 at decode); the comma-split parser surfaces a 400 BadRequest for any non-integer entry instead of silently skipping it.Test plan
./dev/cargo test -p lets-chat-server --test routes_sidebar_categories(9 tests pass: 6 from phases 1a/1b plus 3 new: category reorder, cross-category room move, access refusal).just check(fmt + clippy across standalone + saas).just test(full standalone suite, all green).Follow-ups
Native HTML5 DnD in `server/assets/sidebar_dnd.js` (~80 lines, no library). Two reorderable surfaces share one handler via event delegation off `document`, so HTMX sidebar swaps don't break the bindings: category sections inside `[data-category-list]`, and room rows inside per-category `[data-room-list]` lists. On drop the JS reads the new DOM order, posts `ids=a,b,c` to the list's `data-positions-url`, replaces `#sidebar` with the returned fragment, and reprocesses HTMX attributes on the new tree. Two new endpoints back the JS, both inside `routes/sidebar_categories.rs`: - `PATCH /sidebar/categories/positions` -> `set_category_positions` (UPDATEs `position` per id in a transaction, scoped to the calling user so a spoofed id from a different user is a no-op). - `PATCH /sidebar/categories/{cat_id}/positions` -> `set_room_positions` handles same-category reorder, cross-category drag (room currently in a different category gets upserted), and newly categorizing (room previously uncategorized lands here). Membership-checks every room id via `is_room_accessible` first so the endpoint cannot disclose private-room existence. Body shape is comma-separated (`ids=3,1,2`) rather than the repeated-key form (`ids[]=…`); the latter is not supported by axum's default Form extractor (returns 422 at decode), and the comma-split parser surfaces a 400 BadRequest for any non-integer entry. Uncategorized rooms are intentionally not draggable in phase 2: the existing per-row "move to category" / "Remove from category" dropdown (phase 1b) already covers those transitions without the cross-list drag complexity. 3 new tests cover the endpoints (category reorder, cross-category move, access refusal). All 9 sidebar tests pass; `just check` and `just test` green.