From 53e106dac2a56074d0d7c889860974fd36d7c323 Mon Sep 17 00:00:00 2001 From: Kieran Date: Fri, 13 Dec 2024 09:49:00 -0800 Subject: [PATCH] [Enhancement] Add sorting, pagination, and new attributes to sources index table (#510) * WIP - started improving handling of sorting for sources index table * WIP - Added UI to table to indicate sort column and direction * Refactored toggle liveview into a livecomponent * Added sorting for all table attrs * Added pagination to the sources table * Added tests for updated liveviews and live components * Add tests for new helper methods * Added fancy new CSS to my sources table * Added size to sources table * Adds relative div to ensure that sorting arrow doesn't run away * Fixed da tests --- assets/js/alpine_helpers.js | 6 +- assets/js/app.js | 23 ---- .../custom_components/table_components.ex | 27 ++++- .../custom_components/text_components.ex | 21 ++++ .../controllers/pages/page_html.ex | 18 --- .../pages/page_html/home.html.heex | 2 +- .../sources/source_html/index.html.heex | 10 +- .../sources/source_live/index_table_live.ex | 108 ++++++++++++++++++ .../source_live/index_table_live.html.heex | 41 +++++++ .../source_live/source_enable_toggle.ex | 35 ++++++ .../helpers/pagination_helpers.ex | 45 ++++++++ lib/pinchflat_web/helpers/sorting_helpers.ex | 20 ++++ .../sources/index_table_live_test.exs | 105 ++++++++++++++--- .../sources/source_enable_toggle_test.exs | 26 +++++ .../helpers/pagination_helpers_test.exs | 96 ++++++++++++++++ .../helpers/sorting_helpers_test.exs | 31 +++++ 16 files changed, 547 insertions(+), 67 deletions(-) create mode 100644 lib/pinchflat_web/controllers/sources/source_live/index_table_live.ex create mode 100644 lib/pinchflat_web/controllers/sources/source_live/index_table_live.html.heex create mode 100644 lib/pinchflat_web/controllers/sources/source_live/source_enable_toggle.ex create mode 100644 lib/pinchflat_web/helpers/pagination_helpers.ex create mode 100644 lib/pinchflat_web/helpers/sorting_helpers.ex create mode 100644 test/pinchflat_web/controllers/sources/source_enable_toggle_test.exs create mode 100644 test/pinchflat_web/helpers/pagination_helpers_test.exs create mode 100644 test/pinchflat_web/helpers/sorting_helpers_test.exs diff --git a/assets/js/alpine_helpers.js b/assets/js/alpine_helpers.js index 2d8bd93e..9c2367f8 100644 --- a/assets/js/alpine_helpers.js +++ b/assets/js/alpine_helpers.js @@ -40,5 +40,9 @@ window.dispatchFor = (elementOrId, eventName, detail = {}) => { const element = typeof elementOrId === 'string' ? document.getElementById(elementOrId) : elementOrId - element.dispatchEvent(new CustomEvent(eventName, { detail })) + // This is needed to ensure the DOM has updated before dispatching the event. + // Doing so ensures that the latest DOM state is what's sent to the server + setTimeout(() => { + element.dispatchEvent(new Event(eventName, { bubbles: true, detail })) + }, 0) } diff --git a/assets/js/app.js b/assets/js/app.js index f9accf3b..e6e02193 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -47,29 +47,6 @@ let liveSocket = new LiveSocket(document.body.dataset.socketPath, Socket, { } }) } - }, - 'formless-input': { - mounted() { - const subscribedEvents = this.el.dataset.subscribe.split(' ') - const eventName = this.el.dataset.eventName || '' - const identifier = this.el.dataset.identifier || '' - - subscribedEvents.forEach((domEvent) => { - this.el.addEventListener(domEvent, () => { - // This ensures that the event is pushed to the server after the input value has been updated - // so that the server has the most up-to-date value - setTimeout(() => { - this.pushEvent('formless-input', { - value: this.el.value, - id: identifier, - event: eventName, - dom_id: this.el.id, - dom_event: domEvent - }) - }, 0) - }) - }) - } } } }) diff --git a/lib/pinchflat_web/components/custom_components/table_components.ex b/lib/pinchflat_web/components/custom_components/table_components.ex index 7c661bab..106c45a1 100644 --- a/lib/pinchflat_web/components/custom_components/table_components.ex +++ b/lib/pinchflat_web/components/custom_components/table_components.ex @@ -16,6 +16,8 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do """ attr :rows, :list, required: true attr :table_class, :string, default: "" + attr :sort_key, :string, default: nil + attr :sort_direction, :string, default: nil attr :row_item, :any, default: &Function.identity/1, @@ -24,6 +26,7 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do slot :col, required: true do attr :label, :string attr :class, :string + attr :sort_key, :string end def table(assigns) do @@ -31,8 +34,20 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do - @@ -70,9 +85,9 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do
  • = @total_pages && "cursor-not-allowed" ]} phx-click={@page_number != @total_pages && "page_change"} phx-value-direction="inc" diff --git a/lib/pinchflat_web/components/custom_components/text_components.ex b/lib/pinchflat_web/components/custom_components/text_components.ex index d2bc8f6f..2145bae6 100644 --- a/lib/pinchflat_web/components/custom_components/text_components.ex +++ b/lib/pinchflat_web/components/custom_components/text_components.ex @@ -2,6 +2,7 @@ defmodule PinchflatWeb.CustomComponents.TextComponents do @moduledoc false use Phoenix.Component + alias Pinchflat.Utils.NumberUtils alias PinchflatWeb.CoreComponents @doc """ @@ -125,4 +126,24 @@ defmodule PinchflatWeb.CustomComponents.TextComponents do {@word}{if @count == 1, do: "", else: @suffix} """ end + + @doc """ + Renders a human-readable byte size + """ + + attr :byte_size, :integer, required: true + + def readable_filesize(assigns) do + {num, suffix} = NumberUtils.human_byte_size(assigns.byte_size, precision: 2) + + assigns = + Map.merge(assigns, %{ + num: num, + suffix: suffix + }) + + ~H""" + <.localized_number number={@num} /> {@suffix} + """ + end end diff --git a/lib/pinchflat_web/controllers/pages/page_html.ex b/lib/pinchflat_web/controllers/pages/page_html.ex index f4f26657..16f7731b 100644 --- a/lib/pinchflat_web/controllers/pages/page_html.ex +++ b/lib/pinchflat_web/controllers/pages/page_html.ex @@ -1,23 +1,5 @@ defmodule PinchflatWeb.Pages.PageHTML do use PinchflatWeb, :html - alias Pinchflat.Utils.NumberUtils - embed_templates "page_html/*" - - attr :media_filesize, :integer, required: true - - def readable_media_filesize(assigns) do - {num, suffix} = NumberUtils.human_byte_size(assigns.media_filesize, precision: 2) - - assigns = - Map.merge(assigns, %{ - num: num, - suffix: suffix - }) - - ~H""" - <.localized_number number={@num} /> {@suffix} - """ - end end diff --git a/lib/pinchflat_web/controllers/pages/page_html/home.html.heex b/lib/pinchflat_web/controllers/pages/page_html/home.html.heex index a70eb28a..1579c319 100644 --- a/lib/pinchflat_web/controllers/pages/page_html/home.html.heex +++ b/lib/pinchflat_web/controllers/pages/page_html/home.html.heex @@ -33,7 +33,7 @@ Library Size

    - <.readable_media_filesize media_filesize={@media_item_size} /> + <.readable_filesize byte_size={@media_item_size} />

    diff --git a/lib/pinchflat_web/controllers/sources/source_html/index.html.heex b/lib/pinchflat_web/controllers/sources/source_html/index.html.heex index b6be9231..47615525 100644 --- a/lib/pinchflat_web/controllers/sources/source_html/index.html.heex +++ b/lib/pinchflat_web/controllers/sources/source_html/index.html.heex @@ -11,8 +11,12 @@
    -
    - {live_render(@conn, PinchflatWeb.Sources.IndexTableLive)} -
    + {live_render(@conn, PinchflatWeb.Sources.SourceLive.IndexTableLive, + session: %{ + "initial_sort_key" => :custom_name, + "initial_sort_direction" => :asc, + "results_per_page" => 10 + } + )}
    diff --git a/lib/pinchflat_web/controllers/sources/source_live/index_table_live.ex b/lib/pinchflat_web/controllers/sources/source_live/index_table_live.ex new file mode 100644 index 00000000..5ada1b02 --- /dev/null +++ b/lib/pinchflat_web/controllers/sources/source_live/index_table_live.ex @@ -0,0 +1,108 @@ +defmodule PinchflatWeb.Sources.SourceLive.IndexTableLive do + use PinchflatWeb, :live_view + use Pinchflat.Media.MediaQuery + use Pinchflat.Sources.SourcesQuery + + import PinchflatWeb.Helpers.SortingHelpers + import PinchflatWeb.Helpers.PaginationHelpers + + alias Pinchflat.Repo + alias Pinchflat.Sources.Source + alias Pinchflat.Media.MediaItem + + def mount(_params, session, socket) do + limit = session["results_per_page"] + + initial_params = + Map.merge( + %{ + sort_key: session["initial_sort_key"], + sort_direction: session["initial_sort_direction"] + }, + get_pagination_attributes(sources_query(), 1, limit) + ) + + socket + |> assign(initial_params) + |> set_sources() + |> then(&{:ok, &1}) + end + + def handle_event("page_change", %{"direction" => direction}, %{assigns: assigns} = socket) do + new_page = update_page_number(assigns.page, direction, assigns.total_pages) + + socket + |> assign(get_pagination_attributes(sources_query(), new_page, assigns.limit)) + |> set_sources() + |> then(&{:noreply, &1}) + end + + def handle_event("sort_update", %{"sort_key" => sort_key}, %{assigns: assigns} = socket) do + new_sort_key = String.to_existing_atom(sort_key) + + new_params = %{ + sort_key: new_sort_key, + sort_direction: get_sort_direction(assigns.sort_key, new_sort_key, assigns.sort_direction) + } + + socket + |> assign(new_params) + |> set_sources() + |> then(&{:noreply, &1}) + end + + defp sort_attr(:pending_count), do: dynamic([s, mp, dl, pe], pe.pending_count) + defp sort_attr(:downloaded_count), do: dynamic([s, mp, dl], dl.downloaded_count) + defp sort_attr(:media_size_bytes), do: dynamic([s, mp, dl], dl.media_size_bytes) + defp sort_attr(:media_profile_name), do: dynamic([s, mp], mp.name) + defp sort_attr(:custom_name), do: dynamic([s], s.custom_name) + defp sort_attr(:enabled), do: dynamic([s], s.enabled) + + defp set_sources(%{assigns: assigns} = socket) do + sources = + sources_query() + |> order_by(^[{assigns.sort_direction, sort_attr(assigns.sort_key)}, asc: :id]) + |> limit(^assigns.limit) + |> offset(^assigns.offset) + |> Repo.all() + + assign(socket, %{sources: sources}) + end + + defp sources_query do + downloaded_subquery = + from( + m in MediaItem, + select: %{downloaded_count: count(m.id), source_id: m.source_id, media_size_bytes: sum(m.media_size_bytes)}, + where: ^MediaQuery.downloaded(), + group_by: m.source_id + ) + + pending_subquery = + from( + m in MediaItem, + inner_join: s in assoc(m, :source), + inner_join: mp in assoc(s, :media_profile), + select: %{pending_count: count(m.id), source_id: m.source_id}, + where: ^MediaQuery.pending(), + group_by: m.source_id + ) + + from s in Source, + as: :source, + inner_join: mp in assoc(s, :media_profile), + left_join: d in subquery(downloaded_subquery), + on: d.source_id == s.id, + left_join: p in subquery(pending_subquery), + on: p.source_id == s.id, + on: d.source_id == s.id, + where: is_nil(s.marked_for_deletion_at) and is_nil(mp.marked_for_deletion_at), + preload: [media_profile: mp], + select: map(s, ^Source.__schema__(:fields)), + select_merge: %{ + downloaded_count: coalesce(d.downloaded_count, 0), + pending_count: coalesce(p.pending_count, 0), + media_size_bytes: coalesce(d.media_size_bytes, 0) + } + end +end diff --git a/lib/pinchflat_web/controllers/sources/source_live/index_table_live.html.heex b/lib/pinchflat_web/controllers/sources/source_live/index_table_live.html.heex new file mode 100644 index 00000000..cf2ce18b --- /dev/null +++ b/lib/pinchflat_web/controllers/sources/source_live/index_table_live.html.heex @@ -0,0 +1,41 @@ +
    + <.table rows={@sources} table_class="text-white" sort_key={@sort_key} sort_direction={@sort_direction}> + <:col :let={source} label="Name" sort_key="custom_name" class="truncate max-w-xs"> + <.subtle_link href={~p"/sources/#{source.id}"}> + {source.custom_name} + + + <:col :let={source} label="Pending" sort_key="pending_count"> + <.subtle_link href={~p"/sources/#{source.id}/#tab-pending"}> + <.localized_number number={source.pending_count} /> + + + <:col :let={source} label="Downloaded" sort_key="downloaded_count"> + <.subtle_link href={~p"/sources/#{source.id}/#tab-downloaded"}> + <.localized_number number={source.downloaded_count} /> + + + <:col :let={source} label="Size" sort_key="media_size_bytes"> + <.readable_filesize byte_size={source.media_size_bytes} /> + + <:col :let={source} label="Media Profile" sort_key="media_profile_name"> + <.subtle_link href={~p"/media_profiles/#{source.media_profile_id}"}> + {source.media_profile.name} + + + <:col :let={source} label="Enabled?" sort_key="enabled"> + <.live_component + module={PinchflatWeb.Sources.SourceLive.SourceEnableToggle} + source={source} + id={"source_#{source.id}_enabled"} + /> + + <:col :let={source} label="" class="flex place-content-evenly"> + <.icon_link href={~p"/sources/#{source.id}/edit"} icon="hero-pencil-square" class="mx-1" /> + + + +
    + <.live_pagination_controls page_number={@page} total_pages={@total_pages} /> +
    +
    diff --git a/lib/pinchflat_web/controllers/sources/source_live/source_enable_toggle.ex b/lib/pinchflat_web/controllers/sources/source_live/source_enable_toggle.ex new file mode 100644 index 00000000..57bc3b1d --- /dev/null +++ b/lib/pinchflat_web/controllers/sources/source_live/source_enable_toggle.ex @@ -0,0 +1,35 @@ +defmodule PinchflatWeb.Sources.SourceLive.SourceEnableToggle do + use PinchflatWeb, :live_component + + alias Pinchflat.Sources + alias Pinchflat.Sources.Source + + def render(assigns) do + ~H""" +
    + <.form :let={f} for={@form} phx-change="update" phx-target={@myself} class="enabled_toggle_form"> + <.input id={"source_#{@source_id}_enabled_input"} field={f[:enabled]} type="toggle" /> + +
    + """ + end + + def update(assigns, socket) do + initial_data = %{ + source_id: assigns.source.id, + form: Sources.change_source(%Source{}, assigns.source) + } + + socket + |> assign(initial_data) + |> then(&{:ok, &1}) + end + + def handle_event("update", %{"source" => source_params}, %{assigns: assigns} = socket) do + assigns.source_id + |> Sources.get_source!() + |> Sources.update_source(source_params) + + {:noreply, socket} + end +end diff --git a/lib/pinchflat_web/helpers/pagination_helpers.ex b/lib/pinchflat_web/helpers/pagination_helpers.ex new file mode 100644 index 00000000..50c39286 --- /dev/null +++ b/lib/pinchflat_web/helpers/pagination_helpers.ex @@ -0,0 +1,45 @@ +defmodule PinchflatWeb.Helpers.PaginationHelpers do + @moduledoc """ + Methods for working with pagination, usually in the context of LiveViews or LiveComponents. + + These methods are fairly simple, but they're commonly repeated across different Live entities + """ + + alias Pinchflat.Repo + alias Pinchflat.Utils.NumberUtils + + @doc """ + Given a query, a page number, and a number of records per page, returns a map of pagination attributes. + + Returns map() + """ + def get_pagination_attributes(query, page, records_per_page) do + total_record_count = Repo.aggregate(query, :count, :id) + total_pages = max(ceil(total_record_count / records_per_page), 1) + clamped_page = NumberUtils.clamp(page, 1, total_pages) + + %{ + page: clamped_page, + total_pages: total_pages, + total_record_count: total_record_count, + limit: records_per_page, + offset: (clamped_page - 1) * records_per_page + } + end + + @doc """ + Given a current page number, a direction to move in, and the total number of pages, returns the updated page number. + The updated page number is clamped to the range [1, total_pages]. + + Returns integer() + """ + def update_page_number(current_page, direction, total_pages) do + updated_page = + case to_string(direction) do + "inc" -> current_page + 1 + "dec" -> current_page - 1 + end + + NumberUtils.clamp(updated_page, 1, total_pages) + end +end diff --git a/lib/pinchflat_web/helpers/sorting_helpers.ex b/lib/pinchflat_web/helpers/sorting_helpers.ex new file mode 100644 index 00000000..fb65e629 --- /dev/null +++ b/lib/pinchflat_web/helpers/sorting_helpers.ex @@ -0,0 +1,20 @@ +defmodule PinchflatWeb.Helpers.SortingHelpers do + @moduledoc """ + Methods for working with sorting, usually in the context of LiveViews or LiveComponents. + + These methods are fairly simple, but they're commonly repeated across different Live entities + """ + + @doc """ + Given the old sort attribute, the new sort attribute, and the old sort direction, returns the new sort direction. + + Returns :asc | :desc + """ + def get_sort_direction(old_sort_attr, new_sort_attr, old_sort_direction) do + case {new_sort_attr, old_sort_direction} do + {^old_sort_attr, :desc} -> :asc + {^old_sort_attr, _} -> :desc + _ -> :asc + end + end +end diff --git a/test/pinchflat_web/controllers/sources/index_table_live_test.exs b/test/pinchflat_web/controllers/sources/index_table_live_test.exs index 659bd041..675e47d0 100644 --- a/test/pinchflat_web/controllers/sources/index_table_live_test.exs +++ b/test/pinchflat_web/controllers/sources/index_table_live_test.exs @@ -1,4 +1,4 @@ -defmodule PinchflatWeb.Sources.IndexTableLiveTest do +defmodule PinchflatWeb.Sources.SourceLive.IndexTableLiveTest do use PinchflatWeb.ConnCase import Phoenix.LiveViewTest @@ -6,13 +6,13 @@ defmodule PinchflatWeb.Sources.IndexTableLiveTest do import Pinchflat.ProfilesFixtures alias Pinchflat.Sources.Source - alias PinchflatWeb.Sources.IndexTableLive + alias PinchflatWeb.Sources.SourceLive.IndexTableLive describe "initial rendering" do test "lists all sources", %{conn: conn} do source = source_fixture() - {:ok, _view, html} = live_isolated(conn, IndexTableLive) + {:ok, _view, html} = live_isolated(conn, IndexTableLive, session: create_session()) assert html =~ source.custom_name end @@ -20,7 +20,7 @@ defmodule PinchflatWeb.Sources.IndexTableLiveTest do test "omits sources that have marked_for_deletion_at set", %{conn: conn} do source = source_fixture(marked_for_deletion_at: DateTime.utc_now()) - {:ok, _view, html} = live_isolated(conn, IndexTableLive) + {:ok, _view, html} = live_isolated(conn, IndexTableLive, session: create_session()) refute html =~ source.custom_name end @@ -29,27 +29,102 @@ defmodule PinchflatWeb.Sources.IndexTableLiveTest do media_profile = media_profile_fixture(marked_for_deletion_at: DateTime.utc_now()) source = source_fixture(media_profile_id: media_profile.id) - {:ok, _view, html} = live_isolated(conn, IndexTableLive) + {:ok, _view, html} = live_isolated(conn, IndexTableLive, session: create_session()) refute html =~ source.custom_name end end - describe "when a source is enabled or disabled" do + describe "when testing sorting" do + test "sorts by the custom_name by default", %{conn: conn} do + source1 = source_fixture(custom_name: "Source_B") + source2 = source_fixture(custom_name: "Source_A") + + {:ok, view, _html} = live_isolated(conn, IndexTableLive, session: create_session()) + assert render_element(view, "tbody tr:first-child") =~ source2.custom_name + assert render_element(view, "tbody tr:last-child") =~ source1.custom_name + end + + test "clicking the row will change the sort direction", %{conn: conn} do + source1 = source_fixture(custom_name: "Source_B") + source2 = source_fixture(custom_name: "Source_A") + + {:ok, view, _html} = live_isolated(conn, IndexTableLive, session: create_session()) + + # Click the row to change the sort direction + click_element(view, "th", "Name") + + assert render_element(view, "tbody tr:first-child") =~ source1.custom_name + assert render_element(view, "tbody tr:last-child") =~ source2.custom_name + end + + test "clicking a different row will sort by that attribute", %{conn: conn} do + source1 = source_fixture(custom_name: "Source_A", enabled: true) + source2 = source_fixture(custom_name: "Source_A", enabled: false) + + {:ok, view, _html} = live_isolated(conn, IndexTableLive, session: create_session()) + + # Click the row to change the sort field + click_element(view, "th", "Enabled?") + + assert render_element(view, "tbody tr:first-child") =~ source2.custom_name + assert render_element(view, "tbody tr:last-child") =~ source1.custom_name + + # Click the row to again change the sort direcation + click_element(view, "th", "Enabled?") + assert render_element(view, "tbody tr:first-child") =~ source1.custom_name + assert render_element(view, "tbody tr:last-child") =~ source2.custom_name + end + end + + describe "when testing pagination" do + test "moving to the next page loads new records", %{conn: conn} do + source1 = source_fixture(custom_name: "Source_A") + source2 = source_fixture(custom_name: "Source_B") + + session = Map.merge(create_session(), %{"results_per_page" => 1}) + {:ok, view, _html} = live_isolated(conn, IndexTableLive, session: session) + + assert render_element(view, "tbody") =~ source1.custom_name + refute render_element(view, "tbody") =~ source2.custom_name + + click_element(view, "span.pagination-next") + + refute render_element(view, "tbody") =~ source1.custom_name + assert render_element(view, "tbody") =~ source2.custom_name + end + end + + describe "when testing the enable toggle" do test "updates the source's enabled status", %{conn: conn} do source = source_fixture(enabled: true) - {:ok, view, _html} = live_isolated(conn, IndexTableLive) + {:ok, view, _html} = live_isolated(conn, IndexTableLive, session: create_session()) - params = %{ - "event" => "toggle_enabled", - "id" => source.id, - "value" => "false" - } - - # Send an event to the server directly - render_change(view, "formless-input", params) + view + |> element(".enabled_toggle_form") + |> render_change(%{source: %{"enabled" => false}}) assert %{enabled: false} = Repo.get!(Source, source.id) end end + + defp click_element(view, selector, text_filter \\ nil) do + view + |> element(selector, text_filter) + |> render_click() + end + + defp render_element(view, selector) do + view + |> element(selector) + |> render() + end + + defp create_session do + %{ + "initial_sort_key" => :custom_name, + "initial_sort_direction" => :asc, + "results_per_page" => 10 + } + end end diff --git a/test/pinchflat_web/controllers/sources/source_enable_toggle_test.exs b/test/pinchflat_web/controllers/sources/source_enable_toggle_test.exs new file mode 100644 index 00000000..e1abd1dc --- /dev/null +++ b/test/pinchflat_web/controllers/sources/source_enable_toggle_test.exs @@ -0,0 +1,26 @@ +defmodule PinchflatWeb.Sources.SourceLive.SourceEnableToggleTest do + use PinchflatWeb.ConnCase + + import Phoenix.LiveViewTest + + alias PinchflatWeb.Sources.SourceLive.SourceEnableToggle + + describe "initial rendering" do + test "renders a toggle in the on position if the source is enabled" do + source = %{id: 1, enabled: true} + + html = render_component(SourceEnableToggle, %{id: :foo, source: source}) + + # This is checking the Alpine attrs which is a good-enough proxy for the toggle position + assert html =~ "{ enabled: true }" + end + + test "renders a toggle in the off position if the source is disabled" do + source = %{id: 1, enabled: false} + + html = render_component(SourceEnableToggle, %{id: :foo, source: source}) + + assert html =~ "{ enabled: false }" + end + end +end diff --git a/test/pinchflat_web/helpers/pagination_helpers_test.exs b/test/pinchflat_web/helpers/pagination_helpers_test.exs new file mode 100644 index 00000000..c15a2d35 --- /dev/null +++ b/test/pinchflat_web/helpers/pagination_helpers_test.exs @@ -0,0 +1,96 @@ +defmodule PinchflatWeb.Helpers.PaginationHelpersTest do + use Pinchflat.DataCase + import Pinchflat.SourcesFixtures + + alias Pinchflat.Sources.Source + alias PinchflatWeb.Helpers.PaginationHelpers + + describe "get_pagination_attributes/3" do + test "returns the correct pagination attributes" do + source_fixture() + query = from(s in Source, select: s.id) + page = 1 + records_per_page = 10 + + pagination_attributes = PaginationHelpers.get_pagination_attributes(query, page, records_per_page) + + assert pagination_attributes.page == 1 + assert pagination_attributes.total_pages == 1 + assert pagination_attributes.total_record_count == 1 + assert pagination_attributes.limit == 10 + assert pagination_attributes.offset == 0 + end + + test "returns the correct pagination attributes when there are multiple pages" do + source_fixture() + source_fixture() + + query = from(s in Source, select: s.id) + page = 1 + records_per_page = 1 + + pagination_attributes = PaginationHelpers.get_pagination_attributes(query, page, records_per_page) + + assert pagination_attributes.page == 1 + assert pagination_attributes.total_pages == 2 + assert pagination_attributes.total_record_count == 2 + assert pagination_attributes.limit == 1 + assert pagination_attributes.offset == 0 + end + + test "returns the correct attributes when on a page other than the first" do + source_fixture() + source_fixture() + + query = from(s in Source, select: s.id) + page = 2 + records_per_page = 1 + + pagination_attributes = PaginationHelpers.get_pagination_attributes(query, page, records_per_page) + + assert pagination_attributes.page == 2 + assert pagination_attributes.total_pages == 2 + assert pagination_attributes.total_record_count == 2 + assert pagination_attributes.limit == 1 + assert pagination_attributes.offset == 1 + end + end + + describe "update_page_number/3" do + test "increments the page number" do + current_page = 1 + total_pages = 2 + + updated_page = PaginationHelpers.update_page_number(current_page, :inc, total_pages) + + assert updated_page == 2 + end + + test "decrements the page number" do + current_page = 2 + total_pages = 2 + + updated_page = PaginationHelpers.update_page_number(current_page, :dec, total_pages) + + assert updated_page == 1 + end + + test "doesn't overflow the page number" do + current_page = 2 + total_pages = 2 + + updated_page = PaginationHelpers.update_page_number(current_page, :inc, total_pages) + + assert updated_page == 2 + end + + test "doesn't underflow the page number" do + current_page = 1 + total_pages = 2 + + updated_page = PaginationHelpers.update_page_number(current_page, :dec, total_pages) + + assert updated_page == 1 + end + end +end diff --git a/test/pinchflat_web/helpers/sorting_helpers_test.exs b/test/pinchflat_web/helpers/sorting_helpers_test.exs new file mode 100644 index 00000000..7f1d81b1 --- /dev/null +++ b/test/pinchflat_web/helpers/sorting_helpers_test.exs @@ -0,0 +1,31 @@ +defmodule PinchflatWeb.Helpers.SortingHelpersTest do + use Pinchflat.DataCase + + alias PinchflatWeb.Helpers.SortingHelpers + + describe "get_sort_direction/3" do + test "returns the correct sort direction when the new sort attribute is the same as the old sort attribute" do + old_sort_attr = "name" + new_sort_attr = "name" + old_sort_direction = :desc + + assert SortingHelpers.get_sort_direction(old_sort_attr, new_sort_attr, old_sort_direction) == :asc + end + + test "returns the correct sort direction when the new sort attribute is the same as the old sort attribute in the other direction" do + old_sort_attr = "name" + new_sort_attr = "name" + old_sort_direction = :asc + + assert SortingHelpers.get_sort_direction(old_sort_attr, new_sort_attr, old_sort_direction) == :desc + end + + test "returns the correct sort direction when the new sort attribute is different from the old sort attribute" do + old_sort_attr = "name" + new_sort_attr = "date" + old_sort_direction = :asc + + assert SortingHelpers.get_sort_direction(old_sort_attr, new_sort_attr, old_sort_direction) == :asc + end + end +end
  • - {col[:label]} + +
    + {col[:label]} + <.icon + :if={to_string(@sort_key) == col[:sort_key]} + name={if @sort_direction == :asc, do: "hero-chevron-up", else: "hero-chevron-down"} + class="w-3 h-3 mt-2 ml-1 absolute" + /> +