defmodule LiveAdmin do
@moduledoc docout: [LiveAdmin.READMECompiler]
@type mod_func :: {module(), :atom}
@type func_ref :: :atom | mod_func() | :mfa
@type func_list :: [func_ref] | keyword(func_ref)
@type field_list :: [:atom]
@options_schema [
components: [
type: :non_empty_keyword_list,
type_doc: "list of modules implementing LiveComponent overrides of LiveAdmin views",
keys: [
nav: [type: :atom],
home: [type: :atom],
session: [type: :atom],
new: [type: :atom],
edit: [type: :atom],
list: [type: :atom],
view: [type: :atom]
]
],
ecto_repo: [
type: :atom,
type_doc: "Ecto Repo used to query resource"
],
list_with: [
type: {:or, [:atom, {:tuple, [:atom, :atom]}]},
type_doc:
"`t:func_ref/0` returning `{records, count}` used to fetch records in LiveAdmin :list component"
],
render_with: [
type: {:or, [:atom, {:tuple, [:atom, :atom]}]},
type_doc:
"`t:func_ref/0` used to convert field values to string in LiveAdmin :list component"
],
delete_with: [
type: {:or, [:atom, {:tuple, [:atom, :atom]}]},
type_doc: "`t:func_ref/0` or `false` to disable deleting records"
],
create_with: [
type: {:or, [:atom, {:tuple, [:atom, :atom]}]},
type_doc: "`t:func_ref/0` or `false` to disable creating records"
],
update_with: [
type: {:or, [:atom, {:tuple, [:atom, :atom]}]},
type_doc: "`t:func_ref/0` or `false` to disable updating records"
],
validate_with: [
type: {:or, [:atom, {:tuple, [:atom, :atom]}]},
type_doc:
"`t:func_ref/0` used to validate create/update changesets in LiveAdmin :form component"
],
label_with: [
type: {:or, [:atom, {:tuple, [:atom, :atom]}]},
type_doc:
"`t:func_ref/0` used to convert (association) record to string in LiveAdmin SearchSelect component"
],
title_with: [
type: {:or, [:string, {:tuple, [:atom, :atom]}]},
type_doc: "string literal or MFA returning a string used to render LiveAdmin UI heading"
],
hidden_fields: [
type: {:list, :atom},
type_doc: "`t:field_list/0` to be hidden from LiveView"
],
immutable_fields: [
type: {:list, :atom},
type_doc: "`t:field_list/0` to be disabled in LiveAdmin :form component"
],
actions: [
type:
{:list,
{:or,
[
:atom,
{:tuple, [:atom, :atom]},
{:tuple, [:atom, :atom, :integer]},
{:tuple,
[:atom, {:or, [{:tuple, [:atom, :atom]}, {:tuple, [:atom, :atom, :integer]}]}]}
]}},
type_doc: "`t:func_list/0` taking a record, LiveAdmin session struct, and any extra args"
],
tasks: [
type:
{:list,
{:or,
[
:atom,
{:tuple, [:atom, :atom]},
{:tuple, [:atom, :atom, :integer]},
{:tuple,
[:atom, {:or, [{:tuple, [:atom, :atom]}, {:tuple, [:atom, :atom, :integer]}]}]}
]}},
type_doc: "`t:func_list/0` taking a LiveAdmin session and any extra args"
]
]
@doc """
Defines [NimbleOptions](https://hexdocs.pm/nimble_options/NimbleOptions.html) schema for configuration that can be set at all levels (resource, scope, and application).
Used internally to validate configuration in apps using LiveAdmin.
Supported options:
#{@options_schema |> NimbleOptions.new!() |> NimbleOptions.docs()}
"""
def base_configs_schema, do: @options_schema
def fetch_function(resource, session, function_type, function)
when function_type in [:tasks, :actions] and is_atom(function) do
with result = {_, m, f, _} <-
extract_function_from_config(resource, session, function_type, function),
docs when is_map(docs) <- extract_function_docs(m, f) do
Tuple.append(result, docs)
end
end
def fetch_config(resource, :components, config),
do:
Keyword.merge(
Keyword.fetch!(config, :components),
Keyword.get(resource.__live_admin_config__(), :components, [])
)
def fetch_config(resource, key, config),
do: Keyword.get(resource.__live_admin_config__, key) || Keyword.fetch!(config, key)
def primary_key!(resource) do
[key] = Keyword.fetch!(resource.__live_admin_config__(), :schema).__schema__(:primary_key)
key
end
def route_with_params(assigns, parts \\ []) do
resource_path = parts[:resource_path] || assigns.key
encoded_params =
parts
|> Keyword.get(:params, %{})
|> Enum.into(%{})
|> then(fn params ->
if assigns[:prefix] do
Map.put_new(params, :prefix, assigns[:prefix])
else
params
end
end)
|> Enum.into(%{})
|> Enum.flat_map(fn
{_, nil} -> []
{:sort_attr, val} -> [{:"sort-attr", val}]
{:sort_dir, val} -> [{:"sort-dir", val}]
{:search, val} -> [{:s, val}]
pair -> [pair]
end)
|> Enum.into(%{})
|> case do
params when map_size(params) > 0 -> "?" <> Plug.Conn.Query.encode(params)
_ -> ""
end
segments =
Enum.map(
parts[:segments] || [],
fn segment ->
cond do
is_struct(segment) && Phoenix.Param.impl_for(segment) ->
Phoenix.Param.to_param(segment)
true ->
to_string(segment)
end
end
)
Path.join([assigns.base_path, resource_path] ++ segments) <> encoded_params
end
def session_store,
do: Application.get_env(:live_admin, :session_store, __MODULE__.Session.Agent)
def associated_resource(schema, field_name, resources, part \\ nil) do
with %{related: assoc_schema} <-
schema |> parent_associations() |> Enum.find(&(&1.owner_key == field_name)),
config when not is_nil(config) <-
Enum.find(resources, fn {_, resource} ->
Keyword.fetch!(resource.__live_admin_config__, :schema) == assoc_schema
end) do
case part do
nil -> config
:key -> elem(config, 0)
:resource -> elem(config, 1)
end
else
_ -> nil
end
end
def parent_associations(schema) do
Enum.flat_map(schema.__schema__(:associations), fn assoc_name ->
case schema.__schema__(:association, assoc_name) do
assoc = %{relationship: :parent} -> [assoc]
_ -> []
end
end)
end
def resource_title(resource, session) do
resource
|> fetch_config(:title_with, session)
|> case do
nil ->
resource.__live_admin_config__()
|> Keyword.fetch!(:schema)
|> Module.split()
|> Enum.at(-1)
{m, f} ->
apply(m, f, [])
title when is_binary(title) ->
title
end
end
def record_label(nil, _, _), do: nil
def record_label(record, resource, config) do
resource
|> fetch_config(:label_with, config)
|> case do
nil -> Map.fetch!(record, LiveAdmin.primary_key!(resource))
{m, f} -> apply(m, f, [record])
label when is_atom(label) -> Map.fetch!(record, label)
end
end
def use_i18n?, do: gettext_backend() != LiveAdmin.Gettext
def trans(string, opts \\ []) do
args =
[gettext_backend(), string]
|> then(fn base_args ->
if opts[:inter], do: base_args ++ [opts[:inter]], else: base_args
end)
apply(Gettext, :gettext, args)
end
def gettext_backend, do: Application.get_env(:live_admin, :gettext_backend, LiveAdmin.Gettext)
def resources(router, base_path) do
router
|> Phoenix.Router.routes()
|> Enum.flat_map(fn
%{metadata: %{base_path: ^base_path, resource: resource}} -> [resource]
_ -> []
end)
end
defp extract_function_from_config(resource, session, function_type, function) do
default_arity = if function_type == :tasks, do: 1, else: 2
resource
|> LiveAdmin.fetch_config(function_type, session)
|> Enum.find_value(:error, fn
{name, {m, f, a}} -> name == function && {name, m, f, a}
{name, {m, f}} -> name == function && {name, m, f, default_arity}
{m, f, a} -> f == function && {f, m, f, a}
{m, f} -> f == function && {f, m, f, default_arity}
name -> name == function && {name, resource, name, default_arity}
end)
end
def extract_function_docs(module, function) do
with {_, _, _, _, _, _, module_docs} <- Code.fetch_docs(module),
function_docs <-
Enum.find_value(module_docs, %{}, fn {{_, name, _}, _, _, docs, _} ->
name == function && is_map(docs) && docs
end) do
function_docs
else
{:error, _} -> %{}
end
end
end