From 05812807d6d88f945676aa36a0f8ae6d9f178a28 Mon Sep 17 00:00:00 2001 From: carla Date: Fri, 26 Sep 2025 11:10:14 +0200 Subject: [PATCH] feat: sort header for members list --- .../live/components/sort_header_component.ex | 64 +++++++ lib/mv_web/live/member_live/index.ex | 173 ++++++++++++++---- lib/mv_web/live/member_live/index.html.heex | 142 ++++++++++++-- 3 files changed, 327 insertions(+), 52 deletions(-) create mode 100644 lib/mv_web/live/components/sort_header_component.ex diff --git a/lib/mv_web/live/components/sort_header_component.ex b/lib/mv_web/live/components/sort_header_component.ex new file mode 100644 index 0000000..147001e --- /dev/null +++ b/lib/mv_web/live/components/sort_header_component.ex @@ -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""" + + """ + 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 diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index 47a36ef..066b5e3 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -5,18 +5,18 @@ defmodule MvWeb.MemberLive.Index do import MvWeb.TableComponents @impl true - def mount(_params, _session, socket) do - members = Ash.read!(Mv.Membership.Member) - sorted = Enum.sort_by(members, & &1.first_name) - - {:ok, - socket - |> assign(:page_title, gettext("Members")) + def mount(params, _session, socket) do + socket = + socket + |> assign(:page_title, gettext("Members")) |> assign(:query, "") - |> assign(:sort_field, :first_name) - |> assign(:sort_order, :asc) - |> assign(:members, sorted) - |> assign(:selected_members, [])} + |> assign_new(:sort_field, fn -> :first_name end) + |> assign_new(:sort_order, fn -> :asc end) + |> assign(:selected_members, []) + + # We call handle params to use the query from the URL + {:noreply, socket} = handle_params(params, nil, socket) + {:ok, socket} end # ----------------------------------------------------------------- @@ -45,6 +45,11 @@ defmodule MvWeb.MemberLive.Index do # Handle Events # ----------------------------------------------------------------- + # ----------------------------------------------------------------- + # Handle Events + # ----------------------------------------------------------------- + + # Delete a member @impl true def handle_event("delete", %{"id" => id}, socket) do member = Ash.get!(Mv.Membership.Member, id) @@ -67,32 +72,7 @@ defmodule MvWeb.MemberLive.Index do {:noreply, assign(socket, :selected_members, selected)} end - # Sorts the list of members according to a field, when you click on the column header - @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 - + # Selects all members in the list of members @impl true def handle_event("select_all", _params, socket) do members = socket.assigns.members @@ -109,8 +89,123 @@ defmodule MvWeb.MemberLive.Index do {:noreply, assign(socket, :selected_members, selected)} 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(:desc), do: :asc - defp sort_fun(:asc), do: &<=/2 - defp sort_fun(:desc), do: &>=/2 + defp toggle_order(nil), do: :asc + + # 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 diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex index 410728a..cb2ccd8 100644 --- a/lib/mv_web/live/member_live/index.html.heex +++ b/lib/mv_web/live/member_live/index.html.heex @@ -52,23 +52,139 @@ <:col :let={member} label={ - sort_button(%{ - field: :first_name, - label: gettext("Name"), - sort_field: @sort_field, - sort_order: @sort_order - }) + ~H""" + <.live_component + module={MvWeb.Components.SortHeaderComponent} + id={:sort_first_name} + field={:first_name} + label={gettext("First name")} + sort_field={@sort_field} + sort_order={@sort_order} + /> + """ } > {member.first_name} {member.last_name} - <:col :let={member} label={gettext("Email")}>{member.email} - <:col :let={member} label={gettext("Street")}>{member.street} - <:col :let={member} label={gettext("House Number")}>{member.house_number} - <:col :let={member} label={gettext("Postal Code")}>{member.postal_code} - <:col :let={member} label={gettext("City")}>{member.city} - <:col :let={member} label={gettext("Phone Number")}>{member.phone_number} - <:col :let={member} label={gettext("Join Date")}>{member.join_date} + <:col + :let={member} + label={ + ~H""" + <.live_component + module={MvWeb.Components.SortHeaderComponent} + id={:sort_email} + field={:email} + label={gettext("Email")} + sort_field={@sort_field} + sort_order={@sort_order} + /> + """ + } + > + {member.email} + + <: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 + :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 + :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 + :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 + :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 + :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} + <:action :let={member}>