improvement: add pagination support

This commit is contained in:
Zach Daniel 2021-03-20 20:09:25 -04:00
parent cc3121e6eb
commit 90edb4e266
11 changed files with 291 additions and 176 deletions

View file

@ -75,6 +75,12 @@ Hooks.FormChange = {
}
}
Hooks.MaintainAttrs = {
attrs() { return this.el.getAttribute("data-attrs").split(", ") },
beforeUpdate() { this.prevAttrs = this.attrs().map(name => [name, this.el.getAttribute(name)]) },
updated() { this.prevAttrs.forEach(([name, val]) => this.el.setAttribute(name, val)) }
}
let liveSocket = new LiveSocket(socketPath, Socket, {
hooks: Hooks,
dom: {

View file

@ -10,6 +10,7 @@ defmodule Demo.Tickets.Ticket do
admin do
show_action :read
table_columns [:id, :representative_id, :reporter_id, :subject, :status]
form do
field :description, type: :long_text
end
@ -47,11 +48,18 @@ defmodule Demo.Tickets.Ticket do
pagination [
offset?: true,
keyset?: true,
default_limit: 20,
default_limit: 10,
countable: :by_default
]
end
read :keyset do
pagination [
keyset?: true,
default_limit: 10
]
end
create :open do
accept [:subject, :reporter]
end

View file

@ -18,13 +18,16 @@ defmodule AshAdmin.Components.Resource.DataTable do
data(initialized, :boolean, default: false)
data(data, :any)
data(query, :any, default: nil)
data(page_params, :any, default: nil)
data(page_num, :any, default: nil)
def update(assigns, socket) do
if assigns[:initialized] do
{:ok, socket}
else
socket = assign(socket, assigns)
arguments = socket.assigns[:params]["args"] || %{}
params = socket.assigns[:params] || %{}
arguments = params["args"] || %{}
query =
socket.assigns[:resource]
@ -33,17 +36,59 @@ defmodule AshAdmin.Components.Resource.DataTable do
socket = assign(socket, :query, query)
socket =
if params["page"] do
default_limit =
(socket.assigns[:action] && socket.assigns.action.pagination &&
socket.assigns.action.pagination.default_limit) ||
socket.assigns.action.pagination.max_page_size || 25
count? =
socket.assigns[:action] && socket.assigns.action.pagination &&
socket.assigns.action.pagination.countable
page_params =
AshPhoenix.LiveView.page_from_params(params["page"], default_limit, !!count?)
socket
|> assign(
:page_params,
page_params
)
|> assign(
:page_num,
page_num_from_page_params(page_params)
)
else
socket
|> assign(:page_params, nil)
|> assign(:page_num, 1)
end
socket =
if assigns[:action].pagination do
keep_live(
socket,
:data,
fn socket, page_opts ->
fn socket ->
default_limit =
socket.assigns[:action].pagination.default_limit ||
socket.assigns[:action].pagination.max_page_size || 25
count? = socket.assigns[:action].pagination.countable
page_params =
if socket.assigns[:params]["page"] do
page_from_params(socket.assigns[:params]["page"], default_limit, !!count?)
else
[]
end
assigns[:api].read(socket.assigns.query,
action: socket.assigns[:action].name,
actor: socket.assigns[:actor],
authorize?: socket.assigns[:authorizing],
page: page_opts || []
page: page_params
)
end,
load_until_connected?: true
@ -71,11 +116,11 @@ defmodule AshAdmin.Components.Resource.DataTable do
def render(assigns) do
~H"""
<div class="pt-10 sm:mt-0 bg-gray-300 min-h-screen">
<div class="md:grid md:grid-cols-3 md:gap-6 mx-16 mt-10">
<div class="mt-5 md:mt-0 md:col-span-2">
<div :if={{@action.arguments != []}} class="shadow-lg overflow-hidden sm:rounded-md bg-white">
<div class="px-4 py-5 sm:p-6">
<div class="sm:mt-0 bg-gray-300 min-h-screen">
<div class="md:grid md:grid-cols-3 md:gap-6 md:mx-16 md:pt-10">
<div class="md:mt-0 md:col-span-2">
<div :if={{@action.arguments != []}} class="shadow-lg overflow-hidden pt-2 sm:rounded-md bg-white">
<div class="px-4 sm:p-6">
<Form :if={{@query}} as="query" for={{@query}} change="validate" submit="save" :let={{form: form}}>
{{AshAdmin.Components.Resource.Form.render_attributes(assigns, @resource, @action, form)}}
<div class="px-4 py-3 text-right sm:px-6">
@ -91,8 +136,8 @@ defmodule AshAdmin.Components.Resource.DataTable do
</div>
</div>
</div>
<div :if={{@action.arguments == [] || @params["args"]}} class="h-full mt-8 overflow-scroll">
<div class="shadow-lg overflow-hidden sm:rounded-md bg-white">
<div :if={{@action.arguments == [] || @params["args"]}} class="h-full overflow-scroll">
<div class="shadow-lg overflow-scroll sm:rounded-md bg-white">
<div :if={{ match?({:error, _}, @data) }}>
{{ {:error, %{query: query}} = @data
nil }}
@ -102,19 +147,36 @@ defmodule AshAdmin.Components.Resource.DataTable do
</li>
</ul>
</div>
<Table :if={{ match?({:ok, _data}, @data) }} data={{data(@data)}} resource={{@resource}} api={{@api}} set_actor={{@set_actor}}/>
<div class="px-2">
{{render_pagination_links(assigns, :top)}}
<Table :if={{ match?({:ok, _data}, @data) }} data={{data(@data)}} resource={{@resource}} api={{@api}} set_actor={{@set_actor}} attributes={{AshAdmin.Resource.table_columns(@resource)}}/>
{{render_pagination_links(assigns, :bottom)}}
</div>
</div>
</div>
</div>
"""
end
defp message(error) do
if is_exception(error) do
Exception.message(error)
else
inspect(error)
end
def handle_event("next_page", _, socket) do
params = %{"page" => page_link_params(socket.assigns.data, "next")}
{:noreply,
push_patch(socket, to: self_path(socket.assigns.url_path, socket.assigns.params, params))}
end
def handle_event("prev_page", _, socket) do
params = %{"page" => page_link_params(socket.assigns.data, "prev")}
{:noreply,
push_patch(socket, to: self_path(socket.assigns.url_path, socket.assigns.params, params))}
end
def handle_event("specific_page", %{"page" => page}, socket) do
params = %{"page" => page_link_params(socket.assigns.data, String.to_integer(page))}
{:noreply,
push_patch(socket, to: self_path(socket.assigns.url_path, socket.assigns.params, params))}
end
def handle_event("validate", %{"query" => query}, socket) do
@ -131,74 +193,176 @@ defmodule AshAdmin.Components.Resource.DataTable do
)}
end
# defp middle_page_num(num, trailing_page_nums) do
# if num in trailing_page_nums || num <= 3 do
# "..."
# else
# "...#{num}..."
# end
# end
defp render_pagination_links(assigns, placement) do
~H"""
<div :if={{(offset?(@data) || keyset?(@data)) && show_pagination_links?(@data, placement)}} class="w-5/6 mx-auto">
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div class="flex-1 flex justify-between sm:hidden">
<button :if={{!(keyset?(@data) && is_nil(@params["page"])) && prev_page?(@data)}} :on-click="prev_page" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:text-gray-500">
Previous
</button>
{{render_pagination_information(assigns, true)}}
<button :if={{next_page?(@data)}} :on-click="next_page" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:text-gray-500">
Next
</button>
</div>
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
{{render_pagination_information(assigns)}}
</div>
<div>
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
<button :if={{!(keyset?(@data) && is_nil(@params["page"])) && prev_page?(@data)}} :on-click="prev_page" class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<span class="sr-only">Previous</span>
<!-- Heroicon name: solid/chevron-left -->
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</button>
<span :if={{offset?(@data)}}>
{{render_page_links(assigns, leading_page_nums(@data))}}
{{render_middle_page_num(assigns, @page_num, trailing_page_nums(@data))}}
{{render_page_links(assigns, trailing_page_nums(@data))}}
</span>
<button :if={{next_page?(@data)}} :on-click="next_page" class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<span class="sr-only">Next</span>
<!-- Heroicon name: solid/chevron-right -->
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
</svg>
</button>
</nav>
</div>
</div>
</div>
</div>
"""
end
# defp page_link_params({:ok, page}, target), do: page_link_params(page, target)
defp render_page_links(assigns, page_nums) do
~H"""
<button :on-click="specific_page" phx-value-page={{i}} :for={{i <- page_nums}} class={{"relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50", "bg-gray-300": @page_num == i}}>
{{i}}
</button>
"""
end
# defp page_link_params(page, target) do
# case AshPhoenix.LiveView.page_link_params(page, target) do
# :invalid ->
# nil
defp render_pagination_information(assigns, small? \\ false) do
~H"""
<p class={{"text-sm text-gray-700", "sm:hidden": small?}}>
<span :if={{offset?(@data)}}>
Showing
<span class="font-medium">{{first(@data)}}</span>
to
<span class="font-medium">{{last(@data)}}</span>
of
</span>
<span :if={{count(@data)}}>
<span class="font-medium">{{count(@data)}}</span>
results
</span>
</p>
"""
end
# params ->
# [page: params]
# end
# end
defp page_num_from_page_params(params) do
cond do
!params[:offset] || params[:after] || params[:before] ->
1
# defp show_ellipses?(%Ash.Page.Offset{count: count, limit: limit}) when not is_nil(count) do
# page_nums =
# count
# |> Kernel./(limit)
# |> Float.ceil()
# |> trunc()
params[:offset] && params[:limit] ->
trunc(Float.ceil(params[:offset] / params[:limit])) + 1
# page_nums > 6
# end
true ->
nil
end
end
# defp show_ellipses?({:ok, data}), do: show_ellipses?(data)
# defp show_ellipses?(_), do: false
defp show_pagination_links?({:ok, _page}, :bottom), do: true
defp show_pagination_links?({:ok, page}, :top), do: page.limit >= 20
defp show_pagination_links?(_, _), do: false
# def leading_page_nums({:ok, data}), do: leading_page_nums(data)
# def leading_page_nums(%Ash.Page.Offset{count: nil}), do: []
defp first({:ok, %Ash.Page.Offset{offset: offset}}) do
(offset || 0) + 1
end
# def leading_page_nums(%Ash.Page.Offset{limit: limit, count: count}) do
# page_nums =
# count
# |> Kernel./(limit)
# |> Float.ceil()
# |> trunc()
defp first(_), do: nil
# 1..min(3, page_nums)
# end
defp last({:ok, %Ash.Page.Offset{offset: offset, results: results}}) do
Enum.count(results) + offset
end
# def leading_page_nums(_), do: []
defp last(_), do: nil
# def trailing_page_nums({:ok, data}), do: trailing_page_nums(data)
# def trailing_page_nums(%Ash.Page.Offset{count: nil}), do: []
defp message(error) do
if is_exception(error) do
Exception.message(error)
else
inspect(error)
end
end
# def trailing_page_nums(%Ash.Page.Offset{limit: limit, count: count}) do
# page_nums =
# count
# |> Kernel./(limit)
# |> Float.ceil()
# |> trunc()
defp render_middle_page_num(assigns, num, trailing_page_nums) do
ellipsis? = num in trailing_page_nums || num <= 3
# if page_nums > 3 do
# max(page_nums - 2, 0)..page_nums
# else
# []
# end
# end
~H"""
<span
:if={{show_ellipses?(@data)}}
class={{"relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700", "bg-gray-300": !ellipsis?}}>
<span :if={{ellipsis?}}>
...
</span>
<span :if={{!ellipsis?}}>
{{num}}
</span>
</span>
"""
end
# def handle_event("toggle_filter", _, socket) do
# {:noreply, assign(socket, :filter_open, !socket.assigns.filter_open)}
# end
defp show_ellipses?(%Ash.Page.Offset{count: count, limit: limit}) when not is_nil(count) do
page_nums =
count
|> Kernel./(limit)
|> Float.ceil()
|> trunc()
page_nums > 6
end
defp show_ellipses?({:ok, data}), do: show_ellipses?(data)
defp show_ellipses?(_), do: false
def leading_page_nums({:ok, data}), do: leading_page_nums(data)
def leading_page_nums(%Ash.Page.Offset{count: nil}), do: []
def leading_page_nums(%Ash.Page.Offset{limit: limit, count: count}) do
page_nums =
count
|> Kernel./(limit)
|> Float.ceil()
|> trunc()
1..min(3, page_nums)
end
def leading_page_nums(_), do: []
def trailing_page_nums({:ok, data}), do: trailing_page_nums(data)
def trailing_page_nums(%Ash.Page.Offset{count: nil}), do: []
def trailing_page_nums(%Ash.Page.Offset{limit: limit, count: count}) do
page_nums =
count
|> Kernel./(limit)
|> Float.ceil()
|> trunc()
if page_nums > 3 do
max(page_nums - 2, 4)..page_nums
else
[]
end
end
defp data({:ok, data}), do: data(data)
defp data({:error, _}), do: []
@ -206,49 +370,15 @@ defmodule AshAdmin.Components.Resource.DataTable do
defp data(%Ash.Page.Keyset{results: results}), do: results
defp data(data), do: data
# defp offset?({:ok, data}), do: offset?(data)
# defp offset?(%Ash.Page.Offset{}), do: true
# defp offset?(_), do: false
defp offset?({:ok, data}), do: offset?(data)
defp offset?(%Ash.Page.Offset{}), do: true
defp offset?(_), do: false
# defp keyset?({:ok, data}), do: keyset?(data)
# defp keyset?(%Ash.Page.Keyset{}), do: true
# defp keyset?(_), do: false
defp keyset?({:ok, data}), do: keyset?(data)
defp keyset?(%Ash.Page.Keyset{}), do: true
defp keyset?(_), do: false
# defp offset({:ok, data}), do: offset(data)
# defp offset(%Ash.Page.Offset{offset: offset}), do: offset
# defp offset(_), do: 0
# defp limit({:ok, data}), do: limit(data)
# defp limit(%Ash.Page.Offset{limit: limit}), do: limit
# defp limit(_), do: 0
# defp count({:ok, %{count: count}}), do: count
# defp count(%{count: count}), do: count
# defp count(_), do: nil
# defp run_query() do
# fn filter, sort, fields, context ->
# page_params =
# case context.action.pagination do
# false ->
# false
# %{offset?: true} ->
# context[:page_params] || [offset: 0]
# _ ->
# context[:page_params]
# end
# context.resource
# |> Ash.Query.filter(^filter)
# |> Ash.Query.sort(sort)
# |> Ash.Query.load(fields)
# |> Ash.Query.set_tenant(context.tenant)
# |> context.api.read(
# page: page_params,
# action: context.action.name,
# actor: context.actor,
# authorize?: context.authorizing?
# )
# end
defp count({:ok, %{count: count}}), do: count
defp count(%{count: count}), do: count
defp count(_), do: nil
end

View file

@ -40,8 +40,8 @@ defmodule AshAdmin.Components.Resource.Form do
def render(assigns) do
~H"""
<div class="pt-10 sm:mt-0 bg-gray-300 min-h-screen">
<div class="md:grid md:grid-cols-3 md:gap-6 mx-16 mt-10">
<div class="md:pt-10 sm:mt-0 bg-gray-300 min-h-screen">
<div class="md:grid md:grid-cols-3 md:gap-6 md:mx-16 md:mt-10">
<div class="mt-5 md:mt-0 md:col-span-2">
{{ render_form(assigns) }}
</div>
@ -400,7 +400,7 @@ defmodule AshAdmin.Components.Resource.Form do
<TextArea
form={{ form }}
field={{ name }}
opts={{ type: text_input_type(attribute), placeholder: placeholder(default) }}
opts={{ type: text_input_type(attribute), placeholder: placeholder(default), phx_hook: "MaintainAttrs", data_attrs: "style" }}
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md resize-y"
/>
"""

View file

@ -15,8 +15,6 @@ defmodule AshAdmin.Components.Resource do
prop(authorizing, :boolean, required: true)
prop(tenant, :string, required: true)
prop(recover_filter, :any)
prop(page_params, :any, default: [])
prop(page_num, :integer, default: 1)
prop(url_path, :string, default: "")
prop(params, :map, default: %{})
prop(primary_key, :any, default: nil)

View file

@ -4,7 +4,7 @@ defmodule AshAdmin.Components.Resource.Table do
import AshAdmin.Helpers
alias Surface.Components.LiveRedirect
prop(attributes, :list, default: nil)
prop(attributes, :any, default: nil)
prop(data, :list, default: nil)
prop(resource, :any, required: true)
prop(actions, :boolean, default: true)
@ -14,17 +14,15 @@ defmodule AshAdmin.Components.Resource.Table do
def render(assigns) do
~H"""
<div>
<table class="rounded-t-lg m-5 w-5/6 mx-auto">
<table class="rounded-t-lg m-5 w-5/6 mx-auto text-left">
<thead class="text-left border-b-2">
<th :for={{ attribute <- Ash.Resource.Info.attributes(@resource) |> Enum.filter(&(is_nil(@attributes) || &1 in @attributes)) }}>
<th :for={{ attribute <- attributes(@resource, @attributes) }}>
{{ to_name(attribute.name) }}
</th>
</thead>
<tbody>
<tr :for={{ record <- @data }} class="text-left border-b-2">
<td :for={{ attribute <- Ash.Resource.Info.attributes(@resource) |> Enum.filter(&(is_nil(@attributes) || &1 in @attributes)) }} class="px-4 py-3">
{{ render_attribute(record, attribute) }}
</td>
<tr :for={{ record <- @data }} class="border-b-2">
<td :for={{ attribute <- attributes(@resource, @attributes) }} class="py-3">{{ render_attribute(record, attribute) }}</td>
<td :if={{@actions && actions?(@resource)}}>
<div class="flex h-max justify-items-center">
<div :if={{ AshAdmin.Resource.show_action(@resource) }}>
@ -63,6 +61,16 @@ defmodule AshAdmin.Components.Resource.Table do
"""
end
defp attributes(resource, nil) do
Ash.Resource.Info.attributes(resource)
end
defp attributes(resource, attributes) do
attributes
|> Enum.map(&Ash.Resource.Info.attribute(resource, &1))
|> Enum.filter(& &1)
end
defp render_attribute(record, attribute) do
if Ash.Type.embedded_type?(attribute.type) do
"..."

View file

@ -27,10 +27,6 @@ defmodule AshAdmin.Helpers do
end
def self_path(url_path, socket_params, new_params) do
IO.inspect(url_path)
IO.inspect(socket_params)
IO.inspect(new_params)
url_path <>
"?" <>
Plug.Conn.Query.encode(Map.merge(socket_params || %{}, Enum.into(new_params, %{})))

View file

@ -59,8 +59,7 @@ defmodule AshAdmin.PageLive do
AshAdmin.ActorPlug.session_bool(session["actor_authorizing"]) || false
)
|> assign(:recover_filter, nil)
|> assign(:actor_paused, actor_paused)
|> assign(:page_num, 1)}
|> assign(:actor_paused, actor_paused)}
end
@impl true
@ -94,8 +93,6 @@ defmodule AshAdmin.PageLive do
tab={{ @tab }}
url_path={{ @url_path }}
params={{ @params }}
page_params={{ @page_params }}
page_num={{ @page_num }}
action={{ @action }}
tenant={{ @tenant }}
actor={{ unless @actor_paused, do: @actor }}
@ -126,31 +123,6 @@ defmodule AshAdmin.PageLive do
socket
end
socket =
if params["page"] do
default_limit =
socket.assigns[:action] && socket.assigns.action.pagination &&
socket.assigns.action.pagination.default_limit
count? =
socket.assigns[:action] && socket.assigns.action.pagination &&
socket.assigns.action.pagination.countable
page_params =
AshPhoenix.LiveView.page_from_params(params["page"], default_limit, !!count?)
socket
|> assign(
:page_params,
page_params
)
|> assign(:page_num, page_num_from_page_params(page_params))
else
socket
|> assign(:page_params, nil)
|> assign(:page_num, 1)
end
socket =
if params["primary_key"] do
case decode_primary_key(socket.assigns.resource, params["primary_key"]) do
@ -200,19 +172,6 @@ defmodule AshAdmin.PageLive do
|> Enum.map(& &1.name)
end
defp page_num_from_page_params(params) do
cond do
!params[:offset] || params[:after] || params[:before] ->
1
params[:offset] && params[:limit] ->
trunc(Float.ceil(params[:offset] / params[:limit])) + 1
true ->
nil
end
end
@impl true
def handle_event("toggle_authorizing", _, socket) do
{:noreply,

View file

@ -39,12 +39,20 @@ defmodule AshAdmin.Resource do
type: {:list, :atom},
doc:
"A list of read actions that can be used to show resource details. These actions should accept arguments that produce one record e.g `get_user_by_id`."
],
table_columns: [
type: {:list, :atom},
doc: "The list of attributes to render on the table view."
]
]
}
use Ash.Dsl.Extension, sections: [@admin]
def table_columns(resource) do
Ash.Dsl.Extension.get_opt(resource, [:admin], :table_columns, nil, true)
end
def name(resource) do
Ash.Dsl.Extension.get_opt(resource, [:admin], :name, nil, true) ||
resource

View file

@ -46,8 +46,10 @@ defmodule AshAdmin.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:ash, "~> 1.36 and >= 1.36.12"},
{:ash_phoenix, "~> 0.4 and >= 0.4.2"},
# {:ash, "~> 1.36 and >= 1.36.12"},
{:ash, path: "../ash", override: true},
# {:ash_phoenix, "~> 0.4 and >= 0.4.2"},
{:ash_phoenix, path: "../ash_phoenix"},
{:surface, "~> 0.3.1"},
{:phoenix_live_view, "~> 0.15.4"},
{:phoenix_html, "~> 2.14.1 or ~> 2.15"},

File diff suppressed because one or more lines are too long