Merge pull request 'Show custom fields per default in member overview closes #197 and #153' (#208) from feature/197_custom_fields_overview into main
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #208 Reviewed-by: moritz <moritz@noreply.git.local-it.org>
This commit is contained in:
commit
a132383d81
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
|
## Implementation Decisions
|
||||||
|
|
||||||
### Architecture Patterns
|
### Architecture Patterns
|
||||||
|
|
@ -390,6 +395,7 @@ defmodule Mv.Membership.CustomField do
|
||||||
attribute :value_type, :atom # :string, :integer, :boolean, :date, :email
|
attribute :value_type, :atom # :string, :integer, :boolean, :date, :email
|
||||||
attribute :immutable, :boolean # Can't change after creation
|
attribute :immutable, :boolean # Can't change after creation
|
||||||
attribute :required, :boolean # All members must have this
|
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
|
end
|
||||||
|
|
||||||
# CustomFieldValue stores values
|
# CustomFieldValue stores values
|
||||||
|
|
|
||||||
|
|
@ -94,15 +94,18 @@
|
||||||
- ✅ CustomFieldValue type management
|
- ✅ CustomFieldValue type management
|
||||||
- ✅ Dynamic custom field value assignment to members
|
- ✅ Dynamic custom field value assignment to members
|
||||||
- ✅ Union type storage (JSONB)
|
- ✅ 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:**
|
**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]
|
- [#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)
|
- [#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)
|
- [#153](https://git.local-it.org/local-it/mitgliederverwaltung/issues/153) - Sorting functionalities for custom fields (M, Low priority)
|
||||||
|
|
||||||
**Missing Features:**
|
**Missing Features:**
|
||||||
- ❌ Default field visibility configuration
|
|
||||||
- ❌ Field groups/categories
|
- ❌ Field groups/categories
|
||||||
- ❌ Conditional fields (show field X if field Y = value)
|
- ❌ Conditional fields (show field X if field Y = value)
|
||||||
- ❌ Field validation rules (min/max, regex patterns)
|
- ❌ Field validation rules (min/max, regex patterns)
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ defmodule Mv.Membership.CustomField do
|
||||||
- `description` - Optional human-readable description
|
- `description` - Optional human-readable description
|
||||||
- `immutable` - If true, custom field values cannot be changed after creation
|
- `immutable` - If true, custom field values cannot be changed after creation
|
||||||
- `required` - If true, all members must have this custom field (future feature)
|
- `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
|
## Supported Value Types
|
||||||
- `:string` - Text data (max 10,000 characters)
|
- `:string` - Text data (max 10,000 characters)
|
||||||
|
|
@ -59,10 +60,10 @@ defmodule Mv.Membership.CustomField do
|
||||||
|
|
||||||
actions do
|
actions do
|
||||||
defaults [:read, :update]
|
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
|
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
|
change Mv.Membership.CustomField.Changes.GenerateSlug
|
||||||
validate string_length(:slug, min: 1)
|
validate string_length(:slug, min: 1)
|
||||||
end
|
end
|
||||||
|
|
@ -119,6 +120,12 @@ defmodule Mv.Membership.CustomField do
|
||||||
attribute :required, :boolean,
|
attribute :required, :boolean,
|
||||||
default: false,
|
default: false,
|
||||||
allow_nil?: 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
|
end
|
||||||
|
|
||||||
relationships do
|
relationships do
|
||||||
|
|
|
||||||
|
|
@ -318,6 +318,13 @@ defmodule MvWeb.CoreComponents do
|
||||||
default: &Function.identity/1,
|
default: &Function.identity/1,
|
||||||
doc: "the function for mapping each row before calling the :col and :action slots"
|
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
|
slot :col, required: true do
|
||||||
attr :label, :string
|
attr :label, :string
|
||||||
end
|
end
|
||||||
|
|
@ -335,6 +342,16 @@ defmodule MvWeb.CoreComponents do
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th :for={col <- @col}>{col[:label]}</th>
|
<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 != []}>
|
<th :if={@action != []}>
|
||||||
<span class="sr-only">{gettext("Actions")}</span>
|
<span class="sr-only">{gettext("Actions")}</span>
|
||||||
</th>
|
</th>
|
||||||
|
|
@ -349,6 +366,23 @@ defmodule MvWeb.CoreComponents do
|
||||||
>
|
>
|
||||||
{render_slot(col, @row_item.(row))}
|
{render_slot(col, @row_item.(row))}
|
||||||
</td>
|
</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">
|
<td :if={@action != []} class="w-0 font-semibold">
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<%= for action <- @action do %>
|
<%= for action <- @action do %>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ defmodule MvWeb.CustomFieldLive.Form do
|
||||||
- description - Human-readable explanation
|
- description - Human-readable explanation
|
||||||
- immutable - If true, values cannot be changed after creation (default: false)
|
- immutable - If true, values cannot be changed after creation (default: false)
|
||||||
- required - If true, all members must have this custom field (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
|
## Value Type Selection
|
||||||
- `:string` - Text data (unlimited length)
|
- `: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[:description]} type="text" label={gettext("Description")} />
|
||||||
<.input field={@form[:immutable]} type="checkbox" label={gettext("Immutable")} />
|
<.input field={@form[:immutable]} type="checkbox" label={gettext("Immutable")} />
|
||||||
<.input field={@form[:required]} type="checkbox" label={gettext("Required")} />
|
<.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">
|
<.button phx-disable-with={gettext("Saving...")} variant="primary">
|
||||||
{gettext("Save Custom field")}
|
{gettext("Save Custom field")}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,14 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
"""
|
"""
|
||||||
use MvWeb, :live_view
|
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 """
|
@doc """
|
||||||
Initializes the LiveView state.
|
Initializes the LiveView state.
|
||||||
|
|
||||||
|
|
@ -34,6 +42,16 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
"""
|
"""
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
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 =
|
||||||
socket
|
socket
|
||||||
|> assign(:page_title, gettext("Members"))
|
|> 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_field, fn -> :first_name end)
|
||||||
|> assign_new(:sort_order, fn -> :asc end)
|
|> assign_new(:sort_order, fn -> :asc end)
|
||||||
|> assign(:selected_members, [])
|
|> assign(:selected_members, [])
|
||||||
|
|> assign(:custom_fields_visible, custom_fields_visible)
|
||||||
|
|
||||||
# We call handle params to use the query from the URL
|
# We call handle params to use the query from the URL
|
||||||
{:ok, socket}
|
{:ok, socket}
|
||||||
|
|
@ -60,6 +79,8 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
"""
|
"""
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("delete", %{"id" => id}, socket) do
|
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)
|
member = Ash.get!(Mv.Membership.Member, id)
|
||||||
Ash.destroy!(member)
|
Ash.destroy!(member)
|
||||||
|
|
||||||
|
|
@ -108,7 +129,14 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
"""
|
"""
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:sort, field_str}, socket) do
|
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)
|
{new_field, new_order} = determine_new_sort(field, socket)
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|
|
@ -158,10 +186,38 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
|> maybe_update_search(params)
|
|> maybe_update_search(params)
|
||||||
|> maybe_update_sort(params)
|
|> maybe_update_sort(params)
|
||||||
|> load_members(params["query"])
|
|> load_members(params["query"])
|
||||||
|
|> prepare_dynamic_cols()
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
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
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assign(socket, :dynamic_cols, dynamic_cols)
|
||||||
|
end
|
||||||
|
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
# FUNCTIONS
|
# FUNCTIONS
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
|
|
@ -177,8 +233,8 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
|
|
||||||
# Updates both the active and old SortHeader components
|
# Updates both the active and old SortHeader components
|
||||||
defp update_sort_components(socket, old_field, new_field, new_order) do
|
defp update_sort_components(socket, old_field, new_field, new_order) do
|
||||||
active_id = :"sort_#{new_field}"
|
active_id = to_sort_id(new_field)
|
||||||
old_id = :"sort_#{old_field}"
|
old_id = to_sort_id(old_field)
|
||||||
|
|
||||||
# Update the new SortHeader
|
# Update the new SortHeader
|
||||||
send_update(MvWeb.Components.SortHeaderComponent,
|
send_update(MvWeb.Components.SortHeaderComponent,
|
||||||
|
|
@ -197,11 +253,32 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
socket
|
socket
|
||||||
end
|
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
|
# Builds sort URL and pushes navigation patch
|
||||||
defp push_sort_url(socket, field, order) do
|
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_params = %{
|
||||||
"query" => socket.assigns.query,
|
"query" => socket.assigns.query,
|
||||||
"sort_field" => Atom.to_string(field),
|
"sort_field" => field_str,
|
||||||
"sort_order" => Atom.to_string(order)
|
"sort_order" => Atom.to_string(order)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -214,7 +291,24 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
)}
|
)}
|
||||||
end
|
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
|
defp load_members(socket, search_query) do
|
||||||
query =
|
query =
|
||||||
Mv.Membership.Member
|
Mv.Membership.Member
|
||||||
|
|
@ -232,16 +326,71 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
:join_date
|
: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)
|
||||||
|
|
||||||
# Apply the search filter first
|
# Apply the search filter first
|
||||||
query = apply_search_filter(query, search_query)
|
query = apply_search_filter(query, search_query)
|
||||||
|
|
||||||
# Apply sorting based on current socket state
|
# 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)
|
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(
|
||||||
|
members,
|
||||||
|
socket.assigns.sort_field,
|
||||||
|
socket.assigns.sort_order,
|
||||||
|
socket.assigns.custom_fields_visible
|
||||||
|
)
|
||||||
|
else
|
||||||
|
members
|
||||||
|
end
|
||||||
|
|
||||||
assign(socket, :members, members)
|
assign(socket, :members, members)
|
||||||
end
|
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
|
||||||
|
|> 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
|
# Helper Functions
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
|
|
@ -264,15 +413,24 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
defp toggle_order(nil), do: :asc
|
defp toggle_order(nil), do: :asc
|
||||||
|
|
||||||
# Function to sort the column if needed
|
# 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),
|
defp maybe_sort(query, field, order, _custom_fields) when not is_nil(field) do
|
||||||
do: Ash.Query.sort(query, [{field, :asc}])
|
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),
|
defp maybe_sort(query, _, _, _), do: {query, false}
|
||||||
do: Ash.Query.sort(query, [{field, :desc}])
|
|
||||||
|
|
||||||
defp maybe_sort(query, _, _), do: query
|
|
||||||
|
|
||||||
# Validate that a field is sortable
|
# Validate that a field is sortable
|
||||||
defp valid_sort_field?(field) when is_atom(field) do
|
defp valid_sort_field?(field) when is_atom(field) do
|
||||||
|
|
@ -288,12 +446,188 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
:join_date
|
: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
|
end
|
||||||
|
|
||||||
defp valid_sort_field?(_), do: false
|
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
|
defp maybe_update_sort(socket, %{"sort_field" => sf, "sort_order" => so}) do
|
||||||
field = determine_field(socket.assigns.sort_field, sf)
|
field = determine_field(socket.assigns.sort_field, sf)
|
||||||
order = determine_order(socket.assigns.sort_order, so)
|
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 maybe_update_sort(socket, _), do: socket
|
||||||
|
|
||||||
defp determine_field(default, sf) do
|
# Determine sort field from URL parameter, validating against allowed fields
|
||||||
case sf do
|
defp determine_field(default, ""), do: default
|
||||||
"" ->
|
defp determine_field(default, nil), do: default
|
||||||
default
|
|
||||||
|
|
||||||
nil ->
|
# Determines the valid sort field from a URL parameter.
|
||||||
default
|
#
|
||||||
|
# Validates the field against allowed sort fields (regular member fields or custom fields).
|
||||||
sf when is_binary(sf) ->
|
# Falls back to default if the field is invalid.
|
||||||
sf
|
#
|
||||||
|> String.to_existing_atom()
|
# Parameters:
|
||||||
|> handle_atom_conversion(default)
|
# - `default` - Default field to use if validation fails
|
||||||
|
# - `sf` - Sort field from URL (can be atom, string, nil, or empty string)
|
||||||
sf when is_atom(sf) ->
|
#
|
||||||
handle_atom_conversion(sf, default)
|
# 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_")
|
||||||
default
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_atom_conversion(val, default) when is_atom(val) do
|
defp determine_field(default, sf) when is_atom(sf) do
|
||||||
if valid_sort_field?(val), do: val, else: default
|
if valid_sort_field?(sf), do: sf, else: default
|
||||||
end
|
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
|
defp determine_order(default, so) do
|
||||||
case so do
|
case so do
|
||||||
"" -> default
|
"" -> default
|
||||||
|
|
@ -350,4 +701,36 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
# Keep the previous search query if no new one is provided
|
# Keep the previous search query if no new one is provided
|
||||||
socket
|
socket
|
||||||
end
|
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
|
end
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,9 @@
|
||||||
id="members"
|
id="members"
|
||||||
rows={@members}
|
rows={@members}
|
||||||
row_click={fn member -> JS.navigate(~p"/members/#{member}") end}
|
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> -->
|
<!-- <:col :let={member} label="Id">{member.id}</:col> -->
|
||||||
|
|
@ -185,7 +188,6 @@
|
||||||
>
|
>
|
||||||
{member.join_date}
|
{member.join_date}
|
||||||
</:col>
|
</:col>
|
||||||
|
|
||||||
<:action :let={member}>
|
<:action :let={member}>
|
||||||
<div class="sr-only">
|
<div class="sr-only">
|
||||||
<.link navigate={~p"/members/#{member}"}>{gettext("Show")}</.link>
|
<.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
|
||||||
|
|
||||||
|
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 ""
|
msgstr ""
|
||||||
"Language: en\n"
|
"Language: en\n"
|
||||||
|
|
||||||
#: lib/mv_web/components/core_components.ex:339
|
#: lib/mv_web/components/core_components.ex:356
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Actions"
|
msgid "Actions"
|
||||||
msgstr "Aktionen"
|
msgstr "Aktionen"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:200
|
#: lib/mv_web/live/member_live/index.html.heex:202
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:72
|
#: lib/mv_web/live/user_live/index.html.heex:65
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Are you sure?"
|
msgid "Are you sure?"
|
||||||
msgstr "Bist du sicher?"
|
msgstr "Bist du sicher?"
|
||||||
|
|
@ -28,21 +28,21 @@ msgid "Attempting to reconnect"
|
||||||
msgstr "Verbindung wird wiederhergestellt"
|
msgstr "Verbindung wird wiederhergestellt"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:54
|
#: 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
|
#: lib/mv_web/live/member_live/show.ex:59
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "City"
|
msgid "City"
|
||||||
msgstr "Stadt"
|
msgstr "Stadt"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:202
|
#: lib/mv_web/live/member_live/index.html.heex:204
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:74
|
#: lib/mv_web/live/user_live/index.html.heex:67
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr "Löschen"
|
msgstr "Löschen"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:194
|
#: lib/mv_web/live/member_live/index.html.heex:196
|
||||||
#: lib/mv_web/live/user_live/form.ex:251
|
#: lib/mv_web/live/user_live/form.ex:141
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:66
|
#: lib/mv_web/live/user_live/index.html.heex:59
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Edit"
|
msgid "Edit"
|
||||||
msgstr "Bearbeite"
|
msgstr "Bearbeite"
|
||||||
|
|
@ -54,7 +54,7 @@ msgid "Edit Member"
|
||||||
msgstr "Mitglied bearbeiten"
|
msgstr "Mitglied bearbeiten"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:47
|
#: 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/member_live/show.ex:50
|
||||||
#: lib/mv_web/live/user_live/form.ex:46
|
#: lib/mv_web/live/user_live/form.ex:46
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:44
|
#: lib/mv_web/live/user_live/index.html.heex:44
|
||||||
|
|
@ -70,7 +70,7 @@ msgid "First Name"
|
||||||
msgstr "Vorname"
|
msgstr "Vorname"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:51
|
#: 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
|
#: lib/mv_web/live/member_live/show.ex:56
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Join Date"
|
msgid "Join Date"
|
||||||
|
|
@ -87,8 +87,8 @@ msgstr "Nachname"
|
||||||
msgid "New Member"
|
msgid "New Member"
|
||||||
msgstr "Neues Mitglied"
|
msgstr "Neues Mitglied"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:191
|
#: lib/mv_web/live/member_live/index.html.heex:193
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:63
|
#: lib/mv_web/live/user_live/index.html.heex:56
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Show"
|
msgid "Show"
|
||||||
msgstr "Anzeigen"
|
msgstr "Anzeigen"
|
||||||
|
|
@ -121,7 +121,7 @@ msgid "Exit Date"
|
||||||
msgstr "Austrittsdatum"
|
msgstr "Austrittsdatum"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:56
|
#: 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
|
#: lib/mv_web/live/member_live/show.ex:61
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "House Number"
|
msgid "House Number"
|
||||||
|
|
@ -140,14 +140,14 @@ msgid "Paid"
|
||||||
msgstr "Bezahlt"
|
msgstr "Bezahlt"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:50
|
#: 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
|
#: lib/mv_web/live/member_live/show.ex:55
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Phone Number"
|
msgid "Phone Number"
|
||||||
msgstr "Telefonnummer"
|
msgstr "Telefonnummer"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:57
|
#: 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
|
#: lib/mv_web/live/member_live/show.ex:62
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Postal Code"
|
msgid "Postal Code"
|
||||||
|
|
@ -158,7 +158,7 @@ msgstr "Postleitzahl"
|
||||||
msgid "Save Member"
|
msgid "Save Member"
|
||||||
msgstr "Mitglied speichern"
|
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/custom_field_value_live/form.ex:74
|
||||||
#: lib/mv_web/live/member_live/form.ex:79
|
#: lib/mv_web/live/member_live/form.ex:79
|
||||||
#: lib/mv_web/live/user_live/form.ex:234
|
#: lib/mv_web/live/user_live/form.ex:234
|
||||||
|
|
@ -167,7 +167,7 @@ msgid "Saving..."
|
||||||
msgstr "Speichern..."
|
msgstr "Speichern..."
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:55
|
#: 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
|
#: lib/mv_web/live/member_live/show.ex:60
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Street"
|
msgid "Street"
|
||||||
|
|
@ -183,6 +183,7 @@ msgstr "Dieses Formular dient zur Verwaltung von Mitgliedern und deren Eigenscha
|
||||||
msgid "Id"
|
msgid "Id"
|
||||||
msgstr "ID"
|
msgstr "ID"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index/formatter.ex:65
|
||||||
#: lib/mv_web/live/member_live/show.ex:53
|
#: lib/mv_web/live/member_live/show.ex:53
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "No"
|
msgid "No"
|
||||||
|
|
@ -198,19 +199,20 @@ msgstr "Mitglied anzeigen"
|
||||||
msgid "This is a member record from your database."
|
msgid "This is a member record from your database."
|
||||||
msgstr "Dies ist ein Mitglied aus deiner Datenbank."
|
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
|
#: lib/mv_web/live/member_live/show.ex:53
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr "Ja"
|
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/custom_field_value_live/form.ex:233
|
||||||
#: lib/mv_web/live/member_live/form.ex:138
|
#: lib/mv_web/live/member_live/form.ex:138
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "create"
|
msgid "create"
|
||||||
msgstr "erstellt"
|
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/custom_field_value_live/form.ex:234
|
||||||
#: lib/mv_web/live/member_live/form.ex:139
|
#: lib/mv_web/live/member_live/form.ex:139
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -252,7 +254,7 @@ msgstr "Ihre E-Mail-Adresse wurde bestätigt"
|
||||||
msgid "Your password has successfully been reset"
|
msgid "Your password has successfully been reset"
|
||||||
msgstr "Ihr Passwort wurde erfolgreich zurückgesetzt"
|
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_live/index.ex:120
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:77
|
#: lib/mv_web/live/custom_field_value_live/form.ex:77
|
||||||
#: lib/mv_web/live/member_live/form.ex:82
|
#: lib/mv_web/live/member_live/form.ex:82
|
||||||
|
|
@ -266,7 +268,7 @@ msgstr "Abbrechen"
|
||||||
msgid "Choose a member"
|
msgid "Choose a member"
|
||||||
msgstr "Mitglied auswählen"
|
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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr "Beschreibung"
|
msgstr "Beschreibung"
|
||||||
|
|
@ -286,7 +288,7 @@ msgstr "Aktiviert"
|
||||||
msgid "ID"
|
msgid "ID"
|
||||||
msgstr "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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Immutable"
|
msgid "Immutable"
|
||||||
msgstr "Unveränderlich"
|
msgstr "Unveränderlich"
|
||||||
|
|
@ -308,13 +310,13 @@ msgid "Member"
|
||||||
msgstr "Mitglied"
|
msgstr "Mitglied"
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:19
|
#: 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
|
#: lib/mv_web/live/member_live/index.html.heex:3
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Members"
|
msgid "Members"
|
||||||
msgstr "Mitglieder"
|
msgstr "Mitglieder"
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:50
|
#: lib/mv_web/live/custom_field_live/form.ex:51
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Name"
|
msgid "Name"
|
||||||
msgstr "Name"
|
msgstr "Name"
|
||||||
|
|
@ -357,17 +359,17 @@ msgstr "Passwort-Authentifizierung"
|
||||||
msgid "Profil"
|
msgid "Profil"
|
||||||
msgstr "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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Required"
|
msgid "Required"
|
||||||
msgstr "Erforderlich"
|
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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Select all members"
|
msgid "Select all members"
|
||||||
msgstr "Alle Mitglieder auswählen"
|
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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Select member"
|
msgid "Select member"
|
||||||
msgstr "Mitglied auswählen"
|
msgstr "Mitglied auswählen"
|
||||||
|
|
@ -413,7 +415,7 @@ msgstr "Benutzer*in"
|
||||||
msgid "Value"
|
msgid "Value"
|
||||||
msgstr "Wert"
|
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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Value type"
|
msgid "Value type"
|
||||||
msgstr "Wertetyp"
|
msgstr "Wertetyp"
|
||||||
|
|
@ -569,7 +571,7 @@ msgstr "Benutzer*innen"
|
||||||
msgid "Click to sort"
|
msgid "Click to sort"
|
||||||
msgstr "Klicke um zu sortieren"
|
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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "First name"
|
msgid "First name"
|
||||||
msgstr "Vorname"
|
msgstr "Vorname"
|
||||||
|
|
@ -621,7 +623,7 @@ msgstr "Benutzerdefinierte Feldwerte"
|
||||||
msgid "Custom field"
|
msgid "Custom field"
|
||||||
msgstr "Benutzerdefiniertes Feld"
|
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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Custom field %{action} successfully"
|
msgid "Custom field %{action} successfully"
|
||||||
msgstr "Benutzerdefiniertes Feld erfolgreich %{action}"
|
msgstr "Benutzerdefiniertes Feld erfolgreich %{action}"
|
||||||
|
|
@ -636,7 +638,7 @@ msgstr "Benutzerdefinierter Feldwert erfolgreich %{action}"
|
||||||
msgid "Please select a custom field first"
|
msgid "Please select a custom field first"
|
||||||
msgstr "Bitte wähle zuerst ein Benutzerdefiniertes Feld"
|
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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Save Custom field"
|
msgid "Save Custom field"
|
||||||
msgstr "Benutzerdefiniertes Feld speichern"
|
msgstr "Benutzerdefiniertes Feld speichern"
|
||||||
|
|
@ -646,7 +648,7 @@ msgstr "Benutzerdefiniertes Feld speichern"
|
||||||
msgid "Save Custom field value"
|
msgid "Save Custom field value"
|
||||||
msgstr "Benutzerdefinierten Feldwert speichern"
|
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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Use this form to manage custom_field records in your database."
|
msgid "Use this form to manage custom_field records in your database."
|
||||||
msgstr "Verwende dieses Formular, um Benutzerdefinierte Felder in deiner Datenbank zu verwalten."
|
msgstr "Verwende dieses Formular, um Benutzerdefinierte Felder in deiner Datenbank zu verwalten."
|
||||||
|
|
@ -747,3 +749,12 @@ msgstr "Entverknüpfung geplant"
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Failed to link member: %{error}"
|
msgid "Failed to link member: %{error}"
|
||||||
msgstr ""
|
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 ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/components/core_components.ex:339
|
#: lib/mv_web/components/core_components.ex:356
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Actions"
|
msgid "Actions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:200
|
#: lib/mv_web/live/member_live/index.html.heex:202
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:72
|
#: lib/mv_web/live/user_live/index.html.heex:65
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Are you sure?"
|
msgid "Are you sure?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -29,21 +29,21 @@ msgid "Attempting to reconnect"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:54
|
#: 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
|
#: lib/mv_web/live/member_live/show.ex:59
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "City"
|
msgid "City"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:202
|
#: lib/mv_web/live/member_live/index.html.heex:204
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:74
|
#: lib/mv_web/live/user_live/index.html.heex:67
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:194
|
#: lib/mv_web/live/member_live/index.html.heex:196
|
||||||
#: lib/mv_web/live/user_live/form.ex:251
|
#: lib/mv_web/live/user_live/form.ex:141
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:66
|
#: lib/mv_web/live/user_live/index.html.heex:59
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Edit"
|
msgid "Edit"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -55,7 +55,7 @@ msgid "Edit Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:47
|
#: 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/member_live/show.ex:50
|
||||||
#: lib/mv_web/live/user_live/form.ex:46
|
#: lib/mv_web/live/user_live/form.ex:46
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:44
|
#: lib/mv_web/live/user_live/index.html.heex:44
|
||||||
|
|
@ -71,7 +71,7 @@ msgid "First Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:51
|
#: 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
|
#: lib/mv_web/live/member_live/show.ex:56
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Join Date"
|
msgid "Join Date"
|
||||||
|
|
@ -88,8 +88,8 @@ msgstr ""
|
||||||
msgid "New Member"
|
msgid "New Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:191
|
#: lib/mv_web/live/member_live/index.html.heex:193
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:63
|
#: lib/mv_web/live/user_live/index.html.heex:56
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Show"
|
msgid "Show"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -122,7 +122,7 @@ msgid "Exit Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:56
|
#: 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
|
#: lib/mv_web/live/member_live/show.ex:61
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "House Number"
|
msgid "House Number"
|
||||||
|
|
@ -141,14 +141,14 @@ msgid "Paid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:50
|
#: 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
|
#: lib/mv_web/live/member_live/show.ex:55
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Phone Number"
|
msgid "Phone Number"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:57
|
#: 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
|
#: lib/mv_web/live/member_live/show.ex:62
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Postal Code"
|
msgid "Postal Code"
|
||||||
|
|
@ -159,7 +159,7 @@ msgstr ""
|
||||||
msgid "Save Member"
|
msgid "Save Member"
|
||||||
msgstr ""
|
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/custom_field_value_live/form.ex:74
|
||||||
#: lib/mv_web/live/member_live/form.ex:79
|
#: lib/mv_web/live/member_live/form.ex:79
|
||||||
#: lib/mv_web/live/user_live/form.ex:234
|
#: lib/mv_web/live/user_live/form.ex:234
|
||||||
|
|
@ -168,7 +168,7 @@ msgid "Saving..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:55
|
#: 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
|
#: lib/mv_web/live/member_live/show.ex:60
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Street"
|
msgid "Street"
|
||||||
|
|
@ -184,6 +184,7 @@ msgstr ""
|
||||||
msgid "Id"
|
msgid "Id"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index/formatter.ex:65
|
||||||
#: lib/mv_web/live/member_live/show.ex:53
|
#: lib/mv_web/live/member_live/show.ex:53
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "No"
|
msgid "No"
|
||||||
|
|
@ -199,19 +200,20 @@ msgstr ""
|
||||||
msgid "This is a member record from your database."
|
msgid "This is a member record from your database."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index/formatter.ex:64
|
||||||
#: lib/mv_web/live/member_live/show.ex:53
|
#: lib/mv_web/live/member_live/show.ex:53
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr ""
|
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/custom_field_value_live/form.ex:233
|
||||||
#: lib/mv_web/live/member_live/form.ex:138
|
#: lib/mv_web/live/member_live/form.ex:138
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "create"
|
msgid "create"
|
||||||
msgstr ""
|
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/custom_field_value_live/form.ex:234
|
||||||
#: lib/mv_web/live/member_live/form.ex:139
|
#: lib/mv_web/live/member_live/form.ex:139
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -253,7 +255,7 @@ msgstr ""
|
||||||
msgid "Your password has successfully been reset"
|
msgid "Your password has successfully been reset"
|
||||||
msgstr ""
|
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_live/index.ex:120
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:77
|
#: lib/mv_web/live/custom_field_value_live/form.ex:77
|
||||||
#: lib/mv_web/live/member_live/form.ex:82
|
#: lib/mv_web/live/member_live/form.ex:82
|
||||||
|
|
@ -267,7 +269,7 @@ msgstr ""
|
||||||
msgid "Choose a member"
|
msgid "Choose a member"
|
||||||
msgstr ""
|
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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -287,7 +289,7 @@ msgstr ""
|
||||||
msgid "ID"
|
msgid "ID"
|
||||||
msgstr ""
|
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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Immutable"
|
msgid "Immutable"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -309,13 +311,13 @@ msgid "Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:19
|
#: 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
|
#: lib/mv_web/live/member_live/index.html.heex:3
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Members"
|
msgid "Members"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:50
|
#: lib/mv_web/live/custom_field_live/form.ex:51
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Name"
|
msgid "Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -358,17 +360,17 @@ msgstr ""
|
||||||
msgid "Profil"
|
msgid "Profil"
|
||||||
msgstr ""
|
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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Required"
|
msgid "Required"
|
||||||
msgstr ""
|
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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Select all members"
|
msgid "Select all members"
|
||||||
msgstr ""
|
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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Select member"
|
msgid "Select member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -414,7 +416,7 @@ msgstr ""
|
||||||
msgid "Value"
|
msgid "Value"
|
||||||
msgstr ""
|
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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Value type"
|
msgid "Value type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -570,7 +572,7 @@ msgstr ""
|
||||||
msgid "Click to sort"
|
msgid "Click to sort"
|
||||||
msgstr ""
|
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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "First name"
|
msgid "First name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -622,7 +624,7 @@ msgstr ""
|
||||||
msgid "Custom field"
|
msgid "Custom field"
|
||||||
msgstr ""
|
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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Custom field %{action} successfully"
|
msgid "Custom field %{action} successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -637,7 +639,7 @@ msgstr ""
|
||||||
msgid "Please select a custom field first"
|
msgid "Please select a custom field first"
|
||||||
msgstr ""
|
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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Save Custom field"
|
msgid "Save Custom field"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -647,7 +649,7 @@ msgstr ""
|
||||||
msgid "Save Custom field value"
|
msgid "Save Custom field value"
|
||||||
msgstr ""
|
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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Use this form to manage custom_field records in your database."
|
msgid "Use this form to manage custom_field records in your database."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -699,52 +701,7 @@ msgstr ""
|
||||||
msgid "To confirm deletion, please enter this text:"
|
msgid "To confirm deletion, please enter this text:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/user_live/form.ex:210
|
#: lib/mv_web/live/custom_field_live/form.ex:64
|
||||||
#, elixir-autogen, elixir-format
|
#, 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."
|
msgid "Show in overview"
|
||||||
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}"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,13 @@ msgstr ""
|
||||||
"Language: en\n"
|
"Language: en\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Actions"
|
msgid "Actions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:200
|
#: lib/mv_web/live/member_live/index.html.heex:202
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:72
|
#: lib/mv_web/live/user_live/index.html.heex:65
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Are you sure?"
|
msgid "Are you sure?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -29,21 +29,21 @@ msgid "Attempting to reconnect"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:54
|
#: 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
|
#: lib/mv_web/live/member_live/show.ex:59
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "City"
|
msgid "City"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:202
|
#: lib/mv_web/live/member_live/index.html.heex:204
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:74
|
#: lib/mv_web/live/user_live/index.html.heex:67
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:194
|
#: lib/mv_web/live/member_live/index.html.heex:196
|
||||||
#: lib/mv_web/live/user_live/form.ex:251
|
#: lib/mv_web/live/user_live/form.ex:141
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:66
|
#: lib/mv_web/live/user_live/index.html.heex:59
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Edit"
|
msgid "Edit"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -55,7 +55,7 @@ msgid "Edit Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:47
|
#: 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/member_live/show.ex:50
|
||||||
#: lib/mv_web/live/user_live/form.ex:46
|
#: lib/mv_web/live/user_live/form.ex:46
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:44
|
#: lib/mv_web/live/user_live/index.html.heex:44
|
||||||
|
|
@ -71,7 +71,7 @@ msgid "First Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:51
|
#: 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
|
#: lib/mv_web/live/member_live/show.ex:56
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Join Date"
|
msgid "Join Date"
|
||||||
|
|
@ -88,8 +88,8 @@ msgstr ""
|
||||||
msgid "New Member"
|
msgid "New Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex:191
|
#: lib/mv_web/live/member_live/index.html.heex:193
|
||||||
#: lib/mv_web/live/user_live/index.html.heex:63
|
#: lib/mv_web/live/user_live/index.html.heex:56
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Show"
|
msgid "Show"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -122,7 +122,7 @@ msgid "Exit Date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:56
|
#: 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
|
#: lib/mv_web/live/member_live/show.ex:61
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "House Number"
|
msgid "House Number"
|
||||||
|
|
@ -141,14 +141,14 @@ msgid "Paid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:50
|
#: 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
|
#: lib/mv_web/live/member_live/show.ex:55
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Phone Number"
|
msgid "Phone Number"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:57
|
#: 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
|
#: lib/mv_web/live/member_live/show.ex:62
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Postal Code"
|
msgid "Postal Code"
|
||||||
|
|
@ -159,7 +159,7 @@ msgstr ""
|
||||||
msgid "Save Member"
|
msgid "Save Member"
|
||||||
msgstr ""
|
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/custom_field_value_live/form.ex:74
|
||||||
#: lib/mv_web/live/member_live/form.ex:79
|
#: lib/mv_web/live/member_live/form.ex:79
|
||||||
#: lib/mv_web/live/user_live/form.ex:234
|
#: lib/mv_web/live/user_live/form.ex:234
|
||||||
|
|
@ -168,7 +168,7 @@ msgid "Saving..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/form.ex:55
|
#: 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
|
#: lib/mv_web/live/member_live/show.ex:60
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Street"
|
msgid "Street"
|
||||||
|
|
@ -184,6 +184,7 @@ msgstr ""
|
||||||
msgid "Id"
|
msgid "Id"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index/formatter.ex:65
|
||||||
#: lib/mv_web/live/member_live/show.ex:53
|
#: lib/mv_web/live/member_live/show.ex:53
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "No"
|
msgid "No"
|
||||||
|
|
@ -199,19 +200,20 @@ msgstr ""
|
||||||
msgid "This is a member record from your database."
|
msgid "This is a member record from your database."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index/formatter.ex:64
|
||||||
#: lib/mv_web/live/member_live/show.ex:53
|
#: lib/mv_web/live/member_live/show.ex:53
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr ""
|
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/custom_field_value_live/form.ex:233
|
||||||
#: lib/mv_web/live/member_live/form.ex:138
|
#: lib/mv_web/live/member_live/form.ex:138
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "create"
|
msgid "create"
|
||||||
msgstr ""
|
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/custom_field_value_live/form.ex:234
|
||||||
#: lib/mv_web/live/member_live/form.ex:139
|
#: lib/mv_web/live/member_live/form.ex:139
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
|
|
@ -253,7 +255,7 @@ msgstr ""
|
||||||
msgid "Your password has successfully been reset"
|
msgid "Your password has successfully been reset"
|
||||||
msgstr ""
|
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_live/index.ex:120
|
||||||
#: lib/mv_web/live/custom_field_value_live/form.ex:77
|
#: lib/mv_web/live/custom_field_value_live/form.ex:77
|
||||||
#: lib/mv_web/live/member_live/form.ex:82
|
#: lib/mv_web/live/member_live/form.ex:82
|
||||||
|
|
@ -267,7 +269,7 @@ msgstr ""
|
||||||
msgid "Choose a member"
|
msgid "Choose a member"
|
||||||
msgstr ""
|
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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Description"
|
msgid "Description"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -287,7 +289,7 @@ msgstr ""
|
||||||
msgid "ID"
|
msgid "ID"
|
||||||
msgstr ""
|
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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Immutable"
|
msgid "Immutable"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -309,13 +311,13 @@ msgid "Member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/components/layouts/navbar.ex:19
|
#: 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
|
#: lib/mv_web/live/member_live/index.html.heex:3
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Members"
|
msgid "Members"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/form.ex:50
|
#: lib/mv_web/live/custom_field_live/form.ex:51
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Name"
|
msgid "Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -358,17 +360,17 @@ msgstr ""
|
||||||
msgid "Profil"
|
msgid "Profil"
|
||||||
msgstr ""
|
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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Required"
|
msgid "Required"
|
||||||
msgstr ""
|
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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Select all members"
|
msgid "Select all members"
|
||||||
msgstr ""
|
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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Select member"
|
msgid "Select member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -414,7 +416,7 @@ msgstr ""
|
||||||
msgid "Value"
|
msgid "Value"
|
||||||
msgstr ""
|
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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Value type"
|
msgid "Value type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -570,7 +572,7 @@ msgstr ""
|
||||||
msgid "Click to sort"
|
msgid "Click to sort"
|
||||||
msgstr ""
|
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
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "First name"
|
msgid "First name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -622,7 +624,7 @@ msgstr ""
|
||||||
msgid "Custom field"
|
msgid "Custom field"
|
||||||
msgstr ""
|
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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Custom field %{action} successfully"
|
msgid "Custom field %{action} successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -637,7 +639,7 @@ msgstr ""
|
||||||
msgid "Please select a custom field first"
|
msgid "Please select a custom field first"
|
||||||
msgstr ""
|
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
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Save Custom field"
|
msgid "Save Custom field"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -647,7 +649,7 @@ msgstr ""
|
||||||
msgid "Save Custom field value"
|
msgid "Save Custom field value"
|
||||||
msgstr ""
|
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
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Use this form to manage custom_field records in your database."
|
msgid "Use this form to manage custom_field records in your database."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -748,3 +750,12 @@ msgstr ""
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Failed to link member: %{error}"
|
msgid "Failed to link member: %{error}"
|
||||||
msgstr ""
|
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