Current section

Files

Jump to
beacon_live_admin lib beacon live_admin page_builder table.ex
Raw

lib/beacon/live_admin/page_builder/table.ex

defmodule Beacon.LiveAdmin.PageBuilder.Table do
@moduledoc """
Represents a Table in the LiveAdmin UI, with pagination, filtering, and sorting.
"""
defstruct [:per_page, :current_page, :page_count, :sort_by, :query]
@type t :: %__MODULE__{
per_page: integer(),
current_page: integer(),
page_count: integer(),
sort_by: atom(),
query: String.t()
}
import Beacon.LiveAdmin.Router, only: [beacon_live_admin_path: 4]
alias Beacon.LiveAdmin.PageBuilder.Page
alias Beacon.LiveAdmin.PageBuilder.Table
alias Phoenix.LiveView.Socket
@doc """
Initializes a new `Table` struct.
## Options
* `sort_by` - (required) the field on which to sort the table contents
* `per_page` - the number of items to display per page (defaults to 15)
"""
@spec build(keyword()) :: Table.t()
@spec build(term()) :: nil
def build(opts) when is_list(opts) do
per_page = Keyword.get(opts, :per_page, 15)
sort_by = Keyword.get(opts, :sort_by) || raise ":sort_by is required in :table options"
%__MODULE__{
per_page: per_page,
current_page: 1,
page_count: 0,
sort_by: sort_by,
query: nil
}
end
def build(_opts), do: nil
@doc """
Pushes updated Table data to the websocket.
The Beacon Page containing the table will be automatically updated
"""
@spec update(Socket.t(), keyword()) :: Socket.t()
def update(%Socket{} = socket, new_table) when is_list(new_table) do
new_table = Map.new(new_table)
Phoenix.Component.update(socket, :beacon_page, fn page ->
table = Map.merge(page.table, new_table)
Map.put(page, :table, table)
end)
end
@doc """
Generates a path for the previous page of a paginated table.
## Usage
```
<.link patch={Table.prev_path(@socket, @page)}>
```
"""
@spec prev_path(Socket.t(), Page.t()) :: String.t()
def prev_path(socket, %Page{
site: site,
path: path,
table: %{current_page: current_page} = table
}) do
page = if current_page <= 1, do: 1, else: current_page - 1
query_params = query_params(table, page: page)
beacon_live_admin_path(socket, site, path, query_params)
end
@doc """
Generates a path for the next page of a paginated table.
## Usage
```
<.link patch={Table.next_path(@socket, @page)}>
```
"""
@spec next_path(Socket.t(), Page.t()) :: String.t()
def next_path(socket, %Page{
site: site,
path: path,
table: %{current_page: current_page, page_count: page_count} = table
}) do
page = if current_page >= page_count, do: current_page, else: current_page + 1
query_params = query_params(table, page: page)
beacon_live_admin_path(socket, site, path, query_params)
end
@doc """
Generates a path to navigate from the current page to another given page.
If the current and goto pages are the same, it will still append table params to the path.
## Usage
```
<.link patch={Table.goto_path(@socket, @page)}>
```
"""
@spec goto_path(Socket.t(), Page.t(), Page.t()) :: String.t()
def goto_path(socket, %Page{site: site, path: path, table: table}, page) do
query_params = query_params(table, page: page)
beacon_live_admin_path(socket, site, path, query_params)
end
@doc """
Updates the current page based on incoming params and a `count_fn` which returns the number of
items which have already been seen.
Can be called as a helper inside a `c:Beacon.LiveAdmin.PageBuilder.handle_params/3` callback when
updating params for a Table.
"""
@spec handle_params(Socket.t(), map(), (Page.t() -> integer())) :: Socket.t()
def handle_params(socket, params, count_fn)
def handle_params(%{assigns: %{beacon_page: %{table: %{per_page: per_page, sort_by: sort_by}}}} = socket, params, count_fn) do
current_page = params |> Map.get("page", "1") |> String.to_integer()
page_count = ceil(count_fn.(socket.assigns.beacon_page) / per_page)
sort_by = params |> Map.get("sort_by", sort_by) |> safe_to_atom()
query = Map.get(params, "query", nil)
update(socket,
current_page: current_page,
page_count: page_count,
sort_by: sort_by,
query: query
)
end
def handle_params(socket, _params, _count_fn), do: socket
defp safe_to_atom(value) when is_atom(value), do: value
defp safe_to_atom(value) when is_binary(value), do: String.to_existing_atom(value)
@doc """
Creates a list of query params based on the existing params in a given `table` and a new set of incoming params.
The existing and incoming params will be merged, with precedence for the latter (incoming will overwrite existing).
"""
@spec query_params(Table.t(), keyword()) :: map()
def query_params(%Table{} = table, new_params) when is_list(new_params) do
new_params = Map.new(new_params)
table
|> Map.take([:page, :sort_by, :query])
|> Map.merge(new_params)
|> Map.reject(fn {_k, v} -> v == "" or is_nil(v) end)
end
@doc false
def nav_pages(_current_page, 0 = _page_count, _limit), do: []
def nav_pages(_current_page, page_count, limit) when page_count <= limit do
:lists.seq(1, page_count, 1)
end
def nav_pages(current_page, page_count, limit) do
current_page
|> middle(page_count, limit - 2)
|> head()
|> tail(page_count)
end
defp middle(current_page, page_count, limit) do
# how many pages it needs to distribute between current_page
bounds = (limit - 1) / 2
# move current_page cursor to not cross start/end boundaries
current_page =
cond do
current_page <= 3 ->
max(ceil(bounds) + 1, 3)
current_page - 1 <= floor(bounds) ->
current_page + 1
current_page + ceil(bounds) >= page_count ->
gap = page_count - current_page
offset = limit - ceil(bounds) - gap
current_page - offset
:else ->
current_page
end
# distribute pages around current_page
lower_bound = current_page - floor(bounds)
upper_bound = current_page + ceil(bounds)
:lists.seq(lower_bound, upper_bound, 1)
end
defp head(middle) do
gap = List.first(middle) - 1
cond do
gap == 0 -> middle
gap == 1 -> [1] ++ middle
:else -> [1, :sep] ++ middle
end
end
defp tail(middle, page_count) do
gap = page_count - List.last(middle)
cond do
gap == 0 -> middle
gap == 1 -> middle ++ [page_count]
:else -> middle ++ [:sep, page_count]
end
end
end