Reorder record sets via drag and drop

This commit is contained in:
Claudio Ortolina
2026-02-10 11:16:41 +00:00
parent b4c5c902bc
commit fe748782c7
12 changed files with 192 additions and 25 deletions
+16
View File
@@ -116,6 +116,22 @@ defmodule MusicLibrary.RecordSets do
{:ok, get_record_set!(record_set.id)}
end
def reorder_records_in_set(%RecordSet{} = record_set, ordered_record_ids)
when is_list(ordered_record_ids) do
Repo.transaction(fn ->
ordered_record_ids
|> Enum.with_index()
|> Enum.each(fn {record_id, position} ->
from(i in RecordSetItem,
where: i.record_set_id == ^record_set.id and i.record_id == ^record_id
)
|> Repo.update_all(set: [position: position])
end)
end)
{:ok, get_record_set!(record_set.id)}
end
def move_record_in_set(%RecordSet{} = record_set, record_id, direction)
when direction in [:up, :down] do
items =
@@ -62,7 +62,7 @@ defmodule MusicLibraryWeb.RecordSetLive.Index do
end
defp apply_fallback_index(socket, params) do
if get_in(socket.assigns, [:streams, :record_sets]) == nil do
if socket.assigns[:record_sets] == nil do
socket
|> apply_action(:index, params)
else
@@ -84,7 +84,7 @@ defmodule MusicLibraryWeb.RecordSetLive.Index do
|> assign(:list_params, list_params)
|> assign(:page_title, gettext("Record Sets"))
|> assign(:record_set, nil)
|> stream(:record_sets, sets, reset: true)
|> assign(:record_sets, sets)
end
def back_path(list_params) do
@@ -98,29 +98,24 @@ defmodule MusicLibraryWeb.RecordSetLive.Index do
@impl true
def handle_info(
{MusicLibraryWeb.RecordSetLive.Form, {:created, record_set}},
{MusicLibraryWeb.RecordSetLive.Form, {:created, _record_set}},
socket
) do
{:noreply,
socket
|> stream_insert(:record_sets, record_set, at: 0)
|> load_and_assign_sets(socket.assigns.list_params)}
{:noreply, load_and_assign_sets(socket, socket.assigns.list_params)}
end
def handle_info(
{MusicLibraryWeb.RecordSetLive.Form, {:updated, record_set}},
socket
) do
{:noreply, stream_insert(socket, :record_sets, record_set)}
{:noreply, update_record_set_in_list(socket, record_set)}
end
def handle_info(
{MusicLibraryWeb.RecordSetLive.RecordPicker, {:added, record_set}},
socket
) do
{:noreply,
socket
|> stream_insert(:record_sets, record_set)}
{:noreply, update_record_set_in_list(socket, record_set)}
end
@impl true
@@ -138,31 +133,44 @@ defmodule MusicLibraryWeb.RecordSetLive.Index do
record_set = RecordSets.get_record_set!(id)
{:ok, _} = RecordSets.delete_record_set(record_set)
{:noreply,
socket
|> stream_delete(:record_sets, record_set)
|> load_and_assign_sets(socket.assigns.list_params)}
{:noreply, load_and_assign_sets(socket, socket.assigns.list_params)}
end
def handle_event("remove_record", %{"set-id" => set_id, "record-id" => record_id}, socket) do
record_set = RecordSets.get_record_set!(set_id)
{:ok, updated_set} = RecordSets.remove_record_from_set(record_set, record_id)
{:noreply, stream_insert(socket, :record_sets, updated_set)}
{:noreply, update_record_set_in_list(socket, updated_set)}
end
def handle_event("move_up", %{"set-id" => set_id, "record-id" => record_id}, socket) do
record_set = RecordSets.get_record_set!(set_id)
{:ok, updated_set} = RecordSets.move_record_in_set(record_set, record_id, :up)
{:noreply, stream_insert(socket, :record_sets, updated_set)}
{:noreply, update_record_set_in_list(socket, updated_set)}
end
def handle_event("move_down", %{"set-id" => set_id, "record-id" => record_id}, socket) do
record_set = RecordSets.get_record_set!(set_id)
{:ok, updated_set} = RecordSets.move_record_in_set(record_set, record_id, :down)
{:noreply, stream_insert(socket, :record_sets, updated_set)}
{:noreply, update_record_set_in_list(socket, updated_set)}
end
def handle_event("reorder", %{"set_id" => set_id, "record_ids" => record_ids}, socket) do
record_set = RecordSets.get_record_set!(set_id)
{:ok, updated_set} = RecordSets.reorder_records_in_set(record_set, record_ids)
{:noreply, update_record_set_in_list(socket, updated_set)}
end
defp update_record_set_in_list(socket, updated_set) do
record_sets =
Enum.map(socket.assigns.record_sets, fn set ->
if set.id == updated_set.id, do: updated_set, else: set
end)
assign(socket, :record_sets, record_sets)
end
defp parse_order("alphabetical"), do: :alphabetical
@@ -35,10 +35,11 @@
</div>
<div class="mt-6 space-y-6">
<ul phx-update="stream" id="record-sets-list" class="space-y-6">
<ul id="record-sets-list" class="space-y-6">
<li
:if={@record_sets == []}
id="no-record-sets"
class="hidden only:block p-8 text-center bg-zinc-50 dark:bg-zinc-800 rounded-lg"
class="p-8 text-center bg-zinc-50 dark:bg-zinc-800 rounded-lg"
>
<.icon name="hero-rectangle-stack" class="h-12 w-12 text-zinc-400 mx-auto mb-4" />
<p class="text-zinc-600 dark:text-zinc-400">
@@ -47,8 +48,8 @@
</li>
<li
:for={{dom_id, record_set} <- @streams.record_sets}
id={dom_id}
:for={record_set <- @record_sets}
id={"record-set-#{record_set.id}"}
class="bg-white dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-700 p-4"
>
<div class="flex items-baseline justify-between mb-3">
@@ -106,9 +107,16 @@
</div>
</div>
<div class="flex flex-wrap items-start gap-3 md:overflow-x-auto pb-2">
<div
class="flex flex-wrap items-start gap-3 md:overflow-x-auto pb-2"
id={"record-set-#{record_set.id}-items"}
phx-hook="SortableList"
data-set-id={record_set.id}
>
<div
:for={item <- record_set.items}
data-sortable-item
data-record-id={item.record.id}
class={[
"flex-none w-24 sm:w-32 group relative",
is_nil(item.record.purchased_at) &&
@@ -135,6 +143,16 @@
width={256}
/>
</.link>
<div
data-sortable-handle
class="absolute top-1 left-1 hidden group-hover:flex items-center justify-center rounded-full bg-zinc-100/50 hover:bg-zinc-100/75 dark:bg-zinc-700/50 dark:hover:bg-zinc-700/75 size-5 cursor-grab active:cursor-grabbing"
>
<.icon
name="hero-bars-2"
class="size-3.5 text-zinc-800 dark:text-zinc-200"
aria-hidden="true"
/>
</div>
<div class="absolute top-1 right-1 rounded-full bg-zinc-100/50 hover:bg-zinc-100/75 dark:bg-zinc-700/50 dark:hover:bg-zinc-700/75 size-5">
<.dropdown
id={"item-actions-#{record_set.id}-#{item.record.id}"}
@@ -76,6 +76,13 @@ defmodule MusicLibraryWeb.RecordSetLive.Show do
{:noreply, assign(socket, :record_set, updated_set)}
end
def handle_event("reorder", %{"record_ids" => record_ids}, socket) do
{:ok, updated_set} =
RecordSets.reorder_records_in_set(socket.assigns.record_set, record_ids)
{:noreply, assign(socket, :record_set, updated_set)}
end
defp page_title(:show, record_set), do: record_set.name
defp page_title(:edit, record_set), do: gettext("Edit") <> " · " <> record_set.name
defp page_title(:add_record, record_set), do: gettext("Add Record") <> " · " <> record_set.name
@@ -73,10 +73,13 @@
:if={@record_set.items != []}
class="grid grid-cols-2 sm:grid-cols-4 gap-4"
id="record-set-records"
phx-hook="SortableList"
>
<div
:for={item <- @record_set.items}
id={"record-#{item.record.id}"}
data-sortable-item
data-record-id={item.record.id}
class={[
"group relative",
is_nil(item.record.purchased_at) &&
@@ -103,6 +106,16 @@
width={512}
/>
</.link>
<div
data-sortable-handle
class="absolute top-1 left-1 hidden group-hover:flex items-center justify-center rounded-full bg-zinc-100/50 hover:bg-zinc-100/75 dark:bg-zinc-700/50 dark:hover:bg-zinc-700/75 size-5 cursor-grab active:cursor-grabbing"
>
<.icon
name="hero-bars-2"
class="size-3.5 text-zinc-800 dark:text-zinc-200"
aria-hidden="true"
/>
</div>
<div class="absolute top-1 right-1 rounded-full bg-zinc-100/50 hover:bg-zinc-100/75 dark:bg-zinc-700/50 dark:hover:bg-zinc-700/75 size-5">
<.dropdown
id={"item-actions-#{item.record.id}"}