Sorting header for members list closes #152 #175 #166

Merged
moritz merged 11 commits from feature/152_sorting_default_fields into main 2025-10-30 16:44:50 +01:00
2 changed files with 89 additions and 43 deletions
Showing only changes of commit 85e1f370f6 - Show all commits

View file

@ -2,9 +2,9 @@ defmodule MvWeb.Components.SortHeaderComponent do
@moduledoc """
Sort Header that can be used as column header and sorts a table:
Props:
- field: atom() # AshField for sorting
- label: string() # Column Heading (can be aan heex templyte)
- sort_field: atom() | nil # current sort-field from parent liveview
- field: atom() # Ash Field for sorting
carla marked this conversation as resolved Outdated

ambiguous Unicode character - in comment.

ambiguous Unicode character `-` in comment.
- label: string() # Column Heading (can be an heex template)
carla marked this conversation as resolved Outdated

typo templyte

typo `templyte`
- sort_field: atom() | nil # current sort field from parent liveview
- sort_order: :asc | :desc | nil # current sorting order
"""
use MvWeb, :live_component
@ -19,25 +19,27 @@ defmodule MvWeb.Components.SortHeaderComponent do
@impl true
def render(assigns) do
~H"""
<button
type="button"
aria-label={aria_sort(@field, @sort_field, @sort_order)}
class="btn btn-ghost select-none"
phx-click="sort"
phx-value-field={@field}
phx-target={@myself}
data-testid={@field}
>
{@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 %>
</button>
<div class="tooltip" data-tip={aria_sort(@field, @sort_field, @sort_order)}>
<button
type="button"
aria-label={aria_sort(@field, @sort_field, @sort_order)}
class="btn btn-ghost select-none"
phx-click="sort"
phx-value-field={@field}
phx-target={@myself}
data-testid={@field}
>
{@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 %>
</button>
</div>
"""
end
@ -48,7 +50,7 @@ defmodule MvWeb.Components.SortHeaderComponent do
end
carla marked this conversation as resolved Outdated

ambiguous Unicode character - in comment.

ambiguous Unicode character - in comment.
# -------------------------------------------------
# Hilfsfunktionen für ARIAAttribute & IconSVG
# Hilfsfunktionen für ARIA Attribute & Icon SVG
# -------------------------------------------------
defp aria_sort(field, sort_field, dir) when field == sort_field do
case dir do

View file

@ -4,7 +4,7 @@ defmodule MvWeb.MemberLive.Index do
import Ash.Query
@impl true
def mount(params, _session, socket) do
def mount(_params, _session, socket) do
socket =
socket
|> assign(:page_title, gettext("Members"))
@ -14,7 +14,6 @@ defmodule MvWeb.MemberLive.Index do
|> assign(:selected_members, [])
# We call handle params to use the query from the URL
{:noreply, socket} = handle_params(params, nil, socket)
{:ok, socket}
moritz marked this conversation as resolved Outdated

https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html
"The life-cycle is: mount/3 -> handle_params/3 -> render/1"
Therefore handle_params is called twice, in your mount/3 and afterwards by the LiveView. Each handdle_params loads all the members. Is this line necessary at all? If I just remove it it still works for me.

https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html "The life-cycle is: mount/3 -> handle_params/3 -> render/1" Therefore handle_params is called twice, in your mount/3 and afterwards by the LiveView. Each handdle_params loads all the members. Is this line necessary at all? If I just remove it it still works for me.
end
@ -70,6 +69,7 @@ defmodule MvWeb.MemberLive.Index do
@impl true
def handle_info({:sort, field_str}, socket) do
field = String.to_existing_atom(field_str)
old_field = socket.assigns.sort_field
{new_order, new_field} =
if socket.assigns.sort_field == field do
@ -79,28 +79,38 @@ defmodule MvWeb.MemberLive.Index do
end
active_id = :"sort_#{new_field}"
old_id = :"sort_#{old_field}"
# Update the SortHeader to
# Update the new SortHeader
moritz marked this conversation as resolved Outdated

@moritz wrote in #166 (comment):

If the rows are sorted by a specific column the column header shows a single arrow in the sorting direction. The other rows show a double arrow (up and down). If I sort for another column the double arrow switches into a single arrow, but the previous column header arrow keeps the same. So it's not possible to see which column is the column sorted by. It would be good to reset the sorting arrows of the previous columns. Maybe something more obvious to highlight which is the current sort column would be nice too.

maybe something like this could work to reset the old header component:

   old_id = :"sort_#{old_field}"

    # Update the SortHeader to
    send_update(MvWeb.Components.SortHeaderComponent,
      id: old_id,
      sort_field: new_field,
      sort_order: new_order
    )
