Implements exporting groups closes #428 #435

Merged
carla merged 6 commits from feature/428_export_groups into main 2026-02-23 16:25:42 +01:00
3 changed files with 111 additions and 57 deletions
Showing only changes of commit cb932ad6ef - Show all commits

View file

@ -244,16 +244,22 @@ defmodule Mv.Membership.MemberExport.Build do
defp maybe_sort(query, _field, nil), do: {query, false} defp maybe_sort(query, _field, nil), do: {query, false}
defp maybe_sort(query, field, order) when is_binary(field) do defp maybe_sort(query, field, order) when is_binary(field) do
if custom_field_sort?(field) do cond do
{query, true} field == "groups" ->
else # Groups sort → in-memory nach dem Read (wie Tabelle)
field_atom = String.to_existing_atom(field) {query, true}
if field_atom in (Mv.Constants.member_fields() -- [:notes]) do custom_field_sort?(field) ->
{Ash.Query.sort(query, [{field_atom, String.to_existing_atom(order)}]), false} {query, true}
else
{query, false} true ->
end field_atom = String.to_existing_atom(field)
if field_atom in (Mv.Constants.member_fields() -- [:notes]) do
{Ash.Query.sort(query, [{field_atom, String.to_existing_atom(order)}]), false}
else
{query, false}
end
end end
rescue rescue
ArgumentError -> {query, false} ArgumentError -> {query, false}
@ -263,21 +269,45 @@ defmodule Mv.Membership.MemberExport.Build do
do: [] do: []
defp sort_members_by_custom_field(members, field, order, custom_fields) when is_binary(field) do defp sort_members_by_custom_field(members, field, order, custom_fields) when is_binary(field) do
id_str = String.trim_leading(field, @custom_field_prefix) if field == "groups" do
custom_field = Enum.find(custom_fields, fn cf -> to_string(cf.id) == id_str end) sort_members_by_groups_export(members, order)
else
id_str = String.trim_leading(field, @custom_field_prefix)
custom_field = Enum.find(custom_fields, fn cf -> to_string(cf.id) == id_str end)
if is_nil(custom_field), do: members if is_nil(custom_field), do: members
key_fn = fn member -> key_fn = fn member ->
cfv = find_cfv(member, custom_field) cfv = find_cfv(member, custom_field)
raw = if cfv, do: cfv.value, else: nil raw = if cfv, do: cfv.value, else: nil
MemberExportSort.custom_field_sort_key(custom_field.value_type, raw) MemberExportSort.custom_field_sort_key(custom_field.value_type, raw)
end
members
|> Enum.map(fn m -> {m, key_fn.(m)} end)
|> Enum.sort(fn {_, ka}, {_, kb} -> MemberExportSort.key_lt(ka, kb, order) end)
|> Enum.map(fn {m, _} -> m end)
end
end
defp sort_members_by_groups_export(members, order) do
# Members with groups first, then by first group name alphabetically (min = first by sort order)
# Match table behavior from MvWeb.MemberLive.Index.sort_members_by_groups/2
first_group_name = fn member ->
(member.groups || [])
|> Enum.map(& &1.name)
|> Enum.min(fn -> nil end)
end end
members members
|> Enum.map(fn m -> {m, key_fn.(m)} end) |> Enum.sort_by(fn member ->
|> Enum.sort(fn {_, ka}, {_, kb} -> MemberExportSort.key_lt(ka, kb, order) end) name = first_group_name.(member)
|> Enum.map(fn {m, _} -> m end) # Nil (no groups) sorts last in asc, first in desc
{name == nil, name || ""}
end)
|> then(fn list ->
if order == "desc", do: Enum.reverse(list), else: list
end)
end end
defp find_cfv(member, custom_field) do defp find_cfv(member, custom_field) do

View file

