ML-174: research and plan

This commit is contained in:
Claudio Ortolina
2026-05-09 19:06:29 +01:00
parent a425327162
commit a0539cf2ea
2 changed files with 221 additions and 0 deletions
@@ -0,0 +1,60 @@
---
id: doc-16
title: "Research: Add menu action to empty a set"
type: specification
created_date: "2026-05-09 17:58"
---
# Research: Add menu action to empty a set
## Current state
Both the index (`RecordSetLive.Index`) and show (`RecordSetLive.Show`) views have a dropdown menu on each set with two actions:
- **Edit** — `dropdown_link` that patches to the edit form
- **Delete** — `dropdown_button` with `phx-click="delete_set"` and a `data-confirm` prompt
The `RecordSets` context currently provides `remove_record_from_set/2` for removing individual records, and `delete_record_set/1` for deleting the entire set (including its items via the `has_many` association's `on_delete` behavior). There is no function to remove all items from a set while keeping the set itself.
## Approach
Since the user has confirmed this is a straightforward feature, there's a single clear implementation route:
### 1. Context: Add `empty_record_set/1`
Add a new function to `MusicLibrary.RecordSets` that deletes all `RecordSetItem` rows for a given set in a single DELETE query, then returns the reloaded (now empty) record set. This avoids N+1 deletion and keeps the set metadata intact.
### 2. UI: Add "Empty" action to both dropdowns
In both `index.ex` and `show.ex`, add a new `dropdown_button` with:
- Text: `gettext("Empty")`
- `phx-click="empty_set"`
- `data-confirm` for safety: `gettext("Remove all records from this set?")`
And add the corresponding `handle_event("empty_set", ...)` handler in each LiveView.
### 3. Tests
- Unit test for `RecordSets.empty_record_set/1`
- LiveView test for the "empty_set" event in the index view
- LiveView test for the "empty_set" event in the show view
## Architecture impact
| Touchpoint | Impact |
| ----------------------------------------- | ------------------------------------------------------------------- |
| `MusicLibrary.RecordSets` | New public function `empty_record_set/1` |
| `MusicLibraryWeb.RecordSetLive.Index` | New event handler + dropdown button |
| `MusicLibraryWeb.RecordSetLive.Show` | New event handler + dropdown button |
| No schema changes needed | `RecordSetItem` already has `on_delete: :delete_all` via `has_many` |
| No PubSub / routes / external API changes | This is purely local |
## Performance
The implementation uses a single `DELETE FROM record_set_items WHERE record_set_id = ?` query (bulk delete). No N+1 risk. The reload is a single `get_record_set!/1` call that preloads the (now empty) items list. This is the same pattern already used by `delete_record_set/1` (which relies on cascading deletes) and `remove_record_from_set/2` (which deletes one item and reloads).
## Documentation updates
- `docs/architecture.md` — no changes needed (no new modules, schemas, or subsystems)
- `docs/project-conventions.md` — no changes needed (follows existing patterns)
@@ -0,0 +1,161 @@
---
id: ML-174
title: Add menu action to empty a set
status: To Do
assignee: []
created_date: "2026-05-09 17:57"
updated_date: "2026-05-09 18:05"
labels: []
dependencies: []
references:
- backlog/docs/doc-16 - Research-Add-menu-action-to-empty-a-set.md
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
For record sets, it would be useful to empty the set, which means removing all items in the set in one single operation. The action should be available in the dropdown menu already available in each set (both in the index and show views).
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 Clicking "Empty" in the set dropdown (index view) removes all records from that set and the card updates to show 0/0 records
- [ ] #2 Clicking "Empty" in the set dropdown (show view) removes all records and the grid clears
- [ ] #3 A confirmation prompt appears before emptying the set
- [ ] #4 Emptying a set does not delete the set itself (name, description, and metadata are preserved)
- [ ] #5 The context function `RecordSets.empty_record_set/1` returns `{:ok, record_set}` with an empty items list
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
## Implementation Plan
### 1. Add `empty_record_set/1` to `MusicLibrary.RecordSets`
Add a new public function that bulk-deletes all `RecordSetItem` rows for a given set in a single query, then reloads the set.
**File:** `lib/music_library/record_sets.ex`
```elixir
@spec empty_record_set(RecordSet.t()) :: {:ok, RecordSet.t()}
def empty_record_set(%RecordSet{} = record_set) do
from(i in RecordSetItem, where: i.record_set_id == ^record_set.id)
|> Repo.delete_all()
{:ok, get_record_set!(record_set.id)}
end
```
**Rationale:** `Repo.delete_all` issues a single `DELETE FROM record_set_items WHERE record_set_id = ?` — no N+1 regardless of item count. Only the `id` field is needed from the struct. Not wrapped in a transaction — consistent with `remove_record_from_set/2` and `reorder_records_in_set/2`.
### 2. Add "Empty" button and handler to index view
**File:** `lib/music_library_web/live/record_set_live/index.ex`
Add a `dropdown_button` **below the separator**, grouped with "Delete" (since emptying is a destructive action). Place it between the separator and the existing "Delete" button:
```heex
<.dropdown_separator />
<.dropdown_button
phx-click="empty_set"
phx-value-id={@record_set.id}
data-confirm={gettext("Remove all records from this set?")}
>
{gettext("Empty")}
</.dropdown_button>
<.dropdown_button
phx-click="delete_set"
...
>
{gettext("Delete")}
</.dropdown_button>
```
Add `handle_event`. Pass only the ID via a minimal struct to avoid a redundant `get_record_set!` call (the function itself does the reload):
```elixir
def handle_event("empty_set", %{"id" => id}, socket) do
{:ok, updated_set} = RecordSets.empty_record_set(%RecordSet{id: id})
{:noreply, update_record_set_in_list(socket, updated_set)}
end
```
### 3. Add "Empty" button and handler to show view
**File:** `lib/music_library_web/live/record_set_live/show.ex`
Add the same `dropdown_button` **below the separator**, before "Delete" (same placement as index). Add `handle_event` with a success toast for consistency with `delete_set`:
```elixir
def handle_event("empty_set", _params, socket) do
{:ok, updated_set} = RecordSets.empty_record_set(socket.assigns.record_set)
{:noreply,
socket
|> assign(:record_set, updated_set)
|> put_toast(:info, gettext("Set emptied"))}
end
```
### 4. Add tests
**File:** `test/music_library/record_sets_test.exs`
Add a new `describe "empty_record_set/1"` block with two tests:
```elixir
describe "empty_record_set/1" do
test "removes all items and returns the empty set" do
{set, [_r1, _r2, r3]} = record_set_with_records(3)
{:ok, updated} = RecordSets.empty_record_set(set)
assert updated.items == []
# Set metadata preserved
assert updated.name == set.name
assert updated.description == set.description
# Items are actually deleted from DB
assert [] =
from(i in RecordSetItem, where: i.record_set_id == ^set.id)
|> Repo.all()
end
test "is a no-op on already-empty sets" do
set = record_set()
assert set.items == []
{:ok, updated} = RecordSets.empty_record_set(set)
assert updated.items == []
end
end
```
**No LiveView tests are planned.** The project does not currently have LiveView test files for `RecordSetLive.Index` or `RecordSetLive.Show`. UI acceptance criteria (#1#4 from the acceptance criteria list) will be verified manually using the steps below. The context-level tests above cover acceptance criterion #5.
### Verification
| Step | Verification |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| 1. Context function | Run `mix test test/music_library/record_sets_test.exs` — both tests pass |
| 2. Edge case: already-empty set | Run `mix test test/music_library/record_sets_test.exs:NNN` (line of the no-op test) |
| 3. Index UI | Manual: open record sets index, click dropdown → Empty, confirm prompt appears, confirm → card updates to show 0/0 records |
| 4. Show UI | Manual: open a set show page, click dropdown → Empty, confirm prompt appears, confirm → grid clears, toast "Set emptied" appears |
| 5. Set preserved | After emptying, verify set name and description are still visible on both index card and show page |
| 6. Full test suite | Run `mix test` — all existing tests still pass |
<!-- SECTION:PLAN:END -->
## Definition of Done
<!-- DOD:BEGIN -->
- [ ] #1 `RecordSets.empty_record_set/1` added and tested
- [ ] #2 "Empty" dropdown button present in both index and show views
- [ ] #3 Confirmation prompt shown before emptying
- [ ] #4 Set metadata preserved after emptying
- [ ] #5 All existing tests still pass
<!-- DOD:END -->