defmodule LiveTable.LiveViewHelpers do @moduledoc false defmacro __using__(opts) do quote do use LiveTable.ExportHelpers, schema: unquote(opts[:schema]) use LiveTable.FilterToggleHelpers @impl true # Fetches records based on URL params def handle_params(params, url, socket) do default_sort = get_in(unquote(opts[:table_options]), [:sorting, :default_sort]) current_path = URI.parse(url).path |> String.trim_leading("/") sort_params = Map.get(params, "sort_params", default_sort) |> Enum.map(fn # for default case {k, v} when is_atom(k) and is_atom(v) -> {k, v} # for incoming params from url {k, v} -> {String.to_existing_atom(k), String.to_existing_atom(v)} end) filters = Map.get(params, "filters", %{}) |> Map.put("search", params["search"] || "") |> Enum.reduce(%{}, fn {"search", search_term}, acc -> Map.put(acc, "search", search_term) {key, %{"min" => min, "max" => max}}, acc -> filter = get_filter(key) {min_val, max_val} = parse_range_values(filter.options.type, min, max) updated_filter = filter |> Map.update!(:options, fn options -> options |> Map.put(:current_min, min_val) |> Map.put(:current_max, max_val) end) key = String.to_atom(key) Map.put(acc, key, updated_filter) {key, %{"id" => id}}, acc -> filter = %LiveTable.Select{} = get_filter(key) id = id |> Enum.map(&String.to_integer/1) filter = %{filter | options: Map.update!(filter.options, :selected, &(&1 ++ id))} key = key |> String.to_existing_atom() Map.put(acc, key, filter) {key, custom_data}, acc when is_map(custom_data) -> filter = get_filter(key) case filter do %LiveTable.Transformer{} -> updated_filter = %{ filter | options: Map.put(filter.options, :applied_data, custom_data) } key = String.to_existing_atom(key) Map.put(acc, key, updated_filter) _ -> key = String.to_existing_atom(key) Map.put(acc, key, filter) end {k, _}, acc -> key = k |> String.to_existing_atom() Map.put(acc, key, get_filter(k)) end) options = %{ "sort" => %{ "sortable?" => get_in(unquote(opts[:table_options]), [:sorting, :enabled]), "sort_params" => sort_params }, "pagination" => %{ "paginate?" => get_in(unquote(opts[:table_options]), [:pagination, :enabled]), "page" => params["page"] |> validate_page_num(), "per_page" => params["per_page"] |> validate_per_page() }, "filters" => filters } data_provider = socket.assigns[:data_provider] || unquote(opts[:data_provider]) {resources, updated_options} = case stream_resources(fields(), options, data_provider) do {resources, overflow} -> has_next_page = length(overflow) > 0 options = put_in(options["pagination"][:has_next_page], has_next_page) {resources, options} resources when is_list(resources) -> {resources, options} end # Update LiveSelect components with selected values from URL params socket = socket |> assign_to_socket(resources, unquote(opts[:table_options])) |> assign(:options, updated_options) |> assign(:current_path, current_path) # Update LiveSelect components with selected values from URL params for {key, filter} <- filters do case filter do %LiveTable.Select{options: %{selected: selected}} when selected != [] -> # Get the options for this filter options = case filter.options do %{options: options} when is_list(options) and options != [] -> options %{options_source: {module, function, args}} -> apply(module, function, ["" | args]) _ -> [] end # Find the selected options based on the selected IDs selected_options = Enum.map(selected, fn id -> Enum.find(options, fn %{value: [option_id, _]} -> option_id == id _ -> false end) end) |> Enum.reject(&is_nil/1) # Update the LiveSelect component with the selected options if selected_options != [] do send_update(LiveSelect.Component, id: filter.key, value: selected_options) end _ -> :ok end end {:noreply, socket} end defp assign_to_socket(socket, resources, %{use_streams: true}) do stream(socket, :resources, resources, dom_id: fn resource -> # "#{resource}- "resource-#{Ecto.UUID.generate()}" end, reset: true ) end defp assign_to_socket(socket, resources, %{use_streams: false}) do assign(socket, :resources, resources) end defp validate_page_num(nil), do: "1" defp validate_page_num(n) when is_binary(n) do try do num = String.to_integer(n) cond do num > 0 -> n true -> "1" end rescue ArgumentError -> "1" end end defp validate_per_page(nil), do: "10" defp validate_per_page(n) when is_binary(n) do try do num = String.to_integer(n) cond do num > 0 and num <= 50 -> n true -> "50" end rescue ArgumentError -> "10" end end @impl true # Handles all LiveTable related events like sort, paginate and filter def handle_event("sort", %{"clear_filters" => "true"}, socket) do current_path = socket.assigns.current_path options = socket.assigns.options |> Enum.reduce(%{}, fn {"filters", _v}, acc -> Map.put(acc, "filters", %{}) {_, v}, acc when is_map(v) -> Map.merge(acc, v) end) |> Map.take(~w(page per_page sort_params)) |> Map.reject(fn {_, v} -> v == "" || is_nil(v) end) socket = socket |> push_patch(to: "/#{current_path}?#{Plug.Conn.Query.encode(options)}") {:noreply, socket} end def handle_event("sort", params, socket) do shift_key = Map.get(params, "shift_key", false) sort_params = Map.get(params, "sort", nil) filter_params = Map.get(params, "filters", nil) current_path = socket.assigns.current_path options = socket.assigns.options |> Enum.reduce(%{}, fn {"filters", %{"search" => search_term} = v}, acc -> filters = encode_filters(v) Map.put(acc, "filters", filters) |> Map.put("search", search_term) {_, v}, acc when is_map(v) -> Map.merge(acc, v) end) |> Map.merge(params, fn "filters", v1, v2 when is_map(v1) and is_map(v2) -> v1 _, _, v -> v end) |> update_sort_params(sort_params, shift_key) |> update_filter_params(filter_params) |> Map.take(~w(page per_page search sort_params filters)) |> Map.reject(fn {_, v} -> v == "" || is_nil(v) end) |> remove_unused_keys() socket = socket |> push_patch(to: "/#{current_path}?#{Plug.Conn.Query.encode(options)}") {:noreply, socket} end def handle_event("change", params, socket) do params |> dbg() {:noreply, socket} end def handle_event("submit", params, socket) do params |> dbg() {:noreply, socket} end def handle_event("live_select_change", %{"text" => text, "id" => id}, socket) do options = case get_filter(id) do %LiveTable.Select{ options: %{options: _options, options_source: {module, function, args}} } -> apply(module, function, [text | args]) %LiveTable.Select{options: %{options: options, options_source: nil}} -> options end send_update(LiveSelect.Component, id: id, options: options) {:noreply, socket} end def remove_unused_keys(map) when is_map(map) do map |> Map.reject(fn {key, _value} -> key_string = to_string(key) String.starts_with?(key_string, "_unused") end) |> Enum.map(fn {key, value} -> {key, remove_unused_keys(value)} end) |> Enum.into(%{}) end def remove_unused_keys(value), do: value end end end