diff --git a/assets/js/app.js b/assets/js/app.js index fb152a58..13177c35 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -26,6 +26,7 @@ import { Hooks as FluxonHooks, DOM as FluxonDOM } from "fluxon"; import FormatNumberHook from "./hooks/format-number"; import UniversalSearchNavigationHook from "./hooks/universal-search-navigation"; import RecordPickerNavigationHook from "./hooks/record-picker-navigation"; +import SortableListHook from "./hooks/sortable-list"; import confetti from "canvas-confetti"; import { createLiveToastHook } from "live_toast"; import banner from "./banner"; @@ -34,6 +35,7 @@ let Hooks = FluxonHooks; Hooks.FormatNumber = FormatNumberHook; Hooks.UniversalSearchNavigation = UniversalSearchNavigationHook; Hooks.RecordPickerNavigation = RecordPickerNavigationHook; +Hooks.SortableList = SortableListHook; Hooks.LiveToast = createLiveToastHook(); const csrfToken = document diff --git a/assets/js/hooks/sortable-list.js b/assets/js/hooks/sortable-list.js new file mode 100644 index 00000000..4fa4a6e9 --- /dev/null +++ b/assets/js/hooks/sortable-list.js @@ -0,0 +1,44 @@ +import Sortable from "sortablejs"; + +export default { + mounted() { + this.initSortable(); + }, + + updated() { + if (this.sortable) { + this.sortable.destroy(); + } + this.initSortable(); + }, + + destroyed() { + if (this.sortable) { + this.sortable.destroy(); + } + }, + + initSortable() { + this.sortable = Sortable.create(this.el, { + animation: 150, + handle: "[data-sortable-handle]", + draggable: "[data-sortable-item]", + ghostClass: "opacity-30", + dragClass: "shadow-lg", + onEnd: () => { + const items = this.el.querySelectorAll("[data-sortable-item]"); + const recordIds = Array.from(items).map( + (item) => item.dataset.recordId + ); + + const payload = { record_ids: recordIds }; + const setId = this.el.dataset.setId; + if (setId) { + payload.set_id = setId; + } + + this.pushEvent("reorder", payload); + }, + }); + }, +}; diff --git a/assets/package-lock.json b/assets/package-lock.json index 14a5e208..7d58c49e 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -7,7 +7,8 @@ "dependencies": { "barcode-detector": "^3.0.0", "canvas-confetti": "^1.9.3", - "live_toast": "file:../deps/live_toast" + "live_toast": "file:../deps/live_toast", + "sortablejs": "^1.15.6" }, "devDependencies": { "@tailwindcss/typography": "^0.5.16" @@ -86,6 +87,12 @@ "node": ">=4" } }, + "node_modules/sortablejs": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.6.tgz", + "integrity": "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==", + "license": "MIT" + }, "node_modules/tagged-tag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", diff --git a/assets/package.json b/assets/package.json index b1823ca2..17bdbc26 100644 --- a/assets/package.json +++ b/assets/package.json @@ -2,7 +2,8 @@ "dependencies": { "barcode-detector": "^3.0.0", "canvas-confetti": "^1.9.3", - "live_toast": "file:../deps/live_toast" + "live_toast": "file:../deps/live_toast", + "sortablejs": "^1.15.6" }, "devDependencies": { "@tailwindcss/typography": "^0.5.16" diff --git a/lib/music_library/record_sets.ex b/lib/music_library/record_sets.ex index 0aee6110..5eda129d 100644 --- a/lib/music_library/record_sets.ex +++ b/lib/music_library/record_sets.ex @@ -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 = 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 7f42e441..2d815fea 100644 --- a/lib/music_library_web/live/record_set_live/index.ex +++ b/lib/music_library_web/live/record_set_live/index.ex @@ -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 diff --git a/lib/music_library_web/live/record_set_live/index.html.heex b/lib/music_library_web/live/record_set_live/index.html.heex index 99a4fcce..f8eb9921 100644 --- a/lib/music_library_web/live/record_set_live/index.html.heex +++ b/lib/music_library_web/live/record_set_live/index.html.heex @@ -35,10 +35,11 @@
@@ -47,8 +48,8 @@