Compare commits

...

4 commits

Author SHA1 Message Date
9b64e35cce chore: updated translation
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-30 16:23:33 +02:00
f1e64d7986 test: added tests 2025-09-30 16:19:52 +02:00
953a351cd9 docs: formatting, docs and accessibility fix 2025-09-30 16:19:36 +02:00
1ac89d7c28 feat: sort header for members list 2025-09-26 11:10:14 +02:00
10 changed files with 443 additions and 103 deletions

View file

@ -0,0 +1,61 @@
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
- sort_order: :asc | :desc | nil # current sorting order
"""
use MvWeb, :live_component
@impl true
def update(assigns, socket) do
{:ok, assign(socket, assigns)}
end
# Check if we can add the aria-sort label directly to the daisyUI header
# aria-sort={aria_sort(@field, @sort_field, @sort_order)}
@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>
"""
end
@impl true
def handle_event("sort", %{"field" => field_str}, socket) do
send(self(), {:sort, field_str})
{:noreply, socket}
end
# -------------------------------------------------
# Hilfsfunktionen für ARIAAttribute & IconSVG
# -------------------------------------------------
defp aria_sort(field, sort_field, dir) when field == sort_field do
case dir do
:asc -> gettext("ascending")
:desc -> gettext("descending")
end
end
defp aria_sort(_, _, _), do: gettext("Click to sort")
end

View file

@ -1,21 +1,25 @@
defmodule MvWeb.MemberLive.Index do
use MvWeb, :live_view
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)
def mount(params, _session, socket) do
socket =
socket
|> assign(:page_title, gettext("Members"))
|> assign_new(:sort_field, fn -> :first_name end)
|> assign_new(:sort_order, fn -> :asc end)
|> assign(:selected_members, [])
{:ok,
socket
|> assign(:page_title, gettext("Members"))
|> assign(:sort_field, :first_name)
|> assign(:sort_order, :asc)
|> assign(:members, sorted)
|> assign(:selected_members, [])}
# We call handle params to use the query from the URL
{:noreply, socket} = handle_params(params, nil, socket)
{:ok, socket}
end
# -----------------------------------------------------------------
# Handle Events
# -----------------------------------------------------------------
# Delete a member
@impl true
def handle_event("delete", %{"id" => id}, socket) do
member = Ash.get!(Mv.Membership.Member, id)
@ -37,32 +41,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
@ -79,8 +58,117 @@ 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
# -------------------------------------------------------------
# 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 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}])
# Function to maybe update the sort
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
socket
|> assign(:sort_field, field)
|> assign(:sort_order, order)
end
defp maybe_update_sort(socket, _), do: socket
end

View file

@ -45,23 +45,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>
<:col :let={member} label={gettext("Email")}>{member.email}</:col>
<:col :let={member} label={gettext("Street")}>{member.street}</:col>
<:col :let={member} label={gettext("House Number")}>{member.house_number}</:col>
<:col :let={member} label={gettext("Postal Code")}>{member.postal_code}</:col>
<:col :let={member} label={gettext("City")}>{member.city}</:col>
<:col :let={member} label={gettext("Phone Number")}>{member.phone_number}</:col>
<:col :let={member} label={gettext("Join Date")}>{member.join_date}</:col>
<: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>
<: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}>
<div class="sr-only">

View file

@ -62,5 +62,5 @@ msgstr "Anmelden..."
msgid "Your password has successfully been reset"
msgstr "Das Passwort wurde erfolgreich zurückgesetzt"
msgid "Sign in with Rauthy"
msgstr "Anmelden mit der Vereinscloud"
#~ msgid "Sign in with Rauthy"
#~ msgstr "Anmelden mit der Vereinscloud"

View file

