18 changed files with 1899 additions and 186 deletions
|
|
@ -329,6 +329,11 @@ end
|
|||
|
||||
---
|
||||
|
||||
**PR #208:** *Show custom fields per default in member overview* 🔧
|
||||
- added show_in_overview as attribute to custom fields
|
||||
- show custom fields in member overview per default
|
||||
- can be set to false in the settings for the specific custom field
|
||||
|
||||
## Implementation Decisions
|
||||
|
||||
### Architecture Patterns
|
||||
|
|
@ -390,6 +395,7 @@ defmodule Mv.Membership.CustomField do
|
|||
attribute :value_type, :atom # :string, :integer, :boolean, :date, :email
|
||||
attribute :immutable, :boolean # Can't change after creation
|
||||
attribute :required, :boolean # All members must have this
|
||||
attribute :show_in_overview, :boolean # "If true, this custom field will be displayed in the member overview table"
|
||||
end
|
||||
|
||||
# CustomFieldValue stores values
|
||||
|
|
|
|||
|
|
@ -94,15 +94,18 @@
|
|||
- ✅ CustomFieldValue type management
|
||||
- ✅ Dynamic custom field value assignment to members
|
||||
- ✅ Union type storage (JSONB)
|
||||
- ✅ Default field visibility configuration
|
||||
|
||||
**Closed Issues:**
|
||||
- [#194](https://git.local-it.org/local-it/mitgliederverwaltung/issues/194) - Custom Fields: Harden implementation (S)
|
||||
- [#197](https://git.local-it.org/local-it/mitgliederverwaltung/issues/197) - Custom Fields: Add option to show custom fields in member overview (M)
|
||||
|
||||
**Open Issues:**
|
||||
- [#194](https://git.local-it.org/local-it/mitgliederverwaltung/issues/194) - Custom Fields: Harden implementation (S) [0/3 tasks]
|
||||
- [#157](https://git.local-it.org/local-it/mitgliederverwaltung/issues/157) - Concept how custom fields are handled (M, High priority) [0/4 tasks]
|
||||
- [#161](https://git.local-it.org/local-it/mitgliederverwaltung/issues/161) - Don't show birthday field for default configurations (S, Low priority)
|
||||
- [#153](https://git.local-it.org/local-it/mitgliederverwaltung/issues/153) - Sorting functionalities for custom fields (M, Low priority)
|
||||
|
||||
**Missing Features:**
|
||||
- ❌ Default field visibility configuration
|
||||
- ❌ Field groups/categories
|
||||
- ❌ Conditional fields (show field X if field Y = value)
|
||||
- ❌ Field validation rules (min/max, regex patterns)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ defmodule Mv.Membership.CustomField do
|
|||
- `description` - Optional human-readable description
|
||||
- `immutable` - If true, custom field values cannot be changed after creation
|
||||
- `required` - If true, all members must have this custom field (future feature)
|
||||
- `show_in_overview` - If true, this custom field will be displayed in the member overview table and can be sorted
|
||||
|
||||
## Supported Value Types
|
||||
- `:string` - Text data (max 10,000 characters)
|
||||
|
|
@ -59,10 +60,10 @@ defmodule Mv.Membership.CustomField do
|
|||
|
||||
actions do
|
||||
defaults [:read, :update]
|
||||
default_accept [:name, :value_type, :description, :immutable, :required]
|
||||
default_accept [:name, :value_type, :description, :immutable, :required, :show_in_overview]
|
||||
|
||||
create :create do
|
||||
accept [:name, :value_type, :description, :immutable, :required]
|
||||
accept [:name, :value_type, :description, :immutable, :required, :show_in_overview]
|
||||
change Mv.Membership.CustomField.Changes.GenerateSlug
|
||||
validate string_length(:slug, min: 1)
|
||||
end
|
||||
|
|
@ -119,6 +120,12 @@ defmodule Mv.Membership.CustomField do
|
|||
attribute :required, :boolean,
|
||||
default: false,
|
||||
allow_nil?: false
|
||||
|
||||
attribute :show_in_overview, :boolean,
|
||||
default: true,
|
||||
allow_nil?: false,
|
||||
public?: true,
|
||||
description: "If true, this custom field will be displayed in the member overview table"
|
||||
end
|
||||
|
||||
relationships do
|
||||
|
|
|
|||
|
|
@ -318,6 +318,13 @@ defmodule MvWeb.CoreComponents do
|
|||
default: &Function.identity/1,
|
||||
doc: "the function for mapping each row before calling the :col and :action slots"
|
||||
|
||||
attr :dynamic_cols, :list,
|
||||
default: [],
|
||||
doc: "list of dynamic column definitions with :custom_field and :render functions"
|
||||
|
||||
attr :sort_field, :any, default: nil, doc: "current sort field"
|
||||
attr :sort_order, :atom, default: nil, doc: "current sort order"
|
||||
|
||||
slot :col, required: true do
|
||||
attr :label, :string
|
||||
end
|
||||
|
|
@ -335,6 +342,16 @@ defmodule MvWeb.CoreComponents do
|
|||
<thead>
|
||||
<tr>
|
||||
<th :for={col <- @col}>{col[:label]}</th>
|
||||
<th :for={dyn_col <- @dynamic_cols}>
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:"sort_custom_field_#{dyn_col[:custom_field].id}"}
|
||||
field={"custom_field_#{dyn_col[:custom_field].id}"}
|
||||
label={dyn_col[:custom_field].name}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
/>
|
||||
</th>
|
||||
<th :if={@action != []}>
|
||||
<span class="sr-only">{gettext("Actions")}</span>
|
||||
</th>
|
||||
|
|
@ -349,6 +366,23 @@ defmodule MvWeb.CoreComponents do
|
|||
>
|
||||
{render_slot(col, @row_item.(row))}
|
||||
</td>
|
||||
<td
|
||||
:for={dyn_col <- @dynamic_cols}
|
||||
phx-click={@row_click && @row_click.(row)}
|
||||
class={@row_click && "hover:cursor-pointer"}
|
||||
>
|
||||
{if dyn_col[:render] do
|
||||
rendered = dyn_col[:render].(@row_item.(row))
|
||||
|
||||
if rendered == "" do
|
||||
""
|
||||
else
|
||||
rendered
|
||||
end
|
||||
else
|
||||
""
|
||||
end}
|
||||
</td>
|
||||
<td :if={@action != []} class="w-0 font-semibold">
|
||||
<div class="flex gap-4">
|
||||
<%= for action <- @action do %>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ defmodule MvWeb.CustomFieldLive.Form do
|
|||
- description - Human-readable explanation
|
||||
- immutable - If true, values cannot be changed after creation (default: false)
|
||||
- required - If true, all members must have this custom field (default: false)
|
||||
- show_in_overview - If true, this custom field will be displayed in the member overview table (default: true)
|
||||
|
||||
## Value Type Selection
|
||||
- `:string` - Text data (unlimited length)
|
||||
|
|
@ -60,6 +61,7 @@ defmodule MvWeb.CustomFieldLive.Form do
|
|||
<.input field={@form[:description]} type="text" label={gettext("Description")} />
|
||||
<.input field={@form[:immutable]} type="checkbox" label={gettext("Immutable")} />
|
||||
<.input field={@form[:required]} type="checkbox" label={gettext("Required")} />
|
||||
<.input field={@form[:show_in_overview]} type="checkbox" label={gettext("Show in overview")} />
|
||||
|
||||
<.button phx-disable-with={gettext("Saving...")} variant="primary">
|
||||
{gettext("Save Custom field")}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,14 @@ defmodule MvWeb.MemberLive.Index do
|
|||
"""
|
||||
use MvWeb, :live_view
|
||||
|
||||
require Ash.Query
|
||||
import Ash.Expr
|
||||
|
||||
alias MvWeb.MemberLive.Index.Formatter
|
||||
|
||||
# Prefix used in sort field names for custom fields (e.g., "custom_field_<id>")
|
||||
@custom_field_prefix "custom_field_"
|
||||
|
||||
@doc """
|
||||
Initializes the LiveView state.
|
||||
|
||||
|
|
@ -34,6 +42,16 @@ defmodule MvWeb.MemberLive.Index do
|
|||
"""
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
# Load custom fields that should be shown in overview
|
||||
# Note: Using Ash.read! (bang version) - errors will be handled by Phoenix LiveView
|
||||
# and result in a 500 error page. This is appropriate for LiveViews where errors
|
||||
# should be visible to the user rather than silently failing.
|
||||
custom_fields_visible =
|
||||
Mv.Membership.CustomField
|
||||
|> Ash.Query.filter(expr(show_in_overview == true))
|
||||
|> Ash.Query.sort(name: :asc)
|
||||
|> Ash.read!()
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:page_title, gettext("Members"))
|
||||
|
|
@ -41,6 +59,7 @@ defmodule MvWeb.MemberLive.Index do
|
|||
|> assign_new(:sort_field, fn -> :first_name end)
|
||||
|> assign_new(:sort_order, fn -> :asc end)
|
||||
|> assign(:selected_members, [])
|
||||
|> assign(:custom_fields_visible, custom_fields_visible)
|
||||
|
||||
# We call handle params to use the query from the URL
|
||||
{:ok, socket}
|
||||
|
|
@ -60,6 +79,8 @@ defmodule MvWeb.MemberLive.Index do
|
|||
"""
|
||||
@impl true
|
||||
def handle_event("delete", %{"id" => id}, socket) do
|
||||
# Note: Using bang versions (!) - errors will be handled by Phoenix LiveView
|
||||
# This ensures users see error messages if deletion fails (e.g., permission denied)
|
||||
member = Ash.get!(Mv.Membership.Member, id)
|
||||
Ash.destroy!(member)
|
||||
|
||||
|
|
@ -108,7 +129,14 @@ defmodule MvWeb.MemberLive.Index do
|
|||
"""
|
||||
@impl true
|
||||
def handle_info({:sort, field_str}, socket) do
|
||||
field = String.to_existing_atom(field_str)
|
||||
# Handle both atom and string field names (for custom fields)
|
||||
field =
|
||||
try do
|
||||
String.to_existing_atom(field_str)
|
||||
rescue
|
||||
ArgumentError -> field_str
|
||||
end
|
||||
|
||||
{new_field, new_order} = determine_new_sort(field, socket)
|
||||
|
||||
socket
|
||||
|
|
@ -158,10 +186,38 @@ defmodule MvWeb.MemberLive.Index do
|
|||
|> maybe_update_search(params)
|
||||
|> maybe_update_sort(params)
|
||||
|> load_members(params["query"])
|
||||
|> prepare_dynamic_cols()
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
# Prepares dynamic column definitions for custom fields that should be shown in the overview.
|
||||
#
|
||||
# Creates a list of column definitions, each containing:
|
||||
# - `:custom_field` - The CustomField resource
|
||||
# - `:render` - A function that formats the custom field value for a given member
|
||||
#
|
||||
# Returns the socket with `:dynamic_cols` assigned.
|
||||
defp prepare_dynamic_cols(socket) do
|
||||
dynamic_cols =
|
||||
Enum.map(socket.assigns.custom_fields_visible, fn custom_field ->
|
||||
%{
|
||||
custom_field: custom_field,
|
||||
render: fn member ->
|
||||
case get_custom_field_value(member, custom_field) do
|
||||
nil ->
|
||||
""
|
||||
|
||||
cfv ->
|
||||
Formatter.format_custom_field_value(cfv.value, custom_field)
|
||||
end
|
||||
|
carla marked this conversation as resolved
Outdated
|
||||
end
|
||||
}
|
||||
end)
|
||||
|
||||
assign(socket, :dynamic_cols, dynamic_cols)
|
||||
end
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# FUNCTIONS
|
||||
# -------------------------------------------------------------
|
||||
|
|
@ -177,8 +233,8 @@ defmodule MvWeb.MemberLive.Index do
|
|||
|
||||
# Updates both the active and old SortHeader components
|
||||
defp update_sort_components(socket, old_field, new_field, new_order) do
|
||||
active_id = :"sort_#{new_field}"
|
||||
old_id = :"sort_#{old_field}"
|
||||
active_id = to_sort_id(new_field)
|
||||
old_id = to_sort_id(old_field)
|
||||
|
||||
# Update the new SortHeader
|
||||
send_update(MvWeb.Components.SortHeaderComponent,
|
||||
|
|
@ -197,11 +253,32 @@ defmodule MvWeb.MemberLive.Index do
|
|||
socket
|
||||
end
|
||||
|
||||
# Converts a field (atom or string) to a sort component ID atom
|
||||
# Handles both existing atoms and strings that need to be converted
|
||||
defp to_sort_id(field) when is_binary(field) do
|
||||
try do
|
||||
String.to_existing_atom("sort_#{field}")
|
||||
rescue
|
||||
ArgumentError -> :"sort_#{field}"
|
||||
end
|
||||
end
|
||||
|
||||
defp to_sort_id(field) when is_atom(field) do
|
||||
:"sort_#{field}"
|
||||
end
|
||||
|
||||
# Builds sort URL and pushes navigation patch
|
||||
defp push_sort_url(socket, field, order) do
|
||||
field_str =
|
||||
if is_atom(field) do
|
||||
Atom.to_string(field)
|
||||
else
|
||||
field
|
||||
end
|
||||
|
||||
query_params = %{
|
||||
"query" => socket.assigns.query,
|
||||
"sort_field" => Atom.to_string(field),
|
||||
"sort_field" => field_str,
|
||||
"sort_order" => Atom.to_string(order)
|
||||
}
|
||||
|
||||
|
|
@ -214,7 +291,24 @@ defmodule MvWeb.MemberLive.Index do
|
|||
)}
|
||||
end
|
||||
|
||||
# Load members eg based on a query for sorting
|
||||
# Loads members from the database with custom field values and applies search/sort filters.
|
||||
#
|
||||
# Process:
|
||||
# 1. Builds base query with selected fields
|
||||
# 2. Loads custom field values for visible custom fields (filtered at database level)
|
||||
# 3. Applies search filter if provided
|
||||
# 4. Applies sorting (database-level for regular fields, in-memory for custom fields)
|
||||
#
|
||||
# Performance Considerations:
|
||||
# - Database-level filtering: Custom field values are filtered directly in the database
|
||||
# using Ash relationship filters, reducing memory usage and improving performance.
|
||||
# - In-memory sorting: Custom field sorting is done in memory after loading.
|
||||
# This is suitable for small to medium datasets (<1000 members).
|
||||
# For larger datasets, consider implementing database-level sorting or pagination.
|
||||
# - No pagination: All matching members are loaded at once. For large result sets,
|
||||
# consider implementing pagination (see Issue #165).
|
||||
#
|
||||
# Returns the socket with `:members` assigned.
|
||||
defp load_members(socket, search_query) do
|
||||
query =
|
||||
Mv.Membership.Member
|
||||
|
|
@ -232,16 +326,71 @@ defmodule MvWeb.MemberLive.Index do
|
|||
:join_date
|
||||
])
|
||||
|
||||
# Load custom field values for visible custom fields
|
||||
custom_field_ids_list = Enum.map(socket.assigns.custom_fields_visible, & &1.id)
|
||||
query = load_custom_field_values(query, custom_field_ids_list)
|
||||
|
||||
|
carla marked this conversation as resolved
Outdated
moritz
commented
Maybe it would be less confusing if this overloaded variable would be called Maybe it would be less confusing if this overloaded variable would be called `custom_field_ids_list` and the other one `custom_field_ids_set`
|
||||
# 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)
|
||||
# For custom fields, we sort after loading
|
||||
{query, sort_after_load} =
|
||||
maybe_sort(
|
||||
query,
|
||||
socket.assigns.sort_field,
|
||||
socket.assigns.sort_order,
|
||||
socket.assigns.custom_fields_visible
|
||||
)
|
||||
|
||||
# Note: Using Ash.read! - errors will be handled by Phoenix LiveView
|
||||
# This is appropriate for data loading in LiveViews
|
||||
members = Ash.read!(query)
|
||||
|
||||
# Custom field values are already filtered at the database level in load_custom_field_values/2
|
||||
# No need for in-memory filtering anymore
|
||||
|
||||
# Sort in memory if needed (for custom fields)
|
||||
members =
|
||||
if sort_after_load do
|
||||
sort_members_in_memory(
|
||||
|
carla marked this conversation as resolved
Outdated
moritz
commented
`custom_field_ids` is already set. To reduce some code redundancy and computation overhead you could write:
```
custom_field_ids_set = MapSet.new(custom_field_ids_list)
```
|
||||
members,
|
||||
socket.assigns.sort_field,
|
||||
socket.assigns.sort_order,
|
||||
socket.assigns.custom_fields_visible
|
||||
)
|
||||
else
|
||||
members
|
||||
end
|
||||
|
||||
assign(socket, :members, members)
|
||||
end
|
||||
|
||||
# Load custom field values for the given custom field IDs
|
||||
#
|
||||
# Filters custom field values directly in the database using Ash relationship filters.
|
||||
# This is more efficient than loading all values and filtering in memory.
|
||||
#
|
||||
# Performance: Database-level filtering reduces:
|
||||
# - Memory usage (only visible custom field values are loaded)
|
||||
# - Network transfer (less data from database to application)
|
||||
# - Processing time (no need to iterate through all members and filter)
|
||||
defp load_custom_field_values(query, []) do
|
||||
query
|
||||
end
|
||||
|
||||
defp load_custom_field_values(query, custom_field_ids) when length(custom_field_ids) > 0 do
|
||||
# Filter custom field values at the database level using Ash relationship query
|
||||
# This ensures only visible custom field values are loaded
|
||||
custom_field_values_query =
|
||||
Mv.Membership.CustomFieldValue
|
||||
|
carla marked this conversation as resolved
Outdated
moritz
commented
Is there a reason not to filter the custom_field_values directly in the DB? This would be much more efficient. For example:
Is there a reason not to filter the custom_field_values directly in the DB? This would be much more efficient.
For example:
```
query
|> Ash.Query.load(
custom_field_values: fn custom_field_values_query ->
custom_field_values_query
|> Ash.Query.filter(expr(custom_field_id in ^custom_field_ids))
|> Ash.Query.load(custom_field: [:id, :name, :value_type])
end
)
```
carla
commented
No, good point! No, good point!
|
||||
|> Ash.Query.filter(expr(custom_field_id in ^custom_field_ids))
|
||||
|> Ash.Query.load(custom_field: [:id, :name, :value_type])
|
||||
|
||||
query
|
||||
|> Ash.Query.load(custom_field_values: custom_field_values_query)
|
||||
end
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# Helper Functions
|
||||
# -------------------------------------------------------------
|
||||
|
|
@ -264,15 +413,24 @@ defmodule MvWeb.MemberLive.Index do
|
|||
defp toggle_order(nil), do: :asc
|
||||
|
||||
# Function to sort the column if needed
|
||||
defp maybe_sort(query, nil, _), do: query
|
||||
# Returns {query, sort_after_load} where sort_after_load is true if we need to sort in memory
|
||||
defp maybe_sort(query, nil, _, _), do: {query, false}
|
||||
|
||||
defp maybe_sort(query, field, :asc) when not is_nil(field),
|
||||
do: Ash.Query.sort(query, [{field, :asc}])
|
||||
defp maybe_sort(query, field, order, _custom_fields) when not is_nil(field) do
|
||||
if custom_field_sort?(field) do
|
||||
# Custom fields need to be sorted in memory after loading
|
||||
{query, true}
|
||||
else
|
||||
# Only sort by atom fields (regular member fields) in database
|
||||
if is_atom(field) do
|
||||
{Ash.Query.sort(query, [{field, order}]), false}
|
||||
else
|
||||
{query, false}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_sort(query, field, :desc) when not is_nil(field),
|
||||
do: Ash.Query.sort(query, [{field, :desc}])
|
||||
|
||||
defp maybe_sort(query, _, _), do: query
|
||||
defp maybe_sort(query, _, _, _), do: {query, false}
|
||||
|
||||
# Validate that a field is sortable
|
||||
defp valid_sort_field?(field) when is_atom(field) do
|
||||
|
|
@ -288,12 +446,188 @@ defmodule MvWeb.MemberLive.Index do
|
|||
:join_date
|
||||
]
|
||||
|
||||
field in valid_fields
|
||||
field in valid_fields or custom_field_sort?(field)
|
||||
end
|
||||
|
||||
defp valid_sort_field?(field) when is_binary(field) do
|
||||
custom_field_sort?(field)
|
||||
end
|
||||
|
||||
defp valid_sort_field?(_), do: false
|
||||
|
||||
# Function to maybe update the sort
|
||||
# Check if field is a custom field sort field (format: custom_field_<id>)
|
||||
defp custom_field_sort?(field) when is_atom(field) do
|
||||
field_str = Atom.to_string(field)
|
||||
String.starts_with?(field_str, @custom_field_prefix)
|
||||
end
|
||||
|
||||
defp custom_field_sort?(field) when is_binary(field) do
|
||||
String.starts_with?(field, @custom_field_prefix)
|
||||
end
|
||||
|
||||
defp custom_field_sort?(_), do: false
|
||||
|
||||
# Extracts the custom field ID from a sort field name.
|
||||
#
|
||||
# Sort fields for custom fields use the format: "custom_field_<id>"
|
||||
# This function extracts the ID part.
|
||||
#
|
||||
# Examples:
|
||||
# extract_custom_field_id("custom_field_123") -> "123"
|
||||
# extract_custom_field_id(:custom_field_123) -> "123"
|
||||
# extract_custom_field_id("first_name") -> nil
|
||||
defp extract_custom_field_id(field) when is_atom(field) do
|
||||
field_str = Atom.to_string(field)
|
||||
extract_custom_field_id(field_str)
|
||||
end
|
||||
|
||||
defp extract_custom_field_id(field) when is_binary(field) do
|
||||
case String.split(field, @custom_field_prefix) do
|
||||
["", id_str] -> id_str
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_custom_field_id(_), do: nil
|
||||
|
||||
# Sorts members in memory by a custom field value.
|
||||
#
|
||||
# Process:
|
||||
# 1. Extracts custom field ID from sort field name
|
||||
# 2. Finds the corresponding CustomField resource
|
||||
# 3. Splits members into those with values and those without
|
||||
# 4. Sorts members with values by the extracted value
|
||||
# 5. Combines: sorted values first, then NULL/empty values at the end
|
||||
#
|
||||
# Performance Note:
|
||||
# This function sorts in memory, which is suitable for small to medium datasets (<1000 members).
|
||||
# For larger datasets, consider implementing database-level sorting or pagination.
|
||||
#
|
||||
# Parameters:
|
||||
# - `members` - List of Member resources to sort
|
||||
# - `field` - Sort field name (format: "custom_field_<id>" or atom)
|
||||
# - `order` - Sort order (`:asc` or `:desc`)
|
||||
# - `custom_fields` - List of visible CustomField resources
|
||||
#
|
||||
# Returns the sorted list of members.
|
||||
defp sort_members_in_memory(members, field, order, custom_fields) do
|
||||
custom_field_id_str = extract_custom_field_id(field)
|
||||
|
||||
case custom_field_id_str do
|
||||
nil ->
|
||||
members
|
||||
|
||||
id_str ->
|
||||
sort_members_by_custom_field(members, id_str, order, custom_fields)
|
||||
end
|
||||
end
|
||||
|
||||
# Sorts members by a specific custom field ID
|
||||
defp sort_members_by_custom_field(members, id_str, order, custom_fields) do
|
||||
custom_field = find_custom_field_by_id(custom_fields, id_str)
|
||||
|
||||
case custom_field do
|
||||
nil ->
|
||||
members
|
||||
|
||||
cf ->
|
||||
sort_members_with_custom_field(members, cf, order)
|
||||
end
|
||||
end
|
||||
|
||||
# Finds a custom field by matching its ID string
|
||||
defp find_custom_field_by_id(custom_fields, id_str) do
|
||||
Enum.find(custom_fields, fn cf ->
|
||||
to_string(cf.id) == id_str
|
||||
end)
|
||||
end
|
||||
|
||||
# Sorts members that have a specific custom field
|
||||
defp sort_members_with_custom_field(members, custom_field, order) do
|
||||
# Split members into those with values and those without (NULL/empty)
|
||||
{members_with_values, members_without_values} =
|
||||
split_members_by_value_presence(members, custom_field)
|
||||
|
||||
# Sort members with values
|
||||
sorted_with_values = sort_members_with_values(members_with_values, custom_field, order)
|
||||
|
||||
# Combine: sorted values first, then NULL/empty values at the end
|
||||
sorted_with_values ++ members_without_values
|
||||
end
|
||||
|
||||
# Splits members into those with values and those without
|
||||
defp split_members_by_value_presence(members, custom_field) do
|
||||
Enum.split_with(members, fn member ->
|
||||
has_non_empty_value?(member, custom_field)
|
||||
end)
|
||||
end
|
||||
|
||||
# Checks if a member has a non-empty value for the custom field
|
||||
defp has_non_empty_value?(member, custom_field) do
|
||||
case get_custom_field_value(member, custom_field) do
|
||||
nil ->
|
||||
false
|
||||
|
||||
cfv ->
|
||||
extracted = extract_sort_value(cfv.value, custom_field.value_type)
|
||||
not empty_value?(extracted, custom_field.value_type)
|
||||
end
|
||||
end
|
||||
|
||||
# Sorts members that have values for the custom field
|
||||
defp sort_members_with_values(members_with_values, custom_field, order) do
|
||||
sorted =
|
||||
Enum.sort_by(members_with_values, fn member ->
|
||||
cfv = get_custom_field_value(member, custom_field)
|
||||
extracted = extract_sort_value(cfv.value, custom_field.value_type)
|
||||
normalize_sort_value(extracted, order)
|
||||
end)
|
||||
|
||||
# For DESC, reverse only the members with values
|
||||
if order == :desc do
|
||||
Enum.reverse(sorted)
|
||||
else
|
||||
sorted
|
||||
end
|
||||
end
|
||||
|
||||
# Extracts a sortable value from a custom field value based on its type.
|
||||
#
|
||||
# Handles different value formats:
|
||||
# - `%Ash.Union{}` - Extracts value and type from union
|
||||
# - Direct values - Returns as-is for primitive types
|
||||
#
|
||||
# Returns the extracted value suitable for sorting.
|
||||
defp extract_sort_value(%Ash.Union{value: value, type: type}, _expected_type) do
|
||||
extract_sort_value(value, type)
|
||||
end
|
||||
|
||||
defp extract_sort_value(value, :string) when is_binary(value), do: value
|
||||
defp extract_sort_value(value, :integer) when is_integer(value), do: value
|
||||
defp extract_sort_value(value, :boolean) when is_boolean(value), do: value
|
||||
defp extract_sort_value(%Date{} = date, :date), do: date
|
||||
defp extract_sort_value(value, :email) when is_binary(value), do: value
|
||||
defp extract_sort_value(value, _type), do: to_string(value)
|
||||
|
||||
# Check if a value is considered empty (NULL or empty string)
|
||||
defp empty_value?(value, :string) when is_binary(value) do
|
||||
String.trim(value) == ""
|
||||
end
|
||||
|
||||
defp empty_value?(value, :email) when is_binary(value) do
|
||||
String.trim(value) == ""
|
||||
end
|
||||
|
||||
defp empty_value?(_value, _type), do: false
|
||||
|
||||
# Normalize sort value for DESC order
|
||||
# For DESC, we sort ascending first, then reverse the list
|
||||
# This function is kept for consistency but doesn't need to invert values
|
||||
defp normalize_sort_value(value, _order), do: value
|
||||
|
||||
# Updates sort field and order from URL parameters if present.
|
||||
#
|
||||
# Validates the sort field and order, falling back to defaults if invalid.
|
||||
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)
|
||||
|
|
@ -305,33 +639,50 @@ defmodule MvWeb.MemberLive.Index do
|
|||
|
||||
defp maybe_update_sort(socket, _), do: socket
|
||||
|
||||
defp determine_field(default, sf) do
|
||||
case sf do
|
||||
"" ->
|
||||
default
|
||||
# Determine sort field from URL parameter, validating against allowed fields
|
||||
defp determine_field(default, ""), do: default
|
||||
defp determine_field(default, nil), 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
|
||||
# Determines the valid sort field from a URL parameter.
|
||||
#
|
||||
# Validates the field against allowed sort fields (regular member fields or custom fields).
|
||||
# Falls back to default if the field is invalid.
|
||||
#
|
||||
# Parameters:
|
||||
# - `default` - Default field to use if validation fails
|
||||
# - `sf` - Sort field from URL (can be atom, string, nil, or empty string)
|
||||
#
|
||||
# Returns a valid sort field (atom or string for custom fields).
|
||||
defp determine_field(default, sf) when is_binary(sf) do
|
||||
# Check if it's a custom field sort (starts with "custom_field_")
|
||||
if custom_field_sort?(sf) do
|
||||
if valid_sort_field?(sf), do: sf, else: default
|
||||
else
|
||||
# Try to convert to atom for regular fields
|
||||
try do
|
||||
atom = String.to_existing_atom(sf)
|
||||
if valid_sort_field?(atom), do: atom, else: default
|
||||
rescue
|
||||
ArgumentError -> default
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_atom_conversion(val, default) when is_atom(val) do
|
||||
if valid_sort_field?(val), do: val, else: default
|
||||
defp determine_field(default, sf) when is_atom(sf) do
|
||||
if valid_sort_field?(sf), do: sf, else: default
|
||||
end
|
||||
|
||||
defp handle_atom_conversion(_, default), do: default
|
||||
defp determine_field(default, _), do: default
|
||||
|
||||
# Determines the valid sort order from a URL parameter.
|
||||
#
|
||||
# Validates that the order is either "asc" or "desc", falling back to default if invalid.
|
||||
#
|
||||
# Parameters:
|
||||
# - `default` - Default order to use if validation fails
|
||||
# - `so` - Sort order from URL (string, atom, nil, or empty string)
|
||||
#
|
||||
# Returns `:asc` or `:desc`.
|
||||
defp determine_order(default, so) do
|
||||
case so do
|
||||
"" -> default
|
||||
|
|
@ -350,4 +701,36 @@ defmodule MvWeb.MemberLive.Index do
|
|||
# Keep the previous search query if no new one is provided
|
||||
socket
|
||||
end
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# Helper Functions for Custom Field Values
|
||||
# -------------------------------------------------------------
|
||||
|
||||
# Retrieves the custom field value for a specific member and custom field.
|
||||
#
|
||||
# Searches through the member's `custom_field_values` relationship to find
|
||||
# the value matching the given custom field.
|
||||
#
|
||||
# Returns:
|
||||
# - `%CustomFieldValue{}` if found
|
||||
# - `nil` if not found or if member has no custom field values
|
||||
#
|
||||
# Examples:
|
||||
# get_custom_field_value(member, custom_field) -> %CustomFieldValue{...}
|
||||
# get_custom_field_value(member, non_existent_field) -> nil
|
||||
def get_custom_field_value(member, custom_field) do
|
||||
case member.custom_field_values do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
values when is_list(values) ->
|
||||
Enum.find(values, fn cfv ->
|
||||
cfv.custom_field_id == custom_field.id or
|
||||
(cfv.custom_field && cfv.custom_field.id == custom_field.id)
|
||||
end)
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@
|
|||
id="members"
|
||||
rows={@members}
|
||||
row_click={fn member -> JS.navigate(~p"/members/#{member}") end}
|
||||
dynamic_cols={@dynamic_cols}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
>
|
||||
|
||||
<!-- <:col :let={member} label="Id">{member.id}</:col> -->
|
||||
|
|
@ -185,7 +188,6 @@
|
|||
>
|
||||
{member.join_date}
|
||||
</:col>
|
||||
|
||||
<:action :let={member}>
|
||||
<div class="sr-only">
|
||||
<.link navigate={~p"/members/#{member}"}>{gettext("Show")}</.link>
|
||||
|
|
|
|||
74
lib/mv_web/live/member_live/index/formatter.ex
Normal file
74
lib/mv_web/live/member_live/index/formatter.ex
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
defmodule MvWeb.MemberLive.Index.Formatter do
|
||||
@moduledoc """
|
||||
Formats custom field values for display in the member overview table.
|
||||
|
||||
Handles different value types (string, integer, boolean, date, email) and
|
||||
formats them appropriately for display in the UI.
|
||||
"""
|
||||
use Gettext, backend: MvWeb.Gettext
|
||||
|
||||
@doc """
|
||||
Formats a custom field value for display.
|
||||
|
||||
Handles different input formats:
|
||||
- `nil` - Returns empty string
|
||||
- `%Ash.Union{}` - Extracts value and type from union type
|
||||
- Map (JSONB format) - Extracts type and value from map keys
|
||||
- Direct value - Uses custom_field.value_type to determine format
|
||||
|
||||
## Examples
|
||||
|
||||
iex> format_custom_field_value(nil, %CustomField{value_type: :string})
|
||||
""
|
||||
|
||||
iex> format_custom_field_value("test", %CustomField{value_type: :string})
|
||||
"test"
|
||||
|
||||
iex> format_custom_field_value(true, %CustomField{value_type: :boolean})
|
||||
"Yes"
|
||||
"""
|
||||
def format_custom_field_value(nil, _custom_field), do: ""
|
||||
|
||||
def format_custom_field_value(%Ash.Union{value: value, type: type}, custom_field) do
|
||||
format_value_by_type(value, type, custom_field)
|
||||
end
|
||||
|
||||
def format_custom_field_value(value, custom_field) when is_map(value) do
|
||||
# Handle map format from JSONB
|
||||
type = Map.get(value, "type") || Map.get(value, "_union_type")
|
||||
val = Map.get(value, "value") || Map.get(value, "_union_value")
|
||||
format_value_by_type(val, type, custom_field)
|
||||
end
|
||||
|
||||
def format_custom_field_value(value, custom_field) do
|
||||
format_value_by_type(value, custom_field.value_type, custom_field)
|
||||
end
|
||||
|
||||
# Format value based on type
|
||||
|
||||
defp format_value_by_type(value, :string, _), do: to_string(value)
|
||||
|
||||
defp format_value_by_type(value, :integer, _), do: to_string(value)
|
||||
|
||||
defp format_value_by_type(value, type, _) when type in [:string, :email] and is_binary(value) do
|
||||
# Return empty string if value is empty
|
||||
if String.trim(value) == "", do: "", else: value
|
||||
end
|
||||
|
||||
|
carla marked this conversation as resolved
Outdated
moritz
commented
`:string` and `:email` are formatted the same way, they could be merged:
```
defp format_value_by_type(value, type, _) when type in [:string, :email] and is_binary(value) do
```
|
||||
defp format_value_by_type(value, :email, _), do: to_string(value)
|
||||
|
||||
defp format_value_by_type(value, :boolean, _) when value == true, do: gettext("Yes")
|
||||
defp format_value_by_type(value, :boolean, _) when value == false, do: gettext("No")
|
||||
defp format_value_by_type(value, :boolean, _), do: to_string(value)
|
||||
|
||||
defp format_value_by_type(%Date{} = date, :date, _), do: Date.to_string(date)
|
||||
|
||||
defp format_value_by_type(value, :date, _) when is_binary(value) do
|
||||
case Date.from_iso8601(value) do
|
||||
{:ok, date} -> Date.to_string(date)
|
||||
_ -> value
|
||||
end
|
||||
end
|
||||
|
||||
defp format_value_by_type(value, _type, _), do: to_string(value)
|
||||
end
|
||||
|
|
@ -10,13 +10,13 @@ msgid ""
|
|||
msgstr ""
|
||||
"Language: en\n"
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:339
|
||||
#: lib/mv_web/components/core_components.ex:356
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Actions"
|
||||
msgstr "Aktionen"
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:200
|
||||
#: lib/mv_web/live/user_live/index.html.heex:72
|
||||
#: lib/mv_web/live/member_live/index.html.heex:202
|
||||
#: lib/mv_web/live/user_live/index.html.heex:65
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Are you sure?"
|
||||
msgstr "Bist du sicher?"
|
||||
|
|
@ -28,21 +28,21 @@ msgid "Attempting to reconnect"
|
|||
msgstr "Verbindung wird wiederhergestellt"
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:54
|
||||
#: lib/mv_web/live/member_live/index.html.heex:145
|
||||
#: lib/mv_web/live/member_live/index.html.heex:148
|
||||
#: lib/mv_web/live/member_live/show.ex:59
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "City"
|
||||
msgstr "Stadt"
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:202
|
||||
#: lib/mv_web/live/user_live/index.html.heex:74
|
||||
#: lib/mv_web/live/member_live/index.html.heex:204
|
||||
#: 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:194
|
||||
#: lib/mv_web/live/user_live/form.ex:251
|
||||
#: lib/mv_web/live/user_live/index.html.heex:66
|
||||
#: lib/mv_web/live/member_live/index.html.heex:196
|
||||
#: lib/mv_web/live/user_live/form.ex:141
|
||||
#: lib/mv_web/live/user_live/index.html.heex:59
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit"
|
||||
msgstr "Bearbeite"
|
||||
|
|
@ -54,7 +54,7 @@ msgid "Edit Member"
|
|||
msgstr "Mitglied bearbeiten"
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:47
|
||||
#: lib/mv_web/live/member_live/index.html.heex:77
|
||||
#: lib/mv_web/live/member_live/index.html.heex:80
|
||||
#: lib/mv_web/live/member_live/show.ex:50
|
||||
#: lib/mv_web/live/user_live/form.ex:46
|
||||
#: 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:51
|
||||
#: lib/mv_web/live/member_live/index.html.heex:179
|
||||
#: lib/mv_web/live/member_live/index.html.heex:182
|
||||
#: lib/mv_web/live/member_live/show.ex:56
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Join Date"
|
||||
|
|
@ -87,8 +87,8 @@ msgstr "Nachname"
|
|||
msgid "New Member"
|
||||
msgstr "Neues Mitglied"
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:191
|
||||
#: lib/mv_web/live/user_live/index.html.heex:63
|
||||
#: lib/mv_web/live/member_live/index.html.heex:193
|
||||
#: lib/mv_web/live/user_live/index.html.heex:56
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Show"
|
||||
msgstr "Anzeigen"
|
||||
|
|
@ -121,7 +121,7 @@ msgid "Exit Date"
|
|||
msgstr "Austrittsdatum"
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:56
|
||||
#: lib/mv_web/live/member_live/index.html.heex:111
|
||||
#: lib/mv_web/live/member_live/index.html.heex:114
|
||||
#: lib/mv_web/live/member_live/show.ex:61
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "House Number"
|
||||
|
|
@ -140,14 +140,14 @@ msgid "Paid"
|
|||
msgstr "Bezahlt"
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:50
|
||||
#: lib/mv_web/live/member_live/index.html.heex:162
|
||||
#: lib/mv_web/live/member_live/index.html.heex:165
|
||||
#: lib/mv_web/live/member_live/show.ex:55
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Phone Number"
|
||||
msgstr "Telefonnummer"
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:57
|
||||
#: lib/mv_web/live/member_live/index.html.heex:128
|
||||
#: lib/mv_web/live/member_live/index.html.heex:131
|
||||
#: lib/mv_web/live/member_live/show.ex:62
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Postal Code"
|
||||
|
|
@ -158,7 +158,7 @@ msgstr "Postleitzahl"
|
|||
msgid "Save Member"
|
||||
msgstr "Mitglied speichern"
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:64
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:66
|
||||
#: lib/mv_web/live/custom_field_value_live/form.ex:74
|
||||
#: lib/mv_web/live/member_live/form.ex:79
|
||||
#: lib/mv_web/live/user_live/form.ex:234
|
||||
|
|
@ -167,7 +167,7 @@ msgid "Saving..."
|
|||
msgstr "Speichern..."
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:55
|
||||
#: lib/mv_web/live/member_live/index.html.heex:94
|
||||
#: lib/mv_web/live/member_live/index.html.heex:97
|
||||
#: lib/mv_web/live/member_live/show.ex:60
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Street"
|
||||
|
|
@ -183,6 +183,7 @@ msgstr "Dieses Formular dient zur Verwaltung von Mitgliedern und deren Eigenscha
|
|||
msgid "Id"
|
||||
msgstr "ID"
|
||||
|
||||
#: lib/mv_web/live/member_live/index/formatter.ex:65
|
||||
#: lib/mv_web/live/member_live/show.ex:53
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "No"
|
||||
|
|
@ -198,19 +199,20 @@ msgstr "Mitglied anzeigen"
|
|||
msgid "This is a member record from your database."
|
||||
msgstr "Dies ist ein Mitglied aus deiner Datenbank."
|
||||
|
||||
#: lib/mv_web/live/member_live/index/formatter.ex:64
|
||||
#: lib/mv_web/live/member_live/show.ex:53
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Yes"
|
||||
msgstr "Ja"
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:108
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:110
|
||||
#: lib/mv_web/live/custom_field_value_live/form.ex:233
|
||||
#: lib/mv_web/live/member_live/form.ex:138
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "create"
|
||||
msgstr "erstellt"
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:109
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:111
|
||||
#: lib/mv_web/live/custom_field_value_live/form.ex:234
|
||||
#: lib/mv_web/live/member_live/form.ex:139
|
||||
#, elixir-autogen, elixir-format
|
||||
|
|
@ -252,7 +254,7 @@ msgstr "Ihre E-Mail-Adresse wurde bestätigt"
|
|||
msgid "Your password has successfully been reset"
|
||||
msgstr "Ihr Passwort wurde erfolgreich zurückgesetzt"
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:67
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:69
|
||||
#: lib/mv_web/live/custom_field_live/index.ex:120
|
||||
#: lib/mv_web/live/custom_field_value_live/form.ex:77
|
||||
#: lib/mv_web/live/member_live/form.ex:82
|
||||
|
|
@ -266,7 +268,7 @@ msgstr "Abbrechen"
|
|||
msgid "Choose a member"
|
||||
msgstr "Mitglied auswählen"
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:60
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:61
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Description"
|
||||
msgstr "Beschreibung"
|
||||
|
|
@ -286,7 +288,7 @@ msgstr "Aktiviert"
|
|||
msgid "ID"
|
||||
msgstr "ID"
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:61
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:62
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Immutable"
|
||||
msgstr "Unveränderlich"
|
||||
|
|
@ -308,13 +310,13 @@ msgid "Member"
|
|||
msgstr "Mitglied"
|
||||
|
||||
#: lib/mv_web/components/layouts/navbar.ex:19
|
||||
#: lib/mv_web/live/member_live/index.ex:39
|
||||
#: lib/mv_web/live/member_live/index.ex:57
|
||||
#: lib/mv_web/live/member_live/index.html.heex:3
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Members"
|
||||
msgstr "Mitglieder"
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:50
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:51
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Name"
|
||||
msgstr "Name"
|
||||
|
|
@ -357,17 +359,17 @@ msgstr "Passwort-Authentifizierung"
|
|||
msgid "Profil"
|
||||
msgstr "Profil"
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:62
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:63
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Required"
|
||||
msgstr "Erforderlich"
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:34
|
||||
#: lib/mv_web/live/member_live/index.html.heex:37
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Select all members"
|
||||
msgstr "Alle Mitglieder auswählen"
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:48
|
||||
#: lib/mv_web/live/member_live/index.html.heex:51
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Select member"
|
||||
msgstr "Mitglied auswählen"
|
||||
|
|
@ -413,7 +415,7 @@ msgstr "Benutzer*in"
|
|||
msgid "Value"
|
||||
msgstr "Wert"
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:55
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:56
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Value type"
|
||||
msgstr "Wertetyp"
|
||||
|
|
@ -569,7 +571,7 @@ msgstr "Benutzer*innen"
|
|||
msgid "Click to sort"
|
||||
msgstr "Klicke um zu sortieren"
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:60
|
||||
#: lib/mv_web/live/member_live/index.html.heex:63
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "First name"
|
||||
msgstr "Vorname"
|
||||
|
|
@ -621,7 +623,7 @@ msgstr "Benutzerdefinierte Feldwerte"
|
|||
msgid "Custom field"
|
||||
msgstr "Benutzerdefiniertes Feld"
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:115
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:117
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Custom field %{action} successfully"
|
||||
msgstr "Benutzerdefiniertes Feld erfolgreich %{action}"
|
||||
|
|
@ -636,7 +638,7 @@ msgstr "Benutzerdefinierter Feldwert erfolgreich %{action}"
|
|||
msgid "Please select a custom field first"
|
||||
msgstr "Bitte wähle zuerst ein Benutzerdefiniertes Feld"
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:65
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:67
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Save Custom field"
|
||||
msgstr "Benutzerdefiniertes Feld speichern"
|
||||
|
|
@ -646,7 +648,7 @@ msgstr "Benutzerdefiniertes Feld speichern"
|
|||
msgid "Save Custom field value"
|
||||
msgstr "Benutzerdefinierten Feldwert speichern"
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:45
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:46
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Use this form to manage custom_field records in your database."
|
||||
msgstr "Verwende dieses Formular, um Benutzerdefinierte Felder in deiner Datenbank zu verwalten."
|
||||
|
|
@ -747,3 +749,12 @@ msgstr "Entverknüpfung geplant"
|
|||
#, elixir-autogen, elixir-format
|
||||
msgid "Failed to link member: %{error}"
|
||||
msgstr ""
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:64
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Show in overview"
|
||||
msgstr "In der Mitglieder-Übersicht anzeigen"
|
||||
|
||||
#~ #: lib/mv_web/live/custom_field_live/index.ex:97
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "To confirm deletion, please enter the custom field slug:"
|
||||
#~ msgstr "Um die Löschung zu bestätigen, gib bitte den Slug des benutzerdefinierten Feldes ein:"
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:339
|
||||
#: lib/mv_web/components/core_components.ex:356
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Actions"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:200
|
||||
#: lib/mv_web/live/user_live/index.html.heex:72
|
||||
#: lib/mv_web/live/member_live/index.html.heex:202
|
||||
#: lib/mv_web/live/user_live/index.html.heex:65
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Are you sure?"
|
||||
msgstr ""
|
||||
|
|
@ -29,21 +29,21 @@ msgid "Attempting to reconnect"
|
|||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:54
|
||||
#: lib/mv_web/live/member_live/index.html.heex:145
|
||||
#: lib/mv_web/live/member_live/index.html.heex:148
|
||||
#: lib/mv_web/live/member_live/show.ex:59
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "City"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:202
|
||||
#: lib/mv_web/live/user_live/index.html.heex:74
|
||||
#: lib/mv_web/live/member_live/index.html.heex:204
|
||||
#: 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:194
|
||||
#: lib/mv_web/live/user_live/form.ex:251
|
||||
#: lib/mv_web/live/user_live/index.html.heex:66
|
||||
#: lib/mv_web/live/member_live/index.html.heex:196
|
||||
#: lib/mv_web/live/user_live/form.ex:141
|
||||
#: lib/mv_web/live/user_live/index.html.heex:59
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
|
@ -55,7 +55,7 @@ msgid "Edit Member"
|
|||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:47
|
||||
#: lib/mv_web/live/member_live/index.html.heex:77
|
||||
#: lib/mv_web/live/member_live/index.html.heex:80
|
||||
#: lib/mv_web/live/member_live/show.ex:50
|
||||
#: lib/mv_web/live/user_live/form.ex:46
|
||||
#: 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:51
|
||||
#: lib/mv_web/live/member_live/index.html.heex:179
|
||||
#: lib/mv_web/live/member_live/index.html.heex:182
|
||||
#: lib/mv_web/live/member_live/show.ex:56
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Join Date"
|
||||
|
|
@ -88,8 +88,8 @@ msgstr ""
|
|||
msgid "New Member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:191
|
||||
#: lib/mv_web/live/user_live/index.html.heex:63
|
||||
#: lib/mv_web/live/member_live/index.html.heex:193
|
||||
#: lib/mv_web/live/user_live/index.html.heex:56
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Show"
|
||||
msgstr ""
|
||||
|
|
@ -122,7 +122,7 @@ msgid "Exit Date"
|
|||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:56
|
||||
#: lib/mv_web/live/member_live/index.html.heex:111
|
||||
#: lib/mv_web/live/member_live/index.html.heex:114
|
||||
#: lib/mv_web/live/member_live/show.ex:61
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "House Number"
|
||||
|
|
@ -141,14 +141,14 @@ msgid "Paid"
|
|||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:50
|
||||
#: lib/mv_web/live/member_live/index.html.heex:162
|
||||
#: lib/mv_web/live/member_live/index.html.heex:165
|
||||
#: lib/mv_web/live/member_live/show.ex:55
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Phone Number"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:57
|
||||
#: lib/mv_web/live/member_live/index.html.heex:128
|
||||
#: lib/mv_web/live/member_live/index.html.heex:131
|
||||
#: lib/mv_web/live/member_live/show.ex:62
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Postal Code"
|
||||
|
|
@ -159,7 +159,7 @@ msgstr ""
|
|||
msgid "Save Member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:64
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:66
|
||||
#: lib/mv_web/live/custom_field_value_live/form.ex:74
|
||||
#: lib/mv_web/live/member_live/form.ex:79
|
||||
#: lib/mv_web/live/user_live/form.ex:234
|
||||
|
|
@ -168,7 +168,7 @@ msgid "Saving..."
|
|||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:55
|
||||
#: lib/mv_web/live/member_live/index.html.heex:94
|
||||
#: lib/mv_web/live/member_live/index.html.heex:97
|
||||
#: lib/mv_web/live/member_live/show.ex:60
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Street"
|
||||
|
|
@ -184,6 +184,7 @@ msgstr ""
|
|||
msgid "Id"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index/formatter.ex:65
|
||||
#: lib/mv_web/live/member_live/show.ex:53
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "No"
|
||||
|
|
@ -199,19 +200,20 @@ msgstr ""
|
|||
msgid "This is a member record from your database."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index/formatter.ex:64
|
||||
#: lib/mv_web/live/member_live/show.ex:53
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:108
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:110
|
||||
#: lib/mv_web/live/custom_field_value_live/form.ex:233
|
||||
#: lib/mv_web/live/member_live/form.ex:138
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "create"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:109
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:111
|
||||
#: lib/mv_web/live/custom_field_value_live/form.ex:234
|
||||
#: lib/mv_web/live/member_live/form.ex:139
|
||||
#, elixir-autogen, elixir-format
|
||||
|
|
@ -253,7 +255,7 @@ msgstr ""
|
|||
msgid "Your password has successfully been reset"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:67
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:69
|
||||
#: lib/mv_web/live/custom_field_live/index.ex:120
|
||||
#: lib/mv_web/live/custom_field_value_live/form.ex:77
|
||||
#: lib/mv_web/live/member_live/form.ex:82
|
||||
|
|
@ -267,7 +269,7 @@ msgstr ""
|
|||
msgid "Choose a member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:60
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:61
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
|
|
@ -287,7 +289,7 @@ msgstr ""
|
|||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:61
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:62
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Immutable"
|
||||
msgstr ""
|
||||
|
|
@ -309,13 +311,13 @@ msgid "Member"
|
|||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/layouts/navbar.ex:19
|
||||
#: lib/mv_web/live/member_live/index.ex:39
|
||||
#: lib/mv_web/live/member_live/index.ex:57
|
||||
#: lib/mv_web/live/member_live/index.html.heex:3
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Members"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:50
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:51
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
|
@ -358,17 +360,17 @@ msgstr ""
|
|||
msgid "Profil"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:62
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:63
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Required"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:34
|
||||
#: lib/mv_web/live/member_live/index.html.heex:37
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Select all members"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:48
|
||||
#: lib/mv_web/live/member_live/index.html.heex:51
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Select member"
|
||||
msgstr ""
|
||||
|
|
@ -414,7 +416,7 @@ msgstr ""
|
|||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:55
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:56
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Value type"
|
||||
msgstr ""
|
||||
|
|
@ -570,7 +572,7 @@ msgstr ""
|
|||
msgid "Click to sort"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:60
|
||||
#: lib/mv_web/live/member_live/index.html.heex:63
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "First name"
|
||||
msgstr ""
|
||||
|
|
@ -622,7 +624,7 @@ msgstr ""
|
|||
msgid "Custom field"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:115
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:117
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Custom field %{action} successfully"
|
||||
msgstr ""
|
||||
|
|
@ -637,7 +639,7 @@ msgstr ""
|
|||
msgid "Please select a custom field first"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:65
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:67
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Save Custom field"
|
||||
msgstr ""
|
||||
|
|
@ -647,7 +649,7 @@ msgstr ""
|
|||
msgid "Save Custom field value"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:45
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:46
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Use this form to manage custom_field records in your database."
|
||||
msgstr ""
|
||||
|
|
@ -699,52 +701,7 @@ msgstr ""
|
|||
msgid "To confirm deletion, please enter this text:"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:210
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:64
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "A member with this email already exists. To link with a different member, please change one of the email addresses first."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:185
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Available members"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:152
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Member will be unlinked when you save. Cannot select new member until saved."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:226
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Save to confirm linking."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:169
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Search for a member to link..."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:173
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Search for member to link"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:223
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Selected"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:143
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Unlink Member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:152
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Unlinking scheduled"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:342
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Failed to link member: %{error}"
|
||||
msgid "Show in overview"
|
||||
msgstr ""
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@ msgstr ""
|
|||
"Language: en\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:339
|
||||
#: lib/mv_web/components/core_components.ex:356
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Actions"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:200
|
||||
#: lib/mv_web/live/user_live/index.html.heex:72
|
||||
#: lib/mv_web/live/member_live/index.html.heex:202
|
||||
#: lib/mv_web/live/user_live/index.html.heex:65
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Are you sure?"
|
||||
msgstr ""
|
||||
|
|
@ -29,21 +29,21 @@ msgid "Attempting to reconnect"
|
|||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:54
|
||||
#: lib/mv_web/live/member_live/index.html.heex:145
|
||||
#: lib/mv_web/live/member_live/index.html.heex:148
|
||||
#: lib/mv_web/live/member_live/show.ex:59
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "City"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:202
|
||||
#: lib/mv_web/live/user_live/index.html.heex:74
|
||||
#: lib/mv_web/live/member_live/index.html.heex:204
|
||||
#: 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:194
|
||||
#: lib/mv_web/live/user_live/form.ex:251
|
||||
#: lib/mv_web/live/user_live/index.html.heex:66
|
||||
#: lib/mv_web/live/member_live/index.html.heex:196
|
||||
#: lib/mv_web/live/user_live/form.ex:141
|
||||
#: lib/mv_web/live/user_live/index.html.heex:59
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
|
@ -55,7 +55,7 @@ msgid "Edit Member"
|
|||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:47
|
||||
#: lib/mv_web/live/member_live/index.html.heex:77
|
||||
#: lib/mv_web/live/member_live/index.html.heex:80
|
||||
#: lib/mv_web/live/member_live/show.ex:50
|
||||
#: lib/mv_web/live/user_live/form.ex:46
|
||||
#: 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:51
|
||||
#: lib/mv_web/live/member_live/index.html.heex:179
|
||||
#: lib/mv_web/live/member_live/index.html.heex:182
|
||||
#: lib/mv_web/live/member_live/show.ex:56
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Join Date"
|
||||
|
|
@ -88,8 +88,8 @@ msgstr ""
|
|||
msgid "New Member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:191
|
||||
#: lib/mv_web/live/user_live/index.html.heex:63
|
||||
#: lib/mv_web/live/member_live/index.html.heex:193
|
||||
#: lib/mv_web/live/user_live/index.html.heex:56
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Show"
|
||||
msgstr ""
|
||||
|
|
@ -122,7 +122,7 @@ msgid "Exit Date"
|
|||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:56
|
||||
#: lib/mv_web/live/member_live/index.html.heex:111
|
||||
#: lib/mv_web/live/member_live/index.html.heex:114
|
||||
#: lib/mv_web/live/member_live/show.ex:61
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "House Number"
|
||||
|
|
@ -141,14 +141,14 @@ msgid "Paid"
|
|||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:50
|
||||
#: lib/mv_web/live/member_live/index.html.heex:162
|
||||
#: lib/mv_web/live/member_live/index.html.heex:165
|
||||
#: lib/mv_web/live/member_live/show.ex:55
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Phone Number"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:57
|
||||
#: lib/mv_web/live/member_live/index.html.heex:128
|
||||
#: lib/mv_web/live/member_live/index.html.heex:131
|
||||
#: lib/mv_web/live/member_live/show.ex:62
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Postal Code"
|
||||
|
|
@ -159,7 +159,7 @@ msgstr ""
|
|||
msgid "Save Member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:64
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:66
|
||||
#: lib/mv_web/live/custom_field_value_live/form.ex:74
|
||||
#: lib/mv_web/live/member_live/form.ex:79
|
||||
#: lib/mv_web/live/user_live/form.ex:234
|
||||
|
|
@ -168,7 +168,7 @@ msgid "Saving..."
|
|||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:55
|
||||
#: lib/mv_web/live/member_live/index.html.heex:94
|
||||
#: lib/mv_web/live/member_live/index.html.heex:97
|
||||
#: lib/mv_web/live/member_live/show.ex:60
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Street"
|
||||
|
|
@ -184,6 +184,7 @@ msgstr ""
|
|||
msgid "Id"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index/formatter.ex:65
|
||||
#: lib/mv_web/live/member_live/show.ex:53
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "No"
|
||||
|
|
@ -199,19 +200,20 @@ msgstr ""
|
|||
msgid "This is a member record from your database."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index/formatter.ex:64
|
||||
#: lib/mv_web/live/member_live/show.ex:53
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:108
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:110
|
||||
#: lib/mv_web/live/custom_field_value_live/form.ex:233
|
||||
#: lib/mv_web/live/member_live/form.ex:138
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "create"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:109
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:111
|
||||
#: lib/mv_web/live/custom_field_value_live/form.ex:234
|
||||
#: lib/mv_web/live/member_live/form.ex:139
|
||||
#, elixir-autogen, elixir-format
|
||||
|
|
@ -253,7 +255,7 @@ msgstr ""
|
|||
msgid "Your password has successfully been reset"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:67
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:69
|
||||
#: lib/mv_web/live/custom_field_live/index.ex:120
|
||||
#: lib/mv_web/live/custom_field_value_live/form.ex:77
|
||||
#: lib/mv_web/live/member_live/form.ex:82
|
||||
|
|
@ -267,7 +269,7 @@ msgstr ""
|
|||
msgid "Choose a member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:60
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:61
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
|
|
@ -287,7 +289,7 @@ msgstr ""
|
|||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:61
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:62
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Immutable"
|
||||
msgstr ""
|
||||
|
|
@ -309,13 +311,13 @@ msgid "Member"
|
|||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/layouts/navbar.ex:19
|
||||
#: lib/mv_web/live/member_live/index.ex:39
|
||||
#: lib/mv_web/live/member_live/index.ex:57
|
||||
#: lib/mv_web/live/member_live/index.html.heex:3
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Members"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:50
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:51
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
|
@ -358,17 +360,17 @@ msgstr ""
|
|||
msgid "Profil"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:62
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:63
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Required"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:34
|
||||
#: lib/mv_web/live/member_live/index.html.heex:37
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Select all members"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:48
|
||||
#: lib/mv_web/live/member_live/index.html.heex:51
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Select member"
|
||||
msgstr ""
|
||||
|
|
@ -414,7 +416,7 @@ msgstr ""
|
|||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:55
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:56
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Value type"
|
||||
msgstr ""
|
||||
|
|
@ -570,7 +572,7 @@ msgstr ""
|
|||
msgid "Click to sort"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:60
|
||||
#: lib/mv_web/live/member_live/index.html.heex:63
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "First name"
|
||||
msgstr ""
|
||||
|
|
@ -622,7 +624,7 @@ msgstr ""
|
|||
msgid "Custom field"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:115
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:117
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Custom field %{action} successfully"
|
||||
msgstr ""
|
||||
|
|
@ -637,7 +639,7 @@ msgstr ""
|
|||
msgid "Please select a custom field first"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:65
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:67
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Save Custom field"
|
||||
msgstr ""
|
||||
|
|
@ -647,7 +649,7 @@ msgstr ""
|
|||
msgid "Save Custom field value"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:45
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:46
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Use this form to manage custom_field records in your database."
|
||||
msgstr ""
|
||||
|
|
@ -748,3 +750,12 @@ msgstr ""
|
|||
#, elixir-autogen, elixir-format
|
||||
msgid "Failed to link member: %{error}"
|
||||
msgstr ""
|
||||
#: lib/mv_web/live/custom_field_live/form.ex:64
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Show in overview"
|
||||
msgstr ""
|
||||
|
||||
#~ #: lib/mv_web/live/custom_field_live/index.ex:97
|
||||
#~ #, elixir-autogen, elixir-format
|
||||
#~ msgid "To confirm deletion, please enter the custom field slug:"
|
||||
#~ msgstr ""
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
defmodule Mv.Repo.Migrations.AddShowInOverviewToCustomFields do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
alter table(:custom_fields) do
|
||||
add :show_in_overview, :boolean, null: false, default: true
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
alter table(:custom_fields) do
|
||||
remove :show_in_overview
|
||||
end
|
||||
end
|
||||
end
|
||||
118
priv/resource_snapshots/repo/custom_fields/20251119160509.json
Normal file
118
priv/resource_snapshots/repo/custom_fields/20251119160509.json
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "value_type",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "description",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "immutable",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "required",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "show_in_overview",
|
||||
"type": "boolean"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "9FBFC42DA896058F88DEDAE774614919222BF2EF2F8CB27386D02C2CE67F03DE",
|
||||
"identities": [
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"base_filter": null,
|
||||
"index_name": "custom_fields_unique_name_index",
|
||||
"keys": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "name"
|
||||
}
|
||||
],
|
||||
"name": "unique_name",
|
||||
"nils_distinct?": true,
|
||||
"where": null
|
||||
}
|
||||
],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.Mv.Repo",
|
||||
"schema": null,
|
||||
"table": "custom_fields"
|
||||
}
|
||||
77
test/membership/custom_field_show_in_overview_test.exs
Normal file
77
test/membership/custom_field_show_in_overview_test.exs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
defmodule Mv.Membership.CustomFieldShowInOverviewTest do
|
||||
@moduledoc """
|
||||
Tests for CustomField show_in_overview attribute.
|
||||
|
||||
Tests cover:
|
||||
- Creating custom fields with show_in_overview: true
|
||||
- Creating custom fields with show_in_overview: false (default)
|
||||
- Updating show_in_overview to true
|
||||
- Updating show_in_overview to false
|
||||
"""
|
||||
use Mv.DataCase, async: true
|
||||
|
||||
alias Mv.Membership.CustomField
|
||||
|
||||
describe "show_in_overview attribute" do
|
||||
test "creates custom field with show_in_overview: true" do
|
||||
assert {:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "test_field_show",
|
||||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert custom_field.show_in_overview == true
|
||||
end
|
||||
|
||||
test "creates custom field with show_in_overview: true (default)" do
|
||||
assert {:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "test_field_hide",
|
||||
value_type: :string
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert custom_field.show_in_overview == true
|
||||
end
|
||||
|
||||
test "updates show_in_overview to true" do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "test_field_update",
|
||||
value_type: :string,
|
||||
show_in_overview: false
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert {:ok, updated_field} =
|
||||
custom_field
|
||||
|> Ash.Changeset.for_update(:update, %{show_in_overview: true})
|
||||
|> Ash.update()
|
||||
|
||||
assert updated_field.show_in_overview == true
|
||||
end
|
||||
|
||||
test "updates show_in_overview to false" do
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "test_field_update2",
|
||||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
assert {:ok, updated_field} =
|
||||
custom_field
|
||||
|> Ash.Changeset.for_update(:update, %{show_in_overview: false})
|
||||
|> Ash.update()
|
||||
|
||||
assert updated_field.show_in_overview == false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
defmodule MvWeb.MemberLive.IndexCustomFieldsAccessibilityTest do
|
||||
@moduledoc """
|
||||
Accessibility tests for custom field columns in the member overview.
|
||||
|
||||
Tests cover:
|
||||
- SortHeaderComponent for custom fields has correct ARIA labels
|
||||
- Tab navigation works for custom field columns
|
||||
- Screen reader announcements for sorting
|
||||
"""
|
||||
use MvWeb.ConnCase, async: true
|
||||
import Phoenix.LiveViewTest
|
||||
require Ash.Query
|
||||
|
||||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
||||
|
||||
setup do
|
||||
# Create test member
|
||||
{:ok, member} =
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Alice",
|
||||
last_name: "Anderson",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Create custom field with show_in_overview: true
|
||||
{:ok, field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "membership_number",
|
||||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Create custom field value
|
||||
{:ok, _cfv} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "A001"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
%{member: member, field: field}
|
||||
end
|
||||
|
||||
test "sort header component for custom fields has correct ARIA labels", %{
|
||||
conn: conn,
|
||||
field: field
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
# Check that the sort button has aria-label
|
||||
assert html =~ ~r/aria-label=["']Click to sort["']/i or
|
||||
html =~ ~r/aria-label=["'].*sort.*["']/i
|
||||
|
||||
# Check that data-testid is present for testing
|
||||
assert html =~ ~r/data-testid=["']custom_field_#{field.id}["']/
|
||||
end
|
||||
|
||||
test "sort header component shows correct ARIA label when sorted ascending", %{
|
||||
conn: conn,
|
||||
field: field
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=asc")
|
||||
|
||||
html = render(view)
|
||||
|
||||
# Check that aria-label indicates ascending sort
|
||||
assert html =~ ~r/aria-label=["'].*ascending.*["']/i
|
||||
end
|
||||
|
||||
test "sort header component shows correct ARIA label when sorted descending", %{
|
||||
conn: conn,
|
||||
field: field
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=desc")
|
||||
|
||||
html = render(view)
|
||||
|
||||
# Check that aria-label indicates descending sort
|
||||
assert html =~ ~r/aria-label=["'].*descending.*["']/i
|
||||
end
|
||||
|
||||
test "custom field column header is keyboard accessible", %{conn: conn, field: field} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
# Check that the sort button is a button element (keyboard accessible)
|
||||
assert html =~ ~r/<button[^>]*data-testid=["']custom_field_#{field.id}["']/
|
||||
|
||||
# Button should not have tabindex="-1" (which would remove from tab order)
|
||||
refute html =~ ~r/tabindex=["']-1["']/
|
||||
end
|
||||
|
||||
test "custom field column header has proper semantic structure", %{conn: conn, field: field} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
# Check that custom field name is displayed in the header
|
||||
assert html =~ field.name
|
||||
end
|
||||
end
|
||||
262
test/mv_web/member_live/index_custom_fields_display_test.exs
Normal file
262
test/mv_web/member_live/index_custom_fields_display_test.exs
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
|
||||
@moduledoc """
|
||||
Tests for displaying custom fields in the member overview.
|
||||
|
||||
Tests cover:
|
||||
- Custom fields with show_in_overview: true are displayed
|
||||
- Custom fields with show_in_overview: false are not displayed
|
||||
- Multiple custom fields with show_in_overview: true are all displayed
|
||||
- Custom field values are correctly formatted for different types
|
||||
- Members without custom field values show empty cell or "-"
|
||||
"""
|
||||
use MvWeb.ConnCase, async: true
|
||||
import Phoenix.LiveViewTest
|
||||
require Ash.Query
|
||||
|
||||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
||||
|
||||
setup do
|
||||
# Create test members
|
||||
{:ok, member1} =
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Alice",
|
||||
last_name: "Anderson",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, member2} =
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Bob",
|
||||
last_name: "Brown",
|
||||
email: "bob@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Create custom fields
|
||||
{:ok, field_show_string} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "phone_mobile",
|
||||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, field_hide} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "internal_note",
|
||||
value_type: :string,
|
||||
show_in_overview: false
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, field_show_integer} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "membership_number",
|
||||
value_type: :integer,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, field_show_boolean} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "newsletter",
|
||||
value_type: :boolean,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, field_show_date} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "birthday",
|
||||
value_type: :date,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, field_show_email} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "secondary_email",
|
||||
value_type: :email,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Create custom field values for member1
|
||||
{:ok, _cfv1} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member1.id,
|
||||
custom_field_id: field_show_string.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "+49123456789"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, _cfv2} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member1.id,
|
||||
custom_field_id: field_show_integer.id,
|
||||
value: %{"_union_type" => "integer", "_union_value" => 12_345}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, _cfv3} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member1.id,
|
||||
custom_field_id: field_show_boolean.id,
|
||||
value: %{"_union_type" => "boolean", "_union_value" => true}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, _cfv4} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member1.id,
|
||||
custom_field_id: field_show_date.id,
|
||||
value: %{"_union_type" => "date", "_union_value" => ~D[1990-05-15]}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, _cfv5} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member1.id,
|
||||
custom_field_id: field_show_email.id,
|
||||
value: %{"_union_type" => "email", "_union_value" => "alice.private@example.com"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Create hidden custom field value (should not be displayed)
|
||||
{:ok, _cfv_hidden} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member1.id,
|
||||
custom_field_id: field_hide.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "Internal note"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
%{
|
||||
member1: member1,
|
||||
member2: member2,
|
||||
field_show_string: field_show_string,
|
||||
field_hide: field_hide,
|
||||
field_show_integer: field_show_integer,
|
||||
field_show_boolean: field_show_boolean,
|
||||
field_show_date: field_show_date,
|
||||
field_show_email: field_show_email
|
||||
}
|
||||
end
|
||||
|
||||
test "displays custom field with show_in_overview: true", %{
|
||||
conn: conn,
|
||||
member1: _member1,
|
||||
field_show_string: field
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
# Check that the custom field column header is displayed
|
||||
assert html =~ field.name
|
||||
|
||||
# Check that the value is displayed
|
||||
assert html =~ "+49123456789"
|
||||
end
|
||||
|
||||
test "does not display custom field with show_in_overview: false", %{
|
||||
conn: conn,
|
||||
member1: _member1,
|
||||
field_hide: field
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
# Check that the hidden custom field column header is NOT displayed
|
||||
refute html =~ field.name
|
||||
|
||||
# Check that the value is NOT displayed
|
||||
refute html =~ "Internal note"
|
||||
end
|
||||
|
||||
test "displays multiple custom fields with show_in_overview: true", %{
|
||||
conn: conn,
|
||||
field_show_string: field_string,
|
||||
field_show_integer: field_integer,
|
||||
field_show_boolean: field_boolean
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
# Check that all visible custom field column headers are displayed
|
||||
assert html =~ field_string.name
|
||||
assert html =~ field_integer.name
|
||||
assert html =~ field_boolean.name
|
||||
end
|
||||
|
||||
test "formats string custom field values correctly", %{conn: conn, member1: _member1} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
assert html =~ "+49123456789"
|
||||
end
|
||||
|
||||
test "formats integer custom field values correctly", %{conn: conn, member1: _member1} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
assert html =~ "12345"
|
||||
end
|
||||
|
||||
test "formats boolean custom field values correctly", %{conn: conn, member1: _member1} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
# Boolean should be displayed as "Yes" or "No" or similar
|
||||
# Check for true representation
|
||||
assert html =~ "true" or html =~ "Yes" or html =~ "Ja"
|
||||
end
|
||||
|
||||
test "formats date custom field values correctly", %{conn: conn, member1: _member1} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
# Date should be displayed in readable format
|
||||
assert html =~ "1990" or html =~ "1990-05-15" or html =~ "15.05.1990"
|
||||
end
|
||||
|
||||
test "formats email custom field values correctly", %{conn: conn, member1: _member1} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
assert html =~ "alice.private@example.com"
|
||||
end
|
||||
|
||||
test "shows empty cell or placeholder for members without custom field values", %{
|
||||
conn: conn,
|
||||
member2: _member2,
|
||||
field_show_string: field
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
# The custom field column should exist
|
||||
assert html =~ field.name
|
||||
|
||||
# Member2 should have an empty cell for this field
|
||||
# We check that member2's row exists but doesn't have the value
|
||||
assert html =~ "Bob Brown"
|
||||
# The value should not appear for member2 (only for member1)
|
||||
# We check that the value appears somewhere (for member1) but member2 row should have "-"
|
||||
assert html =~ "+49123456789"
|
||||
end
|
||||
end
|
||||
173
test/mv_web/member_live/index_custom_fields_edge_cases_test.exs
Normal file
173
test/mv_web/member_live/index_custom_fields_edge_cases_test.exs
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
|
||||
@moduledoc """
|
||||
Edge case tests for custom fields in the member overview.
|
||||
|
||||
Tests cover:
|
||||
- Custom field without values (all members have no value)
|
||||
- Very long custom field values are correctly displayed
|
||||
"""
|
||||
use MvWeb.ConnCase, async: true
|
||||
import Phoenix.LiveViewTest
|
||||
require Ash.Query
|
||||
|
||||
alias Mv.Membership.{CustomField, Member}
|
||||
|
||||
test "displays custom field column even when no members have values", %{conn: conn} do
|
||||
# Create test members without custom field values
|
||||
{:ok, _member1} =
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Alice",
|
||||
last_name: "Anderson",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, _member2} =
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Bob",
|
||||
last_name: "Brown",
|
||||
email: "bob@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Create custom field with show_in_overview: true but no values
|
||||
{:ok, field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "membership_number",
|
||||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
# Check that the custom field column header is still displayed
|
||||
assert html =~ field.name
|
||||
end
|
||||
|
||||
test "displays very long custom field values correctly", %{conn: conn} do
|
||||
# Create test member
|
||||
{:ok, member} =
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Alice",
|
||||
last_name: "Anderson",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Create custom field
|
||||
{:ok, field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "long_note",
|
||||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Create very long value (but within limits)
|
||||
long_value = String.duplicate("A", 500)
|
||||
|
||||
{:ok, _cfv} =
|
||||
Mv.Membership.CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => long_value}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
# Check that the value is displayed (may be truncated in UI, but should be present)
|
||||
# We check for at least part of the value
|
||||
assert html =~ "A" or html =~ long_value
|
||||
end
|
||||
|
||||
test "handles multiple custom fields with show_in_overview correctly", %{conn: conn} do
|
||||
# Create test member
|
||||
{:ok, member} =
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Alice",
|
||||
last_name: "Anderson",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Create multiple custom fields with show_in_overview: true
|
||||
{:ok, field1} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "field1",
|
||||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, field2} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "field2",
|
||||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, field3} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "field3",
|
||||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Create values for all fields
|
||||
{:ok, _cfv1} =
|
||||
Mv.Membership.CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: field1.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "Value1"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, _cfv2} =
|
||||
Mv.Membership.CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: field2.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "Value2"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, _cfv3} =
|
||||
Mv.Membership.CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member.id,
|
||||
custom_field_id: field3.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "Value3"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
# Check that all custom field columns are displayed
|
||||
assert html =~ field1.name
|
||||
assert html =~ field2.name
|
||||
assert html =~ field3.name
|
||||
|
||||
# Check that all values are displayed
|
||||
assert html =~ "Value1"
|
||||
assert html =~ "Value2"
|
||||
assert html =~ "Value3"
|
||||
end
|
||||
end
|
||||
459
test/mv_web/member_live/index_custom_fields_sorting_test.exs
Normal file
459
test/mv_web/member_live/index_custom_fields_sorting_test.exs
Normal file
|
|
@ -0,0 +1,459 @@
|
|||
defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
||||
@moduledoc """
|
||||
Tests for sorting by custom fields in the member overview.
|
||||
|
||||
Tests cover:
|
||||
- Sorting by custom field (ascending)
|
||||
- Sorting by custom field (descending)
|
||||
- Sorting by custom field works with search
|
||||
- Sorting by custom field works with URL parameters
|
||||
- Sorting by custom field works with other columns
|
||||
"""
|
||||
use MvWeb.ConnCase, async: true
|
||||
import Phoenix.LiveViewTest
|
||||
require Ash.Query
|
||||
|
||||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
||||
|
||||
setup do
|
||||
# Create test members
|
||||
{:ok, member1} =
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Alice",
|
||||
last_name: "Anderson",
|
||||
email: "alice@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, member2} =
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Bob",
|
||||
last_name: "Brown",
|
||||
email: "bob@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, member3} =
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Charlie",
|
||||
last_name: "Clark",
|
||||
email: "charlie@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Create custom field with show_in_overview: true
|
||||
{:ok, field_string} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "membership_number",
|
||||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, field_integer} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "priority",
|
||||
value_type: :integer,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Create custom field values
|
||||
{:ok, _cfv1} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member1.id,
|
||||
custom_field_id: field_string.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "A001"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, _cfv2} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member2.id,
|
||||
custom_field_id: field_string.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "C003"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, _cfv3} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member3.id,
|
||||
custom_field_id: field_string.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "B002"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, _cfv4} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member1.id,
|
||||
custom_field_id: field_integer.id,
|
||||
value: %{"_union_type" => "integer", "_union_value" => 10}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, _cfv5} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member2.id,
|
||||
custom_field_id: field_integer.id,
|
||||
value: %{"_union_type" => "integer", "_union_value" => 30}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, _cfv6} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member3.id,
|
||||
custom_field_id: field_integer.id,
|
||||
value: %{"_union_type" => "integer", "_union_value" => 20}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
%{
|
||||
member1: member1,
|
||||
member2: member2,
|
||||
member3: member3,
|
||||
field_string: field_string,
|
||||
field_integer: field_integer
|
||||
}
|
||||
end
|
||||
|
||||
test "sorts by custom field ascending", %{conn: conn, field_string: field} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Click on custom field column header to sort
|
||||
view
|
||||
|> element("[data-testid='custom_field_#{field.id}']")
|
||||
|> render_click()
|
||||
|
||||
# Check URL was updated
|
||||
assert_patch(view, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=asc")
|
||||
|
||||
# Verify sort state
|
||||
assert has_element?(view, "[data-testid='custom_field_#{field.id}'][aria-label='ascending']")
|
||||
end
|
||||
|
||||
test "sorts by custom field descending", %{conn: conn, field_string: field} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?sort_field=custom_field_#{field.id}&sort_order=asc")
|
||||
|
||||
# Click again to toggle to descending
|
||||
view
|
||||
|> element("[data-testid='custom_field_#{field.id}']")
|
||||
|> render_click()
|
||||
|
||||
# Check URL was updated
|
||||
assert_patch(view, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=desc")
|
||||
|
||||
# Verify sort state
|
||||
assert has_element?(view, "[data-testid='custom_field_#{field.id}'][aria-label='descending']")
|
||||
end
|
||||
|
||||
test "sorting by custom field works with search", %{conn: conn, field_string: field} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?query=Alice")
|
||||
|
||||
# Click on custom field column header to sort
|
||||
view
|
||||
|> element("[data-testid='custom_field_#{field.id}']")
|
||||
|> render_click()
|
||||
|
||||
# Check URL maintains search query
|
||||
assert_patch(view, "/members?query=Alice&sort_field=custom_field_#{field.id}&sort_order=asc")
|
||||
end
|
||||
|
||||
test "sorting by custom field works with URL parameters", %{conn: conn, field_string: field} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=desc")
|
||||
|
||||
# Check that the sort state is correctly applied
|
||||
assert has_element?(view, "[data-testid='custom_field_#{field.id}'][aria-label='descending']")
|
||||
end
|
||||
|
||||
test "clicking different custom field column resets order to ascending", %{
|
||||
conn: conn,
|
||||
field_string: field_string,
|
||||
field_integer: field_integer
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?query=&sort_field=custom_field_#{field_string.id}&sort_order=desc")
|
||||
|
||||
# Click on a different custom field column
|
||||
view
|
||||
|> element("[data-testid='custom_field_#{field_integer.id}']")
|
||||
|> render_click()
|
||||
|
||||
assert_patch(
|
||||
view,
|
||||
"/members?query=&sort_field=custom_field_#{field_integer.id}&sort_order=asc"
|
||||
)
|
||||
end
|
||||
|
||||
test "clicking regular column after custom field column works", %{
|
||||
conn: conn,
|
||||
field_string: field
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=desc")
|
||||
|
||||
# Click on email column
|
||||
view
|
||||
|> element("[data-testid='email']")
|
||||
|> render_click()
|
||||
|
||||
assert_patch(view, "/members?query=&sort_field=email&sort_order=asc")
|
||||
end
|
||||
|
||||
test "clicking custom field column after regular column works", %{
|
||||
conn: conn,
|
||||
field_string: field
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?query=&sort_field=email&sort_order=desc")
|
||||
|
||||
# Click on custom field column
|
||||
view
|
||||
|> element("[data-testid='custom_field_#{field.id}']")
|
||||
|> render_click()
|
||||
|
||||
assert_patch(view, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=asc")
|
||||
end
|
||||
|
||||
test "NULL values and empty strings are always sorted last (ASC)", %{conn: conn} do
|
||||
# Create additional members with NULL and empty string values
|
||||
{:ok, member_with_value} =
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "WithValue",
|
||||
last_name: "Test",
|
||||
email: "withvalue@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, member_with_empty} =
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "WithEmpty",
|
||||
last_name: "Test",
|
||||
email: "withempty@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, member_with_null} =
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "WithNull",
|
||||
last_name: "Test",
|
||||
email: "withnull@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, member_with_another_value} =
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "AnotherValue",
|
||||
last_name: "Test",
|
||||
email: "another@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Create custom field
|
||||
{:ok, field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "test_field",
|
||||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Create values: one with actual value, one with empty string, one with NULL (no value), another with value
|
||||
{:ok, _cfv1} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member_with_value.id,
|
||||
custom_field_id: field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "Zebra"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, _cfv2} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member_with_empty.id,
|
||||
custom_field_id: field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => ""}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# member_with_null has no custom field value (NULL)
|
||||
|
||||
{:ok, _cfv3} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member_with_another_value.id,
|
||||
custom_field_id: field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "Apple"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=asc")
|
||||
|
||||
html = render(view)
|
||||
|
||||
# Find positions of member first names in the HTML to verify sort order
|
||||
apple_pos = :binary.match(html, member_with_another_value.first_name)
|
||||
zebra_pos = :binary.match(html, member_with_value.first_name)
|
||||
empty_pos = :binary.match(html, member_with_empty.first_name)
|
||||
null_pos = :binary.match(html, member_with_null.first_name)
|
||||
|
||||
assert apple_pos != :nomatch, "AnotherValue (Apple) should be in HTML"
|
||||
assert zebra_pos != :nomatch, "WithValue (Zebra) should be in HTML"
|
||||
assert empty_pos != :nomatch, "WithEmpty should be in HTML"
|
||||
assert null_pos != :nomatch, "WithNull should be in HTML"
|
||||
|
||||
{apple_idx, _} = apple_pos
|
||||
{zebra_idx, _} = zebra_pos
|
||||
{empty_idx, _} = empty_pos
|
||||
{null_idx, _} = null_pos
|
||||
|
||||
# In ASC order: Apple should come before Zebra
|
||||
assert apple_idx < zebra_idx, "Apple should come before Zebra in ASC order"
|
||||
|
||||
# NULL and empty should come after all values
|
||||
assert apple_idx < empty_idx, "Apple should come before empty value"
|
||||
assert apple_idx < null_idx, "Apple should come before NULL value"
|
||||
assert zebra_idx < empty_idx, "Zebra should come before empty value"
|
||||
assert zebra_idx < null_idx, "Zebra should come before NULL value"
|
||||
end
|
||||
|
||||
test "NULL values and empty strings are always sorted last (DESC)", %{conn: conn} do
|
||||
# Create additional members with NULL and empty string values
|
||||
{:ok, member_with_value} =
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "WithValue",
|
||||
last_name: "Test",
|
||||
email: "withvalue@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, member_with_empty} =
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "WithEmpty",
|
||||
last_name: "Test",
|
||||
email: "withempty@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, member_with_null} =
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "WithNull",
|
||||
last_name: "Test",
|
||||
email: "withnull@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, member_with_another_value} =
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "AnotherValue",
|
||||
last_name: "Test",
|
||||
email: "another@example.com"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Create custom field
|
||||
{:ok, field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "test_field",
|
||||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Create values: one with actual value, one with empty string, one with NULL (no value), another with value
|
||||
{:ok, _cfv1} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member_with_value.id,
|
||||
custom_field_id: field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "Apple"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, _cfv2} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member_with_empty.id,
|
||||
custom_field_id: field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => ""}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# member_with_null has no custom field value (NULL)
|
||||
|
||||
{:ok, _cfv3} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member_with_another_value.id,
|
||||
custom_field_id: field.id,
|
||||
value: %{"_union_type" => "string", "_union_value" => "Zebra"}
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=desc")
|
||||
|
||||
html = render(view)
|
||||
|
||||
# Find positions of member first names in the HTML to verify sort order
|
||||
apple_pos = :binary.match(html, member_with_value.first_name)
|
||||
zebra_pos = :binary.match(html, member_with_another_value.first_name)
|
||||
empty_pos = :binary.match(html, member_with_empty.first_name)
|
||||
null_pos = :binary.match(html, member_with_null.first_name)
|
||||
|
||||
assert apple_pos != :nomatch, "WithValue (Apple) should be in HTML"
|
||||
assert zebra_pos != :nomatch, "AnotherValue (Zebra) should be in HTML"
|
||||
assert empty_pos != :nomatch, "WithEmpty should be in HTML"
|
||||
assert null_pos != :nomatch, "WithNull should be in HTML"
|
||||
|
||||
{apple_idx, _} = apple_pos
|
||||
{zebra_idx, _} = zebra_pos
|
||||
{empty_idx, _} = empty_pos
|
||||
{null_idx, _} = null_pos
|
||||
|
||||
# In DESC order: Zebra should come before Apple
|
||||
assert zebra_idx < apple_idx, "Zebra should come before Apple in DESC order"
|
||||
|
||||
# NULL and empty should come after all values
|
||||
assert zebra_idx < empty_idx, "Zebra should come before empty value"
|
||||
assert zebra_idx < null_idx, "Zebra should come before NULL value"
|
||||
assert apple_idx < empty_idx, "Apple should come before empty value"
|
||||
assert apple_idx < null_idx, "Apple should come before NULL value"
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue
This line isn't doing anything?