Extract shared index logic into LiveHelpers.IndexActions

Closes #150
This commit is contained in:
Claudio Ortolina
2026-04-13 23:09:22 +01:00
parent 4fa13d5422
commit e43bccb9bc
6 changed files with 223 additions and 218 deletions
+1
View File
@@ -306,6 +306,7 @@ All authenticated routes live inside a single `live_session` with three `on_moun
| `Auth` | Authentication plugs: login password check, API token validation, session enforcement |
| `ArtistLive.Biography` | Artist biography building/rendering from Wikipedia and Last.fm data |
| `LiveHelpers.Params` | URL query param parsing: pagination, search query, sort order, display mode, fallback index |
| `LiveHelpers.IndexActions` | Shared index page logic (search, pagination, import, delete, display mode) for Collection/Wishlist index pages, parameterized by config map |
| `LiveHelpers.RecordActions` | Shared record action handlers (refresh cover, genres, embeddings, MusicBrainz data) for Collection/Wishlist show pages |
### Controllers
@@ -8,16 +8,24 @@ defmodule MusicLibraryWeb.CollectionLive.Index do
alias MusicLibrary.Chats
alias MusicLibrary.Collection
alias MusicLibrary.Records
alias MusicLibraryWeb.LiveHelpers.IndexActions
alias MusicLibraryWeb.ErrorMessages
@default_records_list_params %{
query: "",
page: 1,
page_size: 72,
order: :purchase
}
defp index_config do
%{
context_module: Collection,
default_order: "purchase",
allowed_orders: [:purchase, :alphabetical, :release],
default_records_list_params: %{query: "", page: 1, page_size: 72, order: :purchase},
purchased_at_fn: fn -> DateTime.utc_now() end,
import_page_title: gettext("Add new Record · Collection"),
section_page_title: gettext("Collection"),
import_success_toast: gettext("Record imported successfully"),
import_error_toast: gettext("Error importing record"),
record_path_fn: fn id -> ~p"/collection/#{id}" end,
index_path_fn: fn qs -> ~p"/collection?#{qs}" end,
base_index_path: ~p"/collection"
}
end
@impl true
def render(assigns) do
@@ -234,6 +242,7 @@ defmodule MusicLibraryWeb.CollectionLive.Index do
{:ok,
socket
|> assign(:current_section, :collection)
|> assign(:index_config, index_config())
|> assign(:import_query, "")
|> assign(:display, :grid)
|> assign(:open_chat, false)
@@ -248,50 +257,31 @@ defmodule MusicLibraryWeb.CollectionLive.Index do
end
defp apply_action(socket, :import, params) do
import_query = params["import_query"] || ""
socket
|> apply_fallback_index(params, :records, &apply_action/3)
|> assign(:page_title, gettext("Add new Record · Collection"))
|> assign(:import_query, import_query)
|> assign(:record, nil)
IndexActions.apply_import_action(socket, params)
end
defp apply_action(socket, :barcode_scan, params) do
socket
|> apply_fallback_index(params, :records, &apply_action/3)
|> apply_fallback_index(params, :records, fn s, :index, p ->
IndexActions.apply_index_action(s, p)
end)
|> assign(:page_title, gettext("Scan barcodes · Collection"))
|> assign(:record, nil)
end
defp apply_action(socket, :edit, %{"id" => id} = params) do
record = Records.get_record!(id)
socket
|> apply_fallback_index(params, :records, &apply_action/3)
|> assign(:page_title, page_title(:edit, record))
|> assign(:record, record)
defp apply_action(socket, :edit, params) do
IndexActions.apply_edit_action(socket, params)
end
defp apply_action(socket, :index, params) do
query = params["query"] || ""
order = parse_order(params["order"] || "purchase", [:purchase, :alphabetical, :release])
total_records = Collection.search_records_count(query)
record_list_params =
@default_records_list_params
|> merge_query(query)
|> merge_order(order)
|> merge_pagination(params, total_records)
socket
|> load_and_assign_records(record_list_params)
|> IndexActions.apply_index_action(params)
|> assign(:open_chat, params["chat"] == "open")
end
@impl true
def handle_info({MusicLibraryWeb.Components.RecordForm, {:saved, _record}}, socket) do
{:noreply, load_and_assign_records(socket, socket.assigns.record_list_params)}
IndexActions.handle_record_saved(socket)
end
def handle_info({MusicLibraryWeb.Components.Chat, :chats_changed}, socket) do
@@ -310,71 +300,19 @@ defmodule MusicLibraryWeb.CollectionLive.Index do
@impl true
def handle_event("delete", %{"id" => id}, socket) do
record = Records.get_record!(id)
{:ok, _} = Records.delete_record(record)
{:noreply, stream_delete(socket, :records, record)}
IndexActions.handle_delete(socket, id)
end
def handle_event("search", %{"query" => query}, socket) do
qs =
@default_records_list_params
|> Map.put(:query, query)
|> Map.take([:query, :page, :page_size])
{:noreply, push_patch(socket, to: ~p"/collection?#{qs}")}
IndexActions.handle_search(socket, query)
end
def handle_event("import", %{"id" => musicbrainz_id, "format" => format}, socket) do
current_time = DateTime.utc_now()
case Records.import_from_musicbrainz_release_group(musicbrainz_id,
format: format,
purchased_at: current_time
) do
{:ok, record} ->
{:noreply,
socket
|> put_toast(:info, gettext("Record imported successfully"))
|> push_navigate(to: ~p"/collection/#{record.id}")}
{:error, reason} ->
{:noreply,
socket
|> put_toast(
:error,
gettext("Error importing record") <> ": " <> ErrorMessages.friendly_message(reason)
)
|> push_patch(to: ~p"/collection")}
end
IndexActions.handle_import(socket, musicbrainz_id, format)
end
def handle_event("set_display", %{"mode" => mode}, socket) do
mode = parse_mode(mode)
{:noreply,
socket
|> assign(:display, mode)
|> load_and_assign_records(socket.assigns.record_list_params)}
end
defp load_and_assign_records(socket, record_list_params) do
offset = page_to_offset(record_list_params.page, record_list_params.page_size)
opts = [
limit: record_list_params.page_size,
offset: offset,
order: record_list_params.order
]
records =
Collection.search_records(record_list_params.query, opts)
socket
|> assign(:page_title, gettext("Collection"))
|> assign(:record, nil)
|> assign(:record_list_params, record_list_params)
|> stream(:records, records, reset: true)
IndexActions.handle_set_display(socket, mode)
end
defp order_path(record_list_params, order) do
@@ -395,21 +333,4 @@ defmodule MusicLibraryWeb.CollectionLive.Index do
~p"/collection?#{qs}"
end
defp page_title(action, record) do
Enum.join(
[
Records.Record.artist_names(record),
"-",
record.title,
"·",
title_segment(action),
"·",
gettext("Collection")
],
" "
)
end
defp title_segment(:edit), do: gettext("Edit")
end
+28 -105
View File
@@ -2,19 +2,28 @@ defmodule MusicLibraryWeb.WishlistLive.Index do
use MusicLibraryWeb, :live_view
import MusicLibraryWeb.Components.Pagination
import MusicLibraryWeb.LiveHelpers.Params
import MusicLibraryWeb.RecordComponents
alias MusicLibrary.Records
alias MusicLibrary.Wishlist
alias MusicLibraryWeb.ErrorMessages
alias MusicLibraryWeb.LiveHelpers.IndexActions
@default_records_list_params %{
query: "",
page: 1,
page_size: 72,
order: :alphabetical
}
defp index_config do
%{
context_module: Wishlist,
default_order: "insertion",
allowed_orders: [:insertion, :alphabetical, :release],
default_records_list_params: %{query: "", page: 1, page_size: 72, order: :alphabetical},
purchased_at_fn: fn -> nil end,
import_page_title: gettext("Add new Record · Wishlist"),
section_page_title: gettext("Wishlist"),
import_success_toast: gettext("Record wishlisted successfully"),
import_error_toast: gettext("Error wishlisting record"),
record_path_fn: fn id -> ~p"/wishlist/#{id}" end,
index_path_fn: fn qs -> ~p"/wishlist?#{qs}" end,
base_index_path: ~p"/wishlist"
}
end
@impl true
def render(assigns) do
@@ -170,6 +179,7 @@ defmodule MusicLibraryWeb.WishlistLive.Index do
{:ok,
socket
|> assign(current_section: :wishlist)
|> assign(:index_config, index_config())
|> assign(:import_query, "")
|> assign(:display, :grid)
|> assign(:current_date, current_date)}
@@ -181,81 +191,33 @@ defmodule MusicLibraryWeb.WishlistLive.Index do
end
defp apply_action(socket, :import, params) do
import_query = params["import_query"] || ""
socket
|> apply_fallback_index(params, :records, &apply_action/3)
|> assign(:page_title, gettext("Add new Record · Wishlist"))
|> assign(:import_query, import_query)
|> assign(:record, nil)
IndexActions.apply_import_action(socket, params)
end
defp apply_action(socket, :edit, %{"id" => id} = params) do
record = Records.get_record!(id)
socket
|> apply_fallback_index(params, :records, &apply_action/3)
|> assign(:page_title, page_title(:edit, record))
|> assign(:record, record)
defp apply_action(socket, :edit, params) do
IndexActions.apply_edit_action(socket, params)
end
defp apply_action(socket, :index, params) do
query = params["query"] || ""
order = parse_order(params["order"] || "insertion", [:insertion, :alphabetical, :release])
total_records = Wishlist.search_records_count(query)
record_list_params =
@default_records_list_params
|> merge_query(query)
|> merge_order(order)
|> merge_pagination(params, total_records)
load_and_assign_records(socket, record_list_params)
IndexActions.apply_index_action(socket, params)
end
@impl true
def handle_info({MusicLibraryWeb.Components.RecordForm, {:saved, _record}}, socket) do
{:noreply, load_and_assign_records(socket, socket.assigns.record_list_params)}
IndexActions.handle_record_saved(socket)
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
record = Records.get_record!(id)
{:ok, _} = Records.delete_record(record)
{:noreply, stream_delete(socket, :records, record)}
IndexActions.handle_delete(socket, id)
end
def handle_event("search", %{"query" => query}, socket) do
qs =
@default_records_list_params
|> Map.put(:query, query)
|> Map.take([:query, :page, :page_size])
{:noreply, push_patch(socket, to: ~p"/wishlist?#{qs}")}
IndexActions.handle_search(socket, query)
end
def handle_event("import", %{"id" => musicbrainz_id, "format" => format}, socket) do
case Records.import_from_musicbrainz_release_group(musicbrainz_id,
format: format,
purchased_at: nil
) do
{:ok, record} ->
{:noreply,
socket
|> put_toast(:info, gettext("Record wishlisted successfully"))
|> push_navigate(to: ~p"/wishlist/#{record.id}")}
{:error, reason} ->
{:noreply,
socket
|> put_toast(
:error,
gettext("Error wishlisting record") <> ": " <> ErrorMessages.friendly_message(reason)
)
|> push_patch(to: ~p"/wishlist")}
end
IndexActions.handle_import(socket, musicbrainz_id, format)
end
def handle_event("add-to-collection", %{"id" => id}, socket) do
@@ -275,29 +237,7 @@ defmodule MusicLibraryWeb.WishlistLive.Index do
end
def handle_event("set_display", %{"mode" => mode}, socket) do
mode = parse_mode(mode)
{:noreply,
socket
|> assign(:display, mode)
|> load_and_assign_records(socket.assigns.record_list_params)}
end
defp load_and_assign_records(socket, record_list_params) do
offset = page_to_offset(record_list_params.page, record_list_params.page_size)
records =
Wishlist.search_records(record_list_params.query,
limit: record_list_params.page_size,
offset: offset,
order: record_list_params.order
)
socket
|> assign(:page_title, gettext("Wishlist"))
|> assign(:record, nil)
|> assign(:record_list_params, record_list_params)
|> stream(:records, records, reset: true)
IndexActions.handle_set_display(socket, mode)
end
defp order_path(record_list_params, order) do
@@ -313,26 +253,9 @@ defmodule MusicLibraryWeb.WishlistLive.Index do
defp back_path(record_list_params) do
qs =
record_list_params
|> Map.take([:query, :page, :page_size])
|> Map.take([:query, :page, :page_size, :order])
|> Enum.filter(fn {_, v} -> v not in ["", nil] end)
~p"/wishlist?#{qs}"
end
defp page_title(action, record) do
Enum.join(
[
Records.Record.artist_names(record),
"-",
record.title,
"·",
title_segment(action),
"·",
gettext("Wishlist")
],
" "
)
end
defp title_segment(:edit), do: gettext("Edit")
end
@@ -0,0 +1,162 @@
defmodule MusicLibraryWeb.LiveHelpers.IndexActions do
@moduledoc false
import LiveToast, only: [put_toast: 3]
import Phoenix.Component, only: [assign: 3]
import Phoenix.LiveView, only: [push_patch: 2, push_navigate: 2, stream: 4, stream_delete: 3]
import MusicLibraryWeb.Components.Pagination, only: [page_to_offset: 2]
import MusicLibraryWeb.LiveHelpers.Params
use Gettext, backend: MusicLibraryWeb.Gettext
alias MusicLibrary.Records
alias MusicLibraryWeb.ErrorMessages
@doc """
Applies the :index action. Reads config from `socket.assigns.index_config`
for context_module, default_order, allowed_orders, and default_records_list_params.
Returns the socket (not wrapped in `{:noreply, ...}`).
"""
def apply_index_action(socket, params) do
config = socket.assigns.index_config
query = params["query"] || ""
order = parse_order(params["order"] || config.default_order, config.allowed_orders)
total_records = config.context_module.search_records_count(query)
record_list_params =
config.default_records_list_params
|> merge_query(query)
|> merge_order(order)
|> merge_pagination(params, total_records)
load_and_assign_records(socket, record_list_params)
end
@doc """
Applies the :import action. Falls back to :index if the stream hasn't
been initialized yet.
Returns the socket (not wrapped in `{:noreply, ...}`).
"""
def apply_import_action(socket, params) do
config = socket.assigns.index_config
import_query = params["import_query"] || ""
socket
|> apply_fallback_index(params, :records, fn s, :index, p -> apply_index_action(s, p) end)
|> assign(:page_title, config.import_page_title)
|> assign(:import_query, import_query)
|> assign(:record, nil)
end
@doc """
Applies the :edit action. Falls back to :index if the stream hasn't
been initialized yet.
Returns the socket (not wrapped in `{:noreply, ...}`).
"""
def apply_edit_action(socket, %{"id" => id} = params) do
config = socket.assigns.index_config
record = Records.get_record!(id)
socket
|> apply_fallback_index(params, :records, fn s, :index, p -> apply_index_action(s, p) end)
|> assign(:page_title, record_page_title(record, config))
|> assign(:record, record)
end
@doc """
Loads records from the context module and assigns them to the socket stream.
Returns the socket (not wrapped in `{:noreply, ...}`).
"""
def load_and_assign_records(socket, record_list_params) do
config = socket.assigns.index_config
offset = page_to_offset(record_list_params.page, record_list_params.page_size)
records =
config.context_module.search_records(record_list_params.query,
limit: record_list_params.page_size,
offset: offset,
order: record_list_params.order
)
socket
|> assign(:page_title, config.section_page_title)
|> assign(:record, nil)
|> assign(:record_list_params, record_list_params)
|> stream(:records, records, reset: true)
end
def handle_delete(socket, id) do
record = Records.get_record!(id)
{:ok, _} = Records.delete_record(record)
{:noreply, stream_delete(socket, :records, record)}
end
def handle_search(socket, query) do
config = socket.assigns.index_config
qs =
config.default_records_list_params
|> Map.put(:query, query)
|> Map.take([:query, :page, :page_size])
{:noreply, push_patch(socket, to: config.index_path_fn.(qs))}
end
def handle_import(socket, musicbrainz_id, format) do
config = socket.assigns.index_config
case Records.import_from_musicbrainz_release_group(musicbrainz_id,
format: format,
purchased_at: config.purchased_at_fn.()
) do
{:ok, record} ->
{:noreply,
socket
|> put_toast(:info, config.import_success_toast)
|> push_navigate(to: config.record_path_fn.(record.id))}
{:error, reason} ->
{:noreply,
socket
|> put_toast(
:error,
config.import_error_toast <> ": " <> ErrorMessages.friendly_message(reason)
)
|> push_patch(to: config.base_index_path)}
end
end
def handle_set_display(socket, mode) do
mode = parse_mode(mode)
{:noreply,
socket
|> assign(:display, mode)
|> load_and_assign_records(socket.assigns.record_list_params)}
end
def handle_record_saved(socket) do
{:noreply, load_and_assign_records(socket, socket.assigns.record_list_params)}
end
defp record_page_title(record, config) do
Enum.join(
[
Records.Record.artist_names(record),
"-",
record.title,
"·",
gettext("Edit"),
"·",
config.section_page_title
],
" "
)
end
end
+1 -2
View File
@@ -52,15 +52,14 @@ msgstr ""
#: lib/music_library_web/components/notes.ex
#: lib/music_library_web/components/record_components.ex
#: lib/music_library_web/live/artist_live/show.ex
#: lib/music_library_web/live/collection_live/index.ex
#: lib/music_library_web/live/collection_live/show.ex
#: lib/music_library_web/live/online_store_template_live/index.ex
#: lib/music_library_web/live/record_set_live/index.ex
#: lib/music_library_web/live/record_set_live/show.ex
#: lib/music_library_web/live/scrobble_rules_live/index.ex
#: lib/music_library_web/live/scrobbled_tracks_live/index.ex
#: lib/music_library_web/live/wishlist_live/index.ex
#: lib/music_library_web/live/wishlist_live/show.ex
#: lib/music_library_web/live_helpers/index_actions.ex
#, elixir-autogen, elixir-format
msgid "Edit"
msgstr ""
+1 -2
View File
@@ -52,15 +52,14 @@ msgstr ""
#: lib/music_library_web/components/notes.ex
#: lib/music_library_web/components/record_components.ex
#: lib/music_library_web/live/artist_live/show.ex
#: lib/music_library_web/live/collection_live/index.ex
#: lib/music_library_web/live/collection_live/show.ex
#: lib/music_library_web/live/online_store_template_live/index.ex
#: lib/music_library_web/live/record_set_live/index.ex
#: lib/music_library_web/live/record_set_live/show.ex
#: lib/music_library_web/live/scrobble_rules_live/index.ex
#: lib/music_library_web/live/scrobbled_tracks_live/index.ex
#: lib/music_library_web/live/wishlist_live/index.ex
#: lib/music_library_web/live/wishlist_live/show.ex
#: lib/music_library_web/live_helpers/index_actions.ex
#, elixir-autogen, elixir-format
msgid "Edit"
msgstr ""