172 lines
5 KiB
Elixir
172 lines
5 KiB
Elixir
defmodule MvWeb.Components.FieldVisibilityDropdownComponent do
|
|
@moduledoc """
|
|
LiveComponent for managing field visibility in the member overview.
|
|
|
|
Provides an accessible dropdown menu where users can select/deselect
|
|
which member fields and custom fields are visible in the table.
|
|
|
|
## Props
|
|
- `:all_fields` - List of all available fields
|
|
- `:custom_fields` - List of CustomField resources
|
|
- `:selected_fields` - Map field_name → boolean
|
|
- `:id` - Component ID
|
|
|
|
## Events sent to parent:
|
|
- `{:field_toggled, field, value}`
|
|
- `{:fields_selected, map}`
|
|
"""
|
|
|
|
use MvWeb, :live_component
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# UPDATE
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@impl true
|
|
def update(assigns, socket) do
|
|
socket =
|
|
socket
|
|
|> assign(assigns)
|
|
|> assign_new(:open, fn -> false end)
|
|
|> assign_new(:all_fields, fn -> [] end)
|
|
|> assign_new(:custom_fields, fn -> [] end)
|
|
|> assign_new(:selected_fields, fn -> %{} end)
|
|
|
|
{:ok, socket}
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# RENDER
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@impl true
|
|
def render(assigns) do
|
|
all_fields = assigns.all_fields || []
|
|
custom_fields = assigns.custom_fields || []
|
|
|
|
all_items =
|
|
Enum.map(member_fields(all_fields), fn field ->
|
|
%{
|
|
value: field_to_string(field),
|
|
label: format_field_label(field)
|
|
}
|
|
end) ++
|
|
Enum.map(custom_fields(all_fields), fn field ->
|
|
%{
|
|
value: field,
|
|
label: format_custom_field_label(field, custom_fields)
|
|
}
|
|
end)
|
|
|
|
assigns = assign(assigns, :all_items, all_items)
|
|
|
|
# LiveComponents require a static HTML element as root, not a function component
|
|
~H"""
|
|
<div>
|
|
<.dropdown_menu
|
|
id="field-visibility-menu"
|
|
icon="hero-adjustments-horizontal"
|
|
button_label={gettext("Columns")}
|
|
items={@all_items}
|
|
checkboxes={true}
|
|
selected={@selected_fields}
|
|
open={@open}
|
|
show_select_buttons={true}
|
|
phx_target={@myself}
|
|
/>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# EVENTS (matching the Core Component API)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@impl true
|
|
def handle_event("toggle_dropdown", _params, socket) do
|
|
{:noreply, assign(socket, :open, !socket.assigns.open)}
|
|
end
|
|
|
|
def handle_event("close_dropdown", _params, socket) do
|
|
{:noreply, assign(socket, :open, false)}
|
|
end
|
|
|
|
# toggle single item
|
|
def handle_event("select_item", %{"item" => item}, socket) do
|
|
current = Map.get(socket.assigns.selected_fields, item, true)
|
|
updated = Map.put(socket.assigns.selected_fields, item, !current)
|
|
|
|
send(self(), {:field_toggled, item, !current})
|
|
{:noreply, assign(socket, :selected_fields, updated)}
|
|
end
|
|
|
|
# select all
|
|
def handle_event("select_all", _params, socket) do
|
|
all =
|
|
socket.assigns.all_fields
|
|
|> Enum.map(&field_to_string/1)
|
|
|> Enum.map(&{&1, true})
|
|
|> Enum.into(%{})
|
|
|
|
send(self(), {:fields_selected, all})
|
|
{:noreply, assign(socket, :selected_fields, all)}
|
|
end
|
|
|
|
# select none
|
|
def handle_event("select_none", _params, socket) do
|
|
none =
|
|
socket.assigns.all_fields
|
|
|> Enum.map(&field_to_string/1)
|
|
|> Enum.map(&{&1, false})
|
|
|> Enum.into(%{})
|
|
|
|
send(self(), {:fields_selected, none})
|
|
{:noreply, assign(socket, :selected_fields, none)}
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# HELPERS (with defensive nil guards)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
defp member_fields(nil), do: []
|
|
|
|
defp member_fields(fields) do
|
|
Enum.filter(fields, fn field ->
|
|
is_atom(field) ||
|
|
(is_binary(field) && not String.starts_with?(field, "custom_field_"))
|
|
end)
|
|
end
|
|
|
|
defp custom_fields(nil), do: []
|
|
|
|
defp custom_fields(fields) do
|
|
Enum.filter(fields, fn field ->
|
|
is_binary(field) && String.starts_with?(field, "custom_field_")
|
|
end)
|
|
end
|
|
|
|
defp field_to_string(field) when is_atom(field), do: Atom.to_string(field)
|
|
defp field_to_string(field) when is_binary(field), do: field
|
|
|
|
defp format_field_label(field) do
|
|
field
|
|
|> field_to_string()
|
|
|> String.replace("_", " ")
|
|
|> String.split()
|
|
|> Enum.map_join(" ", &String.capitalize/1)
|
|
end
|
|
|
|
defp format_custom_field_label(field_string, custom_fields) do
|
|
id = String.trim_leading(field_string, "custom_field_")
|
|
find_custom_field_name(id, field_string, custom_fields)
|
|
end
|
|
|
|
defp find_custom_field_name("", field_string, _custom_fields), do: field_string
|
|
|
|
defp find_custom_field_name(id, _field_string, custom_fields) do
|
|
case Enum.find(custom_fields, fn cf -> to_string(cf.id) == id end) do
|
|
nil -> gettext("Custom Field %{id}", id: id)
|
|
custom_field -> custom_field.name
|
|
end
|
|
end
|
|
end
|