[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
This commit is contained in:
@@ -11,8 +11,12 @@
|
||||
|
||||
<div class="rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark">
|
||||
<div class="max-w-full overflow-x-auto">
|
||||
<div class="flex flex-col gap-10 min-w-max">
|
||||
{live_render(@conn, PinchflatWeb.Sources.IndexTableLive)}
|
||||
</div>
|
||||
{live_render(@conn, PinchflatWeb.Sources.SourceLive.IndexTableLive,
|
||||
session: %{
|
||||
"initial_sort_key" => :custom_name,
|
||||
"initial_sort_direction" => :asc,
|
||||
"results_per_page" => 10
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,41 @@
|
||||
<div class="flex flex-col min-w-max">
|
||||
<.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}
|
||||
</.subtle_link>
|
||||
</:col>
|
||||
<:col :let={source} label="Pending" sort_key="pending_count">
|
||||
<.subtle_link href={~p"/sources/#{source.id}/#tab-pending"}>
|
||||
<.localized_number number={source.pending_count} />
|
||||
</.subtle_link>
|
||||
</:col>
|
||||
<:col :let={source} label="Downloaded" sort_key="downloaded_count">
|
||||
<.subtle_link href={~p"/sources/#{source.id}/#tab-downloaded"}>
|
||||
<.localized_number number={source.downloaded_count} />
|
||||
</.subtle_link>
|
||||
</:col>
|
||||
<:col :let={source} label="Size" sort_key="media_size_bytes">
|
||||
<.readable_filesize byte_size={source.media_size_bytes} />
|
||||
</:col>
|
||||
<: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}
|
||||
</.subtle_link>
|
||||
</:col>
|
||||
<:col :let={source} label="Enabled?" sort_key="enabled">
|
||||
<.live_component
|
||||
module={PinchflatWeb.Sources.SourceLive.SourceEnableToggle}
|
||||
source={source}
|
||||
id={"source_#{source.id}_enabled"}
|
||||
/>
|
||||
</:col>
|
||||
<:col :let={source} label="" class="flex place-content-evenly">
|
||||
<.icon_link href={~p"/sources/#{source.id}/edit"} icon="hero-pencil-square" class="mx-1" />
|
||||
</:col>
|
||||
</.table>
|
||||
|
||||
<section class="flex justify-center my-5">
|
||||
<.live_pagination_controls page_number={@page} total_pages={@total_pages} />
|
||||
</section>
|
||||
</div>
|
||||
@@ -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"""
|
||||
<div>
|
||||
<.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" />
|
||||
</.form>
|
||||
</div>
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user