feat: sort header for members list
This commit is contained in:
parent
e2c00f263e
commit
e280a28b8e
3 changed files with 327 additions and 52 deletions
64
lib/mv_web/live/components/sort_header_component.ex
Normal file
64
lib/mv_web/live/components/sort_header_component.ex
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
defmodule MvWeb.Components.SortHeaderComponent do
|
||||||
|
@moduledoc """
|
||||||
|
Sort Header that can be used as column header and sorts a table:
|
||||||
|
Props:
|
||||||
|
- field: atom() # Ash‑Field for sorting
|
||||||
|
- label: string() # Column Heading (can be aan heex templyte)
|
||||||
|
- sort_field: atom() | nil # current sort-field from parent liveview
|
||||||
|
- sort_order: :asc | :desc | nil # current sorting order
|
||||||
|
"""
|
||||||
|
use MvWeb, :live_component
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def update(assigns, socket) do
|
||||||
|
{:ok, assign(socket, assigns)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-sort={aria_sort(@field, @sort_field, @sort_order)}
|
||||||
|
class="btn btn-ghost select-none"
|
||||||
|
phx-click="sort"
|
||||||
|
phx-value-field={@field}
|
||||||
|
phx-target={@myself}
|
||||||
|
>
|
||||||
|
{@label}
|
||||||
|
<%= if @sort_field == @field do %>
|
||||||
|
<.icon name={if @sort_order == :asc, do: "hero-chevron-up", else: "hero-chevron-down"} />
|
||||||
|
<% else %>
|
||||||
|
<.icon name="hero-chevron-up-down" class="opacity-40" />
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<!-- Screen Reader Info -->
|
||||||
|
<span class="sr-only">
|
||||||
|
({if @sort_field == @field do
|
||||||
|
if @sort_order == :asc, do: gettext("ascending"), else: gettext("descending")
|
||||||
|
else
|
||||||
|
gettext("not sorted")
|
||||||
|
end})
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("sort", %{"field" => field_str}, socket) do
|
||||||
|
send(self(), {:sort, field_str})
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
# -------------------------------------------------
|
||||||
|
# Hilfsfunktionen für ARIA‑Attribute & Icon‑SVG
|
||||||
|
# -------------------------------------------------
|
||||||
|
defp aria_sort(field, sort_field, dir) when field == sort_field do
|
||||||
|
case dir do
|
||||||
|
:asc -> "ascending"
|
||||||
|
:desc -> "descending"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp aria_sort(_, _, _), do: "none"
|
||||||
|
end
|
||||||
|
|
@ -5,18 +5,18 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
import MvWeb.TableComponents
|
import MvWeb.TableComponents
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(params, _session, socket) do
|
||||||
members = Ash.read!(Mv.Membership.Member)
|
socket =
|
||||||
sorted = Enum.sort_by(members, & &1.first_name)
|
socket
|
||||||
|
|> assign(:page_title, gettext("Members"))
|
||||||
{:ok,
|
|
||||||
socket
|
|
||||||
|> assign(:page_title, gettext("Members"))
|
|
||||||
|> assign(:query, "")
|
|> assign(:query, "")
|
||||||
|> assign(:sort_field, :first_name)
|
|> assign_new(:sort_field, fn -> :first_name end)
|
||||||
|> assign(:sort_order, :asc)
|
|> assign_new(:sort_order, fn -> :asc end)
|
||||||
|> assign(:members, sorted)
|
|> assign(:selected_members, [])
|
||||||
|> assign(:selected_members, [])}
|
|
||||||
|
# We call handle params to use the query from the URL
|
||||||
|
{:noreply, socket} = handle_params(params, nil, socket)
|
||||||
|
{:ok, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
|
|
@ -45,6 +45,11 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
# Handle Events
|
# Handle Events
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# Handle Events
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
|
||||||
|
# Delete a member
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("delete", %{"id" => id}, socket) do
|
def handle_event("delete", %{"id" => id}, socket) do
|
||||||
member = Ash.get!(Mv.Membership.Member, id)
|
member = Ash.get!(Mv.Membership.Member, id)
|
||||||
|
|
@ -67,32 +72,7 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
{:noreply, assign(socket, :selected_members, selected)}
|
{:noreply, assign(socket, :selected_members, selected)}
|
||||||
end
|
end
|
||||||
|
|
||||||
# Sorts the list of members according to a field, when you click on the column header
|
# Selects all members in the list of members
|
||||||
@impl true
|
|
||||||
def handle_event("sort", %{"field" => field_str}, socket) do
|
|
||||||
members = socket.assigns.members
|
|
||||||
field = String.to_existing_atom(field_str)
|
|
||||||
|
|
||||||
new_order =
|
|
||||||
if socket.assigns.sort_field == field do
|
|
||||||
toggle_order(socket.assigns.sort_order)
|
|
||||||
else
|
|
||||||
:asc
|
|
||||||
end
|
|
||||||
|
|
||||||
sorted_members =
|
|
||||||
members
|
|
||||||
|> Enum.sort_by(&Map.get(&1, field), sort_fun(new_order))
|
|
||||||
|
|
||||||
{:noreply,
|
|
||||||
socket
|
|
||||||
|> assign(:sort_field, field)
|
|
||||||
|> assign(:sort_order, new_order)
|
|
||||||
|> assign(:members, sorted_members)}
|
|
||||||
end
|
|
||||||
|
|
||||||
# Selects all members in the list of members
|
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("select_all", _params, socket) do
|
def handle_event("select_all", _params, socket) do
|
||||||
members = socket.assigns.members
|
members = socket.assigns.members
|
||||||
|
|
@ -109,8 +89,123 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
{:noreply, assign(socket, :selected_members, selected)}
|
{:noreply, assign(socket, :selected_members, selected)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# Handle Infos from Child Components
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
|
||||||
|
# Sorts the list of members according to a field, when you click on the column header
|
||||||
|
@impl true
|
||||||
|
def handle_info({:sort, field_str}, socket) do
|
||||||
|
field = String.to_existing_atom(field_str)
|
||||||
|
|
||||||
|
{new_order, new_field} =
|
||||||
|
if socket.assigns.sort_field == field do
|
||||||
|
{toggle_order(socket.assigns.sort_order), field}
|
||||||
|
else
|
||||||
|
{:asc, field}
|
||||||
|
end
|
||||||
|
|
||||||
|
active_id = :"sort_#{new_field}"
|
||||||
|
|
||||||
|
# Update the SortHeader to
|
||||||
|
send_update(MvWeb.Components.SortHeaderComponent,
|
||||||
|
id: active_id,
|
||||||
|
sort_field: new_field,
|
||||||
|
sort_order: new_order
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build the URL with queries
|
||||||
|
query_params = %{
|
||||||
|
"sort_field" => Atom.to_string(new_field),
|
||||||
|
"sort_order" => Atom.to_string(new_order)
|
||||||
|
}
|
||||||
|
|
||||||
|
# "/members" is the path you defined in router.ex
|
||||||
|
new_path = "/members?" <> URI.encode_query(query_params)
|
||||||
|
|
||||||
|
# Push the new URL
|
||||||
|
{:noreply,
|
||||||
|
push_patch(socket,
|
||||||
|
to: new_path,
|
||||||
|
# replace true
|
||||||
|
replace: true
|
||||||
|
)}
|
||||||
|
end
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
# Handle Params from the URL
|
||||||
|
# -----------------------------------------------------------------
|
||||||
|
@impl true
|
||||||
|
def handle_params(params, _url, socket) do
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> maybe_update_sort(params)
|
||||||
|
|> load_members()
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# FUNCTIONS
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# Load members eg based on a query for sorting
|
||||||
|
defp load_members(socket) do
|
||||||
|
query =
|
||||||
|
Mv.Membership.Member
|
||||||
|
|> Ash.Query.new()
|
||||||
|
|> Ash.Query.select([
|
||||||
|
:id,
|
||||||
|
:first_name,
|
||||||
|
:last_name,
|
||||||
|
:email,
|
||||||
|
:street,
|
||||||
|
:house_number,
|
||||||
|
:postal_code,
|
||||||
|
:city,
|
||||||
|
:phone_number,
|
||||||
|
:join_date
|
||||||
|
])
|
||||||
|
|> maybe_sort(socket.assigns.sort_field, socket.assigns.sort_order)
|
||||||
|
|
||||||
|
members = Ash.read!(query)
|
||||||
|
assign(socket, :members, members)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_update_sort(socket, %{"sort_field" => sf, "sort_order" => so}) do
|
||||||
|
field =
|
||||||
|
try do
|
||||||
|
String.to_existing_atom(sf)
|
||||||
|
rescue
|
||||||
|
ArgumentError -> socket.assigns.sort_field
|
||||||
|
end
|
||||||
|
|
||||||
|
order = if so in ["asc", "desc"], do: String.to_atom(so), else: socket.assigns.sort_order
|
||||||
|
|
||||||
|
IO.inspect(order)
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> assign(:sort_field, field)
|
||||||
|
|> assign(:sort_order, order)
|
||||||
|
end
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# Helper Functions
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
|
||||||
|
# Functions to toggle sorting order
|
||||||
defp toggle_order(:asc), do: :desc
|
defp toggle_order(:asc), do: :desc
|
||||||
defp toggle_order(:desc), do: :asc
|
defp toggle_order(:desc), do: :asc
|
||||||
defp sort_fun(:asc), do: &<=/2
|
defp toggle_order(nil), do: :asc
|
||||||
defp sort_fun(:desc), do: &>=/2
|
|
||||||
|
# Function to turn a string into an atom only if it already exists
|
||||||
|
defp maybe_atom(nil), do: nil
|
||||||
|
defp maybe_atom(atom) when is_atom(atom), do: atom
|
||||||
|
defp maybe_atom(str) when is_binary(str), do: String.to_existing_atom(str)
|
||||||
|
|
||||||
|
# Function to sort the column if needed
|
||||||
|
defp maybe_sort(query, nil, _), do: query
|
||||||
|
defp maybe_sort(query, field, :asc), do: Ash.Query.sort(query, [{field, :asc}])
|
||||||
|
defp maybe_sort(query, field, :desc), do: Ash.Query.sort(query, [{field, :desc}])
|
||||||
|
# no changes
|
||||||
|
defp maybe_update_sort(socket, _), do: socket
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -52,23 +52,139 @@
|
||||||
<:col
|
<:col
|
||||||
:let={member}
|
:let={member}
|
||||||
label={
|
label={
|
||||||
sort_button(%{
|
~H"""
|
||||||
field: :first_name,
|
<.live_component
|
||||||
label: gettext("Name"),
|
module={MvWeb.Components.SortHeaderComponent}
|
||||||
sort_field: @sort_field,
|
id={:sort_first_name}
|
||||||
sort_order: @sort_order
|
field={:first_name}
|
||||||
})
|
label={gettext("First name")}
|
||||||
|
sort_field={@sort_field}
|
||||||
|
sort_order={@sort_order}
|
||||||
|
/>
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{member.first_name} {member.last_name}
|
{member.first_name} {member.last_name}
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={member} label={gettext("Email")}>{member.email}</:col>
|
<:col
|
||||||
<:col :let={member} label={gettext("Street")}>{member.street}</:col>
|
:let={member}
|
||||||
<:col :let={member} label={gettext("House Number")}>{member.house_number}</:col>
|
label={
|
||||||
<:col :let={member} label={gettext("Postal Code")}>{member.postal_code}</:col>
|
~H"""
|
||||||
<:col :let={member} label={gettext("City")}>{member.city}</:col>
|
<.live_component
|
||||||
<:col :let={member} label={gettext("Phone Number")}>{member.phone_number}</:col>
|
module={MvWeb.Components.SortHeaderComponent}
|
||||||
<:col :let={member} label={gettext("Join Date")}>{member.join_date}</:col>
|
id={:sort_email}
|
||||||
|
field={:email}
|
||||||
|
label={gettext("Email")}
|
||||||
|
sort_field={@sort_field}
|
||||||
|
sort_order={@sort_order}
|
||||||
|
/>
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{member.email}
|
||||||
|
</:col>
|
||||||
|
<:col
|
||||||
|
:let={member}
|
||||||
|
label={
|
||||||
|
~H"""
|
||||||
|
<.live_component
|
||||||
|
module={MvWeb.Components.SortHeaderComponent}
|
||||||
|
id={:sort_street}
|
||||||
|
field={:street}
|
||||||
|
label={gettext("Street")}
|
||||||
|
sort_field={@sort_field}
|
||||||
|
sort_order={@sort_order}
|
||||||
|
/>
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{member.street}
|
||||||
|
</:col>
|
||||||
|
<:col
|
||||||
|
:let={member}
|
||||||
|
label={
|
||||||
|
~H"""
|
||||||
|
<.live_component
|
||||||
|
module={MvWeb.Components.SortHeaderComponent}
|
||||||
|
id={:sort_house_number}
|
||||||
|
field={:house_number}
|
||||||
|
label={gettext("House Number")}
|
||||||
|
sort_field={@sort_field}
|
||||||
|
sort_order={@sort_order}
|
||||||
|
/>
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{member.house_number}
|
||||||
|
</:col>
|
||||||
|
<:col
|
||||||
|
:let={member}
|
||||||
|
label={
|
||||||
|
~H"""
|
||||||
|
<.live_component
|
||||||
|
module={MvWeb.Components.SortHeaderComponent}
|
||||||
|
id={:sort_postal_code}
|
||||||
|
field={:postal_code}
|
||||||
|
label={gettext("Postal Code")}
|
||||||
|
sort_field={@sort_field}
|
||||||
|
sort_order={@sort_order}
|
||||||
|
/>
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{member.postal_code}
|
||||||
|
</:col>
|
||||||
|
<:col
|
||||||
|
:let={member}
|
||||||
|
label={
|
||||||
|
~H"""
|
||||||
|
<.live_component
|
||||||
|
module={MvWeb.Components.SortHeaderComponent}
|
||||||
|
id={:sort_city}
|
||||||
|
field={:city}
|
||||||
|
label={gettext("City")}
|
||||||
|
sort_field={@sort_field}
|
||||||
|
sort_order={@sort_order}
|
||||||
|
/>
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{member.city}
|
||||||
|
</:col>
|
||||||
|
<:col
|
||||||
|
:let={member}
|
||||||
|
label={
|
||||||
|
~H"""
|
||||||
|
<.live_component
|
||||||
|
module={MvWeb.Components.SortHeaderComponent}
|
||||||
|
id={:sort_phone_number}
|
||||||
|
field={:phone_number}
|
||||||
|
label={gettext("Phone Number")}
|
||||||
|
sort_field={@sort_field}
|
||||||
|
sort_order={@sort_order}
|
||||||
|
/>
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{member.phone_number}
|
||||||
|
</:col>
|
||||||
|
<:col
|
||||||
|
:let={member}
|
||||||
|
label={
|
||||||
|
~H"""
|
||||||
|
<.live_component
|
||||||
|
module={MvWeb.Components.SortHeaderComponent}
|
||||||
|
id={:sort_join_date}
|
||||||
|
field={:join_date}
|
||||||
|
label={gettext("Join Date")}
|
||||||
|
sort_field={@sort_field}
|
||||||
|
sort_order={@sort_order}
|
||||||
|
/>
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{member.join_date}
|
||||||
|
</:col>
|
||||||
|
|
||||||
<:action :let={member}>
|
<:action :let={member}>
|
||||||
<div class="sr-only">
|
<div class="sr-only">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue