diff --git a/Justfile b/Justfile index 1874b67..b28dbdc 100644 --- a/Justfile +++ b/Justfile @@ -35,8 +35,8 @@ audit: mix deps.audit mix hex.audit -test: install-dependencies start-database - mix test +test *args: install-dependencies start-database + mix test {{args}} format: mix format diff --git a/lib/mv_web/live/components/sort_header_component.ex b/lib/mv_web/live/components/sort_header_component.ex new file mode 100644 index 0000000..e69357e --- /dev/null +++ b/lib/mv_web/live/components/sort_header_component.ex @@ -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() # Ash‑Field for sorting + - label: string() # Column Heading (can be aan heex templyte) + - sort_field: atom() | nil # current sort-field from parent liveview + - sort_order: :asc | :desc | nil # current sorting order + """ + use MvWeb, :live_component + + @impl true + def update(assigns, socket) do + {:ok, assign(socket, assigns)} + end + + # 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""" + + """ + end + + @impl true + def handle_event("sort", %{"field" => field_str}, socket) do + send(self(), {:sort, field_str}) + {:noreply, socket} + end + + # ------------------------------------------------- + # Hilfsfunktionen für ARIA‑Attribute & Icon‑SVG + # ------------------------------------------------- + defp aria_sort(field, sort_field, dir) when field == sort_field do + case dir do + :asc -> gettext("ascending") + :desc -> gettext("descending") + end + end + + defp aria_sort(_, _, _), do: gettext("Click to sort") +end diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index 0a9d129..7a0de39 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -2,27 +2,109 @@ defmodule MvWeb.MemberLive.Index do use MvWeb, :live_view import Ash.Expr import Ash.Query - 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(:query, "") + |> 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(:query, "") - |> 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 # ----------------------------------------------------------------- - # 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 @impl true def handle_info({:search_changed, q}, socket) do @@ -42,74 +124,73 @@ defmodule MvWeb.MemberLive.Index do end # ----------------------------------------------------------------- - # Handle Events + # Handle Params from the URL # ----------------------------------------------------------------- - @impl true - def handle_event("delete", %{"id" => id}, socket) do - member = Ash.get!(Mv.Membership.Member, id) - Ash.destroy!(member) + def handle_params(params, _url, socket) do + socket = + socket + |> maybe_update_sort(params) + |> load_members() - {:noreply, stream_delete(socket, :members, member)} + {:noreply, socket} 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 + # ------------------------------------------------------------- + # 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) - {:noreply, assign(socket, :selected_members, selected)} + members = Ash.read!(query) + assign(socket, :members, members) 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 - - @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 + # ------------------------------------------------------------- + # 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 diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex index aa7a820..4072a51 100644 --- a/lib/mv_web/live/member_live/index.html.heex +++ b/lib/mv_web/live/member_live/index.html.heex @@ -52,23 +52,139 @@ <:col :let={member} label={ - sort_button(%{ - field: :first_name, - label: gettext("Name"), - sort_field: @sort_field, - sort_order: @sort_order - }) + ~H""" + <.live_component + module={MvWeb.Components.SortHeaderComponent} + id={:sort_first_name} + field={:first_name} + label={gettext("First name")} + sort_field={@sort_field} + sort_order={@sort_order} + /> + """ } > {member.first_name} {member.last_name} - <:col :let={member} label={gettext("Email")}>{member.email} - <:col :let={member} label={gettext("Street")}>{member.street} - <:col :let={member} label={gettext("House Number")}>{member.house_number} - <:col :let={member} label={gettext("Postal Code")}>{member.postal_code} - <:col :let={member} label={gettext("City")}>{member.city} - <:col :let={member} label={gettext("Phone Number")}>{member.phone_number} - <:col :let={member} label={gettext("Join Date")}>{member.join_date} + <:col + :let={member} + label={ + ~H""" + <.live_component + module={MvWeb.Components.SortHeaderComponent} + id={:sort_email} + field={:email} + label={gettext("Email")} + sort_field={@sort_field} + sort_order={@sort_order} + /> + """ + } + > + {member.email} + + <:col + :let={member} + label={ + ~H""" + <.live_component + module={MvWeb.Components.SortHeaderComponent} + id={:sort_street} + field={:street} + label={gettext("Street")} + sort_field={@sort_field} + sort_order={@sort_order} + /> + """ + } + > + {member.street} + + <:col + :let={member} + label={ + ~H""" + <.live_component + module={MvWeb.Components.SortHeaderComponent} + id={:sort_house_number} + field={:house_number} + label={gettext("House Number")} + sort_field={@sort_field} + sort_order={@sort_order} + /> + """ + } + > + {member.house_number} + + <:col + :let={member} + label={ + ~H""" + <.live_component + module={MvWeb.Components.SortHeaderComponent} + id={:sort_postal_code} + field={:postal_code} + label={gettext("Postal Code")} + sort_field={@sort_field} + sort_order={@sort_order} + /> + """ + } + > + {member.postal_code} + + <:col + :let={member} + label={ + ~H""" + <.live_component + module={MvWeb.Components.SortHeaderComponent} + id={:sort_city} + field={:city} + label={gettext("City")} + sort_field={@sort_field} + sort_order={@sort_order} + /> + """ + } + > + {member.city} + + <:col + :let={member} + label={ + ~H""" + <.live_component + module={MvWeb.Components.SortHeaderComponent} + id={:sort_phone_number} + field={:phone_number} + label={gettext("Phone Number")} + sort_field={@sort_field} + sort_order={@sort_order} + /> + """ + } + > + {member.phone_number} + + <:col + :let={member} + label={ + ~H""" + <.live_component + module={MvWeb.Components.SortHeaderComponent} + id={:sort_join_date} + field={:join_date} + label={gettext("Join Date")} + sort_field={@sort_field} + sort_order={@sort_order} + /> + """ + } + > + {member.join_date} + <:action :let={member}>
diff --git a/priv/gettext/de/LC_MESSAGES/auth.po b/priv/gettext/de/LC_MESSAGES/auth.po index 0f2202d..661c269 100644 --- a/priv/gettext/de/LC_MESSAGES/auth.po +++ b/priv/gettext/de/LC_MESSAGES/auth.po @@ -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" diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 1a7cf7e..14ca2d4 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -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" diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index a9bfb08..58a0182 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -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 "" diff --git a/priv/gettext/en/LC_MESSAGES/auth.po b/priv/gettext/en/LC_MESSAGES/auth.po index 1e4e801..59ce742 100644 --- a/priv/gettext/en/LC_MESSAGES/auth.po +++ b/priv/gettext/en/LC_MESSAGES/auth.po @@ -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" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 2f09378..6311138 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -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 "" diff --git a/test/mv_web/components/sort_header_component_test.exs b/test/mv_web/components/sort_header_component_test.exs new file mode 100644 index 0000000..9b1c006 --- /dev/null +++ b/test/mv_web/components/sort_header_component_test.exs @@ -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 diff --git a/test/mv_web/member_live/index_test.exs b/test/mv_web/member_live/index_test.exs index b8b573c..f697d6e 100644 --- a/test/mv_web/member_live/index_test.exs +++ b/test/mv_web/member_live/index_test.exs @@ -1,6 +1,7 @@ defmodule MvWeb.MemberLive.IndexTest do use MvWeb.ConnCase, async: true import Phoenix.LiveViewTest + require Ash.Query test "shows translated title in German", %{conn: conn} do 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 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 = %{ @@ -74,6 +74,42 @@ 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 "" + # 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 conn = conn_with_oidc_user(conn) {:ok, view, _html} = live(conn, "/members") @@ -85,4 +121,31 @@ defmodule MvWeb.MemberLive.IndexTest do assert state.socket.assigns.query == "Friedrich" assert is_list(state.socket.assigns.members) 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