defmodule SelectoComponents.EnhancedTable.Sorting do @moduledoc """ Provides sorting functionality for SelectoComponents tables. Supports single and multi-column sorting with visual indicators. """ use Phoenix.Component alias SelectoComponents.SafeAtom @doc """ Initialize sort state in the socket assigns. """ def init_sort_state(socket) do Phoenix.Component.assign(socket, # List of {column, direction} tuples sort_by: [], # :single or :multi sort_mode: :single ) end @doc """ Handle sort column click event. ## Parameters - column: The column identifier to sort by - socket: The LiveView socket - multi: Boolean indicating if multi-column sort (shift-click) """ def handle_sort_click(column, socket, multi \\ false) do current_sort = socket.assigns[:sort_by] || [] new_sort = if multi && socket.assigns[:sort_mode] == :multi do # Multi-column sorting update_multi_sort(current_sort, column) else # Single column sorting update_single_sort(current_sort, column) end Phoenix.Component.assign(socket, :sort_by, new_sort) end @doc """ Apply sorting to Selecto query. Column-based sorting takes priority over query-based sorting. """ def apply_sort_to_query(selecto, sort_by) when is_list(sort_by) and length(sort_by) > 0 do # Build the new order expressions order_expressions = Enum.map(sort_by, fn {column, direction} -> case direction do :asc -> column :desc -> {:desc, column} _ -> column end end) # Replace the order_by entirely with column-based sorting # This ensures column sorting takes priority put_in(selecto.set.order_by, order_expressions) end def apply_sort_to_query(selecto, _), do: selecto @doc """ Get sort indicator for a column. Returns :asc, :desc, or nil """ def get_sort_indicator(column, sort_by) do case List.keyfind(sort_by || [], column, 0) do {^column, direction} -> direction _ -> nil end end @doc """ Get sort position for multi-column sorting. Returns the position number or nil. """ def get_sort_position(column, sort_by) do sort_by = sort_by || [] case Enum.find_index(sort_by, fn {col, _} -> col == column end) do nil -> nil index -> index + 1 end end @doc """ Render sort indicator component. """ def sort_indicator(assigns) do indicator = get_sort_indicator(assigns.column, assigns[:sort_by]) position = if assigns[:show_position] do get_sort_position(assigns.column, assigns[:sort_by]) else nil end assigns = Phoenix.Component.assign(assigns, indicator: indicator, position: position) ~H""" <%= if @position do %> <%= @position %> <% end %> <%= case @indicator do %> <% :asc -> %> <% :desc -> %> <% _ -> %> <% end %> """ end @doc """ Render sortable column header. """ def sortable_header(assigns) do # Check if resizable is enabled if Map.get(assigns, :resizable, false) && Map.get(assigns, :column_config) do column_config = Map.get(assigns.column_config, assigns.column, %{ width: 150, min_width: 50, max_width: 500 }) assigns = Phoenix.Component.assign(assigns, :col_config, column_config) ~H"""
<%= @label %> <.sort_indicator column={@column} sort_by={@sort_by} show_position={@multi} />
<%!-- Resize handle --%> <%= if Map.get(assigns, :resizable, false) do %>
<% end %> """ else # Original non-resizable header ~H"""
<%= @label %> <.sort_indicator column={@column} sort_by={@sort_by} show_position={@multi} />
""" end end # Private functions defp update_single_sort(current_sort, column) do case List.keyfind(current_sort, column, 0) do {^column, :asc} -> [{column, :desc}] # Remove sort {^column, :desc} -> [] _ -> [{column, :asc}] end end defp update_multi_sort(current_sort, column) do case List.keyfind(current_sort, column, 0) do {^column, :asc} -> # Change to desc List.keyreplace(current_sort, column, 0, {column, :desc}) {^column, :desc} -> # Remove from sort List.keydelete(current_sort, column, 0) nil -> # Add to sort current_sort ++ [{column, :asc}] end end @doc """ Serialize sort state for URL or storage. """ def serialize_sort(sort_by) do Enum.map(sort_by, fn {col, dir} -> %{"column" => to_string(col), "direction" => to_string(dir)} end) end @doc """ Deserialize sort state from URL or storage. """ def deserialize_sort(nil), do: [] def deserialize_sort(sort_data) when is_list(sort_data) do Enum.map(sort_data, fn %{"column" => col, "direction" => dir} -> # Use SafeAtom to prevent atom table exhaustion from user input col_atom = SafeAtom.to_existing(col) dir_atom = SafeAtom.to_sort_direction(dir) if col_atom do {col_atom, dir_atom} else nil end _ -> nil end) |> Enum.reject(&is_nil/1) end def deserialize_sort(_), do: [] end