This commit is contained in:
parent
c974be9ee2
commit
631cf23a0f
6 changed files with 148 additions and 68 deletions
|
|
@ -373,6 +373,7 @@ defmodule MvWeb.CoreComponents do
|
|||
>
|
||||
{if dyn_col[:render] do
|
||||
rendered = dyn_col[:render].(@row_item.(row))
|
||||
|
||||
if rendered == "" do
|
||||
""
|
||||
else
|
||||
|
|
|
|||
|
|
@ -205,7 +205,9 @@ defmodule MvWeb.MemberLive.Index do
|
|||
custom_field: custom_field,
|
||||
render: fn member ->
|
||||
case get_custom_field_value(member, custom_field) do
|
||||
nil -> ""
|
||||
nil ->
|
||||
""
|
||||
|
||||
cfv ->
|
||||
formatted = Formatter.format_custom_field_value(cfv.value, custom_field)
|
||||
if formatted == "", do: "", else: formatted
|
||||
|
|
@ -335,7 +337,13 @@ defmodule MvWeb.MemberLive.Index do
|
|||
|
||||
# Apply sorting based on current socket state
|
||||
# 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
|
||||
# This is appropriate for data loading in LiveViews
|
||||
|
|
@ -346,24 +354,21 @@ defmodule MvWeb.MemberLive.Index do
|
|||
# For large datasets (>1000 members), this could be optimized by filtering
|
||||
# at the database level, but requires more complex Ash queries.
|
||||
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)
|
||||
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)
|
||||
|
||||
members = filter_member_custom_field_values(members, custom_field_ids)
|
||||
|
||||
# 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
|
||||
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)
|
||||
end
|
||||
|
|
@ -381,6 +386,28 @@ defmodule MvWeb.MemberLive.Index do
|
|||
|> Ash.Query.load(custom_field_values: [custom_field: [:id, :name, :value_type]])
|
||||
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
|
||||
# -------------------------------------------------------------
|
||||
|
|
@ -508,45 +535,76 @@ defmodule MvWeb.MemberLive.Index do
|
|||
members
|
||||
|
||||
id_str ->
|
||||
# Find the custom field by matching the ID string
|
||||
custom_field =
|
||||
Enum.find(custom_fields, fn cf ->
|
||||
to_string(cf.id) == id_str
|
||||
end)
|
||||
sort_members_by_custom_field(members, id_str, order, custom_fields)
|
||||
end
|
||||
end
|
||||
|
||||
case custom_field do
|
||||
nil ->
|
||||
members
|
||||
# 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)
|
||||
|
||||
cf ->
|
||||
# Split members into those with values and those without (NULL/empty)
|
||||
{members_with_values, members_without_values} =
|
||||
Enum.split_with(members, fn member ->
|
||||
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)
|
||||
case custom_field do
|
||||
nil ->
|
||||
members
|
||||
|
||||
# Sort members with values
|
||||
sorted_with_values = Enum.sort_by(members_with_values, fn member ->
|
||||
cfv = get_custom_field_value(member, cf)
|
||||
extracted = extract_sort_value(cfv.value, cf.value_type)
|
||||
normalize_sort_value(extracted, order)
|
||||
end)
|
||||
cf ->
|
||||
sort_members_with_custom_field(members, cf, order)
|
||||
end
|
||||
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
|
||||
# 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
|
||||
|
||||
# Combine: sorted values first, then NULL/empty values at the end
|
||||
sorted_with_values ++ members_without_values
|
||||
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
|
||||
|
||||
|
|
@ -569,20 +627,21 @@ defmodule MvWeb.MemberLive.Index do
|
|||
defp extract_sort_value(value, _type), do: to_string(value)
|
||||
|
||||
# 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) == ""
|
||||
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) == ""
|
||||
end
|
||||
defp is_empty_value(_value, _type), do: false
|
||||
|
||||
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.
|
||||
|
|
@ -678,13 +737,17 @@ defmodule MvWeb.MemberLive.Index do
|
|||
# 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
|
||||
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
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -67,7 +67,9 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsAccessibilityTest do
|
|||
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")
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=asc")
|
||||
|
||||
html = render(view)
|
||||
|
||||
|
|
@ -80,7 +82,9 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsAccessibilityTest do
|
|||
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")
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=desc")
|
||||
|
||||
html = render(view)
|
||||
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
|
|||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member1.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()
|
||||
|
||||
|
|
|
|||
|
|
@ -171,4 +171,3 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
|
|||
assert html =~ "Value3"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -174,7 +174,9 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
|
||||
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")
|
||||
|
||||
{: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']")
|
||||
|
|
@ -186,14 +188,19 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
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")
|
||||
|
||||
{: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")
|
||||
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", %{
|
||||
|
|
@ -201,7 +208,9 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
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")
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=desc")
|
||||
|
||||
# Click on email column
|
||||
view
|
||||
|
|
@ -305,7 +314,9 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
|> Ash.create()
|
||||
|
||||
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)
|
||||
|
||||
|
|
@ -414,7 +425,9 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
|> Ash.create()
|
||||
|
||||
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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue