formatting

This commit is contained in:
carla 2025-11-27 14:10:27 +01:00
parent e7c4a4f62f
commit 82bd573276
6 changed files with 148 additions and 68 deletions

View file

@ -373,6 +373,7 @@ defmodule MvWeb.CoreComponents do
> >
{if dyn_col[:render] do {if dyn_col[:render] do
rendered = dyn_col[:render].(@row_item.(row)) rendered = dyn_col[:render].(@row_item.(row))
if rendered == "" do if rendered == "" do
"" ""
else else

View file

@ -205,7 +205,9 @@ defmodule MvWeb.MemberLive.Index do
custom_field: custom_field, custom_field: custom_field,
render: fn member -> render: fn member ->
case get_custom_field_value(member, custom_field) do case get_custom_field_value(member, custom_field) do
nil -> "" nil ->
""
cfv -> cfv ->
formatted = Formatter.format_custom_field_value(cfv.value, custom_field) formatted = Formatter.format_custom_field_value(cfv.value, custom_field)
if formatted == "", do: "", else: formatted if formatted == "", do: "", else: formatted
@ -335,7 +337,13 @@ defmodule MvWeb.MemberLive.Index do
# Apply sorting based on current socket state # Apply sorting based on current socket state
# For custom fields, we sort after loading # 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) {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 # Note: Using Ash.read! - errors will be handled by Phoenix LiveView
# This is appropriate for data loading in LiveViews # This is appropriate for data loading in LiveViews
@ -346,21 +354,18 @@ defmodule MvWeb.MemberLive.Index do
# For large datasets (>1000 members), this could be optimized by filtering # For large datasets (>1000 members), this could be optimized by filtering
# at the database level, but requires more complex Ash queries. # at the database level, but requires more complex Ash queries.
custom_field_ids = MapSet.new(Enum.map(socket.assigns.custom_fields_visible, & &1.id)) custom_field_ids = MapSet.new(Enum.map(socket.assigns.custom_fields_visible, & &1.id))
members = Enum.map(members, fn member ->
# Only filter if custom_field_values is loaded (is a list, not Ash.NotLoaded) members = filter_member_custom_field_values(members, custom_field_ids)
if is_list(member.custom_field_values) do
filtered_values = Enum.filter(member.custom_field_values, fn cfv ->
cfv.custom_field_id in custom_field_ids
end)
%{member | custom_field_values: filtered_values}
else
member
end
end)
# Sort in memory if needed (for custom fields) # Sort in memory if needed (for custom fields)
members = if sort_after_load do members =
sort_members_in_memory(members, socket.assigns.sort_field, socket.assigns.sort_order, socket.assigns.custom_fields_visible) if sort_after_load do
sort_members_in_memory(
members,
socket.assigns.sort_field,
socket.assigns.sort_order,
socket.assigns.custom_fields_visible
)
else else
members members
end end
@ -381,6 +386,28 @@ defmodule MvWeb.MemberLive.Index do
|> Ash.Query.load(custom_field_values: [custom_field: [:id, :name, :value_type]]) |> Ash.Query.load(custom_field_values: [custom_field: [:id, :name, :value_type]])
end end
# Filters custom field values to only visible ones for all members
defp filter_member_custom_field_values(members, custom_field_ids) do
Enum.map(members, fn member ->
filter_single_member_custom_field_values(member, custom_field_ids)
end)
end
# Filters custom field values for a single member
defp filter_single_member_custom_field_values(member, _custom_field_ids)
when not is_list(member.custom_field_values) do
member
end
defp filter_single_member_custom_field_values(member, custom_field_ids) do
filtered_values =
Enum.filter(member.custom_field_values, fn cfv ->
cfv.custom_field_id in custom_field_ids
end)
%{member | custom_field_values: filtered_values}
end
# ------------------------------------------------------------- # -------------------------------------------------------------
# Helper Functions # Helper Functions
# ------------------------------------------------------------- # -------------------------------------------------------------
@ -508,45 +535,76 @@ defmodule MvWeb.MemberLive.Index do
members members
id_str -> id_str ->
# Find the custom field by matching the ID string sort_members_by_custom_field(members, id_str, order, custom_fields)
custom_field = end
Enum.find(custom_fields, fn cf -> end
to_string(cf.id) == id_str
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 case custom_field do
nil -> nil ->
members members
cf -> 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) # Split members into those with values and those without (NULL/empty)
{members_with_values, members_without_values} = {members_with_values, members_without_values} =
Enum.split_with(members, fn member -> split_members_by_value_presence(members, custom_field)
case get_custom_field_value(member, cf) do
nil -> false
cfv ->
extracted = extract_sort_value(cfv.value, cf.value_type)
not is_empty_value(extracted, cf.value_type)
end
end)
# Sort members with values # Sort members with values
sorted_with_values = Enum.sort_by(members_with_values, fn member -> sorted_with_values = sort_members_with_values(members_with_values, custom_field, order)
cfv = get_custom_field_value(member, cf)
extracted = extract_sort_value(cfv.value, cf.value_type)
normalize_sort_value(extracted, order)
end)
# For DESC, reverse only the members with values
sorted_with_values = if order == :desc do
Enum.reverse(sorted_with_values)
else
sorted_with_values
end
# Combine: sorted values first, then NULL/empty values at the end # Combine: sorted values first, then NULL/empty values at the end
sorted_with_values ++ members_without_values sorted_with_values ++ members_without_values
end 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
end end
@ -569,20 +627,21 @@ defmodule MvWeb.MemberLive.Index do
defp extract_sort_value(value, _type), do: to_string(value) defp extract_sort_value(value, _type), do: to_string(value)
# Check if a value is considered empty (NULL or empty string) # Check if a value is considered empty (NULL or empty string)
defp is_empty_value(value, :string) when is_binary(value) do defp empty_value?(value, :string) when is_binary(value) do
String.trim(value) == "" String.trim(value) == ""
end end
defp is_empty_value(value, :email) when is_binary(value) do
defp empty_value?(value, :email) when is_binary(value) do
String.trim(value) == "" String.trim(value) == ""
end end
defp is_empty_value(_value, _type), do: false
defp empty_value?(_value, _type), do: false
# Normalize sort value for DESC order # Normalize sort value for DESC order
# For DESC, we sort ascending first, then reverse the list # For DESC, we sort ascending first, then reverse the list
# This function is kept for consistency but doesn't need to invert values # This function is kept for consistency but doesn't need to invert values
defp normalize_sort_value(value, _order), do: value defp normalize_sort_value(value, _order), do: value
# Updates sort field and order from URL parameters if present. # Updates sort field and order from URL parameters if present.
# #
# Validates the sort field and order, falling back to defaults if invalid. # Validates the sort field and order, falling back to defaults if invalid.
@ -678,13 +737,17 @@ defmodule MvWeb.MemberLive.Index do
# get_custom_field_value(member, non_existent_field) -> nil # get_custom_field_value(member, non_existent_field) -> nil
def get_custom_field_value(member, custom_field) do def get_custom_field_value(member, custom_field) do
case member.custom_field_values do case member.custom_field_values do
nil -> nil nil ->
nil
values when is_list(values) -> values when is_list(values) ->
Enum.find(values, fn cfv -> Enum.find(values, fn cfv ->
cfv.custom_field_id == custom_field.id or cfv.custom_field_id == custom_field.id or
(cfv.custom_field && cfv.custom_field.id == custom_field.id) (cfv.custom_field && cfv.custom_field.id == custom_field.id)
end) end)
_ -> nil
_ ->
nil
end end
end end
end end

View file

@ -67,7 +67,9 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsAccessibilityTest do
field: field field: field
} do } do
conn = conn_with_oidc_user(conn) conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=asc")
{:ok, view, _html} =
live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=asc")
html = render(view) html = render(view)
@ -80,7 +82,9 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsAccessibilityTest do
field: field field: field
} do } do
conn = conn_with_oidc_user(conn) conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=desc")
{:ok, view, _html} =
live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=desc")
html = render(view) html = render(view)

