Compare commits

...

5 commits

Author SHA1 Message Date
78cbd3cf80 formatting
All checks were successful
continuous-integration/drone/push Build is passing
2025-10-09 16:33:00 +02:00
20269bef60 chore: updated translation 2025-10-09 16:33:00 +02:00
1cce972a90 test: added tests 2025-10-09 16:33:00 +02:00
673746f045 docs: formatting, docs and accessibility fix 2025-10-09 16:33:00 +02:00
a91b2ebb1e feat: sort header for members list 2025-10-09 16:33:00 +02:00
10 changed files with 458 additions and 126 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

@ -2,49 +2,27 @@ defmodule MvWeb.MemberLive.Index do
use MvWeb, :live_view use MvWeb, :live_view
import Ash.Expr import Ash.Expr
import Ash.Query import Ash.Query
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"))
|> assign(:query, "")
|> assign_new(:sort_field, fn -> :first_name end)
|> assign_new(:sort_order, fn -> :asc end)
|> assign(:selected_members, [])
{:ok, # We call handle params to use the query from the URL
socket {:noreply, socket} = handle_params(params, nil, socket)
|> assign(:page_title, gettext("Members")) {:ok, socket}
|> assign(:query, "")
|> assign(:sort_field, :first_name)
|> assign(:sort_order, :asc)
|> assign(:members, sorted)
|> assign(:selected_members, [])}
end
# -----------------------------------------------------------------
# Receive messages from any toolbar component
# -----------------------------------------------------------------
# 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
{:noreply,
socket
|> assign(:query, q)
|> assign(:members, members)}
end end
# ----------------------------------------------------------------- # -----------------------------------------------------------------
# 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)
@ -66,32 +44,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
@ -108,8 +61,135 @@ 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
# 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
{:noreply,
socket
|> assign(:query, q)
|> assign(:members, members)}
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(: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 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 end

View file

@ -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">

View file

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

View file

@ -15,7 +15,7 @@ msgstr ""
msgid "Actions" msgid "Actions"
msgstr "Aktionen" 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 #: lib/mv_web/live/user_live/index.html.heex:65
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Are you sure?" msgid "Are you sure?"
@ -28,19 +28,19 @@ msgid "Attempting to reconnect"
msgstr "Verbindung wird wiederhergestellt" msgstr "Verbindung wird wiederhergestellt"
#: lib/mv_web/live/member_live/form.ex:25 #: 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 #: lib/mv_web/live/member_live/show.ex:36
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "City" msgid "City"
msgstr "Stadt" 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 #: lib/mv_web/live/user_live/index.html.heex:67
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Delete" msgid "Delete"
msgstr "Löschen" 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/form.ex:109
#: lib/mv_web/live/user_live/index.html.heex:59 #: lib/mv_web/live/user_live/index.html.heex:59
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -54,7 +54,7 @@ msgid "Edit Member"
msgstr "Mitglied bearbeiten" msgstr "Mitglied bearbeiten"
#: lib/mv_web/live/member_live/form.ex:18 #: 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/member_live/show.ex:27
#: lib/mv_web/live/user_live/form.ex:14 #: lib/mv_web/live/user_live/form.ex:14
#: lib/mv_web/live/user_live/index.html.heex:44 #: lib/mv_web/live/user_live/index.html.heex:44
@ -70,7 +70,7 @@ msgid "First Name"
msgstr "Vorname" msgstr "Vorname"
#: lib/mv_web/live/member_live/form.ex:22 #: 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 #: lib/mv_web/live/member_live/show.ex:33
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Join Date" msgid "Join Date"
@ -87,7 +87,7 @@ msgstr "Nachname"
msgid "New Member" msgid "New Member"
msgstr "Neues Mitglied" 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 #: lib/mv_web/live/user_live/index.html.heex:56
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Show" msgid "Show"
@ -127,7 +127,7 @@ msgid "Exit Date"
msgstr "Austrittsdatum" msgstr "Austrittsdatum"
#: lib/mv_web/live/member_live/form.ex:27 #: 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 #: lib/mv_web/live/member_live/show.ex:38
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "House Number" msgid "House Number"
@ -146,14 +146,14 @@ msgid "Paid"
msgstr "Bezahlt" msgstr "Bezahlt"
#: lib/mv_web/live/member_live/form.ex:21 #: 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 #: lib/mv_web/live/member_live/show.ex:32
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Phone Number" msgid "Phone Number"
msgstr "Telefonnummer" msgstr "Telefonnummer"
#: lib/mv_web/live/member_live/form.ex:28 #: 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 #: lib/mv_web/live/member_live/show.ex:39
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Postal Code" msgid "Postal Code"
@ -173,7 +173,7 @@ msgid "Saving..."
msgstr "Speichern..." msgstr "Speichern..."
#: lib/mv_web/live/member_live/form.ex:26 #: 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 #: lib/mv_web/live/member_live/show.ex:37
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Street" msgid "Street"
@ -318,13 +318,12 @@ msgid "Member"
msgstr "Mitglied" msgstr "Mitglied"
#: lib/mv_web/components/layouts/navbar.ex:14 #: 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 #: lib/mv_web/live/member_live/index.html.heex:3
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Members" msgid "Members"
msgstr "Mitglieder" msgstr "Mitglieder"
#: lib/mv_web/live/member_live/index.html.heex:50
#: lib/mv_web/live/property_type_live/form.ex:16 #: lib/mv_web/live/property_type_live/form.ex:16
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Name" msgid "Name"
@ -469,11 +468,13 @@ msgid "Value type"
msgstr "Wertetyp" msgstr "Wertetyp"
#: lib/mv_web/components/table_components.ex:30 #: lib/mv_web/components/table_components.ex:30
#: lib/mv_web/live/components/sort_header_component.ex:55
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "ascending" msgid "ascending"
msgstr "aufsteigend" msgstr "aufsteigend"
#: lib/mv_web/components/table_components.ex:30 #: lib/mv_web/components/table_components.ex:30
#: lib/mv_web/live/components/sort_header_component.ex:56
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "descending" msgid "descending"
msgstr "absteigend" msgstr "absteigend"
@ -553,7 +554,17 @@ msgstr "Passwort setzen"
msgid "User will be created without a password. Check 'Set Password' to add one." 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." 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 #, elixir-autogen, elixir-format
msgid "or" msgid "Click to sort"
msgstr "oder" 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" msgid "Actions"
msgstr "" 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 #: lib/mv_web/live/user_live/index.html.heex:65
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Are you sure?" msgid "Are you sure?"
@ -29,19 +29,19 @@ msgid "Attempting to reconnect"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:25 #: 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 #: lib/mv_web/live/member_live/show.ex:36
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "City" msgid "City"
msgstr "" 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 #: lib/mv_web/live/user_live/index.html.heex:67
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Delete" msgid "Delete"
msgstr "" 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/form.ex:109
#: lib/mv_web/live/user_live/index.html.heex:59 #: lib/mv_web/live/user_live/index.html.heex:59
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -55,7 +55,7 @@ msgid "Edit Member"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:18 #: 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/member_live/show.ex:27
#: lib/mv_web/live/user_live/form.ex:14 #: lib/mv_web/live/user_live/form.ex:14
#: lib/mv_web/live/user_live/index.html.heex:44 #: lib/mv_web/live/user_live/index.html.heex:44
@ -71,7 +71,7 @@ msgid "First Name"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:22 #: 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 #: lib/mv_web/live/member_live/show.ex:33
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Join Date" msgid "Join Date"
@ -88,7 +88,7 @@ msgstr ""
msgid "New Member" msgid "New Member"
msgstr "" 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 #: lib/mv_web/live/user_live/index.html.heex:56
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Show" msgid "Show"
@ -128,7 +128,7 @@ msgid "Exit Date"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:27 #: 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 #: lib/mv_web/live/member_live/show.ex:38
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "House Number" msgid "House Number"
@ -147,14 +147,14 @@ msgid "Paid"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:21 #: 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 #: lib/mv_web/live/member_live/show.ex:32
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Phone Number" msgid "Phone Number"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:28 #: 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 #: lib/mv_web/live/member_live/show.ex:39
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Postal Code" msgid "Postal Code"
@ -174,7 +174,7 @@ msgid "Saving..."
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:26 #: 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 #: lib/mv_web/live/member_live/show.ex:37
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Street" msgid "Street"
@ -319,13 +319,12 @@ msgid "Member"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:14 #: 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 #: lib/mv_web/live/member_live/index.html.heex:3
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Members" msgid "Members"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:50
#: lib/mv_web/live/property_type_live/form.ex:16 #: lib/mv_web/live/property_type_live/form.ex:16
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Name" msgid "Name"
@ -470,11 +469,13 @@ msgid "Value type"
msgstr "" msgstr ""
#: lib/mv_web/components/table_components.ex:30 #: lib/mv_web/components/table_components.ex:30
#: lib/mv_web/live/components/sort_header_component.ex:55
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "ascending" msgid "ascending"
msgstr "" msgstr ""
#: lib/mv_web/components/table_components.ex:30 #: lib/mv_web/components/table_components.ex:30
#: lib/mv_web/live/components/sort_header_component.ex:56
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "descending" msgid "descending"
msgstr "" msgstr ""
@ -554,7 +555,12 @@ msgstr ""
msgid "User will be created without a password. Check 'Set Password' to add one." msgid "User will be created without a password. Check 'Set Password' to add one."
msgstr "" msgstr ""
#: lib/mv_web/auth_overrides.ex:30 #: lib/mv_web/live/components/sort_header_component.ex:60
#, elixir-autogen, elixir-format #, 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 "" msgstr ""

View file

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

View file

@ -16,7 +16,7 @@ msgstr ""
msgid "Actions" msgid "Actions"
msgstr "" 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 #: lib/mv_web/live/user_live/index.html.heex:65
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Are you sure?" msgid "Are you sure?"
@ -29,19 +29,19 @@ msgid "Attempting to reconnect"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:25 #: 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 #: lib/mv_web/live/member_live/show.ex:36
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "City" msgid "City"
msgstr "" 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 #: lib/mv_web/live/user_live/index.html.heex:67
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Delete" msgid "Delete"
msgstr "" 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/form.ex:109
#: lib/mv_web/live/user_live/index.html.heex:59 #: lib/mv_web/live/user_live/index.html.heex:59
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -55,7 +55,7 @@ msgid "Edit Member"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:18 #: 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/member_live/show.ex:27
#: lib/mv_web/live/user_live/form.ex:14 #: lib/mv_web/live/user_live/form.ex:14
#: lib/mv_web/live/user_live/index.html.heex:44 #: lib/mv_web/live/user_live/index.html.heex:44
@ -71,7 +71,7 @@ msgid "First Name"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:22 #: 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 #: lib/mv_web/live/member_live/show.ex:33
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Join Date" msgid "Join Date"
@ -88,7 +88,7 @@ msgstr ""
msgid "New Member" msgid "New Member"
msgstr "" 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 #: lib/mv_web/live/user_live/index.html.heex:56
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Show" msgid "Show"
@ -128,7 +128,7 @@ msgid "Exit Date"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:27 #: 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 #: lib/mv_web/live/member_live/show.ex:38
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "House Number" msgid "House Number"
@ -147,14 +147,14 @@ msgid "Paid"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:21 #: 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 #: lib/mv_web/live/member_live/show.ex:32
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Phone Number" msgid "Phone Number"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:28 #: 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 #: lib/mv_web/live/member_live/show.ex:39
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Postal Code" msgid "Postal Code"
@ -174,7 +174,7 @@ msgid "Saving..."
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:26 #: 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 #: lib/mv_web/live/member_live/show.ex:37
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Street" msgid "Street"
@ -319,13 +319,12 @@ msgid "Member"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:14 #: 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 #: lib/mv_web/live/member_live/index.html.heex:3
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Members" msgid "Members"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:50
#: lib/mv_web/live/property_type_live/form.ex:16 #: lib/mv_web/live/property_type_live/form.ex:16
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Name" msgid "Name"
@ -470,11 +469,13 @@ msgid "Value type"
msgstr "" msgstr ""
#: lib/mv_web/components/table_components.ex:30 #: lib/mv_web/components/table_components.ex:30
#: lib/mv_web/live/components/sort_header_component.ex:55
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "ascending" msgid "ascending"
msgstr "" msgstr ""
#: lib/mv_web/components/table_components.ex:30 #: lib/mv_web/components/table_components.ex:30
#: lib/mv_web/live/components/sort_header_component.ex:56
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "descending" msgid "descending"
msgstr "" msgstr ""
@ -554,7 +555,17 @@ msgstr "Set Password"
msgid "User will be created without a password. Check 'Set Password' to add one." 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." 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 #, elixir-autogen, elixir-format
msgid "or" msgid "Click to sort"
msgstr "" 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 test "shows translated flash message after creating a member in English", %{conn: conn} do
conn = conn_with_oidc_user(conn) conn = conn_with_oidc_user(conn)
conn = Plug.Test.init_test_session(conn, locale: "en")
{:ok, form_view, _html} = live(conn, "/members/new") {:ok, form_view, _html} = live(conn, "/members/new")
form_data = %{ form_data = %{
@ -74,6 +73,42 @@ defmodule MvWeb.MemberLive.IndexTest do
assert has_element?(index_view, "#flash-group", "Member create successfully") assert has_element?(index_view, "#flash-group", "Member create successfully")
end 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
test "handle_info(:search_changed) updates assigns with search results", %{conn: conn} do test "handle_info(:search_changed) updates assigns with search results", %{conn: conn} do
conn = conn_with_oidc_user(conn) conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members") {:ok, view, _html} = live(conn, "/members")