ML-174: add empty action to record set dropdown menu
Add RecordSets.empty_record_set/1 that bulk-deletes all items in a set via a single DELETE query and returns the reloaded empty set. Add Empty button with confirmation prompt to both index and show view dropdowns.
This commit is contained in:
+30
-14
@@ -1,16 +1,21 @@
|
|||||||
---
|
---
|
||||||
id: ML-174
|
id: ML-174
|
||||||
title: Add menu action to empty a set
|
title: Add menu action to empty a set
|
||||||
status: To Do
|
status: Done
|
||||||
assignee: []
|
assignee: []
|
||||||
created_date: "2026-05-09 17:57"
|
created_date: "2026-05-09 17:57"
|
||||||
updated_date: "2026-05-13 19:36"
|
updated_date: "2026-05-14 16:09"
|
||||||
labels:
|
labels:
|
||||||
- ready
|
- ready
|
||||||
- ui
|
- ui
|
||||||
dependencies: []
|
dependencies: []
|
||||||
references:
|
references:
|
||||||
- backlog/docs/doc-16 - Research-Add-menu-action-to-empty-a-set.md
|
- backlog/docs/doc-16 - Research-Add-menu-action-to-empty-a-set.md
|
||||||
|
modified_files:
|
||||||
|
- lib/music_library/record_sets.ex
|
||||||
|
- lib/music_library_web/live/record_set_live/index.ex
|
||||||
|
- lib/music_library_web/live/record_set_live/show.ex
|
||||||
|
- test/music_library/record_sets_test.exs
|
||||||
---
|
---
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
@@ -25,16 +30,17 @@ For record sets, it would be useful to empty the set, which means removing all i
|
|||||||
|
|
||||||
<!-- AC:BEGIN -->
|
<!-- 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
|
- [x] #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
|
- [x] #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
|
- [x] #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)
|
- [x] #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
|
- [x] #5 The context function `RecordSets.empty_record_set/1` returns `{:ok, record_set}` with an empty items list
|
||||||
<!-- AC:END -->
|
<!-- AC:END -->
|
||||||
<!-- SECTION:PLAN:BEGIN -->
|
|
||||||
|
|
||||||
## Implementation Plan
|
## Implementation Plan
|
||||||
|
|
||||||
|
<!-- SECTION:PLAN:BEGIN -->
|
||||||
|
|
||||||
### 1. Add `empty_record_set/1` to `MusicLibrary.RecordSets`
|
### 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.
|
Add a new public function that bulk-deletes all `RecordSetItem` rows for a given set in a single query, then reloads the set.
|
||||||
@@ -148,13 +154,23 @@ end
|
|||||||
|
|
||||||
<!-- SECTION:PLAN:END -->
|
<!-- SECTION:PLAN:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
|
||||||
|
Added `RecordSets.empty_record_set/1` context function that bulk-deletes all `RecordSetItem` rows via a single `DELETE FROM record_set_items WHERE record_set_id = ?` query and returns the reloaded (empty) set. Added "Empty" dropdown button with confirmation prompt to both index and show LiveViews. Index view updates the card in place; show view shows a "Set emptied" toast. All 983 tests pass.
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
|
|
||||||
|
<!-- SECTION:PLAN:END -->
|
||||||
|
|
||||||
## Definition of Done
|
## Definition of Done
|
||||||
|
|
||||||
<!-- DOD:BEGIN -->
|
<!-- DOD:BEGIN -->
|
||||||
|
|
||||||
- [ ] #1 `RecordSets.empty_record_set/1` added and tested
|
- [x] #1 `RecordSets.empty_record_set/1` added and tested
|
||||||
- [ ] #2 "Empty" dropdown button present in both index and show views
|
- [x] #2 "Empty" dropdown button present in both index and show views
|
||||||
- [ ] #3 Confirmation prompt shown before emptying
|
- [x] #3 Confirmation prompt shown before emptying
|
||||||
- [ ] #4 Set metadata preserved after emptying
|
- [x] #4 Set metadata preserved after emptying
|
||||||
- [ ] #5 All existing tests still pass
|
- [x] #5 All existing tests still pass
|
||||||
<!-- DOD:END -->
|
<!-- DOD:END -->
|
||||||
@@ -88,6 +88,14 @@ defmodule MusicLibrary.RecordSets do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
@spec delete_record_set(RecordSet.t()) :: {:ok, RecordSet.t()} | {:error, Changeset.t()}
|
@spec delete_record_set(RecordSet.t()) :: {:ok, RecordSet.t()} | {:error, Changeset.t()}
|
||||||
def delete_record_set(%RecordSet{} = record_set) do
|
def delete_record_set(%RecordSet{} = record_set) do
|
||||||
Repo.delete(record_set)
|
Repo.delete(record_set)
|
||||||
|
|||||||
@@ -221,6 +221,11 @@ defmodule MusicLibraryWeb.RecordSetLive.Index do
|
|||||||
{:noreply, push_patch(socket, to: ~p"/record-sets?#{qs}")}
|
{:noreply, push_patch(socket, to: ~p"/record-sets?#{qs}")}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
def handle_event("delete_set", %{"id" => id}, socket) do
|
def handle_event("delete_set", %{"id" => id}, socket) do
|
||||||
record_set = RecordSets.get_record_set!(id)
|
record_set = RecordSets.get_record_set!(id)
|
||||||
{:ok, _} = RecordSets.delete_record_set(record_set)
|
{:ok, _} = RecordSets.delete_record_set(record_set)
|
||||||
@@ -319,6 +324,13 @@ defmodule MusicLibraryWeb.RecordSetLive.Index do
|
|||||||
{gettext("Edit")}
|
{gettext("Edit")}
|
||||||
</.dropdown_link>
|
</.dropdown_link>
|
||||||
<.dropdown_separator />
|
<.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
|
<.dropdown_button
|
||||||
phx-click="delete_set"
|
phx-click="delete_set"
|
||||||
phx-value-id={@record_set.id}
|
phx-value-id={@record_set.id}
|
||||||
|
|||||||
@@ -48,6 +48,12 @@ defmodule MusicLibraryWeb.RecordSetLive.Show do
|
|||||||
{gettext("Edit")}
|
{gettext("Edit")}
|
||||||
</.dropdown_link>
|
</.dropdown_link>
|
||||||
<.dropdown_separator />
|
<.dropdown_separator />
|
||||||
|
<.dropdown_button
|
||||||
|
phx-click="empty_set"
|
||||||
|
data-confirm={gettext("Remove all records from this set?")}
|
||||||
|
>
|
||||||
|
{gettext("Empty")}
|
||||||
|
</.dropdown_button>
|
||||||
<.dropdown_button
|
<.dropdown_button
|
||||||
phx-click="delete_set"
|
phx-click="delete_set"
|
||||||
data-confirm={gettext("Are you sure?")}
|
data-confirm={gettext("Are you sure?")}
|
||||||
@@ -236,6 +242,15 @@ defmodule MusicLibraryWeb.RecordSetLive.Show do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
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
|
||||||
|
|
||||||
def handle_event("delete_set", _params, socket) do
|
def handle_event("delete_set", _params, socket) do
|
||||||
case RecordSets.delete_record_set(socket.assigns.record_set) do
|
case RecordSets.delete_record_set(socket.assigns.record_set) do
|
||||||
{:ok, _} ->
|
{:ok, _} ->
|
||||||
|
|||||||
@@ -2535,3 +2535,20 @@ msgstr ""
|
|||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Release Group"
|
msgid "Release Group"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/live/record_set_live/index.ex
|
||||||
|
#: lib/music_library_web/live/record_set_live/show.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Empty"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/live/record_set_live/index.ex
|
||||||
|
#: lib/music_library_web/live/record_set_live/show.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Remove all records from this set?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/live/record_set_live/show.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Set emptied"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -2535,3 +2535,20 @@ msgstr ""
|
|||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Release Group"
|
msgid "Release Group"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/live/record_set_live/index.ex
|
||||||
|
#: lib/music_library_web/live/record_set_live/show.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Empty"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/live/record_set_live/index.ex
|
||||||
|
#: lib/music_library_web/live/record_set_live/show.ex
|
||||||
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
|
msgid "Remove all records from this set?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/music_library_web/live/record_set_live/show.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Set emptied"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ defmodule MusicLibrary.RecordSetsTest do
|
|||||||
import MusicLibrary.Fixtures.Records
|
import MusicLibrary.Fixtures.Records
|
||||||
import MusicLibrary.Fixtures.RecordSets
|
import MusicLibrary.Fixtures.RecordSets
|
||||||
|
|
||||||
|
import Ecto.Query, warn: false
|
||||||
|
|
||||||
alias MusicLibrary.RecordSets
|
alias MusicLibrary.RecordSets
|
||||||
|
alias MusicLibrary.RecordSets.RecordSetItem
|
||||||
|
alias MusicLibrary.Repo
|
||||||
|
|
||||||
describe "search_record_sets/2" do
|
describe "search_record_sets/2" do
|
||||||
test "returns sets matching by name" do
|
test "returns sets matching by name" do
|
||||||
@@ -228,4 +232,28 @@ defmodule MusicLibrary.RecordSetsTest do
|
|||||||
assert Enum.map(moved_down.items, & &1.record.id) == Enum.map(set.items, & &1.record.id)
|
assert Enum.map(moved_down.items, & &1.record.id) == Enum.map(set.items, & &1.record.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user