View file

@ -105,7 +105,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
|> Ash.Changeset.for_create(:create, %{ |> Ash.Changeset.for_create(:create, %{
member_id: member1.id, member_id: member1.id,
custom_field_id: field_show_integer.id, custom_field_id: field_show_integer.id,
value: %{"_union_type" => "integer", "_union_value" => 12345} value: %{"_union_type" => "integer", "_union_value" => 12_345}
}) })
|> Ash.create() |> Ash.create()

View file

@ -171,4 +171,3 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
assert html =~ "Value3" assert html =~ "Value3"
end end
end end

View file

@ -174,7 +174,9 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
test "sorting by custom field works with URL parameters", %{conn: conn, field_string: field} do test "sorting by custom field works with URL parameters", %{conn: conn, field_string: field} do
conn = conn_with_oidc_user(conn) conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=desc")
{:ok, view, _html} =
live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=desc")
# Check that the sort state is correctly applied # Check that the sort state is correctly applied
assert has_element?(view, "[data-testid='custom_field_#{field.id}'][aria-label='descending']") assert has_element?(view, "[data-testid='custom_field_#{field.id}'][aria-label='descending']")
@ -186,14 +188,19 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
field_integer: field_integer field_integer: field_integer
} do } do
conn = conn_with_oidc_user(conn) conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members?query=&sort_field=custom_field_#{field_string.id}&sort_order=desc")
{:ok, view, _html} =
live(conn, "/members?query=&sort_field=custom_field_#{field_string.id}&sort_order=desc")
# Click on a different custom field column # Click on a different custom field column
view view
|> element("[data-testid='custom_field_#{field_integer.id}']") |> element("[data-testid='custom_field_#{field_integer.id}']")
|> render_click() |> render_click()
assert_patch(view, "/members?query=&sort_field=custom_field_#{field_integer.id}&sort_order=asc") assert_patch(
view,
"/members?query=&sort_field=custom_field_#{field_integer.id}&sort_order=asc"
)
end end
test "clicking regular column after custom field column works", %{ test "clicking regular column after custom field column works", %{
@ -201,7 +208,9 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
field_string: field field_string: field
} do } do
conn = conn_with_oidc_user(conn) conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=desc")
{:ok, view, _html} =
live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=desc")
# Click on email column # Click on email column
view view
@ -305,7 +314,9 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|> Ash.create() |> Ash.create()
conn = conn_with_oidc_user(conn) conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=asc")
{:ok, view, _html} =
live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=asc")
html = render(view) html = render(view)
@ -414,7 +425,9 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|> Ash.create() |> Ash.create()
conn = conn_with_oidc_user(conn) conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=desc")
{:ok, view, _html} =
live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=desc")
html = render(view) html = render(view)