neiam /gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Packages
⭐
Log in to mark this repository.
Paginate the repository and organization listings
6280797 · Gabriel Morell · 2026-09-11 02:03
Message
{commit_body(@commit)}
Files changed
modified
lib/git_gud_web/live/org_live/index.ex
+79
−2
@@ -6,6 +6,8 @@ defmodule GitGudWeb.OrgLive.Index do
| 6 | 6 | alias GitGud.Profiles |
| 7 | 7 | alias GitGud.Repositories |
| 8 | 8 | |
| 9 | + @per_page 7 | |
| 10 | + | |
| 9 | 11 | # Cards show one tidy row of pins; the org page has the rest. Also |
| 10 | 12 | # bounds the git work — `PinnedReposGrid` resolves a top language per |
| 11 | 13 | # card, which is a ref lookup each. |
@@ -27,6 +29,38 @@ defmodule GitGudWeb.OrgLive.Index do
| 27 | 29 | |> assign(:page_title, "Organizations")} |
| 28 | 30 | end |
| 29 | 31 | |
| 32 | + @impl true | |
| 33 | + def handle_params(params, _uri, socket) do | |
| 34 | + {:noreply, assign(socket, :page, page_param(params["page"]))} | |
| 35 | + end | |
| 36 | + | |
| 37 | + defp page_param(nil), do: 1 | |
| 38 | + | |
| 39 | + defp page_param(raw) do | |
| 40 | + case Integer.parse(to_string(raw)) do | |
| 41 | + {n, ""} when n > 0 -> n | |
| 42 | + _ -> 1 | |
| 43 | + end | |
| 44 | + end | |
| 45 | + | |
| 46 | + @doc false | |
| 47 | + def per_page, do: @per_page | |
| 48 | + | |
| 49 | + defp page_count(total) when total <= 0, do: 1 | |
| 50 | + defp page_count(total), do: ceil(total / @per_page) | |
| 51 | + | |
| 52 | + # Page 1 keeps the bare /orgs. | |
| 53 | + defp page_path(1), do: ~p"/orgs" | |
| 54 | + defp page_path(n), do: ~p"/orgs/page/#{n}" | |
| 55 | + | |
| 56 | + # A window around the current page, so a long list doesn't render a | |
| 57 | + # link per page. | |
| 58 | + defp page_window(page, pages) do | |
| 59 | + first = max(1, min(page - 2, pages - 4)) | |
| 60 | + last = min(pages, max(page + 2, 5)) | |
| 61 | + first..last//1 | |
| 62 | + end | |
| 63 | + | |
| 30 | 64 | defp pins_for(pins, org), do: pins |> Map.get(org.id, []) |> Enum.take(@pins_per_card) |
| 31 | 65 | |
| 32 | 66 | defp count(counts, org), do: Map.get(counts, org.id, 0) |
@@ -41,13 +75,27 @@ defmodule GitGudWeb.OrgLive.Index do
| 41 | 75 | |
| 42 | 76 | @impl true |
| 43 | 77 | def render(assigns) do |
| 78 | + total = length(assigns.orgs) | |
| 79 | + pages = page_count(total) | |
| 80 | + # A deep link past the end lands on the last page, not on nothing. | |
| 81 | + page = min(assigns.page, pages) | |
| 82 | + | |
| 83 | + assigns = | |
| 84 | + assigns | |
| 85 | + |> assign(:page, page) | |
| 86 | + |> assign(:pages, pages) | |
| 87 | + |> assign(:total, total) | |
| 88 | + |> assign(:visible, Enum.slice(assigns.orgs, (page - 1) * @per_page, @per_page)) | |
| 89 | + | |
| 44 | 90 | ~H""" |
| 45 | 91 | <Layouts.app flash={@flash} current_scope={@current_scope}> |
| 46 | 92 | <div class="space-y-6"> |
| 47 | 93 | <header class="flex items-baseline justify-between gap-4"> |
| 48 | 94 | <div> |
| 49 | 95 | <h1 class="text-2xl font-semibold">Organizations</h1> |
| 50 | − <p class="text-sm opacity-70 mt-1">Orgs you belong to.</p> | |
| 96 | + <p class="text-sm opacity-70 mt-1"> | |
| 97 | + Orgs you belong to.<span :if={@pages > 1}> Page {@page} of {@pages}.</span> | |
| 98 | + </p> | |
| 51 | 99 | </div> |
| 52 | 100 | <.link navigate={~p"/orgs/new"} class="btn btn-sm btn-primary"> |
| 53 | 101 | New organization |
@@ -62,7 +110,7 @@ defmodule GitGudWeb.OrgLive.Index do
| 62 | 110 | |
| 63 | 111 | <ul class="space-y-4"> |
| 64 | 112 | <li |
| 65 | − :for={org <- @orgs} | |
| 113 | + :for={org <- @visible} | |
| 66 | 114 | id={"org-#{org.id}"} |
| 67 | 115 | class="rounded-lg border border-base-300 p-4 space-y-3 transition-colors hover:border-primary/50" |
| 68 | 116 | > |
@@ -134,6 +182,35 @@ defmodule GitGudWeb.OrgLive.Index do
| 134 | 182 | </p> |
| 135 | 183 | </li> |
| 136 | 184 | </ul> |
| 185 | + | |
| 186 | + <nav :if={@pages > 1} class="flex items-center justify-center gap-1" aria-label="Pagination"> | |
| 187 | + <.link | |
| 188 | + :if={@page > 1} | |
| 189 | + patch={page_path(@page - 1)} | |
| 190 | + class="btn btn-sm btn-ghost" | |
| 191 | + rel="prev" | |
| 192 | + > | |
| 193 | + ← Previous | |
| 194 | + </.link> | |
| 195 | + | |
| 196 | + <.link | |
| 197 | + :for={n <- page_window(@page, @pages)} | |
| 198 | + patch={page_path(n)} | |
| 199 | + class={["btn btn-sm", if(n == @page, do: "btn-active", else: "btn-ghost")]} | |
| 200 | + aria-current={n == @page && "page"} | |
| 201 | + > | |
| 202 | + {n} | |
| 203 | + </.link> | |
| 204 | + | |
| 205 | + <.link | |
| 206 | + :if={@page < @pages} | |
| 207 | + patch={page_path(@page + 1)} | |
| 208 | + class="btn btn-sm btn-ghost" | |
| 209 | + rel="next" | |
| 210 | + > | |
| 211 | + Next → | |
| 212 | + </.link> | |
| 213 | + </nav> | |
| 137 | 214 | </div> |
| 138 | 215 | </Layouts.app> |
| 139 | 216 | """ |
modified
lib/git_gud_web/live/repo_live/index.ex
+120
−11
@@ -9,6 +9,8 @@ defmodule GitGudWeb.RepoLive.Index do
| 9 | 9 | |
| 10 | 10 | @sorts ~w(pushed name size) |
| 11 | 11 | |
| 12 | + @per_page 15 | |
| 13 | + | |
| 12 | 14 | @impl true |
| 13 | 15 | def mount(_params, _session, socket) do |
| 14 | 16 | repos = |
@@ -30,10 +32,58 @@ defmodule GitGudWeb.RepoLive.Index do
| 30 | 32 | |> assign(:repos, repos) |
| 31 | 33 | |> assign(:languages, Repositories.top_languages_for(ids)) |
| 32 | 34 | |> assign(:tags, GitGud.Tags.names_by_repository(ids)) |
| 33 | − |> assign(:ci_status, Workflows.latest_run_status_by_repo(ids)) | |
| 34 | − |> assign(:query, "") | |
| 35 | − |> assign(:scope_filter, "all") | |
| 36 | − |> assign(:sort, "pushed")} | |
| 35 | + |> assign(:ci_status, Workflows.latest_run_status_by_repo(ids))} | |
| 36 | + end | |
| 37 | + | |
| 38 | + @impl true | |
| 39 | + def handle_params(params, _uri, socket) do | |
| 40 | + {:noreply, | |
| 41 | + socket | |
| 42 | + |> assign(:query, params["query"] || "") | |
| 43 | + |> assign(:sort, sort_param(params["sort"], "pushed")) | |
| 44 | + |> assign(:scope_filter, scope_param(params["scope"])) | |
| 45 | + |> assign(:page, page_param(params["page"]))} | |
| 46 | + end | |
| 47 | + | |
| 48 | + defp scope_param(scope) when scope in ~w(all mine), do: scope | |
| 49 | + defp scope_param(_), do: "all" | |
| 50 | + | |
| 51 | + defp page_param(nil), do: 1 | |
| 52 | + | |
| 53 | + defp page_param(raw) do | |
| 54 | + case Integer.parse(to_string(raw)) do | |
| 55 | + {n, ""} when n > 0 -> n | |
| 56 | + _ -> 1 | |
| 57 | + end | |
| 58 | + end | |
| 59 | + | |
| 60 | + # The page is a path segment — /repositories/2 — with the filter | |
| 61 | + # state trailing as a query string. Only what differs from the | |
| 62 | + # default rides along, so an unfiltered first page stays a bare | |
| 63 | + # /repositories. | |
| 64 | + defp path_for(assigns, overrides) do | |
| 65 | + merged = | |
| 66 | + %{ | |
| 67 | + "query" => assigns.query, | |
| 68 | + "sort" => assigns.sort, | |
| 69 | + "scope" => assigns.scope_filter, | |
| 70 | + "page" => assigns.page | |
| 71 | + } | |
| 72 | + |> Map.merge(overrides) | |
| 73 | + | |
| 74 | + {page, filters} = Map.pop(merged, "page") | |
| 75 | + | |
| 76 | + query = | |
| 77 | + filters | |
| 78 | + |> Enum.reject(fn {k, v} -> | |
| 79 | + v in [nil, ""] or (k == "sort" and v == "pushed") or (k == "scope" and v == "all") | |
| 80 | + end) | |
| 81 | + |> Map.new() | |
| 82 | + | |
| 83 | + case page_param(page) do | |
| 84 | + 1 -> ~p"/repositories?#{query}" | |
| 85 | + n -> ~p"/repositories/#{n}?#{query}" | |
| 86 | + end | |
| 37 | 87 | end |
| 38 | 88 | |
| 39 | 89 | defp namespace_handle(%{organization: %{handle: h}}) when is_binary(h), do: h |
@@ -49,15 +99,19 @@ defmodule GitGudWeb.RepoLive.Index do
| 49 | 99 | |
| 50 | 100 | @impl true |
| 51 | 101 | def handle_event("filter", params, socket) do |
| 52 | − {:noreply, | |
| 53 | − socket | |
| 54 | − |> assign(:query, params["query"] || socket.assigns.query) | |
| 55 | − |> assign(:sort, sort_param(params["sort"], socket.assigns.sort))} | |
| 102 | + overrides = %{ | |
| 103 | + "query" => params["query"] || socket.assigns.query, | |
| 104 | + "sort" => sort_param(params["sort"], socket.assigns.sort), | |
| 105 | + "page" => 1 | |
| 106 | + } | |
| 107 | + | |
| 108 | + {:noreply, push_patch(socket, to: path_for(socket.assigns, overrides))} | |
| 56 | 109 | end |
| 57 | 110 | |
| 58 | 111 | def handle_event("scope", %{"scope" => scope}, socket) |
| 59 | 112 | when scope in ~w(all mine) do |
| 60 | − {:noreply, assign(socket, :scope_filter, scope)} | |
| 113 | + overrides = %{"scope" => scope, "page" => 1} | |
| 114 | + {:noreply, push_patch(socket, to: path_for(socket.assigns, overrides))} | |
| 61 | 115 | end |
| 62 | 116 | |
| 63 | 117 | defp sort_param(sort, _fallback) when sort in @sorts, do: sort |
@@ -129,9 +183,27 @@ defmodule GitGudWeb.RepoLive.Index do
| 129 | 183 | defp exact_time(nil), do: "no pushes yet" |
| 130 | 184 | defp exact_time(dt), do: Calendar.strftime(dt, "%Y-%m-%d %H:%M UTC") |
| 131 | 185 | |
| 186 | + defp page_count(total) when total <= 0, do: 1 | |
| 187 | + defp page_count(total), do: ceil(total / @per_page) | |
| 188 | + | |
| 189 | + @doc false | |
| 190 | + def per_page, do: @per_page | |
| 191 | + | |
| 132 | 192 | @impl true |
| 133 | 193 | def render(assigns) do |
| 134 | − assigns = assign(assigns, :visible, visible(assigns)) | |
| 194 | + matching = visible(assigns) | |
| 195 | + total = length(matching) | |
| 196 | + pages = page_count(total) | |
| 197 | + # A deep link to a page that no longer exists lands on the last one | |
| 198 | + # rather than on nothing. | |
| 199 | + page = min(assigns.page, pages) | |
| 200 | + | |
| 201 | + assigns = | |
| 202 | + assigns | |
| 203 | + |> assign(:visible, Enum.slice(matching, (page - 1) * @per_page, @per_page)) | |
| 204 | + |> assign(:total, total) | |
| 205 | + |> assign(:pages, pages) | |
| 206 | + |> assign(:page, page) | |
| 135 | 207 | |
| 136 | 208 | ~H""" |
| 137 | 209 | <Layouts.app flash={@flash} current_scope={@current_scope}> |
@@ -140,7 +212,7 @@ defmodule GitGudWeb.RepoLive.Index do
| 140 | 212 | <div> |
| 141 | 213 | <h1 class="text-2xl font-semibold">Repositories</h1> |
| 142 | 214 | <p class="text-sm opacity-70 mt-1"> |
| 143 | − {length(@visible)} of {length(@repos)} shown | |
| 215 | + {@total} of {length(@repos)} shown<span :if={@pages > 1}> · page {@page} of {@pages}</span> | |
| 144 | 216 | </p> |
| 145 | 217 | </div> |
| 146 | 218 | <div :if={signed_in?(@current_scope)} class="flex items-center gap-2 shrink-0"> |
@@ -276,8 +348,45 @@ defmodule GitGudWeb.RepoLive.Index do
| 276 | 348 | </div> |
| 277 | 349 | </li> |
| 278 | 350 | </ul> |
| 351 | + | |
| 352 | + <nav :if={@pages > 1} class="flex items-center justify-center gap-1" aria-label="Pagination"> | |
| 353 | + <.link | |
| 354 | + :if={@page > 1} | |
| 355 | + patch={path_for(assigns, %{"page" => @page - 1})} | |
| 356 | + class="btn btn-sm btn-ghost" | |
| 357 | + rel="prev" | |
| 358 | + > | |
| 359 | + ← Previous | |
| 360 | + </.link> | |
| 361 | + | |
| 362 | + <.link | |
| 363 | + :for={n <- page_window(@page, @pages)} | |
| 364 | + patch={path_for(assigns, %{"page" => n})} | |
| 365 | + class={["btn btn-sm", if(n == @page, do: "btn-active", else: "btn-ghost")]} | |
| 366 | + aria-current={n == @page && "page"} | |
| 367 | + > | |
| 368 | + {n} | |
| 369 | + </.link> | |
| 370 | + | |
| 371 | + <.link | |
| 372 | + :if={@page < @pages} | |
| 373 | + patch={path_for(assigns, %{"page" => @page + 1})} | |
| 374 | + class="btn btn-sm btn-ghost" | |
| 375 | + rel="next" | |
| 376 | + > | |
| 377 | + Next → | |
| 378 | + </.link> | |
| 379 | + </nav> | |
| 279 | 380 | </div> |
| 280 | 381 | </Layouts.app> |
| 281 | 382 | """ |
| 282 | 383 | end |
| 384 | + | |
| 385 | + # A window around the current page, so a thousand repos don't render | |
| 386 | + # a thousand page links. | |
| 387 | + defp page_window(page, pages) do | |
| 388 | + first = max(1, min(page - 2, pages - 4)) | |
| 389 | + last = min(pages, max(page + 2, 5)) | |
| 390 | + first..last//1 | |
| 391 | + end | |
| 283 | 392 | end |
modified
lib/git_gud_web/router.ex
+6
−0
@@ -183,6 +183,9 @@ defmodule GitGudWeb.Router do
| 183 | 183 | live "/r/:owner/:name/fork", RepoLive.Fork, :new |
| 184 | 184 | live "/orgs", OrgLive.Index, :index |
| 185 | 185 | live "/orgs/new", OrgLive.New, :new |
| 186 | + # Not /orgs/2 — an org handle may start with a digit, so that | |
| 187 | + # would be ambiguous with the org actually named "2". | |
| 188 | + live "/orgs/page/:page", OrgLive.Index, :index | |
| 186 | 189 | end |
| 187 | 190 | |
| 188 | 191 | live_session :admin, |
@@ -200,6 +203,9 @@ defmodule GitGudWeb.Router do
| 200 | 203 | # LiveView itself. |
| 201 | 204 | live "/", PageLive.Home, :index |
| 202 | 205 | live "/repositories", RepoLive.Index, :index |
| 206 | + # Page 2 onwards. Declared after /repositories/new and /import, | |
| 207 | + # which live in the authenticated scope above and so match first. | |
| 208 | + live "/repositories/:page", RepoLive.Index, :index | |
| 203 | 209 | live "/u/:handle", UserLive.Profile, :show |
| 204 | 210 | live "/syms", SymsLive.Top, :index |
| 205 | 211 | live "/tags", TagsLive.Index, :index |
modified
test/git_gud_web/live/org_live/index_test.exs
+88
−0
@@ -106,4 +106,92 @@ defmodule GitGudWeb.OrgLive.IndexTest do
| 106 | 106 | |
| 107 | 107 | assert html =~ "New organization" |
| 108 | 108 | end |
| 109 | + | |
| 110 | + describe "pagination" do | |
| 111 | + defp many_orgs(user, n) do | |
| 112 | + for i <- 1..n do | |
| 113 | + {:ok, org} = | |
| 114 | + Organizations.create_organization(user, %{"handle" => "paged-org-#{100 + i}"}) | |
| 115 | + | |
| 116 | + org | |
| 117 | + end | |
| 118 | + end | |
| 119 | + | |
| 120 | + test "no pager while everything fits", %{conn: conn} do | |
| 121 | + user = user_fixture() | |
| 122 | + _ = many_orgs(user, GitGudWeb.OrgLive.Index.per_page()) | |
| 123 | + | |
| 124 | + {:ok, lv, _html} = live(log_in_user(conn, user), ~p"/orgs") | |
| 125 | + | |
| 126 | + refute has_element?(lv, "nav[aria-label=Pagination]") | |
| 127 | + end | |
| 128 | + | |
| 129 | + test "a pager appears at one too many", %{conn: conn} do | |
| 130 | + user = user_fixture() | |
| 131 | + _ = many_orgs(user, GitGudWeb.OrgLive.Index.per_page() + 1) | |
| 132 | + | |
| 133 | + {:ok, lv, html} = live(log_in_user(conn, user), ~p"/orgs") | |
| 134 | + | |
| 135 | + assert has_element?(lv, "nav[aria-label=Pagination]") | |
| 136 | + assert html =~ "Page 1 of 2" | |
| 137 | + end | |
| 138 | + | |
| 139 | + test "only a page's worth renders", %{conn: conn} do | |
| 140 | + user = user_fixture() | |
| 141 | + per = GitGudWeb.OrgLive.Index.per_page() | |
| 142 | + _ = many_orgs(user, per + 1) | |
| 143 | + | |
| 144 | + {:ok, lv, _html} = live(log_in_user(conn, user), ~p"/orgs") | |
| 145 | + | |
| 146 | + shown = lv |> render() |> String.split("id=\"org-") |> length() | |
| 147 | + assert shown - 1 == per | |
| 148 | + end | |
| 149 | + | |
| 150 | + test "a deep link to page 2 works", %{conn: conn} do | |
| 151 | + user = user_fixture() | |
| 152 | + _ = many_orgs(user, GitGudWeb.OrgLive.Index.per_page() + 1) | |
| 153 | + | |
| 154 | + {:ok, _lv, html} = live(log_in_user(conn, user), ~p"/orgs/page/2") | |
| 155 | + | |
| 156 | + assert html =~ "Page 2 of 2" | |
| 157 | + end | |
| 158 | + | |
| 159 | + test "past the end lands on the last page", %{conn: conn} do | |
| 160 | + user = user_fixture() | |
| 161 | + _ = many_orgs(user, GitGudWeb.OrgLive.Index.per_page() + 1) | |
| 162 | + | |
| 163 | + {:ok, _lv, html} = live(log_in_user(conn, user), ~p"/orgs/page/99") | |
| 164 | + | |
| 165 | + assert html =~ "Page 2 of 2" | |
| 166 | + end | |
| 167 | + | |
| 168 | + test "a nonsense page falls back to the first", %{conn: conn} do | |
| 169 | + user = user_fixture() | |
| 170 | + _ = many_orgs(user, GitGudWeb.OrgLive.Index.per_page() + 1) | |
| 171 | + | |
| 172 | + {:ok, _lv, html} = live(log_in_user(conn, user), ~p"/orgs/page/banana") | |
| 173 | + | |
| 174 | + assert html =~ "Page 1 of 2" | |
| 175 | + end | |
| 176 | + | |
| 177 | + test "the pager links use /orgs/page/N", %{conn: conn} do | |
| 178 | + user = user_fixture() | |
| 179 | + _ = many_orgs(user, GitGudWeb.OrgLive.Index.per_page() + 1) | |
| 180 | + | |
| 181 | + {:ok, lv, _html} = live(log_in_user(conn, user), ~p"/orgs") | |
| 182 | + | |
| 183 | + assert has_element?(lv, ~s{nav[aria-label=Pagination] a[href="/orgs/page/2"]}) | |
| 184 | + end | |
| 185 | + | |
| 186 | + test "an org whose handle is a number is still its own page", %{conn: conn} do | |
| 187 | + user = user_fixture() | |
| 188 | + {:ok, numeric} = Organizations.create_organization(user, %{"handle" => "2"}) | |
| 189 | + | |
| 190 | + # /orgs/2 is the org, not page 2 — which is why the pager uses | |
| 191 | + # /orgs/page/N. | |
| 192 | + {:ok, _lv, html} = live(log_in_user(conn, user), ~p"/orgs/#{numeric.handle}") | |
| 193 | + | |
| 194 | + assert html =~ "@2" | |
| 195 | + end | |
| 196 | + end | |
| 109 | 197 | end |
modified
test/git_gud_web/live/repo_live/index_test.exs
+111
−0
@@ -176,4 +176,115 @@ defmodule GitGudWeb.RepoLive.IndexTest do
| 176 | 176 | refute html =~ ">alpha<" |
| 177 | 177 | end |
| 178 | 178 | end |
| 179 | + | |
| 180 | + describe "pagination" do | |
| 181 | + defp many_repos(user, n) do | |
| 182 | + for i <- 1..n do | |
| 183 | + {_u, repo} = repository_fixture(%{owner: user, name: "paged-#{100 + i}"}) | |
| 184 | + repo | |
| 185 | + end | |
| 186 | + end | |
| 187 | + | |
| 188 | + test "no pager while everything fits on one page", %{conn: conn, user: user} do | |
| 189 | + _ = many_repos(user, GitGudWeb.RepoLive.Index.per_page()) | |
| 190 | + | |
| 191 | + {:ok, lv, _html} = live(log_in_user(conn, user), ~p"/repositories") | |
| 192 | + | |
| 193 | + refute has_element?(lv, "nav[aria-label=Pagination]") | |
| 194 | + end | |
| 195 | + | |
| 196 | + test "a pager appears once there's one too many", %{conn: conn, user: user} do | |
| 197 | + _ = many_repos(user, GitGudWeb.RepoLive.Index.per_page() + 1) | |
| 198 | + | |
| 199 | + {:ok, lv, html} = live(log_in_user(conn, user), ~p"/repositories") | |
| 200 | + | |
| 201 | + assert has_element?(lv, "nav[aria-label=Pagination]") | |
| 202 | + assert html =~ "page 1 of 2" | |
| 203 | + end | |
| 204 | + | |
| 205 | + test "only a page's worth renders at a time", %{conn: conn, user: user} do | |
| 206 | + per = GitGudWeb.RepoLive.Index.per_page() | |
| 207 | + _ = many_repos(user, per + 1) | |
| 208 | + | |
| 209 | + {:ok, lv, _html} = live(log_in_user(conn, user), ~p"/repositories") | |
| 210 | + | |
| 211 | + shown = lv |> render() |> String.split("id=\"repo-") |> length() | |
| 212 | + assert shown - 1 == per | |
| 213 | + end | |
| 214 | + | |
| 215 | + test "a deep link to page 2 shows the rest", %{conn: conn, user: user} do | |
| 216 | + per = GitGudWeb.RepoLive.Index.per_page() | |
| 217 | + _ = many_repos(user, per + 1) | |
| 218 | + | |
| 219 | + {:ok, _lv, html} = live(log_in_user(conn, user), ~p"/repositories/2") | |
| 220 | + | |
| 221 | + assert html =~ "page 2 of 2" | |
| 222 | + end | |
| 223 | + | |
| 224 | + test "a deep link past the end lands on the last page", %{conn: conn, user: user} do | |
| 225 | + _ = many_repos(user, GitGudWeb.RepoLive.Index.per_page() + 1) | |
| 226 | + | |
| 227 | + {:ok, _lv, html} = live(log_in_user(conn, user), ~p"/repositories/99") | |
| 228 | + | |
| 229 | + assert html =~ "page 2 of 2" | |
| 230 | + end | |
| 231 | + | |
| 232 | + test "a nonsense page falls back to the first", %{conn: conn, user: user} do | |
| 233 | + _ = many_repos(user, GitGudWeb.RepoLive.Index.per_page() + 1) | |
| 234 | + | |
| 235 | + {:ok, _lv, html} = live(log_in_user(conn, user), ~p"/repositories/banana") | |
| 236 | + | |
| 237 | + assert html =~ "page 1 of 2" | |
| 238 | + end | |
| 239 | + | |
| 240 | + test "the filter travels in the link, so a page keeps its list", %{conn: conn, user: user} do | |
| 241 | + {_u, _match} = repository_fixture(%{owner: user, name: "needle-one"}) | |
| 242 | + _ = many_repos(user, GitGudWeb.RepoLive.Index.per_page() + 1) | |
| 243 | + | |
| 244 | + {:ok, _lv, html} = live(log_in_user(conn, user), ~p"/repositories?query=needle") | |
| 245 | + | |
| 246 | + assert html =~ "needle-one" | |
| 247 | + refute html =~ "paged-101" | |
| 248 | + # One match, so no pager. | |
| 249 | + refute html =~ "aria-label=\"Pagination\"" | |
| 250 | + end | |
| 251 | + | |
| 252 | + test "changing the filter returns to page 1", %{conn: conn, user: user} do | |
| 253 | + _ = many_repos(user, GitGudWeb.RepoLive.Index.per_page() + 1) | |
| 254 | + | |
| 255 | + {:ok, lv, _html} = live(log_in_user(conn, user), ~p"/repositories/2") | |
| 256 | + | |
| 257 | + html = lv |> form("form[phx-change=filter]", %{query: "paged"}) |> render_change() | |
| 258 | + | |
| 259 | + assert html =~ "page 1 of 2" | |
| 260 | + end | |
| 261 | + | |
| 262 | + test "the page is a path segment, not a query parameter", %{conn: conn, user: user} do | |
| 263 | + _ = many_repos(user, GitGudWeb.RepoLive.Index.per_page() + 1) | |
| 264 | + | |
| 265 | + {:ok, lv, _html} = live(log_in_user(conn, user), ~p"/repositories") | |
| 266 | + | |
| 267 | + assert has_element?(lv, ~s{nav[aria-label=Pagination] a[href="/repositories/2"]}) | |
| 268 | + end | |
| 269 | + | |
| 270 | + test "a filtered page keeps the filter in the query string", %{conn: conn, user: user} do | |
| 271 | + _ = many_repos(user, GitGudWeb.RepoLive.Index.per_page() + 1) | |
| 272 | + | |
| 273 | + {:ok, lv, _html} = live(log_in_user(conn, user), ~p"/repositories?query=paged") | |
| 274 | + | |
| 275 | + assert has_element?( | |
| 276 | + lv, | |
| 277 | + ~s{nav[aria-label=Pagination] a[href="/repositories/2?query=paged"]} | |
| 278 | + ) | |
| 279 | + end | |
| 280 | + | |
| 281 | + test "an unfiltered first page keeps a bare URL", %{conn: conn, user: user} do | |
| 282 | + _ = many_repos(user, GitGudWeb.RepoLive.Index.per_page() + 1) | |
| 283 | + | |
| 284 | + {:ok, lv, _html} = live(log_in_user(conn, user), ~p"/repositories/2") | |
| 285 | + | |
| 286 | + # Back to page 1 — the link carries no leftover defaults. | |
| 287 | + assert lv |> element("nav[aria-label=Pagination] a", "1") |> render_click() =~ "page 1 of 2" | |
| 288 | + end | |
| 289 | + end | |
| 179 | 290 | end |
Parents: 71e7254