From dbf871629588e89bc4ea45156aff016654a83359 Mon Sep 17 00:00:00 2001 From: Peter Shoukry Date: Sun, 21 Dec 2025 12:03:30 +0200 Subject: [PATCH] fix: add catch-all can?/3 clause for custom item actions When using custom item_actions in an AshBackpex.LiveResource, the page would crash with FunctionClauseError because the transformer only generated can?/3 clauses for standard actions (:new, :index, :show, :edit, :delete). The fix adds a fallback can?/3 clause that: - Returns true for actions that don't exist on the Ash resource - Checks Ash authorization for actions that do exist on the resource This mirrors the base Backpex.LiveResource behavior while integrating with Ash's authorization system. --- .../transformers/generate_backpex.ex | 19 ++++++++ test/ash_backpex/authz_test.exs | 14 ++++++ test/support/test_live.ex | 43 +++++++++++++++++++ 3 files changed, 76 insertions(+) diff --git a/lib/ash_backpex/live_resource/transformers/generate_backpex.ex b/lib/ash_backpex/live_resource/transformers/generate_backpex.ex index 3cc6f82..60f9e3d 100644 --- a/lib/ash_backpex/live_resource/transformers/generate_backpex.ex +++ b/lib/ash_backpex/live_resource/transformers/generate_backpex.ex @@ -408,6 +408,25 @@ defmodule AshBackpex.LiveResource.Transformers.GenerateBackpex do def can?(_assigns, :delete, _item), do: false end + # Fallback for custom item actions and any other actions + # Checks Ash authorization if a matching action exists, otherwise allows by default + def can?(assigns, action, item) do + case Ash.Resource.Info.action(@resource, action) do + nil -> + true + + ash_action -> + target = + if is_struct(item) and item.__struct__ == @resource do + {item, ash_action.name} + else + {@resource, ash_action.name} + end + + Ash.can?(target, Map.get(assigns, :current_user)) + end + end + def maybe_default_options(assigns) do case assigns do %{field: {attribute_name, _field_cfg}} -> diff --git a/test/ash_backpex/authz_test.exs b/test/ash_backpex/authz_test.exs index 8ff3ae6..79c1d05 100644 --- a/test/ash_backpex/authz_test.exs +++ b/test/ash_backpex/authz_test.exs @@ -41,6 +41,20 @@ defmodule AshBackpex.AuthzTest do end end + describe "AshBackpex.LiveResource :: can? with custom item actions" do + test "returns true for unknown actions that don't exist on the resource" do + # :promote is not an Ash action on Item, so fallback returns true + assert TestCustomItemActionLive.can?(%{current_user: nil}, :promote, %{}) + assert TestCustomItemActionLive.can?(%{current_user: nil}, :some_unknown_action, %{}) + end + + test "checks Ash authorization when action exists on resource" do + # :read exists on Item resource, so it checks Ash.can? + # Item has no policies, so it should return true + assert TestCustomItemActionLive.can?(%{current_user: nil}, :read, %{}) + end + end + describe "AshBackpex.Adapter :: it can" do test "list/3" do user = user() diff --git a/test/support/test_live.ex b/test/support/test_live.ex index f21b20a..fac88f1 100644 --- a/test/support/test_live.ex +++ b/test/support/test_live.ex @@ -113,3 +113,46 @@ defmodule TestLayout do """ end end + +# Custom item action for testing can?/3 fallback +defmodule TestPromoteItemAction do + @moduledoc false + use BackpexWeb, :item_action + + @impl Backpex.ItemAction + def icon(assigns, _item) do + ~H""" + + """ + end + + @impl Backpex.ItemAction + def label(_assigns, _item), do: "Promote" + + @impl Backpex.ItemAction + def handle(socket, _items, _data) do + {:ok, socket} + end +end + +# LiveResource with custom item action for testing can?/3 fallback +defmodule TestCustomItemActionLive do + @moduledoc false + use AshBackpex.LiveResource + + backpex do + resource(AshBackpex.TestDomain.Item) + layout({TestLayout, :admin}) + + item_actions do + action :promote, TestPromoteItemAction + end + + fields do + field(:name) + end + end +end