Merge branch 'main' into feat/299_plz
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
commit
9a7608f9a1
16 changed files with 573 additions and 88 deletions
|
|
@ -615,7 +615,9 @@ defmodule MvWeb.MemberLive.Index do
|
|||
# -----------------------------------------------------------------
|
||||
|
||||
@impl true
|
||||
def handle_params(params, _url, socket) do
|
||||
def handle_params(params, url, socket) do
|
||||
url = url || request_url_from_socket(socket)
|
||||
params = merge_fields_param_from_uri(params, url)
|
||||
prev_sig = build_signature(socket)
|
||||
|
||||
fields_in_url? =
|
||||
|
|
@ -625,20 +627,7 @@ defmodule MvWeb.MemberLive.Index do
|
|||
end
|
||||
|
||||
url_selection = FieldSelection.parse_from_url(params)
|
||||
|
||||
merged_selection =
|
||||
FieldSelection.merge_sources(
|
||||
url_selection,
|
||||
socket.assigns.user_field_selection,
|
||||
%{}
|
||||
)
|
||||
|
||||
final_selection =
|
||||
FieldVisibility.merge_with_global_settings(
|
||||
merged_selection,
|
||||
socket.assigns.settings,
|
||||
socket.assigns.all_custom_fields
|
||||
)
|
||||
final_selection = compute_final_field_selection(fields_in_url?, url_selection, socket)
|
||||
|
||||
visible_member_fields =
|
||||
final_selection
|
||||
|
|
@ -828,6 +817,70 @@ defmodule MvWeb.MemberLive.Index do
|
|||
add_boolean_filters(base_params, boolean_filters)
|
||||
end
|
||||
|
||||
defp compute_final_field_selection(true, url_selection, socket) do
|
||||
only_url =
|
||||
FieldVisibility.selection_from_url_only(url_selection, socket.assigns.all_custom_fields)
|
||||
|
||||
visible_members = FieldVisibility.get_visible_member_fields(only_url)
|
||||
visible_custom = FieldVisibility.get_visible_custom_fields(only_url)
|
||||
|
||||
if visible_members == [] and visible_custom == [] do
|
||||
# URL had only invalid field names; fall back to session + global.
|
||||
compute_final_field_selection(false, url_selection, socket)
|
||||
else
|
||||
only_url
|
||||
end
|
||||
end
|
||||
|
||||
defp compute_final_field_selection(false, url_selection, socket) do
|
||||
merged =
|
||||
FieldSelection.merge_sources(
|
||||
url_selection,
|
||||
socket.assigns.user_field_selection,
|
||||
%{}
|
||||
)
|
||||
|
||||
FieldVisibility.merge_with_global_settings(
|
||||
merged,
|
||||
socket.assigns.settings,
|
||||
socket.assigns.all_custom_fields
|
||||
)
|
||||
end
|
||||
|
||||
# On full page load conn.params has no query string; read "fields" from URI so column visibility is restored.
|
||||
defp request_url_from_socket(socket) do
|
||||
case socket.private[:connect_info] do
|
||||
%Plug.Conn{} = conn -> Plug.Conn.request_url(conn)
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp merge_fields_param_from_uri(params, nil), do: params
|
||||
|
||||
defp merge_fields_param_from_uri(params, %URI{query: query}) when is_binary(query) do
|
||||
case URI.decode_query(query)["fields"] do
|
||||
nil -> params
|
||||
value -> Map.put(params, "fields", value)
|
||||
end
|
||||
end
|
||||
|
||||
defp merge_fields_param_from_uri(params, %URI{}), do: params
|
||||
|
||||
defp merge_fields_param_from_uri(params, url) when is_binary(url) do
|
||||
case URI.parse(url).query do
|
||||
nil ->
|
||||
params
|
||||
|
||||
q ->
|
||||
case URI.decode_query(q)["fields"] do
|
||||
nil -> params
|
||||
value -> Map.put(params, "fields", value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp merge_fields_param_from_uri(params, _), do: params
|
||||
|
||||
defp build_base_params(query, sort_field, sort_order) do
|
||||
%{
|
||||
"query" => query || "",
|
||||
|
|
@ -913,6 +966,15 @@ defmodule MvWeb.MemberLive.Index do
|
|||
query =
|
||||
Ash.Query.load(query, groups: [:id, :name, :slug])
|
||||
|
||||
# Load membership_fee_type when the column is visible or when sorting by it
|
||||
query =
|
||||
if :membership_fee_type in socket.assigns.member_fields_visible or
|
||||
socket.assigns.sort_field in [:membership_fee_type, "membership_fee_type"] do
|
||||
Ash.Query.load(query, membership_fee_type: [:id, :name])
|
||||
else
|
||||
query
|
||||
end
|
||||
|
||||
query = apply_search_filter(query, search_query)
|
||||
|
||||
query = apply_group_filters(query, socket.assigns[:group_filters], socket.assigns[:groups])
|
||||
|
|
@ -1073,6 +1135,10 @@ defmodule MvWeb.MemberLive.Index do
|
|||
field in [:groups, "groups"] ->
|
||||
{query, true}
|
||||
|
||||
# Membership fee type sort -> by related name at DB
|
||||
field in [:membership_fee_type, "membership_fee_type"] ->
|
||||
{Ash.Query.sort(query, [{"membership_fee_type.name", order}]), false}
|
||||
|
||||
# Custom field sort -> after load
|
||||
custom_field_sort?(field) ->
|
||||
{query, true}
|
||||
|
|
@ -1118,11 +1184,16 @@ defmodule MvWeb.MemberLive.Index do
|
|||
defp valid_sort_field_db_or_custom?(field) when is_atom(field) do
|
||||
non_sortable_fields = [:notes]
|
||||
valid_fields = Mv.Constants.member_fields() -- non_sortable_fields
|
||||
field in valid_fields or custom_field_sort?(field) or field == :groups
|
||||
field in valid_fields or custom_field_sort?(field) or field in [:groups, :membership_fee_type]
|
||||
end
|
||||
|
||||
defp valid_sort_field_db_or_custom?(field) when is_binary(field) do
|
||||
normalized = if field == "groups", do: :groups, else: safe_member_field_atom_only(field)
|
||||
normalized =
|
||||
cond do
|
||||
field == "groups" -> :groups
|
||||
field == "membership_fee_type" -> :membership_fee_type
|
||||
true -> safe_member_field_atom_only(field)
|
||||
end
|
||||
|
||||
(normalized != nil and valid_sort_field_db_or_custom?(normalized)) or
|
||||
custom_field_sort?(field)
|
||||
|
|
@ -1647,13 +1718,11 @@ defmodule MvWeb.MemberLive.Index do
|
|||
FieldVisibility.computed_member_fields()
|
||||
|> Enum.filter(&(&1 in member_fields_computed))
|
||||
|
||||
# Include groups in export only if it's visible in the table
|
||||
member_fields_with_groups =
|
||||
if :groups in socket.assigns[:member_fields_visible] do
|
||||
ordered_member_fields_db ++ ["groups"]
|
||||
else
|
||||
ordered_member_fields_db
|
||||
end
|
||||
build_export_member_fields_list(
|
||||
ordered_member_fields_db,
|
||||
socket.assigns[:member_fields_visible]
|
||||
)
|
||||
|
||||
# Order custom fields like the table (same as dynamic_cols / all_custom_fields order)
|
||||
ordered_custom_field_ids =
|
||||
|
|
@ -1674,7 +1743,9 @@ defmodule MvWeb.MemberLive.Index do
|
|||
export_column_order(
|
||||
ordered_member_fields_db,
|
||||
ordered_computed_fields,
|
||||
ordered_custom_field_ids
|
||||
ordered_custom_field_ids,
|
||||
:membership_fee_type in socket.assigns[:member_fields_visible],
|
||||
:groups in socket.assigns[:member_fields_visible]
|
||||
),
|
||||
query: socket.assigns[:query] || nil,
|
||||
sort_field: export_sort_field(socket.assigns[:sort_field]),
|
||||
|
|
@ -1685,6 +1756,41 @@ defmodule MvWeb.MemberLive.Index do
|
|||
}
|
||||
end
|
||||
|
||||
defp expand_db_string_for_export(f, membership_fee_type_visible, computed_strings) do
|
||||
if f == "membership_fee_start_date" do
|
||||
extra =
|
||||
if(membership_fee_type_visible, do: ["membership_fee_type"], else: []) ++
|
||||
if "membership_fee_status" in computed_strings, do: ["membership_fee_status"], else: []
|
||||
|
||||
[f] ++ extra
|
||||
else
|
||||
[f]
|
||||
end
|
||||
end
|
||||
|
||||
defp build_export_member_fields_list(ordered_db, member_fields_visible) do
|
||||
with_extras =
|
||||
Enum.flat_map(ordered_db, fn f ->
|
||||
if f == :membership_fee_start_date and
|
||||
:membership_fee_type in (member_fields_visible || []) do
|
||||
[f, :membership_fee_type]
|
||||
else
|
||||
[f]
|
||||
end
|
||||
end)
|
||||
|
||||
# If fee type is visible but start_date was not in the list, append it
|
||||
with_extras =
|
||||
if :membership_fee_type in (member_fields_visible || []) and
|
||||
:membership_fee_type not in with_extras do
|
||||
with_extras ++ [:membership_fee_type]
|
||||
else
|
||||
with_extras
|
||||
end
|
||||
|
||||
if :groups in (member_fields_visible || []), do: with_extras ++ [:groups], else: with_extras
|
||||
end
|
||||
|
||||
defp export_cycle_status_filter(nil), do: nil
|
||||
defp export_cycle_status_filter(:paid), do: "paid"
|
||||
defp export_cycle_status_filter(:unpaid), do: "unpaid"
|
||||
|
|
@ -1700,31 +1806,41 @@ defmodule MvWeb.MemberLive.Index do
|
|||
defp export_sort_order(o) when is_binary(o), do: o
|
||||
# Build a single ordered list that matches the table order:
|
||||
# - DB fields in Mv.Constants.member_fields() order (already pre-filtered as ordered_member_fields_db)
|
||||
# - computed fields inserted at the correct position (membership_fee_status after membership_fee_start_date)
|
||||
# - membership_fee_type and membership_fee_status inserted after membership_fee_start_date when visible
|
||||
# - groups appended before custom fields when visible
|
||||
# - custom fields appended in the same order as table (already ordered_custom_field_ids)
|
||||
defp export_column_order(
|
||||
ordered_member_fields_db,
|
||||
ordered_computed_fields,
|
||||
ordered_custom_field_ids
|
||||
ordered_custom_field_ids,
|
||||
membership_fee_type_visible,
|
||||
groups_visible
|
||||
) do
|
||||
db_strings = Enum.map(ordered_member_fields_db, &Atom.to_string/1)
|
||||
computed_strings = Enum.map(ordered_computed_fields, &Atom.to_string/1)
|
||||
|
||||
# Place membership_fee_status right after membership_fee_start_date if present in export
|
||||
db_with_computed =
|
||||
Enum.flat_map(db_strings, fn f ->
|
||||
if f == "membership_fee_start_date" and "membership_fee_status" in computed_strings do
|
||||
[f, "membership_fee_status"]
|
||||
else
|
||||
[f]
|
||||
end
|
||||
end)
|
||||
# Place membership_fee_type and membership_fee_status after membership_fee_start_date when present
|
||||
db_with_extras =
|
||||
Enum.flat_map(
|
||||
db_strings,
|
||||
&expand_db_string_for_export(&1, membership_fee_type_visible, computed_strings)
|
||||
)
|
||||
|
||||
# If fee type is visible but start_date was not in the list, append it before computed/groups
|
||||
db_with_extras =
|
||||
if membership_fee_type_visible and "membership_fee_type" not in db_with_extras do
|
||||
db_with_extras ++ ["membership_fee_type"]
|
||||
else
|
||||
db_with_extras
|
||||
end
|
||||
|
||||
# Any remaining computed fields not inserted above (future-proof)
|
||||
remaining_computed =
|
||||
computed_strings
|
||||
|> Enum.reject(&(&1 in db_with_computed))
|
||||
|> Enum.reject(&(&1 in db_with_extras))
|
||||
|
||||
db_with_computed ++ remaining_computed ++ ordered_custom_field_ids
|
||||
result = db_with_extras ++ remaining_computed
|
||||
result = if groups_visible, do: result ++ ["groups"], else: result
|
||||
result ++ ordered_custom_field_ids
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -331,6 +331,28 @@
|
|||
>
|
||||
{MvWeb.MemberLive.Index.format_date(member.membership_fee_start_date)}
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
:if={:membership_fee_type in @member_fields_visible}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:sort_membership_fee_type}
|
||||
field={:membership_fee_type}
|
||||
label={gettext("Fee Type")}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
<%= if member.membership_fee_type do %>
|
||||
{member.membership_fee_type.name}
|
||||
<% else %>
|
||||
<span class="text-base-content/50">—</span>
|
||||
<% end %>
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
:if={:membership_fee_status in @member_fields_visible}
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ defmodule MvWeb.MemberLive.Index.FieldVisibility do
|
|||
alias Mv.Membership.Helpers.VisibilityConfig
|
||||
|
||||
# Single UI key for "Membership Fee Status"; only this appears in the dropdown.
|
||||
# Groups is also a pseudo field (not a DB attribute, but displayed in the table).
|
||||
@pseudo_member_fields [:membership_fee_status, :groups]
|
||||
# Groups and membership_fee_type are also pseudo fields (not in member_fields(), displayed in the table).
|
||||
@pseudo_member_fields [:membership_fee_status, :membership_fee_type, :groups]
|
||||
|
||||
# Export/API may accept this as alias; must not appear in the UI options list.
|
||||
@export_only_alias :payment_status
|
||||
|
|
@ -64,6 +64,25 @@ defmodule MvWeb.MemberLive.Index.FieldVisibility do
|
|||
member_fields ++ custom_field_names
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds field selection from URL only: fields in `url_selection` are visible, all others false.
|
||||
Use when `?fields=...` is in the URL so column visibility is not merged with global settings.
|
||||
"""
|
||||
@spec selection_from_url_only(%{String.t() => boolean()}, [struct()]) :: %{
|
||||
String.t() => boolean()
|
||||
}
|
||||
def selection_from_url_only(url_selection, custom_fields) when is_map(url_selection) do
|
||||
all_fields = get_all_available_fields(custom_fields)
|
||||
|
||||
Enum.reduce(all_fields, %{}, fn field, acc ->
|
||||
field_string = field_to_string(field)
|
||||
visible = Map.get(url_selection, field_string, false)
|
||||
Map.put(acc, field_string, visible)
|
||||
end)
|
||||
end
|
||||
|
||||
def selection_from_url_only(_, _), do: %{}
|
||||
|
||||
@doc """
|
||||
Merges user field selection with global settings.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue