defmodule Phoenix.LiveDashboard.TableComponent do use Phoenix.LiveDashboard.Web, :live_component @limit [50, 100, 500, 1000, 5000] @type params() :: %{ limit: pos_integer(), sort_by: :atom, sort_dir: :desc | :asc, search: binary(), hint: binary() | nil } @impl true def mount(socket) do {:ok, socket} end def normalize_params(params) do params |> validate_required([:columns, :id, :row_fetcher, :title]) |> normalize_columns() |> Map.put_new(:search, true) |> Map.put_new(:limit, @limit) |> Map.put_new(:row_attrs, []) |> Map.put_new(:hint, nil) |> Map.update(:default_sort_by, nil, &(&1 && to_string(&1))) |> Map.put_new_lazy(:rows_name, fn -> Phoenix.Naming.humanize(params.title) |> String.downcase() end) end defp validate_required(params, list) do case Enum.find(list, &(not Map.has_key?(params, &1))) do nil -> :ok key -> raise ArgumentError, "expected #{inspect(key)} parameter to be received" end params end defp normalize_columns(%{columns: columns} = params) when is_list(columns) do %{params | columns: Enum.map(columns, &normalize_column/1)} end defp normalize_columns(%{columns: columns}) do raise ArgumentError, "expected :columns to be a list, received: #{inspect(columns)}" end defp normalize_column(column) do case Access.fetch(column, :field) do {:ok, nil} -> msg = "expected :field parameter to not be nil, column received: #{inspect(column)}" raise ArgumentError, msg {:ok, field} when is_atom(field) or is_binary(field) -> column |> Map.new() |> Map.put_new_lazy(:header, fn -> Phoenix.Naming.humanize(field) end) |> Map.put_new(:header_attrs, []) |> Map.put_new(:format, & &1) |> Map.put_new(:cell_attrs, []) |> Map.put_new(:sortable, nil) {:ok, _} -> msg = "expected :field parameter to be an atom or a string, column received: " raise ArgumentError, msg <> inspect(column) :error -> msg = "expected :field parameter to be received, column received: #{inspect(column)}" raise ArgumentError, msg end end @impl true def update(assigns, socket) do assigns = normalize_table_params(assigns) %{ table_params: table_params, page: page, row_fetcher: row_fetcher } = assigns {rows, total} = row_fetcher.(table_params, page.node) assigns = Map.merge(assigns, %{rows: rows, total: total}) {:ok, assign(socket, assigns)} end defp normalize_table_params(assigns) do %{columns: columns, page: %{params: all_params}, default_sort_by: sort_by} = assigns sortable_columns = sortable_columns(columns) sort_by = all_params |> get_in_or_first("sort_by", sort_by, sortable_columns) |> String.to_atom() sort_dir = all_params |> get_in_or_first("sort_dir", sortable_dirs(columns, sort_by)) |> String.to_atom() limit = if assigns.limit do all_params |> get_in_or_first("limit", Enum.map(assigns.limit, &to_string/1)) |> String.to_integer() else nil end search = all_params["search"] search = if search == "", do: nil, else: search table_params = %{sort_by: sort_by, sort_dir: sort_dir, limit: limit, search: search} Map.put(assigns, :table_params, table_params) end defp sortable_columns(columns) do for column <- columns, column.sortable, do: to_string(column.field) end defp sortable_dirs(columns, field) do case Enum.find(columns, &(&1.field == field)) do %{sortable: :desc} -> ~w(desc asc) %{sortable: :asc} -> ~w(asc desc) end end defp get_in_or_first(params, key, default \\ nil, valid) do value = params[key] if value in valid, do: value, else: default || hd(valid) end @impl true def render(assigns) do ~L"""