Current section

Files

Jump to
ash_admin lib ash_admin components resource generic_action.ex
Raw

lib/ash_admin/components/resource/generic_action.ex

defmodule AshAdmin.Components.Resource.GenericAction do
@moduledoc false
use Phoenix.LiveComponent
import AshAdmin.Helpers
attr :resource, :atom
attr :domain, :atom
attr :action, :any
attr :authorizing, :boolean
attr :actor, :any
attr :url_path, :any
attr :params, :any
attr :tenant, :any, required: true
attr :table, :any
attr :prefix, :any
def render(assigns) do
~H"""
<div class="sm:mt-0 bg-gray-300 min-h-screen">
<%= if Enum.empty?(@action.arguments) do %>
<.form
:let={form}
as={:form}
for={@form}
class="flex flex-row justify-items-center pt-4"
phx-change="validate"
phx-submit="save"
phx-target={@myself}
>
<div :if={form.source.submitted_once?} class="ml-4 mt-4 text-red-500">
<ul>
<li :for={{field, message} <- all_errors(form)}>
<span :if={field}>
{field}:
</span>
<span>
{message}
</span>
</li>
</ul>
</div>
{AshAdmin.Components.Resource.Form.render_attributes(
assigns,
@resource,
@action,
form
)}
<button
type="submit"
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Run
</button>
</.form>
<% else %>
<div class="md:grid md:grid-cols-3 md:gap-6 md:mx-16 md:pt-10 mb-10"></div>
<div class="md:mt-0 md:col-span-2">
<div class="shadow-lg overflow-hidden pt-2 sm:rounded-md bg-white">
<div class="px-4 sm:p-6">
<.form
:let={form}
as={:form}
for={@form}
class="flex flex-row"
phx-change="validate"
phx-submit="save"
phx-target={@myself}
>
<div :if={form.source.submitted_once?} class="ml-4 mt-4 text-red-500">
<ul>
<li :for={{field, message} <- all_errors(form)}>
<span :if={field}>
{field}:
</span>
<span>
{message}
</span>
</li>
</ul>
</div>
{AshAdmin.Components.Resource.Form.render_attributes(
assigns,
@resource,
@action,
form
)}
<div class="px-4 py-3 text-right sm:px-6 my-auto">
<button
type="submit"
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Run
</button>
</div>
</.form>
</div>
</div>
</div>
<% end %>
<%= case @result do %>
<% :pending -> %>
<% :ok -> %>
Success
<% {:ok, result} -> %>
<div class="shadow-lg overflow-auto sm:rounded-md bg-white mx-12 px-8">
<h1>Success</h1>
{render_value(assigns, result, @action.returns, @action.constraints)}
</div>
<% :error -> %>
Action failed
<% end %>
</div>
"""
end
defp render_value(assigns, nil, _, _) do
~H"""
None
"""
end
defp render_value(assigns, value, {:array, type}, constraints) do
assigns = assign(assigns, value: value, type: type, constraints: constraints[:items] || [])
~H"""
<%= for inner_value <- List.wrap(@value) do %>
{render_value(assigns, inner_value, @type, @constraints)}
<hr />
<% end %>
"""
end
defp render_value(assigns, value, type, constraints) do
assigns = assign(assigns, value: value, type: type)
cond do
Ash.Type.NewType.new_type?(type) ->
inner_type = Ash.Type.NewType.subtype_of(type)
constraints = Ash.Type.NewType.constraints(type, constraints)
render_value(assigns, value, inner_type, constraints)
Ash.Type.embedded_type?(type) ->
AshAdmin.Components.Resource.Show.render_show(assigns, value, type, nil, false)
(type == Ash.Type.Struct and
constraints[:instance_of]) && Ash.Resource.Info.resource?(constraints[:instance_of]) ->
AshAdmin.Components.Resource.Show.render_show(
assigns,
value,
constraints[:instance_of],
nil,
false
)
type in [Ash.Type.Map, Ash.Type.Keyword, Ash.Type.Struct] ->
render_keyed_value(assigns, value, type, constraints)
type == Ash.Type.Union ->
case value do
%Ash.Union{type: type, value: value} ->
type = constraints[type][:type]
union_constraints = constraints[type][:constraints] || []
if type do
render_value(assigns, value, type, union_constraints)
else
raw_value(value)
end
value ->
raw_value(value)
end
true ->
raw_value(value)
end
end
defp raw_value(value) when is_binary(value), do: value
defp raw_value(nil), do: "None"
defp raw_value(value) do
inspect(value)
end
defp render_keyed_value(assigns, value, _type, constraints) do
assigns = assign(assigns, value: value, constraints: constraints)
if (is_map(value) || Keyword.keyword?(value)) && Keyword.keyword?(constraints[:fields]) do
~H"""
<%= for {key, config} <- @constraints[:fields] do %>
<div class="block text-sm font-medium text-gray-700">
{to_name(key)}
</div>
<div>
{render_value(assigns, get_key(@value, key), config[:type], config[:constraints])}
</div>
<% end %>
"""
else
raw_value(value)
end
end
defp get_key(value, key) when is_map(value), do: Map.get(value, key)
defp get_key(value, key) do
if Keyword.keyword?(value) do
value[key]
else
nil
end
end
def mount(socket) do
{:ok,
socket
|> assign_new(:result, fn -> :pending end)
|> assign_new(:initialized, fn -> false end)}
end
def update(assigns, socket) do
if assigns[:initialized] do
{:ok, socket}
else
socket = assign(socket, assigns)
context =
if table = socket.assigns[:table] do
%{
data_layer: %{
table: table
}
}
else
%{}
end
form =
AshPhoenix.Form.for_action(socket.assigns.resource, socket.assigns.action.name,
domain: socket.assigns[:domain],
actor: socket.assigns[:actor],
tenant: socket.assigns[:tenant],
authorize?: socket.assigns[:authorizing],
context: context
)
{:ok, assign(socket, initialized: true, form: form)}
end
# else
# socket = assign(socket, assigns)
# params = socket.assigns[:params] || %{}
# arguments = params["args"]
# query =
# socket.assigns[:resource]
# |> AshPhoenix.Form.for_read(socket.assigns.action.name,
# as: "query",
# )
# {query, run_now?} =
# if arguments do
# {Map.put(AshPhoenix.Form.validate(query, arguments), :submitted_once?, true), true}
# else
# {query, socket.assigns.action.arguments == []}
# end
# socket = assign(socket, :query, query)
# socket =
# if params["page"] && socket.assigns.action.pagination do
# default_limit =
# socket.assigns.action.pagination.default_limit ||
# socket.assigns.action.pagination.max_page_size || 25
# count? = !!socket.assigns.action.pagination.countable
# page_params =
# AshPhoenix.LiveView.page_from_params(params["page"], default_limit, count?)
# socket
# |> assign(
# :page_params,
# page_params
# )
# |> assign(
# :page_num,
# page_num_from_page_params(page_params)
# )
# else
# socket
# |> assign(:page_params, nil)
# |> assign(:page_num, 1)
# end
# socket =
# if run_now? do
# if socket.assigns[:tables] not in [[], nil] && !socket.assigns[:table] do
# assign(socket, :data, {:ok, []})
# else
# action_opts =
# if page_params = socket.assigns[:page_params] do
# [page: page_params]
# else
# []
# end
# case AshPhoenix.Form.submit(socket.assigns.query, action_opts: action_opts, params: nil) do
# {:ok, data} -> assign(socket, :data, {:ok, data})
# {:error, query} -> assign(socket, data: {:error, all_errors(query)}, query: query)
# end
# end
# else
# assign(socket, :data, :loading)
# end
# {:ok,
# socket
# |> assign(:initialized, true)}
# end
end
# defp load_fields(query) do
# query
# |> Ash.Query.select([])
# |> Ash.Query.load(AshAdmin.Resource.table_columns(query.resource))
# end
def handle_event("validate", params, socket) do
params = params["form"] || %{}
form = AshPhoenix.Form.validate(socket.assigns.form, params)
{:noreply, assign(socket, form: form)}
end
def handle_event("save", params, socket) do
params = params["form"] || %{}
case AshPhoenix.Form.submit(socket.assigns.form, params: params) do
:ok -> {:noreply, assign(socket, result: :ok)}
{:ok, res} -> {:noreply, assign(socket, result: {:ok, res})}
{:error, form} -> {:noreply, assign(socket, form: form, result: :error)}
end
end
def handle_event("add_form", %{"path" => path} = params, socket) do
type =
case params["type"] do
"lookup" -> :read
_ -> :create
end
form = AshPhoenix.Form.add_form(socket.assigns.form, path, type: type)
{:noreply,
socket
|> assign(:form, form)}
end
def handle_event("remove_form", %{"path" => path}, socket) do
form = AshPhoenix.Form.remove_form(socket.assigns.form, path)
{:noreply,
socket
|> assign(:form, form)}
end
def handle_event("remove_value", %{"path" => path, "field" => field, "index" => index}, socket) do
form =
AshPhoenix.Form.update_form(
socket.assigns.form,
path,
&remove_value(&1, field, index)
)
{:noreply,
socket
|> assign(:form, form)}
end
def handle_event("append_value", %{"path" => path, "field" => field}, socket) do
list =
AshPhoenix.Form.get_form(socket.assigns.form, path)
|> AshPhoenix.Form.value(String.to_existing_atom(field))
|> Kernel.||([])
|> indexed_list()
|> append_to_and_map(nil)
params =
put_in_creating(
socket.assigns.form.params || %{},
Enum.map(
AshPhoenix.Form.parse_path!(socket.assigns.form, path) ++ [field],
&to_string/1
),
list
)
form = AshPhoenix.Form.validate(socket.assigns.form, params)
{:noreply,
socket
|> assign(:form, form)}
end
defp indexed_list(map) when is_map(map) do
map
|> Map.keys()
|> Enum.map(&String.to_integer/1)
|> Enum.sort()
|> Enum.map(&map[to_string(&1)])
rescue
_ ->
List.wrap(map)
end
defp indexed_list(other), do: List.wrap(other)
defp append_to_and_map(list, value) do
list
|> Enum.concat([value])
|> Enum.with_index()
|> Map.new(fn {v, i} ->
{"#{i}", v}
end)
end
defp put_in_creating(map, [key], value) do
Map.put(map || %{}, key, value)
end
defp put_in_creating(list, [key | rest], value) when is_list(list) do
List.update_at(list, String.to_integer(key), &put_in_creating(&1, rest, value))
end
defp put_in_creating(map, [key | rest], value) do
map
|> Kernel.||(%{})
|> Map.put_new(key, %{})
|> Map.update!(key, &put_in_creating(&1, rest, value))
end
defp remove_value(form, field, index) do
current_value =
form
|> AshPhoenix.Form.value(String.to_existing_atom(field))
|> case do
map when is_map(map) ->
map
list ->
list
|> List.wrap()
|> Enum.with_index()
|> Map.new(fn {value, index} ->
{to_string(index), value}
end)
end
new_value = Map.delete(current_value, index)
new_value =
if new_value == %{} do
nil
else
new_value
end
new_params = Map.put(form.params, field, new_value)
AshPhoenix.Form.validate(form, new_params)
end
defp all_errors(form) do
form
|> AshPhoenix.Form.errors(for_path: :all)
|> Enum.flat_map(fn {path, errors} ->
Enum.map(errors, fn {field, message} ->
path = List.wrap(path)
case Enum.reject(path ++ List.wrap(field), &is_nil/1) do
[] ->
{nil, message}
items ->
{Enum.join(items, "."), message}
end
end)
end)
end
end