@ -15,7 +15,7 @@ msgstr ""
msgid "Actions"
msgstr "Aktionen"
#: lib/mv_web/live/member_live/index.html.heex:77
#: lib/mv_web/live/member_live/index.html.heex:193
#: lib/mv_web/live/user_live/index.html.heex:65
#, elixir-autogen, elixir-format
msgid "Are you sure?"
@ -28,19 +28,19 @@ msgid "Attempting to reconnect"
msgstr "Verbindung wird wiederhergestellt"
#: lib/mv_web/live/member_live/form.ex:25
#: lib/mv_web/live/member_live/index.html.heex:62
#: lib/mv_web/live/member_live/index.html.heex:138
#: lib/mv_web/live/member_live/show.ex:36
#, elixir-autogen, elixir-format
msgid "City"
msgstr "Stadt"
#: lib/mv_web/live/member_live/index.html.heex:79
#: lib/mv_web/live/member_live/index.html.heex:195
#: lib/mv_web/live/user_live/index.html.heex:67
#, elixir-autogen, elixir-format
msgid "Delete"
msgstr "Löschen"
#: lib/mv_web/live/member_live/index.html.heex:71
#: lib/mv_web/live/member_live/index.html.heex:187
#: lib/mv_web/live/user_live/form.ex:109
#: lib/mv_web/live/user_live/index.html.heex:59
#, elixir-autogen, elixir-format
@ -54,7 +54,7 @@ msgid "Edit Member"
msgstr "Mitglied bearbeiten"
#: lib/mv_web/live/member_live/form.ex:18
#: lib/mv_web/live/member_live/index.html.heex:58
#: lib/mv_web/live/member_live/index.html.heex:70
#: lib/mv_web/live/member_live/show.ex:27
#: lib/mv_web/live/user_live/form.ex:14
#: lib/mv_web/live/user_live/index.html.heex:44
@ -70,7 +70,7 @@ msgid "First Name"
msgstr "Vorname"
#: lib/mv_web/live/member_live/form.ex:22
#: lib/mv_web/live/member_live/index.html.heex:64
#: lib/mv_web/live/member_live/index.html.heex:172
#: lib/mv_web/live/member_live/show.ex:33
#, elixir-autogen, elixir-format
msgid "Join Date"
@ -87,7 +87,7 @@ msgstr "Nachname"
msgid "New Member"
msgstr "Neues Mitglied"
#: lib/mv_web/live/member_live/index.html.heex:68
#: lib/mv_web/live/member_live/index.html.heex:184
#: lib/mv_web/live/user_live/index.html.heex:56
#, elixir-autogen, elixir-format
msgid "Show"
@ -127,7 +127,7 @@ msgid "Exit Date"
msgstr "Austrittsdatum"
#: lib/mv_web/live/member_live/form.ex:27
#: lib/mv_web/live/member_live/index.html.heex:60
#: lib/mv_web/live/member_live/index.html.heex:104
#: lib/mv_web/live/member_live/show.ex:38
#, elixir-autogen, elixir-format
msgid "House Number"
@ -146,14 +146,14 @@ msgid "Paid"
msgstr "Bezahlt"
#: lib/mv_web/live/member_live/form.ex:21
#: lib/mv_web/live/member_live/index.html.heex:63
#: lib/mv_web/live/member_live/index.html.heex:155
#: lib/mv_web/live/member_live/show.ex:32
#, elixir-autogen, elixir-format
msgid "Phone Number"
msgstr "Telefonnummer"
#: lib/mv_web/live/member_live/form.ex:28
#: lib/mv_web/live/member_live/index.html.heex:61
#: lib/mv_web/live/member_live/index.html.heex:121
#: lib/mv_web/live/member_live/show.ex:39
#, elixir-autogen, elixir-format
msgid "Postal Code"
@ -173,7 +173,7 @@ msgid "Saving..."
msgstr "Speichern..."
#: lib/mv_web/live/member_live/form.ex:26
#: lib/mv_web/live/member_live/index.html.heex:59
#: lib/mv_web/live/member_live/index.html.heex:87
#: lib/mv_web/live/member_live/show.ex:37
#, elixir-autogen, elixir-format
msgid "Street"
@ -318,13 +318,12 @@ msgid "Member"
msgstr "Mitglied"
#: lib/mv_web/components/layouts/navbar.ex:14
#: lib/mv_web/live/member_live/index.ex:12
#: lib/mv_web/live/member_live/index.ex:8
#: lib/mv_web/live/member_live/index.html.heex:3
#, elixir-autogen, elixir-format
msgid "Members"
msgstr "Mitglieder"
#: lib/mv_web/live/member_live/index.html.heex:50
#: lib/mv_web/live/property_type_live/form.ex:16
#, elixir-autogen, elixir-format
msgid "Name"
@ -469,11 +468,13 @@ msgid "Value type"
msgstr "Wertetyp"
#: lib/mv_web/components/table_components.ex:30
#: lib/mv_web/live/components/sort_header_component.ex:55
#, elixir-autogen, elixir-format
msgid "ascending"
msgstr "aufsteigend"
#: lib/mv_web/components/table_components.ex:30
#: lib/mv_web/live/components/sort_header_component.ex:56
#, elixir-autogen, elixir-format
msgid "descending"
msgstr "absteigend"
@ -553,7 +554,17 @@ msgstr "Passwort setzen"
msgid "User will be created without a password. Check 'Set Password' to add one."
msgstr "Benutzer wird ohne Passwort erstellt. Aktivieren Sie 'Passwort setzen', um eines hinzuzufügen."
#: lib/mv_web/auth_overrides.ex:30
#: lib/mv_web/live/components/sort_header_component.ex:60
#, elixir-autogen, elixir-format
msgid "or"
msgstr "oder"
msgid "Click to sort"
msgstr "Klicke um zu sortieren"
#: lib/mv_web/live/member_live/index.html.heex:53
#, elixir-autogen, elixir-format, fuzzy
msgid "First name"
msgstr "Vorname"
#~ #: lib/mv_web/auth_overrides.ex:30
#~ #, elixir-autogen, elixir-format
#~ msgid "or"
#~ msgstr "oder"

