This commit is contained in:
Zach Daniel 2022-06-28 22:22:38 -04:00
parent e8bf9d648d
commit 6ba516aeaa
3 changed files with 333 additions and 270 deletions

View file

@ -49,12 +49,11 @@ defmodule AshPhoenix.Form.Auto do
if you are using liveview, you could actually look up the record using the input from the read action, and then use `AshPhoenix.Form.update_form/3` if you are using liveview, you could actually look up the record using the input from the read action, and then use `AshPhoenix.Form.update_form/3`
to set that looked up record as the data of the `_update` form. to set that looked up record as the data of the `_update` form.
### Many to Many Relationshisp ### Many to Many Relationships
In the case that a manage_change option points to a join relationship, that form is presented via a special nested form called In the case that a manage_change option points to a join relationship, that form is presented via a special nested form called
`_join`. So the first form in `inputs_for(form, :relationship)` would be for the destination, and then inside of that you could say `_join`. So the first form in `inputs_for(form, :relationship)` would be for the destination, and then inside of that you could say
`inputs_for(nested_form, :_join)`. The parameters are merged together during submission. `inputs_for(nested_form, :_join)`. The parameters are merged together during submission.
""" """
@dialyzer {:nowarn_function, rel_to_resource: 2} @dialyzer {:nowarn_function, rel_to_resource: 2}

View file