@moritz wrote in https://git.local-it.org/local-it/mitgliederverwaltung/pulls/166#issuecomment-13801: > If the rows are sorted by a specific column the column header shows a single arrow in the sorting direction. The other rows show a double arrow (up and down). If I sort for another column the double arrow switches into a single arrow, but the previous column header arrow keeps the same. So it's not possible to see which column is the column sorted by. It would be good to reset the sorting arrows of the previous columns. Maybe something more obvious to highlight which is the current sort column would be nice too. maybe something like this could work to reset the old header component: ``` old_id = :"sort_#{old_field}" # Update the SortHeader to send_update(MvWeb.Components.SortHeaderComponent, id: old_id, sort_field: new_field, sort_order: new_order ) ```
send_update(MvWeb.Components.SortHeaderComponent,
id: active_id,
sort_field: new_field,
sort_order: new_order
)
# Reset the current SortHeader
send_update(MvWeb.Components.SortHeaderComponent,
id: old_id,
sort_field: new_field,
sort_order: new_order
)
moritz marked this conversation as resolved Outdated

Best practice: instead of hardcoded paths, render path dynamically:

current_path = socket.view.__live__() |> elem(0)
new_path = current_path <> "?" <> URI.encode_query(query_params)

Also the ~p sigil could be used for creating a path.

Best practice: instead of hardcoded paths, render path dynamically: ``` current_path = socket.view.__live__() |> elem(0) new_path = current_path <> "?" <> URI.encode_query(query_params) ``` Also the `~p` sigil could be used for creating a path.

I tried that already before and did not find a simple solution for getting the current path. So I would keep that static for now.

I tried that already before and did not find a simple solution for getting the current path. So I would keep that static for now.
existing_search_query = socket.assigns.query
# Build the URL with queries
query_params = %{
"query" => existing_search_query,
"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)
# Set the new path with params
new_path = ~p"/members?#{query_params}"
moritz marked this conversation as resolved Outdated

This function ignores the sorting. This leads to reset the sorting while searching.
To avoid duplicate code I would recommend to integrate the search logic into load_members.

This function ignores the sorting. This leads to reset the sorting while searching. To avoid duplicate code I would recommend to integrate the search logic into `load_members`.
# Push the new URL
{:noreply,
push_patch(socket,
to: new_path,
# replace true
replace: true
)}
end
@ -108,19 +118,27 @@ defmodule MvWeb.MemberLive.Index do
# Function to handle search
@impl true
def handle_info({:search_changed, q}, socket) do
members =
if String.trim(q) == "" do
Ash.read!(Mv.Membership.Member)
else
Mv.Membership.Member
|> filter(expr(fragment("search_vector @@ plainto_tsquery('simple', ?)", ^q)))
|> Ash.read!()
end
socket = load_members(socket, q)
existing_field_query = socket.assigns.sort_field
existing_sort_query = socket.assigns.sort_order
# Build the URL with queries
query_params = %{
"query" => q,
"sort_field" => existing_field_query,
"sort_order" => existing_sort_query
}
# Set the new path with params
new_path = ~p"/members?#{query_params}"
# Push the new URL
{:noreply,
socket
|> assign(:query, q)
|> assign(:members, members)}
push_patch(socket,
to: new_path,
replace: true
)}
end
# -----------------------------------------------------------------
@ -130,8 +148,9 @@ defmodule MvWeb.MemberLive.Index do
def handle_params(params, _url, socket) do
socket =
socket
|> maybe_update_search(params)
|> maybe_update_sort(params)
|> load_members()
|> load_members(params["query"])
{:noreply, socket}
end
@ -140,7 +159,7 @@ defmodule MvWeb.MemberLive.Index do
# FUNCTIONS
# -------------------------------------------------------------
# Load members eg based on a query for sorting
defp load_members(socket) do
defp load_members(socket, search_query) do
query =
Mv.Membership.Member
|> Ash.Query.new()
@ -156,7 +175,12 @@ defmodule MvWeb.MemberLive.Index do
:phone_number,
:join_date
])
|> maybe_sort(socket.assigns.sort_field, socket.assigns.sort_order)
# Apply the search filter first
query = apply_search_filter(query, search_query)
# Apply sorting based on current socket state
query = maybe_sort(query, socket.assigns.sort_field, socket.assigns.sort_order)
members = Ash.read!(query)
assign(socket, :members, members)
@ -166,6 +190,16 @@ defmodule MvWeb.MemberLive.Index do
# Helper Functions
# -------------------------------------------------------------
# Function to apply search query
defp apply_search_filter(query, search_query) do
if search_query && String.trim(search_query) != "" do
query
|> filter(expr(fragment("search_vector @@ plainto_tsquery('simple', ?)", ^search_query)))
else
query
end
end
# Functions to toggle sorting order
defp toggle_order(:asc), do: :desc
defp toggle_order(:desc), do: :asc
@ -193,4 +227,14 @@ defmodule MvWeb.MemberLive.Index do
end
defp maybe_update_sort(socket, _), do: socket
# Function to update search parameters
defp maybe_update_search(socket, %{"query" => query}) when query != "" do
assign(socket, :query, query)
end
defp maybe_update_search(socket, _params) do
# Keep the previous search query if no new one is provided
socket
end
end