From f108e6f0626c3d37f3d4c74357f0844d574603ef Mon Sep 17 00:00:00 2001 From: Claudio Ortolina Date: Thu, 14 May 2026 17:10:54 +0100 Subject: [PATCH] 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. --- ...ml-174 - Add-menu-action-to-empty-a-set.md | 44 +++++++++++++------ lib/music_library/record_sets.ex | 8 ++++ .../live/record_set_live/index.ex | 12 +++++ .../live/record_set_live/show.ex | 15 +++++++ priv/gettext/default.pot | 17 +++++++ priv/gettext/en/LC_MESSAGES/default.po | 17 +++++++ test/music_library/record_sets_test.exs | 28 ++++++++++++ 7 files changed, 127 insertions(+), 14 deletions(-) rename backlog/{tasks => completed}/ml-174 - Add-menu-action-to-empty-a-set.md (80%) diff --git a/backlog/tasks/ml-174 - Add-menu-action-to-empty-a-set.md b/backlog/completed/ml-174 - Add-menu-action-to-empty-a-set.md similarity index 80% rename from backlog/tasks/ml-174 - Add-menu-action-to-empty-a-set.md rename to backlog/completed/ml-174 - Add-menu-action-to-empty-a-set.md index 542d43d6..23f3b112 100644 --- a/backlog/tasks/ml-174 - Add-menu-action-to-empty-a-set.md +++ b/backlog/completed/ml-174 - Add-menu-action-to-empty-a-set.md @@ -1,16 +1,21 @@ --- id: ML-174 title: Add menu action to empty a set -status: To Do +status: Done assignee: [] created_date: "2026-05-09 17:57" -updated_date: "2026-05-13 19:36" +updated_date: "2026-05-14 16:09" labels: - ready - ui dependencies: [] references: - 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 @@ -25,16 +30,17 @@ For record sets, it would be useful to empty the set, which means removing all i -- [ ] #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 - - +- [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 +- [x] #2 Clicking "Empty" in the set dropdown (show view) removes all records and the grid clears +- [x] #3 A confirmation prompt appears before emptying the set +- [x] #4 Emptying a set does not delete the set itself (name, description, and metadata are preserved) +- [x] #5 The context function `RecordSets.empty_record_set/1` returns `{:ok, record_set}` with an empty items list + ## 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. @@ -148,13 +154,23 @@ end +## Final Summary + + + +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. + + + + + ## Definition of Done -- [ ] #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 +- [x] #1 `RecordSets.empty_record_set/1` added and tested +- [x] #2 "Empty" dropdown button present in both index and show views +- [x] #3 Confirmation prompt shown before emptying +- [x] #4 Set metadata preserved after emptying +- [x] #5 All existing tests still pass diff --git a/lib/music_library/record_sets.ex b/lib/music_library/record_sets.ex index ddbeced2..244c4318 100644 --- a/lib/music_library/record_sets.ex +++ b/lib/music_library/record_sets.ex @@ -88,6 +88,14 @@ defmodule MusicLibrary.RecordSets do 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()} def delete_record_set(%RecordSet{} = record_set) do Repo.delete(record_set) diff --git a/lib/music_library_web/live/record_set_live/index.ex b/lib/music_library_web/live/record_set_live/index.ex index 4b43d25c..8e86e3da 100644 --- a/lib/music_library_web/live/record_set_live/index.ex +++ b/lib/music_library_web/live/record_set_live/index.ex @@ -221,6 +221,11 @@ defmodule MusicLibraryWeb.RecordSetLive.Index do {:noreply, push_patch(socket, to: ~p"/record-sets?#{qs}")} 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 record_set = RecordSets.get_record_set!(id) {:ok, _} = RecordSets.delete_record_set(record_set) @@ -319,6 +324,13 @@ defmodule MusicLibraryWeb.RecordSetLive.Index do {gettext("Edit")} <.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 phx-click="delete_set" phx-value-id={@record_set.id} diff --git a/lib/music_library_web/live/record_set_live/show.ex b/lib/music_library_web/live/record_set_live/show.ex index e6c8fdd4..bd1c2dd4 100644 --- a/lib/music_library_web/live/record_set_live/show.ex +++ b/lib/music_library_web/live/record_set_live/show.ex @@ -48,6 +48,12 @@ defmodule MusicLibraryWeb.RecordSetLive.Show do {gettext("Edit")} <.dropdown_separator /> + <.dropdown_button + phx-click="empty_set" + data-confirm={gettext("Remove all records from this set?")} + > + {gettext("Empty")} + <.dropdown_button phx-click="delete_set" data-confirm={gettext("Are you sure?")} @@ -236,6 +242,15 @@ defmodule MusicLibraryWeb.RecordSetLive.Show do end @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 case RecordSets.delete_record_set(socket.assigns.record_set) do {:ok, _} -> diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index 12943748..0b73b53b 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -2535,3 +2535,20 @@ msgstr "" #, elixir-autogen, elixir-format msgid "Release Group" 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 "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index b0716e06..54e971b5 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -2535,3 +2535,20 @@ msgstr "" #, elixir-autogen, elixir-format, fuzzy msgid "Release Group" 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 "" diff --git a/test/music_library/record_sets_test.exs b/test/music_library/record_sets_test.exs index 78f10200..5448fea7 100644 --- a/test/music_library/record_sets_test.exs +++ b/test/music_library/record_sets_test.exs @@ -4,7 +4,11 @@ defmodule MusicLibrary.RecordSetsTest do import MusicLibrary.Fixtures.Records import MusicLibrary.Fixtures.RecordSets + import Ecto.Query, warn: false + alias MusicLibrary.RecordSets + alias MusicLibrary.RecordSets.RecordSetItem + alias MusicLibrary.Repo describe "search_record_sets/2" 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) 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