From e27e5b344c76d7d02c528582ec085cdd6c8f1133 Mon Sep 17 00:00:00 2001 From: Christian Tovar Date: Fri, 26 Jan 2024 15:00:42 -0500 Subject: [PATCH] Pages: Sorting and Pagination (#96) * Convert assigns to streams * Add default opt value for offset * Count total pages per page amount * Add basic pagination component * Add basic styling * Add lateral pagination support * Add active page styling * Add query param support for page * Add page query param when searching * Add sorting support * Inline tailwind classes by state * Turn pagination into function component * Include input select on form * Add query into url state * Underscored unused variable * Add conditional rendering * Update beacon --- .../live_admin/components/admin_components.ex | 19 +++ lib/beacon/live_admin/content.ex | 9 +- .../live_admin/live/page_editor_live/index.ex | 126 +++++++++++++++--- mix.lock | 2 +- 4 files changed, 132 insertions(+), 24 deletions(-) diff --git a/lib/beacon/live_admin/components/admin_components.ex b/lib/beacon/live_admin/components/admin_components.ex index 4271b750..98adde90 100644 --- a/lib/beacon/live_admin/components/admin_components.ex +++ b/lib/beacon/live_admin/components/admin_components.ex @@ -621,4 +621,23 @@ defmodule Beacon.LiveAdmin.AdminComponents do """ end + + @doc """ + Renders navigation by defined pagination. + + """ + attr :current_page, :integer, required: true + attr :pages, :integer, required: true + + def pagination(assigns) do + ~H""" +
+ + + +
+ """ + end end diff --git a/lib/beacon/live_admin/content.ex b/lib/beacon/live_admin/content.ex index 82fa2db8..72b27041 100644 --- a/lib/beacon/live_admin/content.ex +++ b/lib/beacon/live_admin/content.ex @@ -94,14 +94,13 @@ defmodule Beacon.LiveAdmin.Content do end def list_pages(site, opts \\ []) do - opts = - opts - |> Keyword.put_new(:query, nil) - |> Keyword.put_new(:per_page, 20) - call(site, Beacon.Content, :list_pages, [site, opts]) end + def count_pages(site) do + call(site, Beacon.Content, :count_pages, [site]) + end + def change_page_variant(site, variant, attrs \\ %{}) do call(site, Beacon.Content, :change_page_variant, [variant, attrs]) end diff --git a/lib/beacon/live_admin/live/page_editor_live/index.ex b/lib/beacon/live_admin/live/page_editor_live/index.ex index 16d9bea7..5ecd60af 100644 --- a/lib/beacon/live_admin/live/page_editor_live/index.ex +++ b/lib/beacon/live_admin/live/page_editor_live/index.ex @@ -6,35 +6,95 @@ defmodule Beacon.LiveAdmin.PageEditorLive.Index do on_mount {Beacon.LiveAdmin.Hooks.Authorized, {:page_editor, :index}} + @per_page 20 + @default_sort :title + @impl true def menu_link(_, :index), do: {:root, "Pages"} @impl true def mount(_params, _session, socket) do - {:ok, assign(socket, :pages, [])} + {:ok, + socket + |> assign( + page: 1, + pages: number_of_pages(socket.assigns.beacon_page.site), + sort: @default_sort, + query: "" + ) + |> stream_configure(:pages, dom_id: &"#{Ecto.UUID.generate()}-#{&1.id}")} end @impl true - def handle_params(%{"query" => query}, _uri, socket) do - pages = list_pages(socket.assigns.beacon_page.site, query: query) - {:noreply, assign(socket, :pages, pages)} - end + def handle_params(params, _uri, socket) do + query = params["query"] + offset = set_offset(params["page"]) + sort = set_sort(params["sort"], socket) + socket = set_page(offset, params["page"], socket) + + pages = + list_pages(socket.assigns.beacon_page.site, + per_page: @per_page, + offset: offset, + query: query, + sort: sort + ) - def handle_params(_params, _uri, socket) do - pages = list_pages(socket.assigns.beacon_page.site) - {:noreply, assign(socket, :pages, pages)} + {:noreply, + socket + |> assign(sort: sort, query: query) + |> stream(:pages, pages, reset: true)} end @impl true - def handle_event("search", %{"search" => %{"query" => query}}, socket) do + def handle_event("search", %{"search" => %{"query" => query, "sort" => sort}}, socket) do + path = + beacon_live_admin_path( + socket, + socket.assigns.beacon_page.site, + "/pages?page=#{socket.assigns.page}&sort=#{sort}#{query_param(query)}" + ) + + {:noreply, + socket + |> assign(sort: set_sort(sort, socket)) + |> push_patch(to: path)} + end + + def handle_event("set-page", %{"page" => page}, socket) do + page + |> String.to_integer() + |> set_page(socket) + end + + def handle_event("prev-page", _, %{assigns: %{page: 1}} = socket) do + {:noreply, socket} + end + + def handle_event("prev-page", _, socket) do + set_page(socket.assigns.page - 1, socket) + end + + def handle_event("next-page", _, %{assigns: %{page: page, pages: page}} = socket) do + {:noreply, socket} + end + + def handle_event("next-page", _, socket) do + set_page(socket.assigns.page + 1, socket) + end + + defp set_page(page, socket) do path = beacon_live_admin_path( socket, socket.assigns.beacon_page.site, - "/pages?query=#{query}" + "/pages?page=#{page}" ) - {:noreply, push_patch(socket, to: path)} + {:noreply, + socket + |> assign(page: page) + |> push_patch(to: path)} end @impl true @@ -50,15 +110,22 @@ defmodule Beacon.LiveAdmin.PageEditorLive.Index do <.simple_form :let={f} for={%{}} as={:search} phx-change="search"> - <.input field={f[:query]} type="search" autofocus={true} placeholder="Search by path or title (showing up to 20 results)" /> +
+
+ <.input field={f[:query]} value={@query} type="search" autofocus={true} placeholder="Search by path or title (showing up to 20 results)" /> +
+
0} class="basis-1/12"> + <.input type="select" field={f[:sort]} value={@sort} options={[{"Title", "title"}, {"Path", "path"}]} /> +
+
<.main_content class="h-[calc(100vh_-_210px)]"> - <.table id="pages" rows={@pages} row_click={fn page -> JS.navigate(beacon_live_admin_path(@socket, @beacon_page.site, "/pages/#{page.id}")) end}> - <:col :let={page} label="Title"><%= page.title %> - <:col :let={page} label="Path"><%= page.path %> - <:col :let={page} label="Status"><%= display_status(page.status) %> - <:action :let={page}> + <.table id="pages" rows={@streams.pages} row_click={fn {_dom_id, page} -> JS.navigate(beacon_live_admin_path(@socket, @beacon_page.site, "/pages/#{page.id}")) end}> + <:col :let={{_, page}} label="Title"><%= page.title %> + <:col :let={{_, page}} label="Path"><%= page.path %> + <:col :let={{_, page}} label="Status"><%= display_status(page.status) %> + <:action :let={{_, page}}>
<.link navigate={beacon_live_admin_path(@socket, @beacon_page.site, "/pages/#{page.id}")}>Show
@@ -67,11 +134,13 @@ defmodule Beacon.LiveAdmin.PageEditorLive.Index do + + <.pagination :if={@pages > 0} current_page={@page} pages={@pages} /> """ end - defp list_pages(site, opts \\ []) do + defp list_pages(site, opts) do site |> Content.list_pages(opts) |> Enum.map(fn page -> @@ -79,7 +148,28 @@ defmodule Beacon.LiveAdmin.PageEditorLive.Index do end) end + defp number_of_pages(site) do + site + |> Content.count_pages() + |> Kernel./(@per_page) + |> ceil() + end + + defp set_offset(nil), do: 0 + defp set_offset(page) when is_binary(page), do: String.to_integer(page) * @per_page - @per_page + defp set_offset(page), do: page * @per_page - @per_page + + defp set_page(0, _page, socket), do: socket + defp set_page(_offset, page, socket), do: assign(socket, page: String.to_integer(page)) + + defp set_sort(nil, socket), do: socket.assigns.sort + defp set_sort("", socket), do: socket.assigns.sort + defp set_sort(sort, _socket), do: String.to_atom(sort) + defp display_status(:unpublished), do: "Unpublished" defp display_status(:published), do: "Published" defp display_status(:created), do: "Draft" + + defp query_param(""), do: "" + defp query_param(query), do: "&query=#{query}" end diff --git a/mix.lock b/mix.lock index ff67215c..18a44c6a 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,6 @@ %{ "accent": {:hex, :accent, "1.1.1", "20257356446d45078b19b91608f74669b407b39af891ee3db9ee6824d1cae19d", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.3", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "6d5afa50d4886e3370e04fa501468cbaa6c4b5fe926f72ccfa844ad9e259adae"}, - "beacon": {:git, "https://github.com/beaconCMS/beacon.git", "4fa1eca70e84ca7157b0e5dcbb4cc60f9f10b25f", []}, + "beacon": {:git, "https://github.com/beaconCMS/beacon.git", "3edd59790e8d28b9d1610203f41396d3bf434015", []}, "castore": {:hex, :castore, "1.0.4", "ff4d0fb2e6411c0479b1d965a814ea6d00e51eb2f58697446e9c41a97d940b28", [:mix], [], "hexpm", "9418c1b8144e11656f0be99943db4caf04612e3eaecefb5dae9a2a87565584f8"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.7", "77de20ac77f0e53f20ca82c563520af0237c301a1ec3ab3bc598e8a96c7ee5d9", [:mix], [{:elixir_make, "~> 0.7.3", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2768b28bf3c2b4f788c995576b39b8cb5d47eb788526d93bd52206c1d8bf4b75"}, "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},