mirror of
https://github.com/ash-project/ash_phoenix.git
synced 2024-09-19 06:42:47 +12:00
improvement: first edition of auto forms
This commit is contained in:
parent
1eee011e26
commit
defbf581dc
11 changed files with 1222 additions and 1994 deletions
|
@ -1,89 +1,23 @@
|
|||
defmodule AshPhoenix do
|
||||
@moduledoc """
|
||||
Various helpers and utilities for working with Ash changesets and queries and phoenix.
|
||||
General helpers for AshPhoenix.
|
||||
|
||||
These will be deprecated at some point, once the work on `AshPhoenix.Form` is complete.
|
||||
"""
|
||||
import AshPhoenix.FormData.Helpers
|
||||
|
||||
@doc false
|
||||
def to_form_error(exception) when is_exception(exception) do
|
||||
case AshPhoenix.FormData.Error.to_form_error(exception) do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
{field, message} ->
|
||||
{field, message, []}
|
||||
|
||||
{field, message, vars} ->
|
||||
{field, message, vars}
|
||||
|
||||
list when is_list(list) ->
|
||||
Enum.map(list, fn item ->
|
||||
case item do
|
||||
{field, message} ->
|
||||
{field, message, []}
|
||||
|
||||
{field, message, vars} ->
|
||||
{field, message, vars}
|
||||
end
|
||||
end)
|
||||
end
|
||||
def hide_errors(%Ash.Changeset{} = changeset) do
|
||||
Ash.Changeset.put_context(changeset, :private, %{ash_phoenix: %{hide_errors: true}})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Allows for manually transforming errors to modify or enable error messages in the form.
|
||||
|
||||
By default, only errors that implement the `AshPhoenix.FormData.Error` protocol will show
|
||||
their errors in forms. This is to protect you from showing strange errors to the user. Using
|
||||
this function, you can intercept those errors (as well as ones that *do* implement the protocol)
|
||||
and return custom form-ready messages for them.
|
||||
|
||||
Example:
|
||||
|
||||
```elixir
|
||||
AshPhoenix.transform_errors(changeset, fn changeset, %MyApp.CustomError{message: message} ->
|
||||
{:id, "Something went wrong while doing the %{thing}", [thing: "request"]}
|
||||
end)
|
||||
|
||||
# Could potentially be used for translation, although not quite ergonomic yet
|
||||
defp translate_error(key, msg, vars) do
|
||||
if vars[:count] do
|
||||
Gettext.dngettext(MyApp.Gettext, "errors", msg, msg, count, opts)
|
||||
else
|
||||
Gettext.dgettext(MyApp.Gettext, "errors", msg, opts)
|
||||
end
|
||||
def hide_errors(%Ash.Query{} = query) do
|
||||
Ash.Query.put_context(query, :private, %{ash_phoenix: %{hide_errors: true}})
|
||||
end
|
||||
|
||||
AshPhoenix.transform_errors(changeset, fn
|
||||
changeset, %MyApp.CustomError{message: message, field: field} ->
|
||||
translate_error(field, message, [foo: :bar])
|
||||
|
||||
changeset, any_error ->
|
||||
if AshPhoenix.FormData.Error.impl_for(any_error) do
|
||||
any_error
|
||||
|> AshPhoenix.FormData.error.to_form_error()
|
||||
|> List.wrap()
|
||||
|> Enum.map(fn {key, msg, vars} ->
|
||||
translate_error(key, msg, vars)
|
||||
end)
|
||||
end
|
||||
end)
|
||||
```
|
||||
"""
|
||||
@spec transform_errors(
|
||||
Ash.Changeset.t() | Ash.Query.t(),
|
||||
(Ash.Query.t() | Ash.Changeset.t(), error :: Ash.Error.t() ->
|
||||
[{field :: atom, message :: String.t(), substituations :: Keyword.t()}])
|
||||
) :: Ash.Query.t() | Ash.Changeset.t()
|
||||
def transform_errors(%Ash.Changeset{} = changeset, transform_errors) do
|
||||
Ash.Changeset.put_context(changeset, :private, %{
|
||||
ash_phoenix: %{transform_errors: transform_errors}
|
||||
})
|
||||
def hiding_errors?(%Ash.Changeset{} = changeset) do
|
||||
changeset.context[:private][:ash_phoenix][:hide_errors] == true
|
||||
end
|
||||
|
||||
def transform_errors(%Ash.Query{} = changeset, transform_errors) do
|
||||
Ash.Query.put_context(changeset, :private, %{
|
||||
ash_phoenix: %{transform_errors: transform_errors}
|
||||
})
|
||||
def hiding_errors?(%Ash.Query{} = query) do
|
||||
query.context[:private][:ash_phoenix][:hide_errors] == true
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -96,11 +30,11 @@ defmodule AshPhoenix do
|
|||
[{atom, {String.t(), Keyword.t()}}] | [String.t()] | map
|
||||
def errors_for(changeset_or_query, opts \\ []) do
|
||||
errors =
|
||||
if hiding_errors?(changeset_or_query) do
|
||||
if AshPhoenix.hiding_errors?(changeset_or_query) do
|
||||
[]
|
||||
else
|
||||
changeset_or_query.errors
|
||||
|> Enum.flat_map(&transform_error(changeset_or_query, &1))
|
||||
|> Enum.flat_map(&AshPhoenix.FormData.Helpers.transform_error(changeset_or_query, &1))
|
||||
|> Enum.filter(fn
|
||||
error when is_exception(error) ->
|
||||
AshPhoenix.FormData.Error.impl_for(error)
|
||||
|
@ -157,867 +91,4 @@ defmodule AshPhoenix do
|
|||
String.replace(acc, "%{#{key}}", to_string(value))
|
||||
end)
|
||||
end
|
||||
|
||||
defp transform_error(_query, {_key, _value, _vars} = error), do: error
|
||||
|
||||
defp transform_error(query, error) do
|
||||
case query.context[:private][:ash_phoenix][:transform_errors] do
|
||||
transformer when is_function(transformer, 2) ->
|
||||
case transformer.(query, error) do
|
||||
error when is_exception(error) ->
|
||||
if AshPhoenix.FormData.Error.impl_for(error) do
|
||||
List.wrap(AshPhoenix.to_form_error(error))
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
{key, value, vars} ->
|
||||
[{key, value, vars}]
|
||||
|
||||
list when is_list(list) ->
|
||||
Enum.flat_map(list, fn
|
||||
error when is_exception(error) ->
|
||||
if AshPhoenix.FormData.Error.impl_for(error) do
|
||||
List.wrap(AshPhoenix.to_form_error(error))
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
{key, value, vars} ->
|
||||
[{key, value, vars}]
|
||||
end)
|
||||
end
|
||||
|
||||
nil ->
|
||||
if AshPhoenix.FormData.Error.impl_for(error) do
|
||||
List.wrap(AshPhoenix.to_form_error(error))
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def hide_errors(%Ash.Changeset{} = changeset) do
|
||||
Ash.Changeset.put_context(changeset, :private, %{ash_phoenix: %{hide_errors: true}})
|
||||
end
|
||||
|
||||
def hide_errors(%Ash.Query{} = query) do
|
||||
Ash.Query.put_context(query, :private, %{ash_phoenix: %{hide_errors: true}})
|
||||
end
|
||||
|
||||
def hiding_errors?(%Ash.Changeset{} = changeset) do
|
||||
changeset.context[:private][:ash_phoenix][:hide_errors] == true
|
||||
end
|
||||
|
||||
def hiding_errors?(%Ash.Query{} = query) do
|
||||
query.context[:private][:ash_phoenix][:hide_errors] == true
|
||||
end
|
||||
|
||||
def set_up_relationship_updates(changeset, relationships) when is_list(relationships) do
|
||||
Enum.reduce(relationships, changeset, &set_up_relationship_updates(&2, &1))
|
||||
end
|
||||
|
||||
@add_value_opts [
|
||||
add: [
|
||||
type: :any,
|
||||
doc: "the value to add to the list",
|
||||
default: nil
|
||||
]
|
||||
]
|
||||
|
||||
@doc """
|
||||
A utility to support "add" buttons on list attributes and arguments used in forms.
|
||||
|
||||
To use, simply pass in the form name of the attribute/argument form as well as the name of the primary/outer form.
|
||||
|
||||
```elixir
|
||||
# In your template, inside a form called `:change`
|
||||
<button phx-click="append_thing" phx-value-path={{form.path <> "[attribute_name]"}}>
|
||||
</button>
|
||||
|
||||
# In the view/component
|
||||
|
||||
def handle_event("append_thing", %{"path" => path}, socket) do
|
||||
changeset = add_value(socket.assigns.changeset, path, "change")
|
||||
{:noreply, assign(socket, changeset: changeset)}
|
||||
end
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
#{Ash.OptionsHelpers.docs(@add_value_opts)}
|
||||
"""
|
||||
@spec add_value(Ash.Changeset.t(), String.t(), String.t(), Keyword.t()) :: Ash.Changeset.t()
|
||||
def add_value(changeset, original_path, outer_form_name, opts \\ []) do
|
||||
opts = Ash.OptionsHelpers.validate!(opts, @add_value_opts)
|
||||
add = opts[:add]
|
||||
|
||||
[^outer_form_name, key | path] = decode_path(original_path)
|
||||
|
||||
if match?({x, y} when not is_nil(x) and not is_nil(y), argument_and_manages(changeset, key)) do
|
||||
add_related(changeset, original_path, outer_form_name, add: add)
|
||||
else
|
||||
attribute_or_argument =
|
||||
Ash.Resource.Info.attribute(changeset.resource, key) ||
|
||||
find_argument(changeset, key)
|
||||
|
||||
if attribute_or_argument do
|
||||
value =
|
||||
case attribute_or_argument do
|
||||
%Ash.Resource.Actions.Argument{} = argument ->
|
||||
changeset
|
||||
|> Ash.Changeset.get_argument(argument.name)
|
||||
|
||||
attribute ->
|
||||
changeset
|
||||
|> Ash.Changeset.get_attribute(attribute.name)
|
||||
end
|
||||
|
||||
new_value = add_to_path(value, path, add)
|
||||
|
||||
case attribute_or_argument do
|
||||
%Ash.Resource.Actions.Argument{} = argument ->
|
||||
Ash.Changeset.set_argument(changeset, argument.name, new_value)
|
||||
|
||||
attribute ->
|
||||
Ash.Changeset.change_attribute(changeset, attribute.name, new_value)
|
||||
end
|
||||
else
|
||||
changeset
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
A utility to support "remove" buttons on list attributes and arguments used in forms.
|
||||
"""
|
||||
@spec remove_value(Ash.Changeset.t(), String.t(), String.t()) :: Ash.Changeset.t()
|
||||
def remove_value(changeset, original_path, outer_form_name) do
|
||||
[^outer_form_name, key | path] = decode_path(original_path)
|
||||
|
||||
attribute_or_argument =
|
||||
Ash.Resource.Info.attribute(changeset.resource, key) ||
|
||||
find_argument(changeset, key)
|
||||
|
||||
if match?({x, y} when not is_nil(x) and not is_nil(y), argument_and_manages(changeset, key)) do
|
||||
{_, changeset} = remove_related(changeset, original_path, outer_form_name)
|
||||
changeset
|
||||
else
|
||||
if attribute_or_argument do
|
||||
value =
|
||||
case attribute_or_argument do
|
||||
%Ash.Resource.Actions.Argument{} = argument ->
|
||||
changeset
|
||||
|> Ash.Changeset.get_argument(argument.name)
|
||||
|> List.wrap()
|
||||
|
||||
attribute ->
|
||||
changeset
|
||||
|> Ash.Changeset.get_attribute(attribute.name)
|
||||
|> List.wrap()
|
||||
end
|
||||
|
||||
new_value = remove_from_path(value, path)
|
||||
|
||||
case attribute_or_argument do
|
||||
%Ash.Resource.Actions.Argument{} = argument ->
|
||||
Ash.Changeset.set_argument(changeset, argument.name, new_value)
|
||||
|
||||
attribute ->
|
||||
Ash.Changeset.change_attribute(changeset, attribute.name, new_value)
|
||||
end
|
||||
else
|
||||
changeset
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp find_argument(changeset, key) do
|
||||
changeset.action && Enum.find(changeset.action.arguments, &(to_string(&1.name) == key))
|
||||
end
|
||||
|
||||
@add_related_opts [
|
||||
use_data?: [
|
||||
type: :boolean,
|
||||
doc: "Provide this if you are using `use_data?` with `inputs_for` for this relationship",
|
||||
default: false
|
||||
],
|
||||
add: [
|
||||
type: :any,
|
||||
doc: "the value to add to the relationship",
|
||||
default: %{}
|
||||
],
|
||||
relationship: [
|
||||
type: :atom,
|
||||
doc: "The relationship being updated, in case it can't be determined from the path"
|
||||
],
|
||||
id: [
|
||||
type: :any,
|
||||
doc:
|
||||
"The value that should be in `meta[:id]` in the manage changeset opts. Defaults to the relationship name. This only needs to be set if an id is also provided for `inputs_for`."
|
||||
]
|
||||
]
|
||||
|
||||
@doc """
|
||||
A utility to support "add" buttons on relationships used in forms.
|
||||
|
||||
To use, simply pass in the form name of the relationship form as well as the name of the primary/outer form.
|
||||
|
||||
```elixir
|
||||
# In your template, inside a form called `:change`
|
||||
<button phx-click="append_thing" phx-value-path={{form.path}}>
|
||||
</button>
|
||||
|
||||
# In the view/component
|
||||
|
||||
def handle_event("append_thing", %{"path" => path}, socket) do
|
||||
changeset = add_related(socket.assigns.changeset, path, "change")
|
||||
{:noreply, assign(socket, changeset: changeset)}
|
||||
end
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
#{Ash.OptionsHelpers.docs(@add_related_opts)}
|
||||
"""
|
||||
@spec add_related(Ash.Changeset.t(), String.t(), String.t(), Keyword.t()) :: Ash.Changeset.t()
|
||||
def add_related(changeset, original_path, outer_form_name, opts \\ []) do
|
||||
opts = Ash.OptionsHelpers.validate!(opts, @add_related_opts)
|
||||
add = Keyword.get(opts, :add, %{})
|
||||
|
||||
[^outer_form_name, key | path] =
|
||||
if is_list(original_path) do
|
||||
original_path
|
||||
else
|
||||
decode_path(original_path)
|
||||
end
|
||||
|
||||
{argument, argument_manages} = argument_and_manages(changeset, key)
|
||||
|
||||
changeset.resource
|
||||
|> Ash.Resource.Info.relationships()
|
||||
|> Enum.find_value(fn relationship ->
|
||||
if relationship.name == opts[:relationship] || to_string(relationship.name) == key ||
|
||||
relationship.name == argument_manages do
|
||||
manage = changeset.relationships[relationship.name] || []
|
||||
|
||||
to_manage =
|
||||
Enum.find_index(manage, fn {_manage, opts} ->
|
||||
opts[:id] == opts[:key] || opts[:id] == opts[:relationship] ||
|
||||
(argument && opts[:id] == argument.name)
|
||||
end)
|
||||
|
||||
{relationship.name,
|
||||
opts[:id] || opts[:relationship] || (argument && argument.name) || relationship.name,
|
||||
to_manage}
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
nil ->
|
||||
changeset
|
||||
|
||||
{rel, id, nil} ->
|
||||
new_relationships =
|
||||
changeset.relationships
|
||||
|> Map.put_new(rel, [])
|
||||
|> Map.update!(rel, fn manages ->
|
||||
to_add =
|
||||
if opts[:use_data?] && loaded?(changeset.data, rel) do
|
||||
changeset.data
|
||||
|> Map.get(rel)
|
||||
|> List.wrap()
|
||||
|> Enum.reduce(nil, fn _, acc ->
|
||||
add_to_path(acc, path, %{})
|
||||
end)
|
||||
|> add_to_path(path, add)
|
||||
else
|
||||
add_to_path(nil, path, add)
|
||||
end
|
||||
|
||||
manages ++ [{to_add, [meta: [id: id]]}]
|
||||
end)
|
||||
|
||||
%{changeset | relationships: new_relationships}
|
||||
|
||||
{rel, _id, index} ->
|
||||
new_relationships =
|
||||
changeset.relationships
|
||||
|> Map.put_new(rel, [])
|
||||
|> Map.update!(rel, fn manages ->
|
||||
List.update_at(manages, index, fn {manage, opts} ->
|
||||
{add_to_path(manage, path, add), opts}
|
||||
end)
|
||||
end)
|
||||
|
||||
%{changeset | relationships: new_relationships}
|
||||
end
|
||||
end
|
||||
|
||||
defp loaded?(data, key) do
|
||||
case Map.get(data, key) do
|
||||
%Ash.NotLoaded{} -> false
|
||||
_ -> true
|
||||
end
|
||||
end
|
||||
|
||||
@remove_related_opts [
|
||||
add: [
|
||||
type: :any,
|
||||
doc: "the value to add to the relationship, defaults to `%{}`",
|
||||
default: %{}
|
||||
],
|
||||
relationship: [
|
||||
type: :atom,
|
||||
doc: "The relationship being updated, in case it can't be determined from the path"
|
||||
],
|
||||
id: [
|
||||
type: :any,
|
||||
doc:
|
||||
"The value that should be in `meta[:id]` in the manage changeset opts. Defaults to the relationship name. This only needs to be set if an id is also provided for `inputs_for`."
|
||||
]
|
||||
]
|
||||
@doc """
|
||||
A utility to support "remove" buttons on relationships used in forms.
|
||||
|
||||
To use, simply pass in the form name of the related form as well as the name of the primary/outer form.
|
||||
|
||||
```elixir
|
||||
# In your template, inside a form called `:change`
|
||||
<button phx-click="remove_thing" phx-value-path={{form.path}}>
|
||||
</button>
|
||||
|
||||
# In the view/component
|
||||
|
||||
def handle_event("remove_thing", %{"path" => path}, socket) do
|
||||
{record, changeset} = remove_related(socket.assigns.changeset, path, "change")
|
||||
{:noreply, assign(socket, record: record, changeset: changeset)}
|
||||
end
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
#{Ash.OptionsHelpers.docs(@remove_related_opts)}
|
||||
"""
|
||||
|
||||
@spec remove_related(Ash.Changeset.t(), String.t(), String.t(), Keyword.t()) ::
|
||||
{Ash.Resource.record(), Ash.Changeset.t()}
|
||||
def remove_related(changeset, path, outer_form_name, opts \\ []) do
|
||||
[^outer_form_name, key | path] = decode_path(path)
|
||||
|
||||
{argument, argument_manages} = argument_and_manages(changeset, key)
|
||||
|
||||
changeset.resource
|
||||
|> Ash.Resource.Info.relationships()
|
||||
|> Enum.find_value(fn relationship ->
|
||||
if relationship.name == opts[:relationship] || to_string(relationship.name) == key ||
|
||||
relationship.name == argument_manages do
|
||||
manage = changeset.relationships[relationship.name] || []
|
||||
|
||||
to_manage =
|
||||
Enum.find_index(manage, fn {_manage, opts} ->
|
||||
opts[:id] == opts[:key] || opts[:id] == opts[:relationship] ||
|
||||
(argument && opts[:id] == argument.name)
|
||||
end)
|
||||
|
||||
{relationship.name, to_manage}
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
nil ->
|
||||
{changeset.data, changeset}
|
||||
|
||||
{rel, index} ->
|
||||
{changeset, index} =
|
||||
if index == nil do
|
||||
{%{changeset | relationships: Map.put(changeset.relationships, rel, [])}, 0}
|
||||
else
|
||||
{changeset, index}
|
||||
end
|
||||
|
||||
new_relationships =
|
||||
changeset.relationships
|
||||
|> Map.put_new(rel, [])
|
||||
|> Map.update!(rel, fn manages ->
|
||||
if path == [] do
|
||||
List.delete_at(manages, index)
|
||||
else
|
||||
List.update_at(manages, index, fn {manage, opts} ->
|
||||
{remove_from_path(manage, path), opts}
|
||||
end)
|
||||
end
|
||||
end)
|
||||
|
||||
new_value =
|
||||
cond do
|
||||
path == [] ->
|
||||
nil
|
||||
|
||||
is_nil(changeset.relationships[rel]) ->
|
||||
nil
|
||||
|
||||
true ->
|
||||
case Enum.at(changeset.relationships[rel], index) do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
{value, _opts} ->
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
changeset = %{changeset | relationships: new_relationships}
|
||||
|
||||
{new_data, new_value} =
|
||||
if changeset.action_type in [:destroy, :update] do
|
||||
case Map.get(changeset.data, rel) do
|
||||
%Ash.NotLoaded{} ->
|
||||
{[], new_value}
|
||||
|
||||
value ->
|
||||
cond do
|
||||
path == [] and is_list(value) ->
|
||||
{Map.update!(changeset.data, rel, fn related ->
|
||||
Enum.map(related, &hide/1)
|
||||
end), []}
|
||||
|
||||
match?([i | _] when is_integer(i), path) and is_list(value) ->
|
||||
[i | _] = path
|
||||
new_value = hide_at_not_hidden(Map.get(changeset.data, rel), i)
|
||||
|
||||
{Map.put(changeset.data, rel, new_value), new_value}
|
||||
|
||||
path == [] || match?([i] when is_integer(i), path) ->
|
||||
{Map.update!(changeset.data, rel, &hide/1), nil}
|
||||
|
||||
true ->
|
||||
{changeset.data, new_value}
|
||||
end
|
||||
end
|
||||
else
|
||||
{changeset.data, []}
|
||||
end
|
||||
|
||||
changeset = mark_removed(changeset, new_value, (argument && argument.name) || rel.name)
|
||||
|
||||
{new_data, %{changeset | data: new_data}}
|
||||
end
|
||||
end
|
||||
|
||||
defp hide_at_not_hidden(values, i) do
|
||||
values
|
||||
|> Enum.reduce({0, []}, fn value, {counter, acc} ->
|
||||
if hidden?(value) do
|
||||
{counter, [value | acc]}
|
||||
else
|
||||
if counter == i do
|
||||
{counter + 1, [hide(value) | acc]}
|
||||
else
|
||||
{counter + 1, [value | acc]}
|
||||
end
|
||||
end
|
||||
end)
|
||||
|> elem(1)
|
||||
|> Enum.reverse()
|
||||
end
|
||||
|
||||
@doc """
|
||||
A utility to support "add" buttons on embedded types used in forms.
|
||||
|
||||
To use, simply pass in the form name of the embedded form as well as the name of the primary/outer form.
|
||||
|
||||
```elixir
|
||||
# In your template, inside a form called `:change`
|
||||
<button phx-click="append_thing" phx-value-path={{form.path}}>
|
||||
</button>
|
||||
|
||||
# In the view/component
|
||||
|
||||
def handle_event("append_thing", %{"path" => path}, socket) do
|
||||
changeset = add_embed(socket.assigns.changeset, path, "change")
|
||||
{:noreply, assign(socket, changeset: changeset)}
|
||||
end
|
||||
```
|
||||
|
||||
You can also pass a specific value to be added, to seed the changes in a customized way.
|
||||
By default, `%{}` is used.
|
||||
"""
|
||||
def add_embed(query, path, outer_form_name, add \\ %{})
|
||||
|
||||
def add_embed(%Ash.Changeset{} = changeset, original_path, outer_form_name, add) do
|
||||
[^outer_form_name, key | path] = decode_path(original_path)
|
||||
|
||||
cond do
|
||||
match?({x, y} when not is_nil(x) and not is_nil(y), argument_and_manages(changeset, key)) ->
|
||||
add_related(changeset, original_path, outer_form_name, add: add)
|
||||
|
||||
attr = Ash.Resource.Info.attribute(changeset.resource, key) ->
|
||||
current_value = Ash.Changeset.get_attribute(changeset, attr.name)
|
||||
|
||||
new_value = add_to_path(current_value, path, add)
|
||||
|
||||
new_value =
|
||||
case attr.type do
|
||||
{:array, _} -> List.wrap(new_value)
|
||||
_ -> new_value
|
||||
end
|
||||
|
||||
changeset
|
||||
|> Ash.Changeset.change_attribute(attr.name, new_value)
|
||||
|> mark_removed(new_value, attr.name)
|
||||
|
||||
arg = Enum.find(changeset.action.arguments, &(&1.name == key || to_string(&1.name) == key)) ->
|
||||
current_value = Ash.Changeset.get_argument(changeset, arg.name)
|
||||
|
||||
new_value = add_to_path(current_value, path, add)
|
||||
|
||||
new_value =
|
||||
case arg.type do
|
||||
{:array, _} -> List.wrap(new_value)
|
||||
_ -> new_value
|
||||
end
|
||||
|
||||
changeset
|
||||
|> Ash.Changeset.set_argument(arg.name, new_value)
|
||||
|> mark_removed(new_value, arg.name)
|
||||
|
||||
true ->
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
def add_embed(%Ash.Query{} = query, path, outer_form_name, add) do
|
||||
[^outer_form_name, key | path] = decode_path(path)
|
||||
arg = Enum.find(query.action.arguments, &(&1.name == key || to_string(&1.name) == key))
|
||||
|
||||
if arg do
|
||||
current_value = Ash.Query.get_argument(query, arg.name)
|
||||
|
||||
new_value = add_to_path(current_value, path, add)
|
||||
|
||||
new_value =
|
||||
case arg.type do
|
||||
{:array, _} -> List.wrap(new_value)
|
||||
_ -> new_value
|
||||
end
|
||||
|
||||
query
|
||||
|> Ash.Changeset.set_argument(arg.name, new_value)
|
||||
|> mark_removed(new_value, arg.name)
|
||||
else
|
||||
query
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
A utility to support "remove" buttons on embedded types used in forms.
|
||||
|
||||
To use, simply pass in the form name of the embedded form as well as the name of the primary/outer form.
|
||||
|
||||
```elixir
|
||||
# In your template, inside a form called `:change`
|
||||
<button phx-click="remove_thing" phx-value-path={{form.path}}>
|
||||
</button>
|
||||
|
||||
# In the view/component
|
||||
|
||||
def handle_event("remove_thing", %{"path" => path}, socket) do
|
||||
changeset = remove_embed(socket.assigns.changeset, path, "change")
|
||||
{:noreply, assign(socket, changeset: changeset)}
|
||||
end
|
||||
```
|
||||
"""
|
||||
def remove_embed(%Ash.Changeset{} = changeset, original_path, outer_form_name) do
|
||||
[^outer_form_name, key | path] = decode_path(original_path)
|
||||
|
||||
cond do
|
||||
match?({x, y} when not is_nil(x) and not is_nil(y), argument_and_manages(changeset, key)) ->
|
||||
{_, changeset} = remove_related(changeset, original_path, outer_form_name)
|
||||
changeset
|
||||
|
||||
attr = Ash.Resource.Info.attribute(changeset.resource, key) ->
|
||||
current_value = Ash.Changeset.get_attribute(changeset, attr.name)
|
||||
|
||||
new_value =
|
||||
if path == [] do
|
||||
nil
|
||||
else
|
||||
new_value = remove_from_path(current_value, path)
|
||||
|
||||
case attr.type do
|
||||
{:array, _} -> List.wrap(new_value)
|
||||
_ -> new_value
|
||||
end
|
||||
end
|
||||
|
||||
changeset
|
||||
|> Ash.Changeset.change_attribute(attr.name, new_value)
|
||||
|> mark_removed(new_value, attr.name)
|
||||
|
||||
arg = Enum.find(changeset.action.arguments, &(&1.name == key || to_string(&1.name) == key)) ->
|
||||
current_value = Ash.Changeset.get_argument(changeset, arg.name)
|
||||
|
||||
new_value =
|
||||
if path == [] do
|
||||
nil
|
||||
else
|
||||
new_value = remove_from_path(current_value, path)
|
||||
|
||||
case arg.type do
|
||||
{:array, _} -> List.wrap(new_value)
|
||||
_ -> new_value
|
||||
end
|
||||
end
|
||||
|
||||
changeset
|
||||
|> Ash.Changeset.set_argument(arg.name, new_value)
|
||||
|> mark_removed(new_value, arg.name)
|
||||
|
||||
true ->
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
def remove_embed(%Ash.Query{} = query, path, outer_form_name) do
|
||||
[^outer_form_name, key | path] = decode_path(path)
|
||||
arg = Enum.find(query.action.arguments, &(&1.name == key || to_string(&1.name) == key))
|
||||
|
||||
if arg do
|
||||
current_value = Ash.Query.get_argument(query, arg.name)
|
||||
|
||||
new_value = remove_from_path(current_value, path)
|
||||
|
||||
new_value =
|
||||
case arg.type do
|
||||
{:array, _} -> List.wrap(new_value)
|
||||
_ -> new_value
|
||||
end
|
||||
|
||||
query
|
||||
|> Ash.Changeset.set_argument(arg.name, new_value)
|
||||
|> mark_removed(new_value, arg.name)
|
||||
else
|
||||
query
|
||||
end
|
||||
end
|
||||
|
||||
defp mark_removed(%Ash.Query{} = query, value, name) do
|
||||
Ash.Query.put_context(query, :private, %{removed_keys: %{name => removed?(value)}})
|
||||
end
|
||||
|
||||
defp mark_removed(%Ash.Changeset{} = changeset, value, name) do
|
||||
Ash.Changeset.put_context(changeset, :private, %{
|
||||
removed_keys: %{name => removed?(value)}
|
||||
})
|
||||
end
|
||||
|
||||
defp removed?(nil), do: true
|
||||
defp removed?([]), do: true
|
||||
|
||||
defp removed?(other) do
|
||||
other
|
||||
|> List.wrap()
|
||||
|> Enum.all?(&hidden?/1)
|
||||
end
|
||||
|
||||
def add_to_path(nil, [], nil) do
|
||||
[nil]
|
||||
end
|
||||
|
||||
def add_to_path(nil, [], add) do
|
||||
add
|
||||
end
|
||||
|
||||
def add_to_path(value, [], nil) when is_list(value) do
|
||||
value ++ [nil]
|
||||
end
|
||||
|
||||
def add_to_path(value, [], add) when is_list(value) do
|
||||
value ++ List.wrap(add)
|
||||
end
|
||||
|
||||
def add_to_path(value, [], add) when is_map(value) do
|
||||
case last_index(value) do
|
||||
:error ->
|
||||
%{"0" => value, "1" => add}
|
||||
|
||||
{:ok, index} ->
|
||||
Map.put(value, index, add)
|
||||
end
|
||||
end
|
||||
|
||||
def add_to_path(value, [key | rest], add) when is_integer(key) and is_list(value) do
|
||||
List.update_at(value, key, &add_to_path(&1, rest, add))
|
||||
end
|
||||
|
||||
def add_to_path(empty, [key | rest], add) when is_integer(key) and empty in [nil, []] do
|
||||
[add_to_path(nil, rest, add)]
|
||||
end
|
||||
|
||||
def add_to_path(value, [0 | rest], add) when is_map(value) do
|
||||
add_to_path(value, rest, add)
|
||||
end
|
||||
|
||||
def add_to_path(value, [key | rest] = path, add) when is_integer(key) and is_map(value) do
|
||||
case last_index(value) do
|
||||
:error ->
|
||||
add_to_path(%{"0" => value}, path, add)
|
||||
|
||||
_ ->
|
||||
add_to_path(value, [to_string(key) | rest], add)
|
||||
end
|
||||
end
|
||||
|
||||
def add_to_path(value, [key | rest], add)
|
||||
when (is_binary(key) or is_atom(key)) and is_map(value) do
|
||||
cond do
|
||||
Map.has_key?(value, key) ->
|
||||
Map.update!(value, key, &add_to_path(&1, rest, add))
|
||||
|
||||
is_atom(key) && Map.has_key?(value, to_string(key)) ->
|
||||
Map.update!(value, to_string(key), &add_to_path(&1, rest, add))
|
||||
|
||||
is_binary(key) && Enum.any?(Map.keys(value), &(to_string(&1) == key)) ->
|
||||
Map.update!(value, String.to_existing_atom(key), &add_to_path(&1, rest, add))
|
||||
|
||||
true ->
|
||||
Map.put(value, key, add_to_path(nil, rest, add))
|
||||
end
|
||||
end
|
||||
|
||||
def add_to_path(nil, [key | rest], add) when is_binary(key) or is_atom(key) do
|
||||
%{key => add_to_path(nil, rest, add)}
|
||||
end
|
||||
|
||||
def add_to_path([item], [key | _] = path, add) when is_binary(key) do
|
||||
[add_to_path(item, path, add)]
|
||||
end
|
||||
|
||||
def add_to_path(_, _, add), do: add
|
||||
|
||||
defp last_index(map) do
|
||||
map
|
||||
|> Map.keys()
|
||||
|> Enum.map(&String.to_integer/1)
|
||||
|> max_plus_one()
|
||||
|> case do
|
||||
nil ->
|
||||
:error
|
||||
|
||||
value ->
|
||||
{:ok, to_string(value)}
|
||||
end
|
||||
rescue
|
||||
_ ->
|
||||
:error
|
||||
end
|
||||
|
||||
defp max_plus_one([]) do
|
||||
nil
|
||||
end
|
||||
|
||||
defp max_plus_one(list) do
|
||||
list
|
||||
|> Enum.max()
|
||||
|> Kernel.+(1)
|
||||
end
|
||||
|
||||
defp remove_from_path(value, [key]) when is_integer(key) and is_list(value) do
|
||||
List.delete_at(value, key)
|
||||
end
|
||||
|
||||
defp remove_from_path(value, [key]) when is_map(value) and (is_binary(key) or is_atom(key)) do
|
||||
cond do
|
||||
is_atom(key) ->
|
||||
if is_struct(value) do
|
||||
Map.put(value, key, nil)
|
||||
else
|
||||
Map.drop(value, [key, to_string(key)])
|
||||
end
|
||||
|
||||
is_binary(key) && Enum.any?(Map.keys(value), &(is_atom(&1) && to_string(&1) == key)) ->
|
||||
if is_struct(value) do
|
||||
Map.put(value, String.to_existing_atom(key), nil)
|
||||
else
|
||||
Map.drop(value, [key, String.to_existing_atom(key)])
|
||||
end
|
||||
|
||||
true ->
|
||||
Map.delete(value, key)
|
||||
end
|
||||
|> case do
|
||||
empty when empty == %{} ->
|
||||
nil
|
||||
|
||||
value ->
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
defp remove_from_path(value, [key | rest]) when is_list(value) and is_integer(key) do
|
||||
List.update_at(value, key, &remove_from_path(&1, rest))
|
||||
end
|
||||
|
||||
defp remove_from_path(value, [key | rest]) when is_integer(key) and is_map(value) do
|
||||
remove_from_path(value, [to_string(key) | rest])
|
||||
end
|
||||
|
||||
defp remove_from_path(value, [key | rest])
|
||||
when is_map(value) and (is_binary(key) or is_atom(key)) do
|
||||
cond do
|
||||
Map.has_key?(value, key) ->
|
||||
Map.update!(value, key, &remove_from_path(&1, rest))
|
||||
|
||||
is_atom(key) && Map.has_key?(value, to_string(key)) ->
|
||||
Map.update!(value, to_string(key), &remove_from_path(&1, rest))
|
||||
|
||||
is_binary(key) && Enum.any?(Map.keys(value), &(is_atom(&1) && to_string(&1) == key)) ->
|
||||
Map.update!(value, String.to_existing_atom(key), &remove_from_path(&1, rest))
|
||||
|
||||
true ->
|
||||
Map.put(value, key, remove_from_path(nil, rest))
|
||||
end
|
||||
end
|
||||
|
||||
defp remove_from_path([item], [key | _] = path) when is_binary(key) do
|
||||
[remove_from_path(item, path)]
|
||||
end
|
||||
|
||||
defp remove_from_path(value, _), do: value
|
||||
|
||||
@doc """
|
||||
A utility for decoding the path of a form into a list.
|
||||
|
||||
For example:
|
||||
|
||||
```elixir
|
||||
change[posts][0][comments][1]
|
||||
["change", "posts", 0, "comments", 1]
|
||||
```
|
||||
"""
|
||||
def decode_path(path) do
|
||||
path = Plug.Conn.Query.decode(path)
|
||||
do_decode_path(path)
|
||||
end
|
||||
|
||||
defp do_decode_path(path) when is_map(path) and path != %{} do
|
||||
path_part = Enum.at(path, 0)
|
||||
rest = do_decode_path(elem(path_part, 1))
|
||||
|
||||
path_part
|
||||
|> elem(0)
|
||||
|> Integer.parse()
|
||||
|> case do
|
||||
{integer, ""} ->
|
||||
[integer | rest]
|
||||
|
||||
_ ->
|
||||
[elem(path_part, 0) | rest]
|
||||
end
|
||||
end
|
||||
|
||||
defp do_decode_path(""), do: []
|
||||
|
||||
defp do_decode_path(other) do
|
||||
[other]
|
||||
end
|
||||
end
|
||||
|
|
393
lib/ash_phoenix/form/auto.ex
Normal file
393
lib/ash_phoenix/form/auto.ex
Normal file
|
@ -0,0 +1,393 @@
|
|||
defmodule AshPhoenix.Form.Auto do
|
||||
@moduledoc """
|
||||
An experimental tool to automatically generate available nested forms based on a resource and action.
|
||||
"""
|
||||
def auto(resource, action) do
|
||||
related(resource, action) ++ embedded(resource, action)
|
||||
end
|
||||
|
||||
def related(resource, action, cycle_preventer \\ nil) do
|
||||
action =
|
||||
if is_atom(action) do
|
||||
Ash.Resource.Info.action(resource, action)
|
||||
else
|
||||
action
|
||||
end
|
||||
|
||||
cycle_preventer = cycle_preventer || MapSet.new()
|
||||
|
||||
if MapSet.member?(cycle_preventer, [resource, action]) do
|
||||
[]
|
||||
else
|
||||
cycle_preventer = MapSet.put(cycle_preventer, [resource, action])
|
||||
|
||||
action.arguments
|
||||
|> Enum.reject(& &1.private?)
|
||||
|> Enum.flat_map(fn arg ->
|
||||
case find_manage_change(arg, action) do
|
||||
nil ->
|
||||
[]
|
||||
|
||||
manage_opts ->
|
||||
[{arg, manage_opts}]
|
||||
end
|
||||
end)
|
||||
|> Enum.map(fn {arg, manage_opts} ->
|
||||
relationship = Ash.Resource.Info.relationship(resource, manage_opts[:relationship])
|
||||
|
||||
defaults =
|
||||
if manage_opts[:opts][:type] do
|
||||
Ash.Changeset.manage_relationship_opts(manage_opts[:opts][:type])
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
manage_opts =
|
||||
Ash.Changeset.ManagedRelationshipHelpers.sanitize_opts(
|
||||
relationship,
|
||||
Keyword.merge(defaults, manage_opts)
|
||||
)
|
||||
|
||||
opts = [
|
||||
type: cardinality_to_type(relationship.cardinality),
|
||||
data: relationship_fetcher(relationship),
|
||||
forms: [],
|
||||
updater: fn opts ->
|
||||
opts
|
||||
|> add_create_action(manage_opts, relationship, cycle_preventer)
|
||||
|> add_read_action(manage_opts, relationship, cycle_preventer)
|
||||
|> add_update_action(manage_opts, relationship, cycle_preventer)
|
||||
|> add_nested_forms()
|
||||
end
|
||||
]
|
||||
|
||||
{arg.name, opts}
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defp add_nested_forms(opts) do
|
||||
Keyword.update!(opts, :forms, fn forms ->
|
||||
forms =
|
||||
if forms[:update_action] do
|
||||
forms ++ set_for_type(auto(opts[:resource], opts[:update_action]), :update)
|
||||
else
|
||||
forms
|
||||
end
|
||||
|
||||
forms =
|
||||
if forms[:create_action] do
|
||||
forms ++ set_for_type(auto(opts[:resource], opts[:create_action]), :create)
|
||||
else
|
||||
forms
|
||||
end
|
||||
|
||||
forms =
|
||||
if forms[:destroy_action] do
|
||||
forms ++ set_for_type(auto(opts[:resource], opts[:destroy_action]), :destroy)
|
||||
else
|
||||
forms
|
||||
end
|
||||
|
||||
if forms[:read_action] do
|
||||
forms ++ set_for_type(auto(opts[:resource], opts[:read_action]), :read)
|
||||
else
|
||||
forms
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp set_for_type(forms, type) do
|
||||
Enum.map(forms, fn {key, value} ->
|
||||
{key, Keyword.put(value, :for_type, type)}
|
||||
end)
|
||||
end
|
||||
|
||||
defp add_read_action(opts, manage_opts, relationship, cycle_preventer) do
|
||||
manage_opts
|
||||
|> Ash.Changeset.ManagedRelationshipHelpers.on_lookup_update_action(relationship)
|
||||
|> List.wrap()
|
||||
|> Enum.sort_by(&(elem(&1, 0) == :join))
|
||||
|> case do
|
||||
[] ->
|
||||
opts
|
||||
|
||||
[{source_dest_or_join, action_name} | rest] ->
|
||||
resource =
|
||||
case source_dest_or_join do
|
||||
:source ->
|
||||
relationship.source
|
||||
|
||||
:destination ->
|
||||
relationship.destination
|
||||
|
||||
:join ->
|
||||
relationship.through
|
||||
end
|
||||
|
||||
opts
|
||||
|> Keyword.put(:read_resource, resource)
|
||||
|> Keyword.put(:read_action, action_name)
|
||||
|> Keyword.update!(
|
||||
:forms,
|
||||
&(&1 ++
|
||||
related(resource, action_name, cycle_preventer))
|
||||
)
|
||||
|> add_join_form(relationship, rest)
|
||||
end
|
||||
end
|
||||
|
||||
defp add_create_action(opts, manage_opts, relationship, cycle_preventer) do
|
||||
manage_opts
|
||||
|> Ash.Changeset.ManagedRelationshipHelpers.on_no_match_destination_actions(relationship)
|
||||
|> List.wrap()
|
||||
|> Enum.sort_by(&(elem(&1, 0) == :join))
|
||||
|> case do
|
||||
[] ->
|
||||
opts
|
||||
|
||||
[{source_dest_or_join, action_name} | rest] ->
|
||||
resource =
|
||||
case source_dest_or_join do
|
||||
:source ->
|
||||
relationship.source
|
||||
|
||||
:destination ->
|
||||
relationship.destination
|
||||
|
||||
:join ->
|
||||
relationship.through
|
||||
end
|
||||
|
||||
opts
|
||||
|> Keyword.put(:create_resource, resource)
|
||||
|> Keyword.put(:create_action, action_name)
|
||||
|> Keyword.update!(
|
||||
:forms,
|
||||
&(&1 ++
|
||||
related(resource, action_name, cycle_preventer))
|
||||
)
|
||||
|> add_join_form(relationship, rest)
|
||||
end
|
||||
end
|
||||
|
||||
defp add_update_action(opts, manage_opts, relationship, cycle_preventer) do
|
||||
manage_opts
|
||||
|> Ash.Changeset.ManagedRelationshipHelpers.on_match_destination_actions(relationship)
|
||||
|> List.wrap()
|
||||
|> Enum.sort_by(&(elem(&1, 0) == :join))
|
||||
|> case do
|
||||
[] ->
|
||||
opts
|
||||
|
||||
[{source_dest_or_join, action_name} | rest] ->
|
||||
resource =
|
||||
case source_dest_or_join do
|
||||
:source ->
|
||||
relationship.source
|
||||
|
||||
:destination ->
|
||||
relationship.destination
|
||||
|
||||
:join ->
|
||||
relationship.through
|
||||
end
|
||||
|
||||
opts
|
||||
|> Keyword.put(:update_resource, resource)
|
||||
|> Keyword.put(:update_action, action_name)
|
||||
|> Keyword.update!(
|
||||
:forms,
|
||||
&(&1 ++
|
||||
related(resource, action_name, cycle_preventer))
|
||||
)
|
||||
|> add_join_form(relationship, rest)
|
||||
end
|
||||
end
|
||||
|
||||
defp add_join_form(opts, _relationship, []), do: opts
|
||||
|
||||
defp add_join_form(opts, relationship, [{:join, action, _}]) do
|
||||
action = Ash.Resource.Info.action(relationship.through, action)
|
||||
|
||||
case action.type do
|
||||
:update ->
|
||||
Keyword.update!(opts, :forms, fn forms ->
|
||||
Keyword.put(forms, :_join,
|
||||
resource: relationship.through,
|
||||
data: &get_join(&1, &2, relationship),
|
||||
update_action: action.name
|
||||
)
|
||||
end)
|
||||
|
||||
:create ->
|
||||
Keyword.update!(opts, :forms, fn forms ->
|
||||
Keyword.put(forms, :_join,
|
||||
resource: relationship.through,
|
||||
data: &get_join(&1, &2, relationship),
|
||||
create_action: action.name
|
||||
)
|
||||
end)
|
||||
|
||||
:destroy ->
|
||||
Keyword.update!(opts, :forms, fn forms ->
|
||||
Keyword.put(forms, :_join,
|
||||
resource: relationship.through,
|
||||
data: &get_join(&1, &2, relationship),
|
||||
create_action: action.name,
|
||||
merge?: true
|
||||
)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_join(parent, prev_path, relationship) do
|
||||
case Enum.find(prev_path, &(&1.__struct__ == relationship.source)) do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
root ->
|
||||
case Map.get(root, relationship.join_relationship) do
|
||||
value when is_list(value) ->
|
||||
Enum.find(value, fn join ->
|
||||
Map.get(join, relationship.destination_field_on_join_table) ==
|
||||
Map.get(parent, relationship.destination_field)
|
||||
end)
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp relationship_fetcher(relationship) do
|
||||
fn parent ->
|
||||
case Map.get(parent, relationship.name) do
|
||||
%Ash.NotLoaded{} ->
|
||||
if relationship.cardinality == :many do
|
||||
[]
|
||||
end
|
||||
|
||||
value ->
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp cardinality_to_type(:many), do: :list
|
||||
defp cardinality_to_type(:one), do: :single
|
||||
|
||||
def embedded(resource, action, cycle_preventer \\ nil) do
|
||||
action =
|
||||
if is_atom(action) do
|
||||
Ash.Resource.Info.action(resource, action)
|
||||
else
|
||||
action
|
||||
end
|
||||
|
||||
cycle_preventer = cycle_preventer || MapSet.new()
|
||||
|
||||
if MapSet.member?(cycle_preventer, [resource, action]) do
|
||||
[]
|
||||
else
|
||||
cycle_preventer = MapSet.put(cycle_preventer, [resource, action])
|
||||
|
||||
resource
|
||||
|> accepted_attributes(action)
|
||||
|> Enum.concat(action.arguments)
|
||||
|> Enum.filter(&Ash.Type.embedded_type?(&1.type))
|
||||
|> Enum.reject(&match?({:array, {:array, _}}, &1.type))
|
||||
|> Enum.map(fn attr ->
|
||||
type =
|
||||
case attr.type do
|
||||
{:array, _} ->
|
||||
:list
|
||||
|
||||
_ ->
|
||||
:single
|
||||
end
|
||||
|
||||
embed = unwrap_type(attr.type)
|
||||
|
||||
data =
|
||||
case type do
|
||||
:list ->
|
||||
fn parent ->
|
||||
if parent do
|
||||
Map.get(parent, attr.name) || []
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
:single ->
|
||||
fn parent ->
|
||||
if parent do
|
||||
Map.get(parent, attr.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
create_action =
|
||||
if attr.constraints[:create_action] do
|
||||
Ash.Resource.Info.action(embed, attr.constraints[:create_action])
|
||||
else
|
||||
Ash.Resource.Info.primary_action(embed, :create)
|
||||
end
|
||||
|
||||
update_action =
|
||||
if attr.constraints[:update_action] do
|
||||
Ash.Resource.Info.action(embed, attr.constraints[:update_action])
|
||||
else
|
||||
Ash.Resource.Info.primary_action(embed, :update)
|
||||
end
|
||||
|
||||
{attr.name,
|
||||
[
|
||||
type: type,
|
||||
resource: embed,
|
||||
create_action: create_action.name,
|
||||
update_action: update_action.name,
|
||||
data: data,
|
||||
forms:
|
||||
embedded(embed, create_action, cycle_preventer) ++
|
||||
embedded(embed, update_action, cycle_preventer)
|
||||
]}
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defp unwrap_type({:array, type}), do: unwrap_type(type)
|
||||
defp unwrap_type(type), do: type
|
||||
|
||||
@doc false
|
||||
def accepted_attributes(resource, action) do
|
||||
resource
|
||||
|> Ash.Resource.Info.public_attributes()
|
||||
|> only_accepted(action)
|
||||
end
|
||||
|
||||
defp only_accepted(_attributes, %{type: :read}), do: []
|
||||
|
||||
defp only_accepted(attributes, %{accept: nil, reject: reject}) do
|
||||
Enum.filter(attributes, &(&1.name not in reject || []))
|
||||
end
|
||||
|
||||
defp only_accepted(attributes, %{accept: accept, reject: reject}) do
|
||||
attributes
|
||||
|> Enum.filter(&(&1.name in accept))
|
||||
|> Enum.filter(&(&1.name not in reject || []))
|
||||
end
|
||||
|
||||
defp find_manage_change(argument, action) do
|
||||
Enum.find_value(action.changes, fn
|
||||
%{change: {Ash.Resource.Change.ManageRelationship, opts}} ->
|
||||
if opts[:argument] == argument.name do
|
||||
opts
|
||||
end
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end)
|
||||
end
|
||||
end
|
File diff suppressed because it is too large
Load diff
|
@ -5,6 +5,29 @@ defmodule AshPhoenix.Form.NoActionConfigured do
|
|||
%__MODULE__{action: opts[:action], path: opts[:path]}
|
||||
end
|
||||
|
||||
def message(%{action: :read, path: path}) do
|
||||
"""
|
||||
Attempted to add a form at path: #{inspect(path)}, but no `read_action` was configured.
|
||||
|
||||
For example:
|
||||
Form.for_create(
|
||||
Resource,
|
||||
:action,
|
||||
params,
|
||||
forms: [
|
||||
# For forms over existing data
|
||||
nested_form: [
|
||||
type: :list,
|
||||
as: "form_name",
|
||||
read_action: :read_action_name,
|
||||
resource: RelatedResource,
|
||||
update_action: :create_action_name
|
||||
]
|
||||
]
|
||||
)
|
||||
"""
|
||||
end
|
||||
|
||||
def message(%{action: :create, path: path}) do
|
||||
"""
|
||||
Attempted to add a form at path: #{inspect(path)}, but no `create_action` was configured.
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
defmodule AshPhoenix.Form.NoFormConfigured do
|
||||
defexception [:field]
|
||||
defexception [:field, :available]
|
||||
|
||||
def exception(opts) do
|
||||
%__MODULE__{field: opts[:field]}
|
||||
%__MODULE__{field: opts[:field], available: opts[:available]}
|
||||
end
|
||||
|
||||
def message(%{field: field}) do
|
||||
def message(%{field: field, available: available}) do
|
||||
"""
|
||||
#{field} must be configured in the form to be used with `inputs_for`. For example:
|
||||
|
||||
Available forms:
|
||||
|
||||
#{available |> Enum.map(&"* #{&1}") |> Enum.join("\n")}
|
||||
|
||||
Example Setup:
|
||||
|
||||
Form.for_create(
|
||||
Resource,
|
||||
:action,
|
||||
|
|
|
@ -58,28 +58,6 @@ defimpl Phoenix.HTML.FormData, for: Ash.Changeset do
|
|||
[]
|
||||
end
|
||||
|
||||
hidden =
|
||||
changeset.resource
|
||||
|> Ash.Resource.Info.attributes()
|
||||
|> Enum.filter(&Ash.Type.embedded_type?(&1.type))
|
||||
|> Enum.reduce(hidden, fn attribute, hidden ->
|
||||
case Ash.Changeset.fetch_change(changeset, attribute.name) do
|
||||
{:ok, empty} when empty in [nil, []] ->
|
||||
Keyword.put(hidden, attribute.name, nil)
|
||||
|
||||
_ ->
|
||||
hidden
|
||||
end
|
||||
end)
|
||||
|
||||
removed_embed_values =
|
||||
changeset.context[:private][:removed_keys]
|
||||
|> Kernel.||(%{})
|
||||
|> Enum.filter(&elem(&1, 1))
|
||||
|> Enum.map(fn {name, _} -> {name, nil} end)
|
||||
|
||||
hidden = hidden ++ removed_embed_values
|
||||
|
||||
%Phoenix.HTML.Form{
|
||||
source: changeset,
|
||||
impl: __MODULE__,
|
||||
|
@ -94,122 +72,13 @@ defimpl Phoenix.HTML.FormData, for: Ash.Changeset do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def to_form(changeset, form, field, opts) do
|
||||
{name, opts} = Keyword.pop(opts, :as)
|
||||
{id, opts} = Keyword.pop(opts, :id)
|
||||
{prepend, opts} = Keyword.pop(opts, :prepend, [])
|
||||
{append, opts} = Keyword.pop(opts, :append, [])
|
||||
{use_data?, opts} = Keyword.pop(opts, :use_data?, false)
|
||||
id = to_string(id || form.id <> "_#{field}")
|
||||
name = to_string(name || form.name <> "[#{field}]")
|
||||
|
||||
{source, resource, data} =
|
||||
cond do
|
||||
arg = changeset.action && get_argument(changeset.action, field) ->
|
||||
case argument_and_manages(changeset, arg.name) do
|
||||
{nil, _} ->
|
||||
case get_embedded(arg.type) do
|
||||
nil ->
|
||||
raise "Cannot use `form_for` with an argument unless the type is an embedded resource or that argument manages a relationship"
|
||||
|
||||
resource ->
|
||||
data = Ash.Changeset.get_argument(changeset, arg.name)
|
||||
|
||||
data =
|
||||
case arg.type do
|
||||
{:array, _} ->
|
||||
List.wrap(data)
|
||||
|
||||
_ ->
|
||||
data
|
||||
end
|
||||
|
||||
{arg, resource, data}
|
||||
end
|
||||
|
||||
{argument, rel} ->
|
||||
if rel do
|
||||
rel = Ash.Resource.Info.relationship(changeset.resource, rel)
|
||||
|
||||
data =
|
||||
relationship_data(
|
||||
changeset,
|
||||
rel,
|
||||
use_data?,
|
||||
opts[:id] || argument.name || rel.name
|
||||
)
|
||||
|
||||
data =
|
||||
case argument.type do
|
||||
{:array, _} ->
|
||||
List.wrap(data)
|
||||
|
||||
_ ->
|
||||
if is_list(data) do
|
||||
List.last(data)
|
||||
else
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
{rel, rel.destination, data}
|
||||
else
|
||||
raise "Cannot use `form_for` with an argument unless the type is an embedded resource or that argument manages a relationship"
|
||||
end
|
||||
end
|
||||
|
||||
rel = Ash.Resource.Info.relationship(changeset.resource, field) ->
|
||||
data = relationship_data(changeset, rel, use_data?, opts[:id] || rel.name)
|
||||
|
||||
data =
|
||||
if rel.cardinality == :many && data do
|
||||
List.wrap(data)
|
||||
else
|
||||
data
|
||||
end
|
||||
|
||||
{rel, rel.destination, data}
|
||||
|
||||
attr = Ash.Resource.Info.attribute(changeset.resource, field) ->
|
||||
case get_embedded(attr.type) do
|
||||
nil ->
|
||||
raise "Cannot use `form_for` with an attribute unless the type is an embedded resource"
|
||||
|
||||
resource ->
|
||||
data = Ash.Changeset.get_attribute(changeset, attr.name)
|
||||
|
||||
data =
|
||||
case attr.type do
|
||||
{:array, _} ->
|
||||
List.wrap(data)
|
||||
|
||||
_ ->
|
||||
data
|
||||
end
|
||||
|
||||
{attr, resource, data}
|
||||
end
|
||||
|
||||
true ->
|
||||
raise "Cannot use `form_for` with anything except embedded resources in attributes/arguments"
|
||||
end
|
||||
|
||||
data =
|
||||
if is_list(data) do
|
||||
prepend ++ data ++ append
|
||||
else
|
||||
unwrap(prepend) || unwrap(append) || data
|
||||
end
|
||||
|
||||
data
|
||||
|> to_nested_form(changeset, source, resource, id, name, opts)
|
||||
|> List.wrap()
|
||||
def to_form(_changeset, _form, _field, _opts) do
|
||||
raise """
|
||||
Using `inputs_for` with an `Ash.Query` is no longer supported.
|
||||
See the documentation for `AshPhoenix.Form` for more information on the new implementation.
|
||||
"""
|
||||
end
|
||||
|
||||
defp unwrap([]), do: nil
|
||||
defp unwrap([value | _]), do: value
|
||||
defp unwrap(value), do: value
|
||||
|
||||
@impl true
|
||||
def input_validations(changeset, _, field) do
|
||||
attribute_or_argument =
|
||||
|
|
|
@ -10,31 +10,6 @@ defmodule AshPhoenix.FormData.Helpers do
|
|||
Enum.find(action.arguments, &(to_string(&1.name) == field))
|
||||
end
|
||||
|
||||
def argument_and_manages(changeset, key) do
|
||||
with action when not is_nil(action) <- changeset.action,
|
||||
argument when not is_nil(argument) <-
|
||||
Enum.find(changeset.action.arguments, &(&1.name == key || to_string(&1.name) == key)),
|
||||
manage_change when not is_nil(manage_change) <-
|
||||
find_manage_change(argument, changeset.action) do
|
||||
{argument, manage_change}
|
||||
else
|
||||
_ ->
|
||||
{nil, nil}
|
||||
end
|
||||
end
|
||||
|
||||
defp find_manage_change(argument, action) do
|
||||
Enum.find_value(action.changes, fn
|
||||
%{change: {Ash.Resource.Change.ManageRelationship, opts}} ->
|
||||
if opts[:argument] == argument.name do
|
||||
opts[:relationship]
|
||||
end
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end)
|
||||
end
|
||||
|
||||
def type_to_form_type(type) do
|
||||
case Ash.Type.ecto_type(type) do
|
||||
:integer -> :number_input
|
||||
|
@ -95,15 +70,15 @@ defmodule AshPhoenix.FormData.Helpers do
|
|||
end)
|
||||
end
|
||||
|
||||
defp transform_error(_form, {_key, _value, _vars} = error), do: error
|
||||
def transform_error(_form, {_key, _value, _vars} = error), do: error
|
||||
|
||||
defp transform_error(form, error) do
|
||||
def transform_error(form, error) do
|
||||
case form.transform_errors do
|
||||
transformer when is_function(transformer, 2) ->
|
||||
case transformer.(form.source, error) do
|
||||
error when is_exception(error) ->
|
||||
if AshPhoenix.FormData.Error.impl_for(error) do
|
||||
List.wrap(AshPhoenix.to_form_error(error))
|
||||
List.wrap(to_form_error(error))
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
@ -115,7 +90,7 @@ defmodule AshPhoenix.FormData.Helpers do
|
|||
Enum.flat_map(list, fn
|
||||
error when is_exception(error) ->
|
||||
if AshPhoenix.FormData.Error.impl_for(error) do
|
||||
List.wrap(AshPhoenix.to_form_error(error))
|
||||
List.wrap(to_form_error(error))
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
@ -127,497 +102,51 @@ defmodule AshPhoenix.FormData.Helpers do
|
|||
|
||||
nil ->
|
||||
if AshPhoenix.FormData.Error.impl_for(error) do
|
||||
List.wrap(AshPhoenix.to_form_error(error))
|
||||
List.wrap(to_form_error(error))
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_embedded({:array, type}), do: get_embedded(type)
|
||||
# defp set_source_context(changeset, {relationship, original_changeset}) do
|
||||
# case original_changeset.context[:manage_relationship_source] do
|
||||
# nil ->
|
||||
# Ash.Changeset.set_context(changeset, %{
|
||||
# manage_relationship_source: [
|
||||
# {relationship.source, relationship.name, original_changeset}
|
||||
# ]
|
||||
# })
|
||||
|
||||
def get_embedded(type) when is_atom(type) do
|
||||
if Ash.Resource.Info.embedded?(type) do
|
||||
type
|
||||
end
|
||||
end
|
||||
# value ->
|
||||
# Ash.Changeset.set_context(changeset, %{
|
||||
# manage_relationship_source:
|
||||
# value ++ [{relationship.source, relationship.name, original_changeset}]
|
||||
# })
|
||||
# end
|
||||
# end
|
||||
|
||||
def get_embedded(_), do: nil
|
||||
|
||||
def relationship_data(changeset, %{cardinality: :one} = rel, use_data?, id) do
|
||||
case get_managed(changeset, rel.name, id) do
|
||||
defp to_form_error(exception) when is_exception(exception) do
|
||||
case AshPhoenix.FormData.Error.to_form_error(exception) do
|
||||
nil ->
|
||||
if use_data? do
|
||||
changeset_data(changeset, rel)
|
||||
else
|
||||
nil
|
||||
end
|
||||
nil
|
||||
|
||||
{manage, _opts} ->
|
||||
case manage do
|
||||
nil ->
|
||||
nil
|
||||
{field, message} ->
|
||||
{field, message, []}
|
||||
|
||||
[] ->
|
||||
nil
|
||||
{field, message, vars} ->
|
||||
{field, message, vars}
|
||||
|
||||
value ->
|
||||
value =
|
||||
if is_list(value) do
|
||||
List.last(value)
|
||||
else
|
||||
value
|
||||
end
|
||||
list when is_list(list) ->
|
||||
Enum.map(list, fn item ->
|
||||
case item do
|
||||
{field, message} ->
|
||||
{field, message, []}
|
||||
|
||||
if use_data? do
|
||||
data = changeset_data(changeset, rel)
|
||||
|
||||
if data do
|
||||
data
|
||||
|> Ash.Changeset.new()
|
||||
|> Map.put(:params, value)
|
||||
else
|
||||
value
|
||||
end
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def relationship_data(changeset, rel, use_data?, id) do
|
||||
case get_managed(changeset, rel.name, id) do
|
||||
nil ->
|
||||
if use_data? do
|
||||
changeset_data(changeset, rel)
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
{manage, _opts} ->
|
||||
manage =
|
||||
if is_map(manage) do
|
||||
case map_input_to_list(manage) do
|
||||
:error ->
|
||||
manage
|
||||
|
||||
{:ok, manage} ->
|
||||
manage
|
||||
end
|
||||
{field, message, vars} ->
|
||||
{field, message, vars}
|
||||
end
|
||||
|
||||
if use_data? do
|
||||
changeset
|
||||
|> changeset_data(rel)
|
||||
|> zip_changes(manage)
|
||||
else
|
||||
manage
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp zip_changes(data, manage) when not is_list(data) do
|
||||
zip_changes(List.wrap(data), manage)
|
||||
end
|
||||
|
||||
defp zip_changes(data, manage) when not is_list(manage) do
|
||||
zip_changes(data, List.wrap(manage))
|
||||
end
|
||||
|
||||
defp zip_changes([], manage) do
|
||||
manage
|
||||
end
|
||||
|
||||
defp zip_changes([record | rest_data], [manage | rest_manage]) do
|
||||
[
|
||||
Ash.Changeset.new(record, %{})
|
||||
|> Map.put(:params, manage)
|
||||
] ++
|
||||
zip_changes(rest_data, rest_manage)
|
||||
end
|
||||
|
||||
defp zip_changes(records, []) do
|
||||
records
|
||||
end
|
||||
|
||||
defp changeset_data(changeset, rel) do
|
||||
data = Map.get(changeset.data, rel.name)
|
||||
|
||||
case data do
|
||||
%Ash.NotLoaded{} ->
|
||||
default_data(rel)
|
||||
|
||||
data ->
|
||||
if is_list(data) do
|
||||
Enum.reject(data, &hidden?/1)
|
||||
else
|
||||
if hidden?(data) do
|
||||
nil
|
||||
else
|
||||
data
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp map_input_to_list(input) when input == %{} do
|
||||
:error
|
||||
end
|
||||
|
||||
defp map_input_to_list(input) do
|
||||
input
|
||||
|> Enum.reduce_while({:ok, []}, fn
|
||||
{key, value}, {:ok, acc} when is_integer(key) ->
|
||||
{:cont, {:ok, [{key, value} | acc]}}
|
||||
|
||||
{key, value}, {:ok, acc} when is_binary(key) ->
|
||||
case Integer.parse(key) do
|
||||
{int, ""} ->
|
||||
{:cont, {:ok, [{int, value} | acc]}}
|
||||
|
||||
_ ->
|
||||
{:halt, :error}
|
||||
end
|
||||
|
||||
_, _ ->
|
||||
{:halt, :error}
|
||||
end)
|
||||
|> case do
|
||||
{:ok, value} ->
|
||||
{:ok,
|
||||
value
|
||||
|> Enum.sort_by(&elem(&1, 0))
|
||||
|> Enum.map(&elem(&1, 1))}
|
||||
|
||||
:error ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp default_data(%{cardinality: :many}), do: []
|
||||
defp default_data(%{cardinality: :one}), do: nil
|
||||
|
||||
defp get_managed(changeset, relationship_name, id) do
|
||||
manage = changeset.relationships[relationship_name] || []
|
||||
|
||||
Enum.find(manage, fn {_, opts} -> opts[:meta][:id] == id end)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def to_nested_form(
|
||||
data,
|
||||
original_changeset,
|
||||
%{cardinality: _} = relationship,
|
||||
resource,
|
||||
id,
|
||||
name,
|
||||
opts
|
||||
)
|
||||
when is_list(data) do
|
||||
changesets =
|
||||
Enum.map(
|
||||
data,
|
||||
&related_data_to_changeset(resource, &1, opts, relationship, original_changeset)
|
||||
)
|
||||
|
||||
changesets =
|
||||
if AshPhoenix.hiding_errors?(original_changeset) do
|
||||
Enum.map(changesets, &AshPhoenix.hide_errors/1)
|
||||
else
|
||||
changesets
|
||||
end
|
||||
|
||||
for {changeset, index} <- Enum.with_index(changesets) do
|
||||
index_string = Integer.to_string(index)
|
||||
|
||||
hidden =
|
||||
if changeset.action_type in [:update, :destroy] do
|
||||
changeset.data
|
||||
|> Map.take(Ash.Resource.Info.primary_key(changeset.resource))
|
||||
|> Enum.to_list()
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
%Phoenix.HTML.Form{
|
||||
source: changeset,
|
||||
impl: Phoenix.HTML.FormData.impl_for(changeset),
|
||||
id: id <> "_" <> index_string,
|
||||
name: name <> "[" <> index_string <> "]",
|
||||
index: index,
|
||||
errors: form_for_errors(changeset, opts),
|
||||
data: changeset.data,
|
||||
params: changeset.params,
|
||||
hidden: hidden,
|
||||
options: opts
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def to_nested_form(nil, _, _, _, _, _, _) do
|
||||
nil
|
||||
end
|
||||
|
||||
def to_nested_form(
|
||||
data,
|
||||
original_changeset,
|
||||
%{cardinality: _} = relationship,
|
||||
resource,
|
||||
id,
|
||||
name,
|
||||
opts
|
||||
) do
|
||||
changeset = related_data_to_changeset(resource, data, opts, relationship, original_changeset)
|
||||
|
||||
changeset =
|
||||
if AshPhoenix.hiding_errors?(original_changeset) do
|
||||
AshPhoenix.hide_errors(changeset)
|
||||
else
|
||||
changeset
|
||||
end
|
||||
|
||||
hidden =
|
||||
if changeset.action_type in [:update, :destroy] do
|
||||
changeset.data
|
||||
|> Map.take(Ash.Resource.Info.primary_key(changeset.resource))
|
||||
|> Enum.to_list()
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
%Phoenix.HTML.Form{
|
||||
source: changeset,
|
||||
impl: Phoenix.HTML.FormData.impl_for(changeset),
|
||||
id: id,
|
||||
name: name,
|
||||
errors: form_for_errors(changeset, opts),
|
||||
data: changeset.data,
|
||||
params: changeset.params,
|
||||
hidden: hidden,
|
||||
options: opts
|
||||
}
|
||||
end
|
||||
|
||||
def to_nested_form(
|
||||
data,
|
||||
original_changeset,
|
||||
attribute,
|
||||
resource,
|
||||
id,
|
||||
name,
|
||||
opts
|
||||
)
|
||||
when is_list(data) do
|
||||
create_action =
|
||||
action!(resource, :create, attribute.constraints[:create_action] || opts[:create_action]).name
|
||||
|
||||
update_action =
|
||||
action!(resource, :update, attribute.constraints[:update_action] || opts[:update_action]).name
|
||||
|
||||
changesets =
|
||||
data
|
||||
|> Enum.map(fn data ->
|
||||
if is_struct(data) do
|
||||
Ash.Changeset.for_update(data, update_action, params(data), actor: opts[:actor])
|
||||
else
|
||||
Ash.Changeset.for_create(resource, create_action, data, actor: opts[:actor])
|
||||
end
|
||||
end)
|
||||
|
||||
changesets =
|
||||
if AshPhoenix.hiding_errors?(original_changeset) do
|
||||
Enum.map(changesets, &AshPhoenix.hide_errors/1)
|
||||
else
|
||||
changesets
|
||||
end
|
||||
|
||||
for {changeset, index} <- Enum.with_index(changesets) do
|
||||
index_string = Integer.to_string(index)
|
||||
|
||||
hidden =
|
||||
if changeset.action_type in [:update, :destroy] do
|
||||
changeset.data
|
||||
|> Map.take(Ash.Resource.Info.primary_key(changeset.resource))
|
||||
|> Enum.to_list()
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
%Phoenix.HTML.Form{
|
||||
source: changeset,
|
||||
impl: Phoenix.HTML.FormData.impl_for(changeset),
|
||||
id: id <> "_" <> index_string,
|
||||
name: name <> "[" <> index_string <> "]",
|
||||
index: index,
|
||||
errors: form_for_errors(changeset, opts),
|
||||
data: changeset.data,
|
||||
params: changeset.params,
|
||||
hidden: hidden,
|
||||
options: opts
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def to_nested_form(
|
||||
data,
|
||||
original_changeset,
|
||||
attribute,
|
||||
resource,
|
||||
id,
|
||||
name,
|
||||
opts
|
||||
) do
|
||||
create_action =
|
||||
action!(resource, :create, attribute.constraints[:create_action] || opts[:create_action]).name
|
||||
|
||||
update_action =
|
||||
action!(resource, :update, attribute.constraints[:update_action] || opts[:update_action]).name
|
||||
|
||||
changeset =
|
||||
cond do
|
||||
is_struct(data) ->
|
||||
Ash.Changeset.for_update(data, update_action, params(data), actor: opts[:actor])
|
||||
|
||||
is_nil(data) ->
|
||||
nil
|
||||
|
||||
true ->
|
||||
Ash.Changeset.for_create(resource, create_action, params(data), actor: opts[:actor])
|
||||
end
|
||||
|
||||
if changeset do
|
||||
changeset =
|
||||
if AshPhoenix.hiding_errors?(original_changeset) do
|
||||
AshPhoenix.hide_errors(changeset)
|
||||
else
|
||||
changeset
|
||||
end
|
||||
|
||||
hidden =
|
||||
if changeset.action_type in [:update, :destroy] do
|
||||
changeset.data
|
||||
|> Map.take(Ash.Resource.Info.primary_key(changeset.resource))
|
||||
|> Enum.to_list()
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
%Phoenix.HTML.Form{
|
||||
source: changeset,
|
||||
impl: Phoenix.HTML.FormData.impl_for(changeset),
|
||||
id: id,
|
||||
name: name,
|
||||
errors: form_for_errors(changeset, opts),
|
||||
data: changeset.data,
|
||||
params: changeset.params,
|
||||
hidden: hidden,
|
||||
options: opts
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def hidden?(nil), do: false
|
||||
|
||||
def hidden?(%_{} = data) do
|
||||
Ash.Resource.Info.get_metadata(data, :private)[:hidden?]
|
||||
end
|
||||
|
||||
def hidden?(_), do: false
|
||||
|
||||
def hide(nil), do: nil
|
||||
|
||||
def hide(record) do
|
||||
Ash.Resource.Info.put_metadata(record, :private, %{hidden?: true})
|
||||
end
|
||||
|
||||
defp related_data_to_changeset(resource, data, opts, relationship, source_changeset) do
|
||||
if is_struct(data) do
|
||||
if opts[:update_action] == :_raw do
|
||||
Ash.Changeset.new(data)
|
||||
else
|
||||
update_action = action!(resource, :update, opts[:update_action])
|
||||
|
||||
data
|
||||
|> case do
|
||||
%Ash.Changeset{} = changeset ->
|
||||
changeset
|
||||
|
||||
other ->
|
||||
Ash.Changeset.new(other)
|
||||
end
|
||||
|> set_source_context({relationship, source_changeset})
|
||||
|> Ash.Changeset.for_update(update_action.name, params(data), actor: opts[:actor])
|
||||
end
|
||||
else
|
||||
if opts[:create_action] == :_raw do
|
||||
resource
|
||||
|> Ash.Changeset.new(take_attributes(data, resource))
|
||||
|> set_source_context({relationship, source_changeset})
|
||||
|> Map.put(:params, data)
|
||||
else
|
||||
create_action = action!(resource, :create, opts[:create_action])
|
||||
|
||||
resource
|
||||
|> Ash.Changeset.new()
|
||||
|> set_source_context({relationship, source_changeset})
|
||||
|> Ash.Changeset.for_create(create_action.name, data, actor: opts[:actor])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp params(%Ash.Changeset{params: params}), do: params
|
||||
defp params(_), do: nil
|
||||
|
||||
defp set_source_context(changeset, {relationship, original_changeset}) do
|
||||
case original_changeset.context[:manage_relationship_source] do
|
||||
nil ->
|
||||
Ash.Changeset.set_context(changeset, %{
|
||||
manage_relationship_source: [
|
||||
{relationship.source, relationship.name, original_changeset}
|
||||
]
|
||||
})
|
||||
|
||||
value ->
|
||||
Ash.Changeset.set_context(changeset, %{
|
||||
manage_relationship_source:
|
||||
value ++ [{relationship.source, relationship.name, original_changeset}]
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
def take_attributes(data, resource) do
|
||||
attributes =
|
||||
resource
|
||||
|> Ash.Resource.Info.attributes()
|
||||
|> Enum.map(&to_string(&1.name))
|
||||
|
||||
Map.take(data, attributes)
|
||||
end
|
||||
|
||||
def action!(resource, type, nil) do
|
||||
case Ash.Resource.Info.primary_action(resource, type) do
|
||||
nil ->
|
||||
raise """
|
||||
No `#{type}_action` configured, and no primary action of type #{type} found on #{
|
||||
inspect(resource)
|
||||
}
|
||||
"""
|
||||
|
||||
action ->
|
||||
action
|
||||
end
|
||||
end
|
||||
|
||||
def action!(resource, _type, action) do
|
||||
case Ash.Resource.Info.action(resource, action) do
|
||||
nil ->
|
||||
raise """
|
||||
No such action #{action} on resource #{inspect(resource)}
|
||||
"""
|
||||
|
||||
action ->
|
||||
action
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,72 +31,11 @@ defimpl Phoenix.HTML.FormData, for: Ash.Query do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def to_form(query, form, field, opts) do
|
||||
{name, opts} = Keyword.pop(opts, :as)
|
||||
{id, opts} = Keyword.pop(opts, :id)
|
||||
{prepend, opts} = Keyword.pop(opts, :prepend, [])
|
||||
{append, opts} = Keyword.pop(opts, :append, [])
|
||||
id = to_string(id || form.id <> "_#{field}")
|
||||
name = to_string(name || form.name <> "[#{field}]")
|
||||
|
||||
arguments =
|
||||
if query.action do
|
||||
query.action.arguments
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
arg =
|
||||
Enum.find(
|
||||
arguments,
|
||||
&(&1.name == field || to_string(&1.name) == field)
|
||||
)
|
||||
|
||||
{source, resource, data} =
|
||||
if arg do
|
||||
case get_embedded(arg.type) do
|
||||
nil ->
|
||||
raise "Cannot use `form_for` with an argument unless the type is an embedded resource"
|
||||
|
||||
resource ->
|
||||
data = Ash.Query.get_argument(query, arg.name)
|
||||
|
||||
data =
|
||||
case arg.type do
|
||||
{:array, _} ->
|
||||
List.wrap(data)
|
||||
|
||||
_ ->
|
||||
data
|
||||
end
|
||||
|
||||
{arg, resource, data}
|
||||
end
|
||||
else
|
||||
raise "Cannot use `form_for` with anything except embedded resources in attributes/arguments"
|
||||
end
|
||||
|
||||
data =
|
||||
if is_list(data) do
|
||||
Enum.reject(prepend ++ data ++ append, &(&1 == ""))
|
||||
else
|
||||
if data == "" do
|
||||
nil
|
||||
else
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
data
|
||||
|> to_nested_form(
|
||||
query,
|
||||
source,
|
||||
resource,
|
||||
id,
|
||||
name,
|
||||
opts
|
||||
)
|
||||
|> List.wrap()
|
||||
def to_form(_query, _form, _field, _opts) do
|
||||
raise """
|
||||
Using `inputs_for` with an `Ash.Query` is no longer supported.
|
||||
See the documentation for `AshPhoenix.Form` for more information on the new implementation.
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
|
@ -1,43 +1,4 @@
|
|||
defmodule AshPhoenixTest do
|
||||
use ExUnit.Case
|
||||
doctest AshPhoenix
|
||||
|
||||
describe "add_to_path/3" do
|
||||
import AshPhoenix, only: [add_to_path: 3]
|
||||
|
||||
test "a simple key is added to a map" do
|
||||
assert add_to_path(%{}, ["key"], 1) == %{"key" => 1}
|
||||
end
|
||||
|
||||
test "when the value is nil, the result is a map" do
|
||||
assert add_to_path(nil, ["key"], 1) == %{"key" => 1}
|
||||
end
|
||||
|
||||
test "when the value is an empty list, the result is the value" do
|
||||
assert add_to_path([], ["key"], 1) == 1
|
||||
end
|
||||
|
||||
test "when the value is a non-empty list (as a map), the value is added to the list" do
|
||||
assert add_to_path(%{"0" => 1}, [], 1) == %{"0" => 1, "1" => 1}
|
||||
end
|
||||
|
||||
test "when the value is a non-empty list, the value is added to the list" do
|
||||
assert add_to_path([1], [], 1) == [1, 1]
|
||||
end
|
||||
|
||||
test "when the value is a list and the key is an integer and the index is not present, the result is the original list" do
|
||||
assert add_to_path([%{}], [1, "key"], 1) == [%{}]
|
||||
end
|
||||
|
||||
test "when the add is a map and the value is a list, the map is added to the list" do
|
||||
assert add_to_path(%{}, ["key"], %{"foo" => "bar"}) == %{"key" => %{"foo" => "bar"}}
|
||||
end
|
||||
|
||||
test "when the 0th index is modified on a list but the value is not yet in a list, it is modified" do
|
||||
assert add_to_path(%{"field_id" => 1}, [0, "value"], %{}) == %{
|
||||
"field_id" => 1,
|
||||
"value" => %{}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
24
test/auto_form_test.exs
Normal file
24
test/auto_form_test.exs
Normal file
|
@ -0,0 +1,24 @@
|
|||
defmodule AshPhoenix.AutoFormTest do
|
||||
use ExUnit.Case
|
||||
|
||||
alias AshPhoenix.Form.Auto
|
||||
alias AshPhoenix.Test.{Api, Comment, Post}
|
||||
import AshPhoenix.Form, only: [update_opts: 1]
|
||||
|
||||
test "it works for simple relationships" do
|
||||
forms =
|
||||
Post
|
||||
|> auto_forms(:create)
|
||||
|> update_opts()
|
||||
|> Keyword.get(:forms)
|
||||
|
||||
assert forms[:comments][:update_action] == :update
|
||||
assert forms[:comments][:create_action] == :create
|
||||
assert forms[:linked_posts][:update_action] == :update
|
||||
assert forms[:linked_posts][:create_action] == :create
|
||||
end
|
||||
|
||||
defp auto_forms(resource, action) do
|
||||
[forms: Auto.auto(resource, action)]
|
||||
end
|
||||
end
|
|
@ -1,9 +1,9 @@
|
|||
defmodule AshPhoenix.ChangesetTest do
|
||||
use ExUnit.Case
|
||||
import Phoenix.HTML.Form, only: [form_for: 2, inputs_for: 2, inputs_for: 3]
|
||||
import Phoenix.HTML.Form, only: [form_for: 2]
|
||||
|
||||
alias Phoenix.HTML.FormData
|
||||
alias AshPhoenix.Test.{Api, Comment, Post}
|
||||
alias AshPhoenix.Test.{Post}
|
||||
|
||||
describe "form_for fields" do
|
||||
test "it should show simple field values" do
|
||||
|
@ -15,112 +15,4 @@ defmodule AshPhoenix.ChangesetTest do
|
|||
assert FormData.input_value(form.source, form, :text) == "text"
|
||||
end
|
||||
end
|
||||
|
||||
describe "form_for relationships belongs_to" do
|
||||
test "it should show nothing in `inputs_for` by default" do
|
||||
form =
|
||||
Comment
|
||||
|> Ash.Changeset.for_create(:create, %{text: "text"})
|
||||
|> form_for("action")
|
||||
|
||||
assert inputs_for(form, :post) == []
|
||||
end
|
||||
|
||||
test "when a value has been appended to the relationship, a form is created" do
|
||||
form =
|
||||
Comment
|
||||
|> Ash.Changeset.for_create(:create, %{text: "text"})
|
||||
|> AshPhoenix.add_related("change[post]", "change")
|
||||
|> form_for("action")
|
||||
|
||||
assert [
|
||||
%Phoenix.HTML.Form{source: %Ash.Changeset{resource: AshPhoenix.Test.Post}}
|
||||
] = inputs_for(form, :post)
|
||||
end
|
||||
|
||||
test "it will use the data if configured" do
|
||||
post = Post |> Ash.Changeset.for_create(:create, %{text: "text"}) |> Api.create!()
|
||||
|
||||
form =
|
||||
Comment
|
||||
|> Ash.Changeset.for_create(:create, %{text: "text"})
|
||||
|> Ash.Changeset.replace_relationship(:post, post)
|
||||
|> Api.create!()
|
||||
|> Ash.Changeset.for_update(:update)
|
||||
|> form_for("action")
|
||||
|
||||
assert [%Phoenix.HTML.Form{source: %Ash.Changeset{resource: AshPhoenix.Test.Post}}] =
|
||||
inputs_for(form, :post, use_data?: true)
|
||||
end
|
||||
|
||||
test "adding from the relationship works in conjunction with `use_data`" do
|
||||
form =
|
||||
Comment
|
||||
|> Ash.Changeset.for_create(:create, %{text: "text"})
|
||||
|> Api.create!()
|
||||
|> Ash.Changeset.for_update(:update)
|
||||
|> AshPhoenix.add_related("change[post]", "change")
|
||||
|> form_for("action")
|
||||
|
||||
assert [%Phoenix.HTML.Form{source: %Ash.Changeset{resource: AshPhoenix.Test.Post}}] =
|
||||
inputs_for(form, :post, use_data?: true)
|
||||
end
|
||||
|
||||
test "removing from the relationship works in conjunction with `use_data`" do
|
||||
post = Post |> Ash.Changeset.for_create(:create, %{text: "text"}) |> Api.create!()
|
||||
|
||||
{_record, changeset} =
|
||||
Comment
|
||||
|> Ash.Changeset.for_create(:create, %{text: "text"})
|
||||
|> Ash.Changeset.replace_relationship(:post, post)
|
||||
|> Api.create!()
|
||||
|> Ash.Changeset.for_update(:update)
|
||||
|> AshPhoenix.remove_related("change[post]", "change")
|
||||
|
||||
form = form_for(changeset, "action")
|
||||
|
||||
assert [] = inputs_for(form, :post, use_data?: true)
|
||||
end
|
||||
end
|
||||
|
||||
describe "form_for relationships has_many" do
|
||||
test "it should show nothing in `inputs_for` by default" do
|
||||
form =
|
||||
Post
|
||||
|> Ash.Changeset.for_create(:create, %{text: "text"})
|
||||
|> form_for("action")
|
||||
|
||||
assert inputs_for(form, :comments) == []
|
||||
end
|
||||
|
||||
test "when a value has been appended to the relationship, a form is created" do
|
||||
form =
|
||||
Post
|
||||
|> Ash.Changeset.for_create(:create, %{text: "text"})
|
||||
|> AshPhoenix.add_related("change[comments]", "change")
|
||||
|> form_for("action")
|
||||
|
||||
assert [
|
||||
%Phoenix.HTML.Form{source: %Ash.Changeset{resource: AshPhoenix.Test.Comment}}
|
||||
] = inputs_for(form, :comments)
|
||||
end
|
||||
|
||||
test "adding from the relationship works in conjunction with `use_data`" do
|
||||
comment = Comment |> Ash.Changeset.for_create(:create, %{text: "text"}) |> Api.create!()
|
||||
|
||||
form =
|
||||
Post
|
||||
|> Ash.Changeset.for_create(:create, %{text: "text"})
|
||||
|> Ash.Changeset.append_to_relationship(:comments, comment)
|
||||
|> Api.create!()
|
||||
|> Ash.Changeset.for_update(:update)
|
||||
|> AshPhoenix.add_related("change[comments]", "change", use_data?: true)
|
||||
|> form_for("action")
|
||||
|
||||
assert [
|
||||
%Phoenix.HTML.Form{source: %Ash.Changeset{resource: AshPhoenix.Test.Comment}},
|
||||
%Phoenix.HTML.Form{source: %Ash.Changeset{resource: AshPhoenix.Test.Comment}}
|
||||
] = inputs_for(form, :comments, use_data?: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue