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(extract_member_field_keys(all_fields), fn field -> %{ value: field_to_string(field), label: format_field_label(field) } end) ++ Enum.map(extract_custom_field_keys(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"""
<.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} />
""" 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 extract_member_field_keys(nil), do: [] defp extract_member_field_keys(fields) do prefix = Mv.Constants.custom_field_prefix() Enum.filter(fields, fn field -> is_atom(field) || (is_binary(field) && not String.starts_with?(field, prefix)) end) end defp extract_custom_field_keys(nil), do: [] defp extract_custom_field_keys(fields) do prefix = Mv.Constants.custom_field_prefix() Enum.filter(fields, fn field -> is_binary(field) && String.starts_with?(field, prefix) 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, Mv.Constants.custom_field_prefix()) 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