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
|
defmodule AshPhoenix do
|
||||||
@moduledoc """
|
@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
|
def hide_errors(%Ash.Changeset{} = changeset) do
|
||||||
|
Ash.Changeset.put_context(changeset, :private, %{ash_phoenix: %{hide_errors: true}})
|
||||||
@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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
def hide_errors(%Ash.Query{} = query) do
|
||||||
Allows for manually transforming errors to modify or enable error messages in the form.
|
Ash.Query.put_context(query, :private, %{ash_phoenix: %{hide_errors: true}})
|
||||||
|
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
AshPhoenix.transform_errors(changeset, fn
|
def hiding_errors?(%Ash.Changeset{} = changeset) do
|
||||||
changeset, %MyApp.CustomError{message: message, field: field} ->
|
changeset.context[:private][:ash_phoenix][:hide_errors] == true
|
||||||
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}
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def transform_errors(%Ash.Query{} = changeset, transform_errors) do
|
def hiding_errors?(%Ash.Query{} = query) do
|
||||||
Ash.Query.put_context(changeset, :private, %{
|
query.context[:private][:ash_phoenix][:hide_errors] == true
|
||||||
ash_phoenix: %{transform_errors: transform_errors}
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -96,11 +30,11 @@ defmodule AshPhoenix do
|
||||||
[{atom, {String.t(), Keyword.t()}}] | [String.t()] | map
|
[{atom, {String.t(), Keyword.t()}}] | [String.t()] | map
|
||||||
def errors_for(changeset_or_query, opts \\ []) do
|
def errors_for(changeset_or_query, opts \\ []) do
|
||||||
errors =
|
errors =
|
||||||
if hiding_errors?(changeset_or_query) do
|
if AshPhoenix.hiding_errors?(changeset_or_query) do
|
||||||
[]
|
[]
|
||||||
else
|
else
|
||||||
changeset_or_query.errors
|
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
|
|> Enum.filter(fn
|
||||||
error when is_exception(error) ->
|
error when is_exception(error) ->
|
||||||
AshPhoenix.FormData.Error.impl_for(error)
|
AshPhoenix.FormData.Error.impl_for(error)
|
||||||
|
@ -157,867 +91,4 @@ defmodule AshPhoenix do
|
||||||
String.replace(acc, "%{#{key}}", to_string(value))
|
String.replace(acc, "%{#{key}}", to_string(value))
|
||||||
end)
|
end)
|
||||||
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
|
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]}
|
%__MODULE__{action: opts[:action], path: opts[:path]}
|
||||||
end
|
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
|
def message(%{action: :create, path: path}) do
|
||||||
"""
|
"""
|
||||||
Attempted to add a form at path: #{inspect(path)}, but no `create_action` was configured.
|
Attempted to add a form at path: #{inspect(path)}, but no `create_action` was configured.
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
defmodule AshPhoenix.Form.NoFormConfigured do
|
defmodule AshPhoenix.Form.NoFormConfigured do
|
||||||
defexception [:field]
|
defexception [:field, :available]
|
||||||
|
|
||||||
def exception(opts) do
|
def exception(opts) do
|
||||||
%__MODULE__{field: opts[:field]}
|
%__MODULE__{field: opts[:field], available: opts[:available]}
|
||||||
end
|
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:
|
#{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(
|
Form.for_create(
|
||||||
Resource,
|
Resource,
|
||||||
:action,
|
:action,
|
||||||
|
|
|
@ -58,28 +58,6 @@ defimpl Phoenix.HTML.FormData, for: Ash.Changeset do
|
||||||
[]
|
[]
|
||||||
end
|
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{
|
%Phoenix.HTML.Form{
|
||||||
source: changeset,
|
source: changeset,
|
||||||
impl: __MODULE__,
|
impl: __MODULE__,
|
||||||
|
@ -94,122 +72,13 @@ defimpl Phoenix.HTML.FormData, for: Ash.Changeset do
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def to_form(changeset, form, field, opts) do
|
def to_form(_changeset, _form, _field, _opts) do
|
||||||
{name, opts} = Keyword.pop(opts, :as)
|
raise """
|
||||||
{id, opts} = Keyword.pop(opts, :id)
|
Using `inputs_for` with an `Ash.Query` is no longer supported.
|
||||||
{prepend, opts} = Keyword.pop(opts, :prepend, [])
|
See the documentation for `AshPhoenix.Form` for more information on the new implementation.
|
||||||
{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()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp unwrap([]), do: nil
|
|
||||||
defp unwrap([value | _]), do: value
|
|
||||||
defp unwrap(value), do: value
|
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def input_validations(changeset, _, field) do
|
def input_validations(changeset, _, field) do
|
||||||
attribute_or_argument =
|
attribute_or_argument =
|
||||||
|
|
|
@ -10,31 +10,6 @@ defmodule AshPhoenix.FormData.Helpers do
|
||||||
Enum.find(action.arguments, &(to_string(&1.name) == field))
|
Enum.find(action.arguments, &(to_string(&1.name) == field))
|
||||||
end
|
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
|
def type_to_form_type(type) do
|
||||||
case Ash.Type.ecto_type(type) do
|
case Ash.Type.ecto_type(type) do
|
||||||
:integer -> :number_input
|
:integer -> :number_input
|
||||||
|
@ -95,15 +70,15 @@ defmodule AshPhoenix.FormData.Helpers do
|
||||||
end)
|
end)
|
||||||
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
|
case form.transform_errors do
|
||||||
transformer when is_function(transformer, 2) ->
|
transformer when is_function(transformer, 2) ->
|
||||||
case transformer.(form.source, error) do
|
case transformer.(form.source, error) do
|
||||||
error when is_exception(error) ->
|
error when is_exception(error) ->
|
||||||
if AshPhoenix.FormData.Error.impl_for(error) do
|
if AshPhoenix.FormData.Error.impl_for(error) do
|
||||||
List.wrap(AshPhoenix.to_form_error(error))
|
List.wrap(to_form_error(error))
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
@ -115,7 +90,7 @@ defmodule AshPhoenix.FormData.Helpers do
|
||||||
Enum.flat_map(list, fn
|
Enum.flat_map(list, fn
|
||||||
error when is_exception(error) ->
|
error when is_exception(error) ->
|
||||||
if AshPhoenix.FormData.Error.impl_for(error) do
|
if AshPhoenix.FormData.Error.impl_for(error) do
|
||||||
List.wrap(AshPhoenix.to_form_error(error))
|
List.wrap(to_form_error(error))
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
@ -127,497 +102,51 @@ defmodule AshPhoenix.FormData.Helpers do
|
||||||
|
|
||||||
nil ->
|
nil ->
|
||||||
if AshPhoenix.FormData.Error.impl_for(error) do
|
if AshPhoenix.FormData.Error.impl_for(error) do
|
||||||
List.wrap(AshPhoenix.to_form_error(error))
|
List.wrap(to_form_error(error))
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
end
|
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
|
# value ->
|
||||||
if Ash.Resource.Info.embedded?(type) do
|
# Ash.Changeset.set_context(changeset, %{
|
||||||
type
|
# manage_relationship_source:
|
||||||
end
|
# value ++ [{relationship.source, relationship.name, original_changeset}]
|
||||||
end
|
# })
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
|
||||||
def get_embedded(_), do: nil
|
defp to_form_error(exception) when is_exception(exception) do
|
||||||
|
case AshPhoenix.FormData.Error.to_form_error(exception) do
|
||||||
def relationship_data(changeset, %{cardinality: :one} = rel, use_data?, id) do
|
|
||||||
case get_managed(changeset, rel.name, id) do
|
|
||||||
nil ->
|
nil ->
|
||||||
if use_data? do
|
nil
|
||||||
changeset_data(changeset, rel)
|
|
||||||
else
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
{manage, _opts} ->
|
{field, message} ->
|
||||||
case manage do
|
{field, message, []}
|
||||||
nil ->
|
|
||||||
nil
|
|
||||||
|
|
||||||
[] ->
|
{field, message, vars} ->
|
||||||
nil
|
{field, message, vars}
|
||||||
|
|
||||||
value ->
|
list when is_list(list) ->
|
||||||
value =
|
Enum.map(list, fn item ->
|
||||||
if is_list(value) do
|
case item do
|
||||||
List.last(value)
|
{field, message} ->
|
||||||
else
|
{field, message, []}
|
||||||
value
|
|
||||||
end
|
|
||||||
|
|
||||||
if use_data? do
|
{field, message, vars} ->
|
||||||
data = changeset_data(changeset, rel)
|
{field, message, vars}
|
||||||
|
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -31,72 +31,11 @@ defimpl Phoenix.HTML.FormData, for: Ash.Query do
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def to_form(query, form, field, opts) do
|
def to_form(_query, _form, _field, _opts) do
|
||||||
{name, opts} = Keyword.pop(opts, :as)
|
raise """
|
||||||
{id, opts} = Keyword.pop(opts, :id)
|
Using `inputs_for` with an `Ash.Query` is no longer supported.
|
||||||
{prepend, opts} = Keyword.pop(opts, :prepend, [])
|
See the documentation for `AshPhoenix.Form` for more information on the new implementation.
|
||||||
{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()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
|
@ -1,43 +1,4 @@
|
||||||
defmodule AshPhoenixTest do
|
defmodule AshPhoenixTest do
|
||||||
use ExUnit.Case
|
use ExUnit.Case
|
||||||
doctest AshPhoenix
|
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
|
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
|
defmodule AshPhoenix.ChangesetTest do
|
||||||
use ExUnit.Case
|
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 Phoenix.HTML.FormData
|
||||||
alias AshPhoenix.Test.{Api, Comment, Post}
|
alias AshPhoenix.Test.{Post}
|
||||||
|
|
||||||
describe "form_for fields" do
|
describe "form_for fields" do
|
||||||
test "it should show simple field values" 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"
|
assert FormData.input_value(form.source, form, :text) == "text"
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
Loading…
Reference in a new issue