|
|
@ -8,10 +8,10 @@ defmodule MvWeb.Components.SearchBarComponent do
|
|||
use MvWeb, :live_component
|
||||
|
||||
@impl true
|
||||
def update(_assigns, socket) do
|
||||
def update(%{query: query}, socket) do
|
||||
socket =
|
||||
socket
|
||||
|> assign_new(:query, fn -> "" end)
|
||||
|> assign_new(:query, fn -> query || "" end)
|
||||
|> assign_new(:placeholder, fn -> gettext("Search...") end)
|
||||
|
||||
{:ok, socket}
|
||||
|
|
@ -20,7 +20,7 @@ defmodule MvWeb.Components.SearchBarComponent do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<form phx-change="search" phx-target={@myself} class="flex" role="search" aria-label="Search">
|
||||
<form phx-submit="search" phx-target={@myself} class="flex" role="search" aria-label="Search">
|
||||
<label class="input">
|
||||
<svg
|
||||
class="h-[1em] opacity-50"
|
||||
|
|
@ -44,6 +44,9 @@ defmodule MvWeb.Components.SearchBarComponent do
|
|||
placeholder={@placeholder}
|
||||
value={@query}
|
||||
name="query"
|
||||
data-testid="search-input"
|
||||
phx-change="search"
|
||||
phx-target={@myself}
|
||||
phx-debounce="300"
|
||||
/>
|
||||
</label>
|
||||
|
|
|
|||
64
lib/mv_web/live/components/sort_header_component.ex
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
defmodule MvWeb.Components.SortHeaderComponent do
|
||||
@moduledoc """
|
||||
Sort Header that can be used as column header and sorts a table:
|
||||
Props:
|
||||
- field: atom() # Ash Field for sorting
|
||||
|
carla marked this conversation as resolved
Outdated
|
||||
- label: string() # Column Heading (can be an heex template)
|
||||
|
carla marked this conversation as resolved
Outdated
moritz
commented
typo typo `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"""
|
||||
<div class="tooltip" data-tip={aria_sort(@field, @sort_field, @sort_order)}>
|
||||
<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>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("sort", %{"field" => field_str}, socket) do
|
||||
send(self(), {:sort, field_str})
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
|
carla marked this conversation as resolved
Outdated
moritz
commented
ambiguous Unicode character - in comment. ambiguous Unicode character - in comment.
|
||||
# -------------------------------------------------
|
||||
# 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")
|
||||
nil -> gettext("Click to sort")
|
||||
end
|
||||
end
|
||||
|
||||
defp aria_sort(_, _, _), do: gettext("Click to sort")
|
||||
end
|
||||
|
|
@ -2,49 +2,26 @@ 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)
|
||||
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, [])}
|
||||
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)}
|
||||
# We call handle params to use the query from the URL
|
||||
{:ok, socket}
|
||||
|
moritz marked this conversation as resolved
Outdated
moritz
commented
https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html
"The life-cycle is: mount/3 -> handle_params/3 -> render/1"
Therefore handle_params is called twice, in your mount/3 and afterwards by the LiveView. Each handdle_params loads all the members. Is this line necessary at all? If I just remove it it still works for me.
|
||||
end
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Handle Events
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
# Delete a member
|
||||
@impl true
|
||||
def handle_event("delete", %{"id" => id}, socket) do
|
||||
member = Ash.get!(Mv.Membership.Member, id)
|
||||
|
|
@ -67,32 +44,7 @@ defmodule MvWeb.MemberLive.Index do
|
|||
{:noreply, assign(socket, :selected_members, selected)}
|
||||
end
|
||||
|
||||
# Sorts the list of members according to a field, when you click on the column header
|
||||
@impl true
|
||||
def handle_event("sort", %{"field" => field_str}, socket) do
|
||||
members = socket.assigns.members
|
||||
field = String.to_existing_atom(field_str)
|
||||
|
||||
new_order =
|
||||
if socket.assigns.sort_field == field do
|
||||
toggle_order(socket.assigns.sort_order)
|
||||
else
|
||||
:asc
|
||||
end
|
||||
|
||||
sorted_members =
|
||||
members
|
||||
|> Enum.sort_by(&Map.get(&1, field), sort_fun(new_order))
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:sort_field, field)
|
||||
|> assign(:sort_order, new_order)
|
||||
|> assign(:members, sorted_members)}
|
||||
end
|
||||
|
||||
# Selects all members in the list of members
|
||||
|
||||
# Selects all members in the list of members
|
||||
@impl true
|
||||
def handle_event("select_all", _params, socket) do
|
||||
members = socket.assigns.members
|
||||
|
|
@ -109,8 +61,235 @@ defmodule MvWeb.MemberLive.Index do
|
|||
{:noreply, assign(socket, :selected_members, selected)}
|
||||
end
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Handle Infos from Child Components
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
# Sorts the list of members according to a field, when you click on the column header
|
||||
@impl true
|
||||
def handle_info({:sort, field_str}, socket) do
|
||||
field = String.to_existing_atom(field_str)
|
||||
old_field = socket.assigns.sort_field
|
||||
|
||||
{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}"
|
||||
old_id = :"sort_#{old_field}"
|
||||
|
||||
# Update the new SortHeader
|
||||
|
moritz marked this conversation as resolved
Outdated
moritz
commented
@moritz wrote in #166 (comment):
maybe something like this could work to reset the old header component:
@moritz wrote in https://git.local-it.org/local-it/mitgliederverwaltung/pulls/166#issuecomment-13801:
> If the rows are sorted by a specific column the column header shows a single arrow in the sorting direction. The other rows show a double arrow (up and down). If I sort for another column the double arrow switches into a single arrow, but the previous column header arrow keeps the same. So it's not possible to see which column is the column sorted by. It would be good to reset the sorting arrows of the previous columns. Maybe something more obvious to highlight which is the current sort column would be nice too.
maybe something like this could work to reset the old header component:
```
old_id = :"sort_#{old_field}"
# Update the SortHeader to
send_update(MvWeb.Components.SortHeaderComponent,
id: old_id,
sort_field: new_field,
sort_order: new_order
)
```
|
||||
send_update(MvWeb.Components.SortHeaderComponent,
|
||||
id: active_id,
|
||||
sort_field: new_field,
|
||||
sort_order: new_order
|
||||
)
|
||||
|
||||
# Reset the current SortHeader
|
||||
send_update(MvWeb.Components.SortHeaderComponent,
|
||||
id: old_id,
|
||||
sort_field: new_field,
|
||||
sort_order: new_order
|
||||
)
|
||||
|
moritz marked this conversation as resolved
Outdated
moritz
commented
Best practice: instead of hardcoded paths, render path dynamically: Also the Best practice: instead of hardcoded paths, render path dynamically:
```
current_path = socket.view.__live__() |> elem(0)
new_path = current_path <> "?" <> URI.encode_query(query_params)
```
Also the `~p` sigil could be used for creating a path.
carla
commented
I tried that already before and did not find a simple solution for getting the current path. So I would keep that static for now. I tried that already before and did not find a simple solution for getting the current path. So I would keep that static for now.
|
||||
|
||||
existing_search_query = socket.assigns.query
|
||||
|
||||
# Build the URL with queries
|
||||
query_params = %{
|
||||
"query" => existing_search_query,
|
||||
"sort_field" => Atom.to_string(new_field),
|
||||
"sort_order" => Atom.to_string(new_order)
|
||||
}
|
||||
|
||||
# Set the new path with params
|
||||
new_path = ~p"/members?#{query_params}"
|
||||
|
||||
|
moritz marked this conversation as resolved
Outdated
moritz
commented
This function ignores the sorting. This leads to reset the sorting while searching. This function ignores the sorting. This leads to reset the sorting while searching.
To avoid duplicate code I would recommend to integrate the search logic into `load_members`.
|
||||
# Push the new URL
|
||||
{:noreply,
|
||||
push_patch(socket,
|
||||
to: new_path,
|
||||
replace: true
|
||||
)}
|
||||
end
|
||||
|
||||
# Function to handle search
|
||||
@impl true
|
||||
def handle_info({:search_changed, q}, socket) do
|
||||
socket = load_members(socket, q)
|
||||
|
||||
existing_field_query = socket.assigns.sort_field
|
||||
existing_sort_query = socket.assigns.sort_order
|
||||
|
||||
# Build the URL with queries
|
||||
query_params = %{
|
||||
"query" => q,
|
||||
"sort_field" => existing_field_query,
|
||||
"sort_order" => existing_sort_query
|
||||
}
|
||||
|
||||
# Set the new path with params
|
||||
new_path = ~p"/members?#{query_params}"
|
||||
|
||||
# Push the new URL
|
||||
{:noreply,
|
||||
push_patch(socket,
|
||||
to: new_path,
|
||||
replace: true
|
||||
)}
|
||||
end
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Handle Params from the URL
|
||||
# -----------------------------------------------------------------
|
||||
@impl true
|
||||
def handle_params(params, _url, socket) do
|
||||
socket =
|
||||
socket
|
||||
|> maybe_update_search(params)
|
||||
|> maybe_update_sort(params)
|
||||
|> load_members(params["query"])
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# FUNCTIONS
|
||||
# -------------------------------------------------------------
|
||||
# Load members eg based on a query for sorting
|
||||
defp load_members(socket, search_query) 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
|
||||
])
|
||||
|
||||
# Apply the search filter first
|
||||
query = apply_search_filter(query, search_query)
|
||||
|
||||
# Apply sorting based on current socket state
|
||||
query = maybe_sort(query, socket.assigns.sort_field, socket.assigns.sort_order)
|
||||
|
||||
members = Ash.read!(query)
|
||||
assign(socket, :members, members)
|
||||
end
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# Helper Functions
|
||||
# -------------------------------------------------------------
|
||||
|
||||
# Function to apply search query
|
||||
defp apply_search_filter(query, search_query) do
|
||||
if search_query && String.trim(search_query) != "" do
|
||||
query
|
||||
|> filter(expr(fragment("search_vector @@ plainto_tsquery('simple', ?)", ^search_query)))
|
||||
else
|
||||
query
|
||||
end
|
||||
end
|
||||
|
||||
# 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) when not is_nil(field),
|
||||
do: Ash.Query.sort(query, [{field, :asc}])
|
||||
|
||||
defp maybe_sort(query, field, :desc) when not is_nil(field),
|
||||
do: Ash.Query.sort(query, [{field, :desc}])
|
||||
|
||||
defp maybe_sort(query, _, _), do: query
|
||||
|
||||
# Validate that a field is sortable
|
||||
defp valid_sort_field?(field) when is_atom(field) do
|
||||
valid_fields = [
|
||||
:first_name,
|
||||
:last_name,
|
||||
:email,
|
||||
:street,
|
||||
:house_number,
|
||||
:postal_code,
|
||||
:city,
|
||||
:phone_number,
|
||||
:join_date
|
||||
]
|
||||
|
||||
field in valid_fields
|
||||
end
|
||||
|
||||
defp valid_sort_field?(_), do: false
|
||||
|
||||
# Function to maybe update the sort
|
||||
defp maybe_update_sort(socket, %{"sort_field" => sf, "sort_order" => so}) do
|
||||
field = determine_field(socket.assigns.sort_field, sf)
|
||||
order = determine_order(socket.assigns.sort_order, so)
|
||||
|
||||
socket
|
||||
|> assign(:sort_field, field)
|
||||
|> assign(:sort_order, order)
|
||||
end
|
||||
|
||||
defp maybe_update_sort(socket, _), do: socket
|
||||
|
||||
defp determine_field(default, sf) do
|
||||
case sf do
|
||||
"" ->
|
||||
default
|
||||
|
||||
nil ->
|
||||
default
|
||||
|
||||
sf when is_binary(sf) ->
|
||||
sf
|
||||
|> String.to_existing_atom()
|
||||
|> handle_atom_conversion(default)
|
||||
|
||||
sf when is_atom(sf) ->
|
||||
handle_atom_conversion(sf, default)
|
||||
|
||||
_ ->
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_atom_conversion(val, default) when is_atom(val) do
|
||||
if valid_sort_field?(val), do: val, else: default
|
||||
end
|
||||
|
||||
defp handle_atom_conversion(_, default), do: default
|
||||
|
||||
defp determine_order(default, so) do
|
||||
case so do
|
||||
"" -> default
|
||||
nil -> default
|
||||
so when so in ["asc", "desc"] -> String.to_atom(so)
|
||||
_ -> default
|
||||
end
|
||||
end
|
||||
|
||||
# Function to update search parameters
|
||||
defp maybe_update_search(socket, %{"query" => query}) when query != "" do
|
||||
assign(socket, :query, query)
|
||||
end
|
||||
|
||||
defp maybe_update_search(socket, _params) do
|
||||
# Keep the previous search query if no new one is provided
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
<:col :let={member} label={gettext("Email")}>{member.email}</:col>
|
||||
<:col :let={member} label={gettext("Street")}>{member.street}</:col>
|
||||
<:col :let={member} label={gettext("House Number")}>{member.house_number}</:col>
|
||||
<:col :let={member} label={gettext("Postal Code")}>{member.postal_code}</:col>
|
||||
<:col :let={member} label={gettext("City")}>{member.city}</:col>
|
||||
<:col :let={member} label={gettext("Phone Number")}>{member.phone_number}</:col>
|
||||
<:col :let={member} label={gettext("Join Date")}>{member.join_date}</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:sort_email}
|
||||
field={:email}
|
||||
label={gettext("Email")}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
{member.email}
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:sort_street}
|
||||
field={:street}
|
||||
label={gettext("Street")}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
{member.street}
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:sort_house_number}
|
||||
field={:house_number}
|
||||
label={gettext("House Number")}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
{member.house_number}
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:sort_postal_code}
|
||||
field={:postal_code}
|
||||
label={gettext("Postal Code")}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
{member.postal_code}
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:sort_city}
|
||||
field={:city}
|
||||
label={gettext("City")}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
{member.city}
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:sort_phone_number}
|
||||
field={:phone_number}
|
||||
label={gettext("Phone Number")}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
{member.phone_number}
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:sort_join_date}
|
||||
field={:join_date}
|
||||
label={gettext("Join Date")}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
{member.join_date}
|
||||
</:col>
|
||||
|
||||
<:action :let={member}>
|
||||
<div class="sr-only">
|
||||
|
|
|
|||
|
|
@ -61,3 +61,6 @@ 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"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ msgstr ""
|
|||
msgid "Actions"
|
||||
msgstr "Aktionen"
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:84
|
||||
#: 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:69
|
||||
#: lib/mv_web/live/member_live/show.ex:37
|
||||
#: 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:86
|
||||
#: 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:78
|
||||
#: 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,8 +54,8 @@ msgid "Edit Member"
|
|||
msgstr "Mitglied bearbeiten"
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:18
|
||||
#: lib/mv_web/live/member_live/index.html.heex:65
|
||||
#: lib/mv_web/live/member_live/show.ex:28
|
||||
#: 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
|
||||
#: lib/mv_web/live/user_live/show.ex:25
|
||||
|
|
@ -70,8 +70,8 @@ msgid "First Name"
|
|||
msgstr "Vorname"
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:22
|
||||
#: lib/mv_web/live/member_live/index.html.heex:71
|
||||
#: lib/mv_web/live/member_live/show.ex:34
|
||||
#: 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"
|
||||
msgstr "Beitrittsdatum"
|
||||
|
|
@ -87,7 +87,7 @@ msgstr "Nachname"
|
|||
msgid "New Member"
|
||||
msgstr "Neues Mitglied"
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:75
|
||||
#: 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,8 +127,8 @@ msgid "Exit Date"
|
|||
msgstr "Austrittsdatum"
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:27
|
||||
#: lib/mv_web/live/member_live/index.html.heex:67
|
||||
#: lib/mv_web/live/member_live/show.ex:39
|
||||
#: 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"
|
||||
msgstr "Hausnummer"
|
||||
|
|
@ -146,15 +146,15 @@ msgid "Paid"
|
|||
msgstr "Bezahlt"
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:21
|
||||
#: lib/mv_web/live/member_live/index.html.heex:70
|
||||
#: lib/mv_web/live/member_live/show.ex:33
|
||||
#: 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:68
|
||||
#: lib/mv_web/live/member_live/show.ex:40
|
||||
#: 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"
|
||||
msgstr "Postleitzahl"
|
||||
|
|
@ -173,8 +173,8 @@ msgid "Saving..."
|
|||
msgstr "Speichern..."
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:26
|
||||
#: lib/mv_web/live/member_live/index.html.heex:66
|
||||
#: lib/mv_web/live/member_live/show.ex:38
|
||||
#: 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"
|
||||
msgstr "Straße"
|
||||
|
|
@ -317,14 +317,13 @@ msgstr "Benutzer*innen auflisten"
|
|||
msgid "Member"
|
||||
msgstr "Mitglied"
|
||||
|
||||
#: lib/mv_web/components/layouts/navbar.ex:19
|
||||
#: lib/mv_web/live/member_live/index.ex:14
|
||||
#: lib/mv_web/components/layouts/navbar.ex:14
|
||||
#: 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:57
|
||||
#: 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"
|
||||
|
|
@ -600,10 +601,15 @@ msgstr "Dunklen Modus umschalten"
|
|||
#: lib/mv_web/live/components/search_bar_component.ex:15
|
||||
#: lib/mv_web/live/member_live/index.html.heex:15
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Search..."
|
||||
msgstr ""
|
||||
msgid "Click to sort"
|
||||
msgstr "Klicke um zu sortieren"
|
||||
|
||||
#: lib/mv_web/components/layouts/navbar.ex:20
|
||||
#: lib/mv_web/live/member_live/index.html.heex:53
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Users"
|
||||
msgstr "Benutzer*innen"
|
||||
msgid "First name"
|
||||
msgstr "Vorname"
|
||||
|
||||
#~ #: lib/mv_web/auth_overrides.ex:30
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "or"
|
||||
#~ msgstr "oder"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ msgstr ""
|
|||
msgid "Actions"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:84
|
||||
#: 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:69
|
||||
#: lib/mv_web/live/member_live/show.ex:37
|
||||
#: 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:86
|
||||
#: 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:78
|
||||
#: 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,8 +55,8 @@ msgid "Edit Member"
|
|||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:18
|
||||
#: lib/mv_web/live/member_live/index.html.heex:65
|
||||
#: lib/mv_web/live/member_live/show.ex:28
|
||||
#: 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
|
||||
#: lib/mv_web/live/user_live/show.ex:25
|
||||
|
|
@ -71,8 +71,8 @@ msgid "First Name"
|
|||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:22
|
||||
#: lib/mv_web/live/member_live/index.html.heex:71
|
||||
#: lib/mv_web/live/member_live/show.ex:34
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
|
@ -88,7 +88,7 @@ msgstr ""
|
|||
msgid "New Member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:75
|
||||
#: 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,8 +128,8 @@ msgid "Exit Date"
|
|||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:27
|
||||
#: lib/mv_web/live/member_live/index.html.heex:67
|
||||
#: lib/mv_web/live/member_live/show.ex:39
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
|
@ -147,15 +147,15 @@ msgid "Paid"
|
|||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:21
|
||||
#: lib/mv_web/live/member_live/index.html.heex:70
|
||||
#: lib/mv_web/live/member_live/show.ex:33
|
||||
#: 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:68
|
||||
#: lib/mv_web/live/member_live/show.ex:40
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
|
@ -174,8 +174,8 @@ msgid "Saving..."
|
|||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:26
|
||||
#: lib/mv_web/live/member_live/index.html.heex:66
|
||||
#: lib/mv_web/live/member_live/show.ex:38
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
|
@ -318,14 +318,13 @@ msgstr ""
|
|||
msgid "Member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/layouts/navbar.ex:19
|
||||
#: lib/mv_web/live/member_live/index.ex:14
|
||||
#: lib/mv_web/components/layouts/navbar.ex:14
|
||||
#: 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:57
|
||||
#: 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 ""
|
||||
|
|
@ -607,4 +608,12 @@ msgstr ""
|
|||
#: lib/mv_web/components/layouts/navbar.ex:20
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Users"
|
||||
#: lib/mv_web/live/components/sort_header_component.ex:60
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Click to sort"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:53
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "First name"
|
||||
msgstr ""
|
||||
|
|
|
|||
|
|
@ -58,3 +58,6 @@ msgstr ""
|
|||
|
||||
msgid "Your password has successfully been reset"
|
||||
msgstr ""
|
||||
|
||||
#~ msgid "Sign in with Rauthy"
|
||||
#~ msgstr "Sign in with Vereinscloud"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ msgstr ""
|
|||
msgid "Actions"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:84
|
||||
#: 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:69
|
||||
#: lib/mv_web/live/member_live/show.ex:37
|
||||
#: 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:86
|
||||
#: 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:78
|
||||
#: 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,8 +55,8 @@ msgid "Edit Member"
|
|||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:18
|
||||
#: lib/mv_web/live/member_live/index.html.heex:65
|
||||
#: lib/mv_web/live/member_live/show.ex:28
|
||||
#: 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
|
||||
#: lib/mv_web/live/user_live/show.ex:25
|
||||
|
|
@ -71,8 +71,8 @@ msgid "First Name"
|
|||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:22
|
||||
#: lib/mv_web/live/member_live/index.html.heex:71
|
||||
#: lib/mv_web/live/member_live/show.ex:34
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
|
@ -88,7 +88,7 @@ msgstr ""
|
|||
msgid "New Member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:75
|
||||
#: 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,8 +128,8 @@ msgid "Exit Date"
|
|||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:27
|
||||
#: lib/mv_web/live/member_live/index.html.heex:67
|
||||
#: lib/mv_web/live/member_live/show.ex:39
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
|
@ -147,15 +147,15 @@ msgid "Paid"
|
|||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:21
|
||||
#: lib/mv_web/live/member_live/index.html.heex:70
|
||||
#: lib/mv_web/live/member_live/show.ex:33
|
||||
#: 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:68
|
||||
#: lib/mv_web/live/member_live/show.ex:40
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
|
@ -174,8 +174,8 @@ msgid "Saving..."
|
|||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:26
|
||||
#: lib/mv_web/live/member_live/index.html.heex:66
|
||||
#: lib/mv_web/live/member_live/show.ex:38
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
|
@ -318,14 +318,13 @@ msgstr ""
|
|||
msgid "Member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/layouts/navbar.ex:19
|
||||
#: lib/mv_web/live/member_live/index.ex:14
|
||||
#: lib/mv_web/components/layouts/navbar.ex:14
|
||||
#: 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:57
|
||||
#: 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,57 +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/live/user_live/show.ex:30
|
||||
#: lib/mv_web/live/components/sort_header_component.ex:60
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Click to sort"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:53
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Linked Member"
|
||||
msgid "First name"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/show.ex:41
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Linked User"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/show.ex:40
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "No member linked"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/show.ex:51
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "No user linked"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/show.ex:14
|
||||
#: lib/mv_web/live/member_live/show.ex:16
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Back to members list"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/show.ex:13
|
||||
#: lib/mv_web/live/user_live/show.ex:15
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Back to users list"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/layouts/navbar.ex:27
|
||||
#: lib/mv_web/components/layouts/navbar.ex:33
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Select language"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/layouts/navbar.ex:40
|
||||
#: lib/mv_web/components/layouts/navbar.ex:60
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Toggle dark mode"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/components/search_bar_component.ex:15
|
||||
#: lib/mv_web/live/member_live/index.html.heex:15
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Search..."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/layouts/navbar.ex:20
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Users"
|
||||
msgstr ""
|
||||
#~ #: lib/mv_web/auth_overrides.ex:30
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "or"
|
||||
#~ msgstr ""
|
||||
|
|
|
|||
|
|
@ -88,6 +88,18 @@ for member_attrs <- [
|
|||
city: "Berlin",
|
||||
street: "Kastanienallee",
|
||||
house_number: "8"
|
||||
},
|
||||
%{
|
||||
first_name: "Marianne",
|
||||
last_name: "Wagner",
|
||||
email: "marianne.wagner@example.de",
|
||||
birth_date: ~D[1978-11-08],
|
||||
join_date: ~D[2022-11-10],
|
||||
paid: true,
|
||||
phone_number: "+49301122334",
|
||||
city: "Berlin",
|
||||
street: "Kastanienallee",
|
||||
house_number: "8"
|
||||
}
|
||||
] do
|
||||
# Use upsert to prevent duplicates based on email
|
||||
|
|
|
|||
|
|
@ -18,14 +18,14 @@ defmodule MvWeb.Components.SearchBarComponentTest do
|
|||
html =
|
||||
view
|
||||
|> element("form[role=search]")
|
||||
|> render_change(%{"query" => "Friedrich"})
|
||||
|> render_submit(%{"query" => "Friedrich"})
|
||||
|
||||
refute html =~ "Greta"
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("form[role=search]")
|
||||
|> render_change(%{"query" => "Greta"})
|
||||
|> render_submit(%{"query" => "Greta"})
|
||||
|
||||
refute html =~ "Friedrich"
|
||||
end
|
||||
|
|
|
|||
319
test/mv_web/components/sort_header_component_test.exs
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
defmodule MvWeb.Components.SortHeaderComponentTest do
|
||||
use MvWeb.ConnCase, async: true
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
describe "rendering" do
|
||||
test "renders with correct attributes", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Test that the component renders with correct attributes
|
||||
assert has_element?(view, "[data-testid='first_name']")
|
||||
assert has_element?(view, "button[phx-value-field='city']")
|
||||
|
moritz marked this conversation as resolved
Outdated
moritz
commented
Maybe some more tests like:
Maybe some more tests like:
- Test that all sortable headers exist
- Test initial state shows correct icon
- Test aria-label
|
||||
assert has_element?(view, "button[phx-value-field='first_name']", "First name")
|
||||
end
|
||||
|
||||
test "renders all sortable headers", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
sortable_fields = [
|
||||
:first_name,
|
||||
:email,
|
||||
:street,
|
||||
:house_number,
|
||||
:postal_code,
|
||||
:city,
|
||||
:phone_number,
|
||||
:join_date
|
||||
]
|
||||
|
||||
for field <- sortable_fields do
|
||||
assert has_element?(view, "button[phx-value-field='#{field}']")
|
||||
end
|
||||
end
|
||||
|
||||
test "renders correct labels", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Test specific labels
|
||||
assert has_element?(view, "button[phx-value-field='first_name']", "First name")
|
||||
assert has_element?(view, "button[phx-value-field='email']", "Email")
|
||||
assert has_element?(view, "button[phx-value-field='city']", "City")
|
||||
end
|
||||
end
|
||||
|
||||
describe "sort icons" do
|
||||
test "shows neutral icon for specific field when not sorted", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# The neutral icon has the opcity class we can test for
|
||||
# Test that EMAIL field specifically shows neutral icon
|
||||
assert has_element?(view, "[data-testid='email'] .opacity-40")
|
||||
|
||||
# Test that CITY field specifically shows neutral icon
|
||||
assert has_element?(view, "[data-testid='city'] .opacity-40")
|
||||
end
|
||||
|
||||
test "shows ascending icon for specific field when sorted ascending", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, html} = live(conn, "/members?query=&sort_field=city&sort_order=asc")
|
||||
|
||||
# Test that FIRST_NAME field specifically shows ascending icon
|
||||
# Test CSS classes - no opacity for active state
|
||||
refute has_element?(view, "[data-testid='city'] .opacity-40")
|
||||
|
||||
# Test that OTHER fields still show neutral icons
|
||||
assert has_element?(view, "[data-testid='first_name'] .opacity-40")
|
||||
|
||||
# Test HTML content - should contain chevronup AND chevron up down
|
||||
assert html =~ "hero-chevron-up"
|
||||
assert html =~ "hero-chevron-up-down"
|
||||
|
||||
# Count occurrences to ensure only one ascending icon
|
||||
up_count = html |> String.split("hero-chevron-up ") |> length() |> Kernel.-(1)
|
||||
# Should be exactly one chevronup icon
|
||||
assert up_count == 1
|
||||
end
|
||||
|
||||
test "shows descending icon for specific field when sorted descending", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members?query=&sort_field=email&sort_order=desc")
|
||||
|
||||
# Count occurrences to ensure only one descending icon
|
||||
down_count = html |> String.split("hero-chevron-down ") |> length() |> Kernel.-(1)
|
||||
# Should be exactly one chevrondown icon
|
||||
assert down_count == 1
|
||||
end
|
||||
|
||||
test "multiple fields can have different icon states", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?query=&sort_field=city&sort_order=asc")
|
||||
|
||||
# CITY field should be active (ascending)
|
||||
refute has_element?(view, "[data-testid='city'] .opacity-40")
|
||||
|
||||
# All other fields should be neutral
|
||||
assert has_element?(view, "[data-testid='first_name'] .opacity-40")
|
||||
assert has_element?(view, "[data-testid='email'] .opacity-40")
|
||||
assert has_element?(view, "[data-testid='street'] .opacity-40")
|
||||
assert has_element?(view, "[data-testid='house_number'] .opacity-40")
|
||||
assert has_element?(view, "[data-testid='postal_code'] .opacity-40")
|
||||
assert has_element?(view, "[data-testid='phone_number'] .opacity-40")
|
||||
assert has_element?(view, "[data-testid='join_date'] .opacity-40")
|
||||
end
|
||||
|
||||
test "icon state changes correctly when clicking different fields", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Start: all fields neutral except first name as default
|
||||
assert has_element?(view, "[data-testid='city'] .opacity-40")
|
||||
refute has_element?(view, "[data-testid='first_name'] .opacity-40")
|
||||
assert has_element?(view, "[data-testid='email'] .opacity-40")
|
||||
|
||||
# Click city - should become active
|
||||
view
|
||||
|> element("button[phx-value-field='city']")
|
||||
|> render_click()
|
||||
|
||||
# city should be active, email should still be neutral
|
||||
refute has_element?(view, "[data-testid='city'] .opacity-40")
|
||||
assert has_element?(view, "[data-testid='email'] .opacity-40")
|
||||
|
||||
# Click email - should switch active field
|
||||
view
|
||||
|> element("button[phx-value-field='email']")
|
||||
|> render_click()
|
||||
|
||||
# email should be active, city should be neutral again
|
||||
refute has_element?(view, "[data-testid='email'] .opacity-40")
|
||||
assert has_element?(view, "[data-testid='city'] .opacity-40")
|
||||
end
|
||||
|
||||
test "specific field shows correct icon for each sort state", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
|
||||
# Test EMAIL field specifically
|
||||
{:ok, view, html_asc} = live(conn, "/members?sort_field=email&sort_order=asc")
|
||||
assert html_asc =~ "hero-chevron-up"
|
||||
refute has_element?(view, "[data-testid='email'] .opacity-40")
|
||||
|
||||
{:ok, view, html_desc} = live(conn, "/members?sort_field=email&sort_order=desc")
|
||||
assert html_desc =~ "hero-chevron-down"
|
||||
refute has_element?(view, "[data-testid='email'] .opacity-40")
|
||||
|
||||
{:ok, view, html_neutral} = live(conn, "/members")
|
||||
assert html_neutral =~ "hero-chevron-up-down"
|
||||
assert has_element?(view, "[data-testid='email'] .opacity-40")
|
||||
end
|
||||
|
||||
test "icon distribution is correct for all fields", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
|
||||
# Test neutral state - all fields except first name (default) should show neutral icons
|
||||
{:ok, _view, html_neutral} = live(conn, "/members")
|
||||
|
||||
# Count neutral icons (should be 7 - one for each field)
|
||||
neutral_count =
|
||||
html_neutral |> String.split("hero-chevron-up-down") |> length() |> Kernel.-(1)
|
||||
|
||||
assert neutral_count == 7
|
||||
|
||||
# Count active icons (should be 1)
|
||||
up_count = html_neutral |> String.split("hero-chevron-up ") |> length() |> Kernel.-(1)
|
||||
down_count = html_neutral |> String.split("hero-chevron-down ") |> length() |> Kernel.-(1)
|
||||
assert up_count == 1
|
||||
assert down_count == 0
|
||||
|
||||
# Test ascending state - one field active, others neutral
|
||||
{:ok, _view, html_asc} = live(conn, "/members?sort_field=first_name&sort_order=asc")
|
||||
|
||||
# Should have exactly 1 ascending icon and 7 neutral icons
|
||||
up_count = html_asc |> String.split("hero-chevron-up ") |> length() |> Kernel.-(1)
|
||||
neutral_count = html_asc |> String.split("hero-chevron-up-down") |> length() |> Kernel.-(1)
|
||||
down_count = html_asc |> String.split("hero-chevron-down ") |> length() |> Kernel.-(1)
|
||||
|
||||
assert up_count == 1
|
||||
assert neutral_count == 7
|
||||
assert down_count == 0
|
||||
end
|
||||
end
|
||||
|
||||
describe "accessibility" do
|
||||
test "sets aria-label correctly for unsorted state", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Check aria-label for unsorted state
|
||||
assert has_element?(view, "button[phx-value-field='city'][aria-label='Click to sort']")
|
||||
end
|
||||
|
||||
test "sets aria-label correctly for ascending sort", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?sort_field=first_name&sort_order=asc")
|
||||
|
||||
# Check aria-label for ascending sort
|
||||
assert has_element?(view, "button[phx-value-field='first_name'][aria-label='ascending']")
|
||||
end
|
||||
|
||||
test "sets aria-label correctly for descending sort", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?sort_field=first_name&sort_order=desc")
|
||||
|
||||
# Check aria-label for descending sort
|
||||
assert has_element?(view, "button[phx-value-field='first_name'][aria-label='descending']")
|
||||
end
|
||||
|
||||
test "includes tooltip with correct aria-label", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?sort_field=first_name&sort_order=asc")
|
||||
|
||||
# Check that tooltip div exists with correct data-tip
|
||||
assert has_element?(view, "[data-testid='first_name']")
|
||||
assert has_element?(view, "button[phx-value-field='first_name'][aria-label='ascending']")
|
||||
end
|
||||
|
||||
test "aria-labels work for all sortable fields", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?sort_field=email&sort_order=desc")
|
||||
|
||||
# Test aria-labels for different fields
|
||||
assert has_element?(view, "button[phx-value-field='email'][aria-label='descending']")
|
||||
|
||||
assert has_element?(
|
||||
view,
|
||||
"button[phx-value-field='first_name'][aria-label='Click to sort']"
|
||||
)
|
||||
|
||||
assert has_element?(view, "button[phx-value-field='city'][aria-label='Click to sort']")
|
||||
end
|
||||
end
|
||||
|
||||
describe "component behavior" do
|
||||
test "clicking sends sort message to parent", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Click on the first name sort header
|
||||
view
|
||||
|> element("button[phx-value-field='first_name']")
|
||||
|> render_click()
|
||||
|
||||
# The component should send a message to the parent LiveView
|
||||
# This is tested indirectly through the URL change in integration tests
|
||||
end
|
||||
|
||||
test "component handles different field types correctly", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Test that different field types render correctly
|
||||
assert has_element?(view, "button[phx-value-field='first_name']")
|
||||
assert has_element?(view, "button[phx-value-field='email']")
|
||||
assert has_element?(view, "button[phx-value-field='join_date']")
|
||||
end
|
||||
end
|
||||
|
||||
describe "edge cases" do
|
||||
test "handles invalid sort field gracefully", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, html} = live(conn, "/members?sort_field=invalid_field&sort_order=asc")
|
||||
|
||||
# Should not crash and should default sorting for first name
|
||||
assert html =~ "hero-chevron-up-down"
|
||||
refute has_element?(view, "[data-testid='first_name'] .opacity-40")
|
||||
end
|
||||
|
||||
test "handles invalid sort order gracefully", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, html} = live(conn, "/members?sort_field=first_name&sort_order=invalid")
|
||||
|
||||
# Should default to ascending
|
||||
assert html =~ "hero-chevron-up"
|
||||
refute has_element?(view, "[data-testid='first_name'] [aria-label='ascending']")
|
||||
end
|
||||
|
||||
test "handles empty sort parameters", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, html} = live(conn, "/members?sort_field=&sort_order=")
|
||||
|
||||
# Should show neutral icons
|
||||
assert html =~ "hero-chevron-up-down"
|
||||
assert has_element?(view, "[data-testid='city'] .opacity-40")
|
||||
end
|
||||
end
|
||||
|
||||
describe "icon state transitions" do
|
||||
test "icon changes when sorting state changes", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Start with neutral state
|
||||
assert has_element?(view, "[data-testid='city'] .opacity-40")
|
||||
|
||||
# Click to sort ascending
|
||||
view
|
||||
|> element("button[phx-value-field='city']")
|
||||
|> render_click()
|
||||
|
||||
# Should now be ascending (no opacity class)
|
||||
refute has_element?(view, "[data-testid='city'] .opacity-40")
|
||||
end
|
||||
|
||||
test "multiple fields can be tested for icon states", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, html} = live(conn, "/members?sort_field=email&sort_order=desc")
|
||||
|
||||
# Email should be active (descending)
|
||||
assert html =~ "hero-chevron-down"
|
||||
refute has_element?(view, "[data-testid='email'] .opacity-40")
|
||||
|
||||
# Other fields should be neutral
|
||||
assert has_element?(view, "[data-testid='first_name'] .opacity-40")
|
||||
assert has_element?(view, "[data-testid='city'] .opacity-40")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -56,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 = %{
|
||||
|
|
@ -75,6 +74,143 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
assert has_element?(index_view, "#flash-group", "Member create successfully")
|
||||
end
|
||||
|
||||
describe "sorting integration" 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 with the name of the field
|
||||
|
moritz marked this conversation as resolved
Outdated
moritz
commented
ambiguous Unicode character ambiguous Unicode character `-` in comment.
|
||||
# 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?query=&sort_field=email&sort_order=asc")
|
||||
|
||||
|
moritz marked this conversation as resolved
Outdated
moritz
commented
ambiguous Unicode character ambiguous Unicode character `-` in comment.
|
||||
# Second click – toggles to DESC
|
||||
view
|
||||
|> element("[data-testid='email']")
|
||||
|> render_click()
|
||||
|
||||
assert_patch(view, "/members?query=&sort_field=email&sort_order=desc")
|
||||
end
|
||||
|
||||
test "clicking different column header resets order to ascending", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?sort_field=email&sort_order=desc")
|
||||
|
||||
# Click on a different column
|
||||
view
|
||||
|> element("[data-testid='first_name']")
|
||||
|> render_click()
|
||||
|
||||
assert_patch(view, "/members?query=&sort_field=first_name&sort_order=asc")
|
||||
end
|
||||
|
||||
test "all sortable columns work correctly", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# default ascending sorting with first name
|
||||
assert has_element?(view, "[data-testid='first_name'][aria-label='ascending']")
|
||||
|
||||
sortable_fields = [
|
||||
:email,
|
||||
:street,
|
||||
:house_number,
|
||||
:postal_code,
|
||||
:city,
|
||||
:phone_number,
|
||||
:join_date
|
||||
]
|
||||
|
||||
for field <- sortable_fields do
|
||||
view
|
||||
|> element("[data-testid='#{field}']")
|
||||
|> render_click()
|
||||
|
||||
assert_patch(view, "/members?query=&sort_field=#{field}&sort_order=asc")
|
||||
end
|
||||
end
|
||||
|
||||
test "sorting works with search query", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?query=test")
|
||||
|
||||
view
|
||||
|> element("[data-testid='email']")
|
||||
|> render_click()
|
||||
|
||||
assert_patch(view, "/members?query=test&sort_field=email&sort_order=asc")
|
||||
end
|
||||
|
||||
test "sorting maintains search query when toggling order", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?query=test&sort_field=email&sort_order=asc")
|
||||
|
||||
view
|
||||
|> element("[data-testid='email']")
|
||||
|> render_click()
|
||||
|
||||
assert_patch(view, "/members?query=test&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)
|
||||
{:ok, view, _html} = live(conn, "/members?query=&sort_field=email&sort_order=desc")
|
||||
|
||||
# Check that the sort state is correctly applied
|
||||
assert has_element?(view, "[data-testid='email'][aria-label='descending']")
|
||||
end
|
||||
|
||||
test "handle_params handles invalid sort field gracefully", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?query=&sort_field=invalid_field&sort_order=asc")
|
||||
|
||||
# Should not crash and should show default first name order
|
||||
assert has_element?(view, "[data-testid='first_name'][aria-label='ascending']")
|
||||
end
|
||||
|
||||
test "handle_params preserves search query with sort params", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?query=test&sort_field=email&sort_order=desc")
|
||||
|
||||
# Both search and sort should be preserved
|
||||
assert has_element?(view, "[data-testid='email'][aria-label='descending']")
|
||||
end
|
||||
end
|
||||
|
||||
describe "search and sort integration" do
|
||||
test "search maintains sort state", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?query=&sort_field=email&sort_order=desc")
|
||||
|
||||
# Perform search
|
||||
view
|
||||
|> element("[data-testid='search-input']")
|
||||
|> render_change(%{value: "test"})
|
||||
|
||||
# Sort state should be maintained
|
||||
assert has_element?(view, "[data-testid='email'][aria-label='descending']")
|
||||
end
|
||||
|
||||
test "sort maintains search state", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?query=test&sort_field=email&sort_order=asc")
|
||||
|
||||
# Perform sort
|
||||
view
|
||||
|> element("[data-testid='email']")
|
||||
|> render_click()
|
||||
|
||||
# Search state should be maintained
|
||||
assert_patch(view, "/members?query=test&sort_field=email&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")
|
||||
|
|
|
|||
ambiguous Unicode character
-in comment.