View file

@ -16,7 +16,7 @@ msgstr ""
msgid "Actions"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:77
#: lib/mv_web/live/member_live/index.html.heex:193
#: lib/mv_web/live/user_live/index.html.heex:65
#, elixir-autogen, elixir-format
msgid "Are you sure?"
@ -29,19 +29,19 @@ msgid "Attempting to reconnect"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:25
#: lib/mv_web/live/member_live/index.html.heex:62
#: lib/mv_web/live/member_live/index.html.heex:138
#: lib/mv_web/live/member_live/show.ex:36
#, elixir-autogen, elixir-format
msgid "City"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:79
#: lib/mv_web/live/member_live/index.html.heex:195
#: lib/mv_web/live/user_live/index.html.heex:67
#, elixir-autogen, elixir-format
msgid "Delete"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:71
#: lib/mv_web/live/member_live/index.html.heex:187
#: lib/mv_web/live/user_live/form.ex:109
#: lib/mv_web/live/user_live/index.html.heex:59
#, elixir-autogen, elixir-format
@ -55,7 +55,7 @@ msgid "Edit Member"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:18
#: lib/mv_web/live/member_live/index.html.heex:58
#: lib/mv_web/live/member_live/index.html.heex:70
#: lib/mv_web/live/member_live/show.ex:27
#: lib/mv_web/live/user_live/form.ex:14
#: lib/mv_web/live/user_live/index.html.heex:44
@ -71,7 +71,7 @@ msgid "First Name"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:22
#: lib/mv_web/live/member_live/index.html.heex:64
#: lib/mv_web/live/member_live/index.html.heex:172
#: lib/mv_web/live/member_live/show.ex:33
#, elixir-autogen, elixir-format
msgid "Join Date"
@ -88,7 +88,7 @@ msgstr ""
msgid "New Member"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:68
#: lib/mv_web/live/member_live/index.html.heex:184
#: lib/mv_web/live/user_live/index.html.heex:56
#, elixir-autogen, elixir-format
msgid "Show"
@ -128,7 +128,7 @@ msgid "Exit Date"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:27
#: lib/mv_web/live/member_live/index.html.heex:60
#: lib/mv_web/live/member_live/index.html.heex:104
#: lib/mv_web/live/member_live/show.ex:38
#, elixir-autogen, elixir-format
msgid "House Number"
@ -147,14 +147,14 @@ msgid "Paid"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:21
#: lib/mv_web/live/member_live/index.html.heex:63
#: lib/mv_web/live/member_live/index.html.heex:155
#: lib/mv_web/live/member_live/show.ex:32
#, elixir-autogen, elixir-format
msgid "Phone Number"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:28
#: lib/mv_web/live/member_live/index.html.heex:61
#: lib/mv_web/live/member_live/index.html.heex:121
#: lib/mv_web/live/member_live/show.ex:39
#, elixir-autogen, elixir-format
msgid "Postal Code"
@ -174,7 +174,7 @@ msgid "Saving..."
msgstr ""
#: lib/mv_web/live/member_live/form.ex:26
#: lib/mv_web/live/member_live/index.html.heex:59
#: lib/mv_web/live/member_live/index.html.heex:87
#: lib/mv_web/live/member_live/show.ex:37
#, elixir-autogen, elixir-format
msgid "Street"
@ -319,13 +319,12 @@ msgid "Member"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:14
#: lib/mv_web/live/member_live/index.ex:12
#: lib/mv_web/live/member_live/index.ex:8
#: lib/mv_web/live/member_live/index.html.heex:3
#, elixir-autogen, elixir-format
msgid "Members"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:50
#: lib/mv_web/live/property_type_live/form.ex:16
#, elixir-autogen, elixir-format
msgid "Name"
@ -470,11 +469,13 @@ msgid "Value type"
msgstr ""
#: lib/mv_web/components/table_components.ex:30
#: lib/mv_web/live/components/sort_header_component.ex:55
#, elixir-autogen, elixir-format
msgid "ascending"
msgstr ""
#: lib/mv_web/components/table_components.ex:30
#: lib/mv_web/live/components/sort_header_component.ex:56
#, elixir-autogen, elixir-format
msgid "descending"
msgstr ""
@ -554,7 +555,12 @@ msgstr ""
msgid "User will be created without a password. Check 'Set Password' to add one."
msgstr ""
#: lib/mv_web/auth_overrides.ex:30
#: lib/mv_web/live/components/sort_header_component.ex:60
#, elixir-autogen, elixir-format
msgid "or"
msgid "Click to sort"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:53
#, elixir-autogen, elixir-format
msgid "First name"
msgstr ""