@ -341,17 +341,23 @@ defmodule MvWeb.MemberExportController do
defp maybe_sort_export(query, _field, nil), do: {query, false} defp maybe_sort_export(query, _field, nil), do: {query, false}
defp maybe_sort_export(query, field, order) when is_binary(field) do defp maybe_sort_export(query, field, order) when is_binary(field) do
if custom_field_sort?(field) do cond do
# Custom field sort → in-memory nach dem Read (wie Tabelle) field == "groups" ->
{query, true} # Groups sort → in-memory nach dem Read (wie Tabelle)
else {query, true}
field_atom = String.to_existing_atom(field)
if field_atom in (Mv.Constants.member_fields() -- [:notes]) do custom_field_sort?(field) ->
{Ash.Query.sort(query, [{field_atom, String.to_existing_atom(order)}]), false} # Custom field sort → in-memory nach dem Read (wie Tabelle)
else {query, true}
{query, false}
end true ->
field_atom = String.to_existing_atom(field)
if field_atom in (Mv.Constants.member_fields() -- [:notes]) do
{Ash.Query.sort(query, [{field_atom, String.to_existing_atom(order)}]), false}
else
{query, false}
end
end end
rescue rescue
ArgumentError -> {query, false} ArgumentError -> {query, false}
@ -370,35 +376,60 @@ defmodule MvWeb.MemberExportController do
defp sort_members_by_custom_field_export(members, field, order, custom_fields) defp sort_members_by_custom_field_export(members, field, order, custom_fields)
when is_binary(field) do when is_binary(field) do
order = order || "asc" order = order || "asc"
id_str = String.trim_leading(field, @custom_field_prefix)
custom_field = if field == "groups" do
Enum.find(custom_fields, fn cf -> to_string(cf.id) == id_str end) sort_members_by_groups_export(members, order)
if is_nil(custom_field) do
members
else else
# Match table: id_str = String.trim_leading(field, @custom_field_prefix)
# 1) values first, empty last
# 2) sort only values
# 3) for desc, reverse only the values-part
{with_values, without_values} =
Enum.split_with(members, fn member ->
has_non_empty_custom_field_value?(member, custom_field)
end)
sorted_with_values = custom_field =
Enum.sort_by(with_values, fn member -> Enum.find(custom_fields, fn cf -> to_string(cf.id) == id_str end)
extract_member_sort_value(member, custom_field)
end)
sorted_with_values = if is_nil(custom_field) do
if order == "desc", do: Enum.reverse(sorted_with_values), else: sorted_with_values members
else
# Match table:
# 1) values first, empty last
# 2) sort only values
# 3) for desc, reverse only the values-part
{with_values, without_values} =
Enum.split_with(members, fn member ->
has_non_empty_custom_field_value?(member, custom_field)
end)
sorted_with_values ++ without_values sorted_with_values =
Enum.sort_by(with_values, fn member ->
extract_member_sort_value(member, custom_field)
end)
sorted_with_values =
if order == "desc", do: Enum.reverse(sorted_with_values), else: sorted_with_values
sorted_with_values ++ without_values
end
end end
end end
defp sort_members_by_groups_export(members, order) do
# Members with groups first, then by first group name alphabetically (min = first by sort order)
# Match table behavior from MvWeb.MemberLive.Index.sort_members_by_groups/2
first_group_name = fn member ->
(member.groups || [])
|> Enum.map(& &1.name)
|> Enum.min(fn -> nil end)
end
members
|> Enum.sort_by(fn member ->
name = first_group_name.(member)
# Nil (no groups) sorts last in asc, first in desc
{name == nil, name || ""}
end)
|> then(fn list ->
if order == "desc", do: Enum.reverse(list), else: list
end)
end
defp has_non_empty_custom_field_value?(member, custom_field) do defp has_non_empty_custom_field_value?(member, custom_field) do
case find_cfv(member, custom_field) do case find_cfv(member, custom_field) do
nil -> nil ->

View file

@ -1066,13 +1066,6 @@ defmodule MvWeb.MemberLive.Index do
end end
end end
defp computed_field?(field) do
computed_atoms = FieldVisibility.computed_member_fields()
computed_strings = Enum.map(computed_atoms, &Atom.to_string/1)
(is_atom(field) and field in computed_atoms) or
(is_binary(field) and field in computed_strings)
end
defp apply_sort_to_query(query, field, order) do defp apply_sort_to_query(query, field, order) do
cond do cond do