@ -848,45 +848,49 @@ defmodule AshPhoenix.Form do
new_forms = new_forms =
Enum.reduce(form_params, forms, fn {index, params}, forms -> Enum.reduce(form_params, forms, fn {index, params}, forms ->
case Enum.find(form.forms[key] || [], &matcher.(&1, params, form, key, index)) do if params["_ignore"] == "true" do
nil -> Map.put_new(forms, key, [])
create_action = else
opts[:create_action] || case Enum.find(form.forms[key] || [], &matcher.(&1, params, form, key, index)) do
raise AshPhoenix.Form.NoActionConfigured, nil ->
path: form.name <> "[#{key}][#{index}]", create_action =
action: :create opts[:create_action] ||
raise AshPhoenix.Form.NoActionConfigured,
path: form.name <> "[#{key}][#{index}]",
action: :create
resource = resource =
opts[:create_resource] || opts[:resource] || opts[:create_resource] || opts[:resource] ||
raise AshPhoenix.Form.NoResourceConfigured, raise AshPhoenix.Form.NoResourceConfigured,
path: Enum.reverse(trail, [key]) path: Enum.reverse(trail, [key])
new_form = new_form =
for_action(resource, create_action, for_action(resource, create_action,
params: params, params: params,
forms: opts[:forms] || [], forms: opts[:forms] || [],
errors: errors?, errors: errors?,
prev_data_trail: prev_data_trail, prev_data_trail: prev_data_trail,
transform_errors: form.transform_errors, transform_errors: form.transform_errors,
as: form.name <> "[#{key}][#{index}]", as: form.name <> "[#{key}][#{index}]",
id: form.id <> "_#{key}_#{index}" id: form.id <> "_#{key}_#{index}"
) )
Map.update(forms, key, [new_form], &(&1 ++ [new_form])) Map.update(forms, key, [new_form], &(&1 ++ [new_form]))
matching_form -> matching_form ->
validated = validated =
validate(matching_form, params, validate(matching_form, params,
errors: errors?, errors: errors?,
prev_data_trail?: prev_data_trail prev_data_trail?: prev_data_trail
) )
|> Map.put(:as, form.name <> "[#{key}][#{index}]") |> Map.put(:as, form.name <> "[#{key}][#{index}]")
|> Map.put(:id, form.id <> "_#{key}_#{index}") |> Map.put(:id, form.id <> "_#{key}_#{index}")
Map.update(forms, key, [validated], fn nested_forms -> Map.update(forms, key, [validated], fn nested_forms ->
nested_forms ++ nested_forms ++
[validated] [validated]
end) end)
end
end end
end) end)
@ -898,35 +902,39 @@ defmodule AshPhoenix.Form do
new_forms new_forms
end end
else else
if form.forms[key] do if form_params["_ignore"] == "true" do
new_form = Map.put_new(forms, key, nil)
validate(form.forms[key], form_params, errors: errors?, matcher: matcher)
Map.put(forms, key, new_form)
else else
create_action = if form.forms[key] do
opts[:create_action] || new_form =
raise AshPhoenix.Form.NoActionConfigured, validate(form.forms[key], form_params, errors: errors?, matcher: matcher)
path: form.name <> "[#{key}]",
action: :create
resource = Map.put(forms, key, new_form)
opts[:create_resource] || opts[:resource] || else
raise AshPhoenix.Form.NoResourceConfigured, create_action =
path: form.name <> "[#{key}]" opts[:create_action] ||
raise AshPhoenix.Form.NoActionConfigured,
path: form.name <> "[#{key}]",
action: :create
new_form = resource =
for_action(resource, create_action, opts[:create_resource] || opts[:resource] ||
params: form_params, raise AshPhoenix.Form.NoResourceConfigured,
forms: opts[:forms] || [], path: form.name <> "[#{key}]"
errors: errors?,
prev_data_trail: prev_data_trail,
transform_errors: form.transform_errors,
as: form.name <> "[#{key}]",
id: form.id <> "_#{key}"
)
Map.put(forms, key, new_form) new_form =
for_action(resource, create_action,
params: form_params,
forms: opts[:forms] || [],
errors: errors?,
prev_data_trail: prev_data_trail,
transform_errors: form.transform_errors,
as: form.name <> "[#{key}]",
id: form.id <> "_#{key}"
)
Map.put(forms, key, new_form)
end
end end
end end
@ -2847,52 +2855,57 @@ defmodule AshPhoenix.Form do
transform_errors transform_errors
) do ) do
if (opts[:type] || :single) == :single do if (opts[:type] || :single) == :single do
if map(form_params)["_form_type"] == "read" do if map(form_params)["_ignore"] != "true" do
read_action = if map(form_params)["_form_type"] == "read" do
opts[:read_action] || read_action =
raise AshPhoenix.Form.NoActionConfigured, opts[:read_action] ||
path: Enum.reverse(trail, [key]), raise AshPhoenix.Form.NoActionConfigured,
action: :read path: Enum.reverse(trail, [key]),
action: :read
resource = resource =
opts[:read_resource] || opts[:resource] || opts[:read_resource] || opts[:resource] ||
raise AshPhoenix.Form.NoResourceConfigured, raise AshPhoenix.Form.NoResourceConfigured,
path: Enum.reverse(trail, [key]) path: Enum.reverse(trail, [key])
for_action(resource, read_action, for_action(resource, read_action,
params: form_params, params: form_params,
forms: opts[:forms] || [], forms: opts[:forms] || [],
errors: error?, errors: error?,
prev_data_trail: prev_data_trail, prev_data_trail: prev_data_trail,
transform_errors: transform_errors, transform_errors: transform_errors,
as: name <> "[#{key}]", as: name <> "[#{key}]",
id: id <> "_#{key}" id: id <> "_#{key}"
) )
else else
create_action = create_action =
opts[:create_action] || opts[:create_action] ||
raise AshPhoenix.Form.NoActionConfigured, raise AshPhoenix.Form.NoActionConfigured,
path: Enum.reverse(trail, [key]), path: Enum.reverse(trail, [key]),
action: :create action: :create
resource = resource =
opts[:create_resource] || opts[:resource] || opts[:create_resource] || opts[:resource] ||
raise AshPhoenix.Form.NoResourceConfigured, raise AshPhoenix.Form.NoResourceConfigured,
path: Enum.reverse(trail, [key]) path: Enum.reverse(trail, [key])
for_action(resource, create_action, for_action(resource, create_action,
params: form_params, params: form_params,
forms: opts[:forms] || [], forms: opts[:forms] || [],
errors: error?, errors: error?,
prev_data_trail: prev_data_trail, prev_data_trail: prev_data_trail,
transform_errors: transform_errors, transform_errors: transform_errors,
as: name <> "[#{key}]", as: name <> "[#{key}]",
id: id <> "_#{key}" id: id <> "_#{key}"
) )
end
end end
else else
form_params form_params
|> indexed_list() |> indexed_list()
|> Enum.reject(fn {form_params, _} ->
map(form_params)["_ignore"] == "true"
end)
|> Enum.with_index() |> Enum.with_index()
|> Enum.map(fn {{form_params, original_index}, index} -> |> Enum.map(fn {{form_params, original_index}, index} ->
if map(form_params)["_form_type"] == "read" do if map(form_params)["_form_type"] == "read" do
@ -2965,67 +2978,112 @@ defmodule AshPhoenix.Form do
end end
if (opts[:type] || :single) == :single do if (opts[:type] || :single) == :single do
if data do if map(form_params)["_ignore"] != "true" do
case map(form_params)["_form_type"] || "update" do if data do
"update" -> case map(form_params)["_form_type"] || "update" do
update_action = "update" ->
opts[:update_action] || update_action =
raise AshPhoenix.Form.NoActionConfigured, opts[:update_action] ||
path: Enum.reverse(trail, [key]), raise AshPhoenix.Form.NoActionConfigured,
action: :update path: Enum.reverse(trail, [key]),
action: :update
for_action(data, update_action, for_action(data, update_action,
params: form_params, params: form_params,
forms: opts[:forms] || [], forms: opts[:forms] || [],
errors: error?, errors: error?,
prev_data_trail: prev_data_trail, prev_data_trail: prev_data_trail,
transform_errors: transform_errors, transform_errors: transform_errors,
as: name <> "[#{key}]", as: name <> "[#{key}]",
id: id <> "_#{key}" id: id <> "_#{key}"
) )
"destroy" -> "destroy" ->
destroy_action = destroy_action =
opts[:destroy_action] || opts[:destroy_action] ||
raise AshPhoenix.Form.NoActionConfigured, raise AshPhoenix.Form.NoActionConfigured,
path: Enum.reverse(trail, [key]), path: Enum.reverse(trail, [key]),
action: :destroy action: :destroy
for_action(data, destroy_action, for_action(data, destroy_action,
params: form_params, params: form_params,
forms: opts[:forms] || [], forms: opts[:forms] || [],
errors: error?, errors: error?,
prev_data_trail: prev_data_trail, prev_data_trail: prev_data_trail,
transform_errors: transform_errors, transform_errors: transform_errors,
as: name <> "[#{key}]", as: name <> "[#{key}]",
id: id <> "_#{key}" id: id <> "_#{key}"
) )
end
else
case map(form_params)["_form_type"] || "create" do
"create" ->
create_action =
opts[:create_action] ||
raise AshPhoenix.Form.NoActionConfigured,
path: Enum.reverse(trail, [key]),
action: :create
resource =
opts[:create_resource] || opts[:resource] ||
raise AshPhoenix.Form.NoResourceConfigured,
path: Enum.reverse(trail, [key])
for_action(resource, create_action,
params: form_params,
forms: opts[:forms] || [],
errors: error?,
prev_data_trail: prev_data_trail,
transform_errors: transform_errors,
as: name <> "[#{key}]",
id: id <> "_#{key}"
)
"read" ->
resource =
opts[:read_resource] || opts[:resource] ||
raise AshPhoenix.Form.NoResourceConfigured,
path: Enum.reverse(trail, [key])
read_action =
opts[:read_action] ||
raise AshPhoenix.Form.NoActionConfigured,
path: Enum.reverse(trail, [key]),
action: :read
for_action(resource, read_action,
params: form_params,
forms: opts[:forms] || [],
errors: error?,
prev_data_trail: prev_data_trail,
transform_errors: transform_errors,
as: name <> "[#{key}]",
id: id <> "_#{key}"
)
other ->
raise "unexpected form type for form with no data #{other} with params: #{inspect(form_params)}"
end
end end
else end
case map(form_params)["_form_type"] || "create" do else
"create" -> data = List.wrap(data)
create_action =
opts[:create_action] ||
raise AshPhoenix.Form.NoActionConfigured,
path: Enum.reverse(trail, [key]),
action: :create
resource = form_params
opts[:create_resource] || opts[:resource] || |> indexed_list()
raise AshPhoenix.Form.NoResourceConfigured, |> Enum.with_index()
path: Enum.reverse(trail, [key]) |> Enum.reduce({[], List.wrap(data)}, fn {{form_params, original_index}, index},
{forms, data} ->
if form_params["_ignore"] == true do
case data do
[_item | rest] ->
{forms, rest}
for_action(resource, create_action, _ ->
params: form_params, {forms, data}
forms: opts[:forms] || [], end
errors: error?, else
prev_data_trail: prev_data_trail, if map(form_params)["_form_type"] == "read" do
transform_errors: transform_errors,
as: name <> "[#{key}]",
id: id <> "_#{key}"
)
"read" ->
resource = resource =
opts[:read_resource] || opts[:resource] || opts[:read_resource] || opts[:resource] ||
raise AshPhoenix.Form.NoResourceConfigured, raise AshPhoenix.Form.NoResourceConfigured,
@ -3037,90 +3095,35 @@ defmodule AshPhoenix.Form do
path: Enum.reverse(trail, [key]), path: Enum.reverse(trail, [key]),
action: :read action: :read
for_action(resource, read_action, form =
params: form_params, for_action(resource, read_action,
forms: opts[:forms] || [], params: add_index(form_params, original_index, opts),
errors: error?, forms: opts[:forms] || [],
prev_data_trail: prev_data_trail, errors: error?,
transform_errors: transform_errors, prev_data_trail: prev_data_trail,
as: name <> "[#{key}]", transform_errors: transform_errors,
id: id <> "_#{key}" as: name <> "[#{key}][#{index}]",
) id: id <> "_#{key}_#{index}"
)
other -> {[form | forms], data}
raise "unexpected form type for form with no data #{other} with params: #{inspect(form_params)}" else
end case find_form_match(data, form_params, opts) do
end [nil | rest] ->
else create_action =
data = List.wrap(data) opts[:create_action] ||
raise AshPhoenix.Form.NoActionConfigured,
path: Enum.reverse(trail, [key]),
action: :create
form_params resource =
|> indexed_list() opts[:create_resource] || opts[:resource] ||
|> Enum.with_index() raise AshPhoenix.Form.NoResourceConfigured,
|> Enum.reduce({[], List.wrap(data)}, fn {{form_params, original_index}, index}, path: Enum.reverse(trail, [key])
{forms, data} ->
if map(form_params)["_form_type"] == "read" do
resource =
opts[:read_resource] || opts[:resource] ||
raise AshPhoenix.Form.NoResourceConfigured,
path: Enum.reverse(trail, [key])
read_action = form =
opts[:read_action] || for_action(resource, create_action,
raise AshPhoenix.Form.NoActionConfigured, params: add_index(form_params, original_index, opts),
path: Enum.reverse(trail, [key]),
action: :read
form =
for_action(resource, read_action,
params: add_index(form_params, original_index, opts),
forms: opts[:forms] || [],
errors: error?,
prev_data_trail: prev_data_trail,
transform_errors: transform_errors,
as: name <> "[#{key}][#{index}]",
id: id <> "_#{key}_#{index}"
)
{[form | forms], data}
else
case find_form_match(data, form_params, opts) do
[nil | rest] ->
create_action =
opts[:create_action] ||
raise AshPhoenix.Form.NoActionConfigured,
path: Enum.reverse(trail, [key]),
action: :create
resource =
opts[:create_resource] || opts[:resource] ||
raise AshPhoenix.Form.NoResourceConfigured,
path: Enum.reverse(trail, [key])
form =
for_action(resource, create_action,
params: add_index(form_params, original_index, opts),
forms: opts[:forms] || [],
errors: error?,
prev_data_trail: prev_data_trail,
transform_errors: transform_errors,
as: name <> "[#{key}][#{index}]",
id: id <> "_#{key}_#{index}"
)
{[form | forms], rest}
[data | rest] ->
form =
if map(form_params)["_form_type"] == "destroy" do
destroy_action =
opts[:destroy_action] ||
raise AshPhoenix.Form.NoActionConfigured,
path: Enum.reverse(trail, [key]),
action: :destroy
for_action(data, destroy_action,
params: form_params,
forms: opts[:forms] || [], forms: opts[:forms] || [],
errors: error?, errors: error?,
prev_data_trail: prev_data_trail, prev_data_trail: prev_data_trail,
@ -3128,50 +3131,72 @@ defmodule AshPhoenix.Form do
as: name <> "[#{key}][#{index}]", as: name <> "[#{key}][#{index}]",
id: id <> "_#{key}_#{index}" id: id <> "_#{key}_#{index}"
) )
else
update_action =
opts[:update_action] ||
raise AshPhoenix.Form.NoActionConfigured,
path: Enum.reverse(trail, [key]),
action: :update
for_action(data, update_action, {[form | forms], rest}
[data | rest] ->
form =
if map(form_params)["_form_type"] == "destroy" do
destroy_action =
opts[:destroy_action] ||
raise AshPhoenix.Form.NoActionConfigured,
path: Enum.reverse(trail, [key]),
action: :destroy
for_action(data, destroy_action,
params: form_params,
forms: opts[:forms] || [],
errors: error?,
prev_data_trail: prev_data_trail,
transform_errors: transform_errors,
as: name <> "[#{key}][#{index}]",
id: id <> "_#{key}_#{index}"
)
else
update_action =
opts[:update_action] ||
raise AshPhoenix.Form.NoActionConfigured,
path: Enum.reverse(trail, [key]),
action: :update
for_action(data, update_action,
params: form_params,
forms: opts[:forms] || [],
errors: error?,
prev_data_trail: prev_data_trail,
transform_errors: transform_errors,
as: name <> "[#{key}][#{index}]",
id: id <> "_#{key}_#{index}"
)
end
{[form | forms], rest}
[] ->
resource =
opts[:create_resource] || opts[:resource] ||
raise AshPhoenix.Form.NoResourceConfigured,
path: Enum.reverse(trail, [key])
create_action =
opts[:create_action] ||
raise AshPhoenix.Form.NoActionConfigured,
path: Enum.reverse(trail, [key]),
action: :create
form =
for_action(resource, create_action,
params: form_params, params: form_params,
forms: opts[:forms] || [], forms: opts[:forms] || [],
errors: error?, errors: error?,
prev_data_trail: prev_data_trail,
transform_errors: transform_errors, transform_errors: transform_errors,
prev_data_trail: prev_data_trail,
as: name <> "[#{key}][#{index}]", as: name <> "[#{key}][#{index}]",
id: id <> "_#{key}_#{index}" id: id <> "_#{key}_#{index}"
) )
end
{[form | forms], rest} {[form | forms], []}
end
[] ->
resource =
opts[:create_resource] || opts[:resource] ||
raise AshPhoenix.Form.NoResourceConfigured,
path: Enum.reverse(trail, [key])
create_action =
opts[:create_action] ||
raise AshPhoenix.Form.NoActionConfigured,
path: Enum.reverse(trail, [key]),
action: :create
form =
for_action(resource, create_action,
params: form_params,
forms: opts[:forms] || [],
errors: error?,
transform_errors: transform_errors,
prev_data_trail: prev_data_trail,
as: name <> "[#{key}][#{index}]",
id: id <> "_#{key}_#{index}"
)
{[form | forms], []}
end end
end end
end) end)

View file

@ -117,6 +117,45 @@ defmodule AshPhoenix.FormTest do
assert Form.params(form) == %{"comments" => [%{"id" => comment.id}]} assert Form.params(form) == %{"comments" => [%{"id" => comment.id}]}
end end
test "ignoring a form filters it from the parameters" do
post =
Post
|> Ash.Changeset.new(%{text: "post"})
|> Api.create!()
comment =
Comment
|> Ash.Changeset.new(%{text: "comment"})
|> Ash.Changeset.replace_relationship(:post, post)
|> Api.create!()
form =
post
|> Form.for_update(:update_with_replace,
api: Api,
forms: [
comments: [
read_resource: Comment,
type: :list,
read_action: :read,
data: [comment]
]
]
)
assert [comment_form] = inputs_for(form_for(form, "blah"), :comments)
assert Phoenix.HTML.Form.input_value(comment_form, :text) == "comment"
form = Form.validate(form, %{"comments" => [%{"id" => comment.id, "_ignore" => "true"}]})
assert Form.params(form) == %{"comments" => []}
form = Form.validate(form, %{"comments" => [%{"id" => comment.id, "_ignore" => "false"}]})
assert Form.params(form) == %{"comments" => [%{"id" => comment.id}]}
end
describe "the .changed? field is updated as data changes" do describe "the .changed? field is updated as data changes" do
test "it is false for a create form with no changes" do test "it is false for a create form with no changes" do
form = form =