View file

@ -59,5 +59,5 @@ msgstr ""
msgid "Your password has successfully been reset"
msgstr ""
msgid "Sign in with Rauthy"
msgstr "Sign in with Vereinscloud"
#~ msgid "Sign in with Rauthy"
#~ msgstr "Sign in with Vereinscloud"

View file

@ -16,7 +16,7 @@ msgstr ""
msgid "Actions"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:77
#: lib/mv_web/live/member_live/index.html.heex:193
#: lib/mv_web/live/user_live/index.html.heex:65
#, elixir-autogen, elixir-format
msgid "Are you sure?"
@ -29,19 +29,19 @@ msgid "Attempting to reconnect"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:25
#: lib/mv_web/live/member_live/index.html.heex:62
#: lib/mv_web/live/member_live/index.html.heex:138
#: lib/mv_web/live/member_live/show.ex:36
#, elixir-autogen, elixir-format
msgid "City"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:79
#: lib/mv_web/live/member_live/index.html.heex:195
#: lib/mv_web/live/user_live/index.html.heex:67
#, elixir-autogen, elixir-format
msgid "Delete"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:71
#: lib/mv_web/live/member_live/index.html.heex:187
#: lib/mv_web/live/user_live/form.ex:109
#: lib/mv_web/live/user_live/index.html.heex:59
#, elixir-autogen, elixir-format
@ -55,7 +55,7 @@ msgid "Edit Member"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:18
#: lib/mv_web/live/member_live/index.html.heex:58
#: lib/mv_web/live/member_live/index.html.heex:70
#: lib/mv_web/live/member_live/show.ex:27
#: lib/mv_web/live/user_live/form.ex:14
#: lib/mv_web/live/user_live/index.html.heex:44
@ -71,7 +71,7 @@ msgid "First Name"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:22
#: lib/mv_web/live/member_live/index.html.heex:64
#: lib/mv_web/live/member_live/index.html.heex:172
#: lib/mv_web/live/member_live/show.ex:33
#, elixir-autogen, elixir-format
msgid "Join Date"
@ -88,7 +88,7 @@ msgstr ""
msgid "New Member"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:68
#: lib/mv_web/live/member_live/index.html.heex:184
#: lib/mv_web/live/user_live/index.html.heex:56
#, elixir-autogen, elixir-format
msgid "Show"
@ -128,7 +128,7 @@ msgid "Exit Date"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:27
#: lib/mv_web/live/member_live/index.html.heex:60
#: lib/mv_web/live/member_live/index.html.heex:104
#: lib/mv_web/live/member_live/show.ex:38
#, elixir-autogen, elixir-format
msgid "House Number"
@ -147,14 +147,14 @@ msgid "Paid"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:21
#: lib/mv_web/live/member_live/index.html.heex:63
#: lib/mv_web/live/member_live/index.html.heex:155
#: lib/mv_web/live/member_live/show.ex:32
#, elixir-autogen, elixir-format
msgid "Phone Number"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:28
#: lib/mv_web/live/member_live/index.html.heex:61
#: lib/mv_web/live/member_live/index.html.heex:121
#: lib/mv_web/live/member_live/show.ex:39
#, elixir-autogen, elixir-format
msgid "Postal Code"
@ -174,7 +174,7 @@ msgid "Saving..."
msgstr ""
#: lib/mv_web/live/member_live/form.ex:26
#: lib/mv_web/live/member_live/index.html.heex:59
#: lib/mv_web/live/member_live/index.html.heex:87
#: lib/mv_web/live/member_live/show.ex:37
#, elixir-autogen, elixir-format
msgid "Street"
@ -319,13 +319,12 @@ msgid "Member"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:14
#: lib/mv_web/live/member_live/index.ex:12
#: lib/mv_web/live/member_live/index.ex:8
#: lib/mv_web/live/member_live/index.html.heex:3
#, elixir-autogen, elixir-format
msgid "Members"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:50
#: lib/mv_web/live/property_type_live/form.ex:16
#, elixir-autogen, elixir-format
msgid "Name"
@ -470,11 +469,13 @@ msgid "Value type"
msgstr ""
#: lib/mv_web/components/table_components.ex:30
#: lib/mv_web/live/components/sort_header_component.ex:55
#, elixir-autogen, elixir-format
msgid "ascending"
msgstr ""
#: lib/mv_web/components/table_components.ex:30
#: lib/mv_web/live/components/sort_header_component.ex:56
#, elixir-autogen, elixir-format
msgid "descending"
msgstr ""
@ -554,7 +555,17 @@ msgstr "Set Password"
msgid "User will be created without a password. Check 'Set Password' to add one."
msgstr "User will be created without a password. Check 'Set Password' to add one."
#: lib/mv_web/auth_overrides.ex:30
#: lib/mv_web/live/components/sort_header_component.ex:60
#, elixir-autogen, elixir-format
msgid "or"
msgid "Click to sort"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:53
#, elixir-autogen, elixir-format, fuzzy
msgid "First name"
msgstr ""
#~ #: lib/mv_web/auth_overrides.ex:30
#~ #, elixir-autogen, elixir-format
#~ msgid "or"
#~ msgstr ""

