diff --git a/demo/lib/demo_web/item_actions/duplicate_tag.ex b/demo/lib/demo_web/item_actions/duplicate_tag.ex index 0522d684..3e9d5c39 100644 --- a/demo/lib/demo_web/item_actions/duplicate_tag.ex +++ b/demo/lib/demo_web/item_actions/duplicate_tag.ex @@ -70,6 +70,6 @@ defmodule DemoWeb.ItemActions.DuplicateTag do put_flash(socket, :error, "Error while duplicating item.") end - {:noreply, socket} + {:ok, socket} end end diff --git a/demo/lib/demo_web/item_actions/soft_delete.ex b/demo/lib/demo_web/item_actions/soft_delete.ex index da5e1528..c74c303b 100644 --- a/demo/lib/demo_web/item_actions/soft_delete.ex +++ b/demo/lib/demo_web/item_actions/soft_delete.ex @@ -78,7 +78,7 @@ defmodule DemoWeb.ItemActions.SoftDelete do |> put_flash(:error, error_message(socket.assigns, error, items)) end - {:noreply, socket} + {:ok, socket} end defp success_message(assigns, [_item]) do diff --git a/demo/lib/demo_web/resource_actions/email.ex b/demo/lib/demo_web/resource_actions/email.ex index 586a71b1..4195d2ec 100644 --- a/demo/lib/demo_web/resource_actions/email.ex +++ b/demo/lib/demo_web/resource_actions/email.ex @@ -37,10 +37,12 @@ defmodule DemoWeb.ResourceActions.Email do end @impl Backpex.ResourceAction - def handle(_socket, _data) do + def handle(socket, _data) do # Send mail # We suppose there was no error. - {:ok, "An email has been successfully sent to the specified users."} + socket = Phoenix.LiveView.put_flash(socket, :info, "An email has been successfully sent to the specified users.") + + {:ok, socket} end end diff --git a/demo/lib/demo_web/resource_actions/upload.ex b/demo/lib/demo_web/resource_actions/upload.ex index eb663955..1783793c 100644 --- a/demo/lib/demo_web/resource_actions/upload.ex +++ b/demo/lib/demo_web/resource_actions/upload.ex @@ -54,7 +54,11 @@ defmodule DemoWeb.ResourceActions.Upload do end @impl Backpex.ResourceAction - def handle(_socket, _data), do: {:ok, "File was uploaded successfully."} + def handle(socket, _data) do + socket = Phoenix.LiveView.put_flash(socket, :info, "File was uploaded successfully.") + + {:ok, socket} + end defp list_existing_files(_item), do: [] diff --git a/guides/upgrading/v0.9.md b/guides/upgrading/v0.9.md index 2d4be226..363284e2 100644 --- a/guides/upgrading/v0.9.md +++ b/guides/upgrading/v0.9.md @@ -71,6 +71,15 @@ end Although the change is relatively small, if you are using public functions of the `Backpex.LiveResource` directly, check the updated function definitions in the module documentation. -## Resource Action ad Item Action `init_change/1` is renamed +## Resource Action and Item Action `init_change/1` is renamed The term `init_change` was confusing because the result is being used as the base schema / item for the changeset function. Therefore we renamed the function to `base_schema/1` for both Item Actions and Resource Actions. + +## Resource Action and Item Action `handle` functions behave differently + +Both Item Action and Resource Action `handle` functions now have to return either `{:ok, socket}` or `{:error, changeset}`. A flash message is no longer added to the socket automatically. + +Make sure to read the improved documentation for the `handle` functions to understand how you should use them now: + +- `c:Backpex.ItemAction.handle/3` +- `c:Backpex.ResourceAction.handle/2` diff --git a/lib/backpex/item_actions/delete.ex b/lib/backpex/item_actions/delete.ex index 093a0eae..1bbbca11 100644 --- a/lib/backpex/item_actions/delete.ex +++ b/lib/backpex/item_actions/delete.ex @@ -60,7 +60,7 @@ defmodule Backpex.ItemActions.Delete do |> put_flash(:error, error_message(socket.assigns, error, items)) end - {:noreply, socket} + {:ok, socket} end defp success_message(assigns, [_item]) do diff --git a/lib/backpex/item_actions/edit.ex b/lib/backpex/item_actions/edit.ex index 32391486..19ca4d99 100644 --- a/lib/backpex/item_actions/edit.ex +++ b/lib/backpex/item_actions/edit.ex @@ -22,6 +22,6 @@ defmodule Backpex.ItemActions.Edit do def handle(socket, [item | _items], _data) do path = Router.get_path(socket, socket.assigns.live_resource, socket.assigns.params, :edit, item) - {:noreply, Phoenix.LiveView.push_patch(socket, to: path)} + {:ok, Phoenix.LiveView.push_patch(socket, to: path)} end end diff --git a/lib/backpex/item_actions/item_action.ex b/lib/backpex/item_actions/item_action.ex index 37d4c95e..2e98b049 100644 --- a/lib/backpex/item_actions/item_action.ex +++ b/lib/backpex/item_actions/item_action.ex @@ -74,10 +74,20 @@ defmodule Backpex.ItemAction do @callback confirm(assigns :: map()) :: binary() @doc """ - Performs the action. It takes the socket and the casted and validated data (received from [`Ecto.Changeset.apply_action/2`](https://hexdocs.pm/ecto/Ecto.Changeset.html#apply_action/2)). + Performs the action. It takes the socket, the list of affected items, and the casted and validated data (received from [`Ecto.Changeset.apply_action/2`](https://hexdocs.pm/ecto/Ecto.Changeset.html#apply_action/2)). + + You must return either `{:ok, socket}` or `{:error, changeset}`. + + If `{:ok, socket}` is returned, the action is considered successful by Backpex and the action modal is closed. However, you can add an error flash message to the socket to indicate that something has gone wrong. + + If `{:error, changeset}` is returned, the changeset is used to update the form to display the errors. Note that Backpex already validates the form for you. + Therefore it is only necessary in rare cases to perform additional validation and return a changeset from `c:handle/3`. + For example, if you are building a duplicate action and can only check for a unique constraint when inserting the duplicate element. + + You are only allowed to return `{:error, changeset}` if the action has a form. Otherwise Backpex will throw an ArgumentError. """ @callback handle(socket :: Phoenix.LiveView.Socket.t(), items :: list(map()), params :: map() | struct()) :: - {:noreply, Phoenix.LiveView.Socket.t()} | {:reply, map(), Phoenix.LiveView.Socket.t()} + {:ok, Phoenix.LiveView.Socket.t()} | {:error, Ecto.Changeset.t()} @optional_callbacks confirm: 1, confirm_label: 1, cancel_label: 1, changeset: 3, fields: 0 diff --git a/lib/backpex/item_actions/show.ex b/lib/backpex/item_actions/show.ex index 71b5f602..fba39f19 100644 --- a/lib/backpex/item_actions/show.ex +++ b/lib/backpex/item_actions/show.ex @@ -21,6 +21,6 @@ defmodule Backpex.ItemActions.Show do @impl Backpex.ItemAction def handle(socket, [item | _items], _data) do path = Router.get_path(socket, socket.assigns.live_resource, socket.assigns.params, :show, item) - {:noreply, Phoenix.LiveView.push_patch(socket, to: path)} + {:ok, Phoenix.LiveView.push_patch(socket, to: path)} end end diff --git a/lib/backpex/live_components/form_component.ex b/lib/backpex/live_components/form_component.ex index ded6166b..750e9c2c 100644 --- a/lib/backpex/live_components/form_component.ex +++ b/lib/backpex/live_components/form_component.ex @@ -291,26 +291,24 @@ defmodule Backpex.FormComponent do } = socket assocs = Map.get(assigns, :assocs, []) + params = drop_readonly_changes(params, fields, assigns) result = item |> Resource.change(params, fields, assigns, live_resource, assocs: assocs) |> Ecto.Changeset.apply_action(:insert) - case result do - {:ok, data} -> - resource_action_result = resource_action.module.handle(socket, data) + with {:ok, data} <- result, + {:ok, socket} <- resource_action.module.handle(socket, data) do + handle_uploads(socket, data) - if match?({:ok, _msg}, resource_action_result), do: handle_uploads(socket, data) - - socket = - socket - |> assign(:show_form_errors, false) - |> put_flash_message(resource_action_result) - |> push_navigate(to: return_to) - - {:noreply, socket} + socket = + socket + |> assign(:show_form_errors, false) + |> push_navigate(to: return_to) + {:noreply, socket} + else {:error, changeset} -> form = Phoenix.Component.to_form(changeset, as: :change) @@ -322,6 +320,16 @@ defmodule Backpex.FormComponent do send(self(), {:update_changeset, changeset}) {:noreply, socket} + + unexpected_return -> + raise ArgumentError, """ + Invalid return value from #{inspect(resource_action.module)}.handle/2. + + Expected: {:ok, socket} or {:error, changeset} + Got: #{inspect(unexpected_return)} + + Resource Actions must return {:ok, socket} or {:error, changeset}. + """ end end @@ -338,27 +346,23 @@ defmodule Backpex.FormComponent do } = assigns } = socket + params = drop_readonly_changes(params, fields, assigns) + result = item |> Backpex.Resource.change(params, fields, assigns, live_resource) |> Ecto.Changeset.apply_action(:insert) - case result do - {:ok, data} -> - selected_items = - Enum.filter(selected_items, fn item -> - live_resource.can?(socket.assigns, action_key, item) - end) - - {message, socket} = - socket - |> assign(:show_form_errors, false) - |> assign(selected_items: []) - |> assign(select_all: false) - |> action_to_confirm.module.handle(selected_items, data) - - {message, push_patch(socket, to: return_to)} + with {:ok, data} <- result, + selected_items <- Enum.filter(selected_items, &live_resource.can?(socket.assigns, action_key, &1)), + {:ok, socket} <- action_to_confirm.module.handle(socket, selected_items, data) do + socket + |> assign(:show_form_errors, false) + |> assign(:selected_items, []) + |> assign(:select_all, false) + {:noreply, push_patch(socket, to: return_to)} + else {:error, changeset} -> form = Phoenix.Component.to_form(changeset, as: :change) @@ -368,6 +372,16 @@ defmodule Backpex.FormComponent do |> assign(:form, form) {:noreply, socket} + + unexpected_return -> + raise ArgumentError, """ + Invalid return value from #{inspect(action_to_confirm.module)}.handle/2. + + Expected: {:ok, socket} or {:error, changeset} + Got: #{inspect(unexpected_return)} + + Item Actions with form fields must return {:ok, socket} or {:error, changeset}. + """ end end @@ -380,15 +394,6 @@ defmodule Backpex.FormComponent do Map.drop(change, read_only) end - defp put_flash_message(socket, {type, msg}) do - socket - |> clear_flash() - |> put_flash(flash_key(type), msg) - end - - defp flash_key(:ok), do: :info - defp flash_key(:error), do: :error - defp put_upload_change(change, socket, action) do Enum.reduce(socket.assigns.fields, change, fn {_name, %{upload_key: upload_key} = field_options} = _field, acc -> diff --git a/lib/backpex/live_resource.ex b/lib/backpex/live_resource.ex index dc5e97ce..d21e79ad 100644 --- a/lib/backpex/live_resource.ex +++ b/lib/backpex/live_resource.ex @@ -1199,11 +1199,25 @@ defmodule Backpex.LiveResource do %{live_resource: live_resource} = socket.assigns items = Enum.filter(items, fn item -> live_resource.can?(socket.assigns, key, item) end) - socket - |> assign(action_to_confirm: nil) - |> assign(selected_items: []) - |> assign(select_all: false) - |> action.module.handle(items, %{}) + case action.module.handle(socket, items, %{}) do + {:ok, socket} -> + socket + |> assign(action_to_confirm: nil) + |> assign(selected_items: []) + |> assign(select_all: false) + + {:noreply, socket} + + unexpected_return -> + raise ArgumentError, """ + Invalid return value from #{inspect(action.module)}.handle/3. + + Expected: {:ok, socket} + Got: #{inspect(unexpected_return)} + + Item Actions with no form fields must return {:ok, socket}. + """ + end end defp primary_value(socket, item) do diff --git a/lib/backpex/resource_action.ex b/lib/backpex/resource_action.ex index ccb76b06..22445126 100644 --- a/lib/backpex/resource_action.ex +++ b/lib/backpex/resource_action.ex @@ -54,12 +54,18 @@ defmodule Backpex.ResourceAction do ) :: Ecto.Changeset.t() @doc """ - The handle function for the corresponding action. It receives the socket and casted and validated data (received from [`Ecto.Changeset.apply_action/2`](https://hexdocs.pm/ecto/Ecto.Changeset.html#apply_action/2)) and will be called when the form is valid and submitted. + Performs the action. It takes the socket and the casted and validated data (received from [`Ecto.Changeset.apply_action/2`](https://hexdocs.pm/ecto/Ecto.Changeset.html#apply_action/2)). - It must return either `{:ok, binary()}` or `{:error, binary()}` + You must return either `{:ok, socket}` or `{:error, changeset}`. + + If `{:ok, socket}` is returned, the action is considered successful by Backpex and the action modal is closed. However, you can add an error flash message to the socket to indicate that something has gone wrong. + + If `{:error, changeset}` is returned, the changeset is used to update the form to display the errors. Note that Backpex already validates the form for you. Therefore it is only necessary in rare cases to perform additional validation and return a changeset from `c:handle/3`. + + You have to use `Phoenix.LiveView.put_flash/3` along with the socket to show a success or error message. """ @callback handle(socket :: Phoenix.LiveView.Socket.t(), data :: map()) :: - {:ok, binary()} | {:error, binary()} + {:ok, Phoenix.LiveView.Socket.t()} | {:error, Ecto.Changeset.t()} @doc """ Defines `Backpex.ResourceAction` behaviour.