ML-169.10: rescope to list pages and use async handling
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
id: ML-169.10
|
id: ML-169.10
|
||||||
title: "Fix: Missing optimistic UI for delete operations"
|
title: "Fix: Async optimistic UI for index/list delete streams"
|
||||||
status: To Do
|
status: To Do
|
||||||
assignee: []
|
assignee: []
|
||||||
created_date: "2026-05-19 11:48"
|
created_date: "2026-05-19 11:48"
|
||||||
|
updated_date: "2026-05-20 09:41"
|
||||||
labels:
|
labels:
|
||||||
- perf
|
- perf
|
||||||
- fix
|
- fix
|
||||||
@@ -15,7 +16,8 @@ references:
|
|||||||
Phase-4-Performance-Audit-Low-Hanging-Fruit-Findings.md
|
Phase-4-Performance-Audit-Low-Hanging-Fruit-Findings.md
|
||||||
modified_files:
|
modified_files:
|
||||||
- lib/music_library_web/live_helpers/index_actions.ex
|
- lib/music_library_web/live_helpers/index_actions.ex
|
||||||
- lib/music_library_web/live/collection_live/show.ex
|
- lib/music_library_web/live/collection_live/index.ex
|
||||||
|
- lib/music_library_web/live/wishlist_live/index.ex
|
||||||
- lib/music_library_web/live/scrobbled_tracks_live/index.ex
|
- lib/music_library_web/live/scrobbled_tracks_live/index.ex
|
||||||
parent_task_id: ML-169
|
parent_task_id: ML-169
|
||||||
priority: medium
|
priority: medium
|
||||||
@@ -26,34 +28,39 @@ ordinal: 31000
|
|||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
|
||||||
Add optimistic UI for delete operations so the UI removes items immediately without waiting for the database transaction.
|
Add optimistic UI for delete operations only where the item is rendered from a LiveView stream on an index/list page. The item should be removed from the stream immediately, while the database delete runs through LiveView-owned async handling so failures can be reported and rolled back.
|
||||||
|
|
||||||
## Problem
|
## Problem
|
||||||
|
|
||||||
All delete operations wait for the database transaction to complete before updating the UI. `stream_delete` is already used in some places, but the pattern is inconsistent. CollectionLive.Show deletes don't use `stream_delete` at all (they navigate away).
|
Index/list delete handlers currently wait for the database delete before updating the UI. `stream_delete` is used after the delete succeeds, so the interaction can feel slower than necessary.
|
||||||
|
|
||||||
|
This task is intentionally scoped to stream-backed list views only:
|
||||||
|
|
||||||
|
- Collection index and wishlist index record streams via `IndexActions.handle_delete/2`
|
||||||
|
- Scrobbled tracks index stream
|
||||||
|
|
||||||
|
Show-page deletes are out of scope. `CollectionLive.Show` and `WishlistLive.Show` navigate away after delete and do not have a list stream to optimistically update. Their failure and rollback behavior should remain synchronous unless a separate navigation-specific design is created.
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
- `lib/music_library_web/live_helpers/index_actions.ex:94-98` — `handle_delete`
|
- `lib/music_library_web/live_helpers/index_actions.ex` — remove record from `:records` immediately and start a LiveView async delete operation
|
||||||
- `lib/music_library_web/live/collection_live/show.ex:381-385` — `handle_event("delete")`
|
- `lib/music_library_web/live/collection_live/index.ex` — handle async record delete success/failure for collection index
|
||||||
- `lib/music_library_web/live/scrobbled_tracks_live/index.ex:305-310` — `handle_event("delete")`
|
- `lib/music_library_web/live/wishlist_live/index.ex` — handle async record delete success/failure for wishlist index
|
||||||
|
- `lib/music_library_web/live/scrobbled_tracks_live/index.ex` — remove track from `:tracks` immediately and handle async track delete success/failure
|
||||||
## Impact
|
|
||||||
|
|
||||||
Adds unnecessary latency to delete operations that could feel instant. While delete failures are rare, the benefit of snappy UI is worth the minimal risk.
|
|
||||||
|
|
||||||
## Fix
|
## Fix
|
||||||
|
|
||||||
Use `stream_delete` immediately, then perform the DB operation async. If DB fails, re-insert the item and show error toast.
|
Use LiveView-owned async handling (`start_async/3` plus `handle_async/3`, or an equivalent pattern that sends results back to the LiveView process). Do not use bare `Task.start/1`, because it cannot update the socket, reinsert the stream item, or show an error toast.
|
||||||
|
|
||||||
```elixir
|
Expected behavior:
|
||||||
def handle_delete(socket, id) do
|
|
||||||
record = Records.get_record!(id)
|
1. Fetch the item needed for rollback.
|
||||||
socket = stream_delete(socket, :records, record)
|
2. Immediately call `stream_delete/3` on the relevant stream.
|
||||||
Task.start(fn -> Records.delete_record(record) end)
|
3. Start an async delete operation owned by the LiveView.
|
||||||
{:noreply, socket}
|
4. On success, leave the stream as-is and optionally broadcast/refresh any cross-view state the existing design requires.
|
||||||
end
|
5. On failure or async exit, reinsert the original item into the stream and show a user-facing error toast.
|
||||||
```
|
|
||||||
|
For error text, use the project’s existing user-facing error conventions (`ErrorMessages.friendly_message/1` where an error reason is shown).
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
@@ -61,9 +68,10 @@ end
|
|||||||
|
|
||||||
<!-- AC:BEGIN -->
|
<!-- AC:BEGIN -->
|
||||||
|
|
||||||
- [ ] #1 Delete operations remove items from UI immediately (no DB wait)
|
- [ ] #1 Index/list delete operations remove the item from the relevant stream before waiting for the database delete
|
||||||
- [ ] #2 Database delete still executes successfully in background
|
- [ ] #2 Database delete still executes through LiveView-owned async handling, not bare `Task.start/1`
|
||||||
- [ ] #3 If DB delete fails, item is re-inserted into stream and error toast shown
|
- [ ] #3 Async delete failure or exit reinserts the original item into the stream and shows an error toast
|
||||||
- [ ] #4 Existing delete confirmation modal still works correctly
|
- [ ] #4 Existing delete confirmation prompts still work correctly
|
||||||
- [ ] #5 No orphaned UI state after failed delete
|
- [ ] #5 Show-page delete handlers are not changed as part of this task
|
||||||
|
- [ ] #6 Collection and wishlist index delete behavior remains shared through `IndexActions` where practical
|
||||||
<!-- AC:END -->
|
<!-- AC:END -->
|
||||||
|
|||||||
Reference in New Issue
Block a user