View file

@ -0,0 +1,12 @@
defmodule MvWeb.Components.SortHeaderComponentTest do
use MvWeb.ConnCase, async: true
use Phoenix.Component
import Phoenix.LiveViewTest
test "renders sort header with correct attributes", %{conn: conn} do
conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members")
assert view |> element("[data-testid='first_name']")
end
end

View file

@ -55,7 +55,6 @@ defmodule MvWeb.MemberLive.IndexTest do
test "shows translated flash message after creating a member in English", %{conn: conn} do
conn = conn_with_oidc_user(conn)
conn = Plug.Test.init_test_session(conn, locale: "en")
{:ok, form_view, _html} = live(conn, "/members/new")
form_data = %{
@ -73,4 +72,40 @@ defmodule MvWeb.MemberLive.IndexTest do
assert has_element?(index_view, "#flash-group", "Member create successfully")
end
describe "sorting interaction" do
test "clicking a column header toggles sort order and updates the URL", %{conn: conn} do
conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members")
# The component data test ids are built as "<field>"
# First click should sort ASC
view
|> element("[data-testid='email']")
|> render_click()
# The LiveView pushes a patch with the new query params
assert_patch(view, "/members?sort_field=email&sort_order=asc")
# Second click toggles to DESC
view
|> element("[data-testid='email']")
|> render_click()
assert_patch(view, "/members?sort_field=email&sort_order=desc")
end
end
describe "URL param handling" do
test "handle_params reads sort query and applies it", %{conn: conn} do
conn = conn_with_oidc_user(conn)
url = "/members?sort_field=email&sort_order=desc"
conn = get(conn, url)
# The LiveView must have parsed the params and stored them as atoms.
assert conn.assigns.sort_field == :email
assert conn.assigns.sort_order == :desc
end
end
end