Compare commits

...

7 commits

Author SHA1 Message Date
e8d440f74f formatting
All checks were successful
continuous-integration/drone/push Build is passing
2025-10-09 17:55:56 +02:00
bc5715e63e chore: updated translation 2025-10-09 17:55:56 +02:00
a15a46037f test: added tests 2025-10-09 17:55:56 +02:00
10df5f8bb9 docs: formatting, docs and accessibility fix 2025-10-09 17:55:56 +02:00
e280a28b8e feat: sort header for members list 2025-10-09 17:55:56 +02:00
e2c00f263e Merge pull request 'Fix error when deleting members' (#148) from fix-member-deletion into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #148
Reviewed-by: carla <carla@noreply.git.local-it.org>
2025-10-09 16:45:41 +02:00
7d2b719ca2
Fix error when deleting members
All checks were successful
continuous-integration/drone/push Build is passing
2025-10-09 16:42:44 +02:00
11 changed files with 499 additions and 138 deletions

View file

@ -35,8 +35,8 @@ audit:
mix deps.audit mix deps.audit
mix hex.audit mix hex.audit
test: install-dependencies start-database test *args: install-dependencies start-database
mix test mix test {{args}}
format: format:
mix format mix format

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,27 +2,109 @@ 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 end
# ----------------------------------------------------------------- # -----------------------------------------------------------------
# Receive messages from any toolbar component # Handle Events
# ----------------------------------------------------------------- # -----------------------------------------------------------------
# Delete a member
@impl true
def handle_event("delete", %{"id" => id}, socket) do
member = Ash.get!(Mv.Membership.Member, id)
Ash.destroy!(member)
updated_members = Enum.reject(socket.assigns.members, &(&1.id == id))
{:noreply, assign(socket, :members, updated_members)}
end
# Selects one member in the list of members
@impl true
def handle_event("select_member", %{"id" => id}, socket) do
selected =
if id in socket.assigns.selected_members do
List.delete(socket.assigns.selected_members, id)
else
[id | socket.assigns.selected_members]
end
{:noreply, assign(socket, :selected_members, selected)}
end
# Selects all members in the list of members
@impl true
def handle_event("select_all", _params, socket) do
members = socket.assigns.members
all_ids = Enum.map(members, & &1.id)
selected =
if Enum.sort(socket.assigns.selected_members) == Enum.sort(all_ids) do
[]
else
all_ids
end
{: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
# Function to handle search # Function to handle search
@impl true @impl true
def handle_info({:search_changed, q}, socket) do def handle_info({:search_changed, q}, socket) do
@ -42,74 +124,73 @@ defmodule MvWeb.MemberLive.Index do
end end
# ----------------------------------------------------------------- # -----------------------------------------------------------------
# Handle Events # Handle Params from the URL
# ----------------------------------------------------------------- # -----------------------------------------------------------------
@impl true @impl true
def handle_event("delete", %{"id" => id}, socket) do def handle_params(params, _url, socket) do
member = Ash.get!(Mv.Membership.Member, id) socket =
Ash.destroy!(member) socket
|> maybe_update_sort(params)
|> load_members()
{:noreply, stream_delete(socket, :members, member)} {:noreply, socket}
end end
# Selects one member in the list of members # -------------------------------------------------------------
@impl true # FUNCTIONS
def handle_event("select_member", %{"id" => id}, socket) do # -------------------------------------------------------------
selected = # Load members eg based on a query for sorting
if id in socket.assigns.selected_members do defp load_members(socket) do
List.delete(socket.assigns.selected_members, id) query =
else Mv.Membership.Member
[id | socket.assigns.selected_members] |> Ash.Query.new()
end |> 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)
{:noreply, assign(socket, :selected_members, selected)} members = Ash.read!(query)
assign(socket, :members, members)
end end
# Sorts the list of members according to a field, when you click on the column header # -------------------------------------------------------------
@impl true # Helper Functions
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
def handle_event("select_all", _params, socket) do
members = socket.assigns.members
all_ids = Enum.map(members, & &1.id)
selected =
if Enum.sort(socket.assigns.selected_members) == Enum.sort(all_ids) do
[]
else
all_ids
end
{:noreply, assign(socket, :selected_members, selected)}
end
# 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

@ -1,6 +1,7 @@
defmodule MvWeb.MemberLive.IndexTest do defmodule MvWeb.MemberLive.IndexTest do
use MvWeb.ConnCase, async: true use MvWeb.ConnCase, async: true
import Phoenix.LiveViewTest import Phoenix.LiveViewTest
require Ash.Query
test "shows translated title in German", %{conn: conn} do test "shows translated title in German", %{conn: conn} do
conn = conn_with_oidc_user(conn) conn = conn_with_oidc_user(conn)
@ -55,7 +56,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 +74,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")
@ -85,4 +121,31 @@ defmodule MvWeb.MemberLive.IndexTest do
assert state.socket.assigns.query == "Friedrich" assert state.socket.assigns.query == "Friedrich"
assert is_list(state.socket.assigns.members) assert is_list(state.socket.assigns.members)
end end
test "can delete a member without error", %{conn: conn} do
# Create a test member first
{:ok, member} =
Mv.Membership.create_member(%{
first_name: "Test",
last_name: "User",
email: "test@example.com"
})
conn = conn_with_oidc_user(conn)
{:ok, index_view, _html} = live(conn, "/members")
# Verify the member is displayed
assert has_element?(index_view, "#members", "Test User")
# Click the delete link for this member
index_view
|> element("a", "Delete")
|> render_click()
# Verify the member is no longer displayed
refute has_element?(index_view, "#members", "Test User")
# Verify the member was actually deleted from the database
assert not (Mv.Membership.Member |> Ash.Query.filter(id == ^member.id) |> Ash.exists?())
end
end end