defmodule Exzeitable do
@moduledoc """
# Exzeitable. Check README for usage instructions.
"""
@doc "Expands into the gigantic monstrosity that is Exzeitable"
defmacro __using__(opts) do
alias Exzeitable.{Database, Params}
search_string =
opts
|> Params.set_fields()
|> Database.tsvector_string()
# coveralls-ignore-stop
quote do
use Phoenix.LiveView
use Phoenix.HTML
import Ecto.Query
alias Phoenix.LiveView.Helpers
alias Exzeitable.{Database, Filter, Format, HTML, Params, Validation}
@callback render(map) :: {:ok, iolist}
@type socket :: Phoenix.LiveView.Socket.t()
@doc """
Convenience helper so LiveView doesn't have to be called directly
## Example
```
<%= YourAppWeb.Live.Site.live_table(@conn, query: @query) %>
```
"""
defdelegate build_table(assigns), to: HTML, as: :build
@spec live_table(Plug.Conn.t(), keyword) :: {:safe, iolist}
def live_table(conn, opts \\ []) do
Phoenix.Component.live_render(conn, __MODULE__,
# Live component ID
id: Keyword.get(unquote(opts), :id, 1),
session: %{"params" => Params.new(opts, unquote(opts), __MODULE__)}
)
end
###########################
######## CALLBACKS ########
###########################
@doc "Initial setup on page load"
@spec mount(:not_mounted_at_router | map, map, socket) :: {:ok, socket}
def mount(:not_mounted_at_router, assigns, socket) do
assigns = Map.new(assigns, fn {k, v} -> {String.to_atom(k), v} end)
socket =
socket
|> assign(assigns)
|> maybe_get_records()
|> maybe_set_refresh()
{:ok, socket}
end
def mount(_map, assigns, socket) do
assigns = %{params: Params.new([], unquote(opts), __MODULE__)}
socket =
socket
|> assign(assigns)
|> maybe_get_records()
|> maybe_set_refresh()
{:ok, socket}
end
@doc "Clicking the hide button hides the column"
@spec handle_event(String.t(), map, socket) :: {:noreply, socket}
def handle_event("hide_column", %{"column" => column}, socket) do
%{assigns: %{params: %Params{fields: fields}}} = socket
fields = Kernel.put_in(fields, [String.to_existing_atom(column), :hidden], true)
{:noreply, assign_params(socket, :fields, fields)}
end
@doc "Clicking the show button shows the column"
def handle_event("show_column", %{"column" => column}, socket) do
%{assigns: %{params: %Params{fields: fields}}} = socket
fields = Kernel.put_in(fields, [String.to_existing_atom(column), :hidden], false)
{:noreply, assign_params(socket, :fields, fields)}
end
@doc "Hide all the show buttons"
def handle_event("hide_buttons", _, socket) do
{:noreply, assign_params(socket, :show_field_buttons, false)}
end
@doc "Show all the show buttons"
def handle_event("show_buttons", _, socket) do
{:noreply, assign_params(socket, :show_field_buttons, true)}
end
@doc "Changes page when pagination buttons are clicked"
def handle_event("change_page", %{"page" => page}, %{assigns: %{params: params}} = socket) do
new_params = Map.put(params, :page, String.to_integer(page))
socket
|> assign_params(:page, new_params.page)
|> assign_params(:list, Database.get_records(new_params))
|> then(&{:noreply, &1})
end
@doc "Clicking the sort button sorts the column"
def handle_event(
"sort_column",
%{"column" => column},
%{assigns: %{params: params}} = socket
) do
column = String.to_existing_atom(column)
new_order =
case params.order do
[asc: ^column] -> [desc: column]
_ -> [asc: column]
end
new_params = Map.merge(params, %{order: new_order, page: 1})
socket
|> assign_params(:page, 1)
|> assign_params(:order, new_order)
|> assign_params(:list, Database.get_records(new_params))
|> then(&{:noreply, &1})
end
@doc "Typing into the search box... searches. Crazy, right?"
def handle_event("search", %{"search" => %{"search" => search}}, socket) do
socket
|> assign_params(:search, search)
|> assign_params(:page, 1)
|> maybe_get_records()
|> then(&{:noreply, &1})
end
@doc "Refresh periodically grabs new records from the database"
def handle_info(:refresh, socket) do
{:noreply, maybe_get_records(socket)}
end
defp maybe_get_records(%{assigns: %{params: params}} = socket) do
if connected?(socket) do
socket
|> assign_params(:list, Database.get_records(params))
|> assign_params(:count, Database.get_record_count(params))
else
socket
|> assign_params(:list, [])
|> assign_params(:count, 0)
end
end
defp maybe_set_refresh(%{socket: %{assigns: %{refresh: refresh}}} = socket)
when is_integer(refresh) do
with true <- connected?(socket),
{:ok, _tref} <- :timer.send_interval(refresh, self(), :refresh) do
socket
else
_ -> socket
end
end
defp maybe_set_refresh(socket) do
socket
end
defp assign_params(%{assigns: %{params: params}} = socket, key, value) do
params
|> Map.put(key, value)
|> then(&assign(socket, :params, &1))
end
# Need to unquote the search string because string interpolation is not allowed.
@spec do_search(Ecto.Query.t(), String.t()) :: Ecto.Query.t()
def do_search(query, search) do
where(query, fragment(unquote(search_string), ^Database.prefix_search(search)))
end
end
end
end