- {render_attributes(assigns, @record, @destination, @name)}
+ {render_attributes(assigns, @record, @destination, @name, @fields)}
<.link
:if={AshAdmin.Resource.show_action(@destination)}
@@ -306,7 +307,8 @@ defmodule AshAdmin.Components.Resource.Show do
destination_format_fields: AshAdmin.Resource.format_fields(destination),
context: context,
destination_attribute: destination_attribute,
- relationship_name: name
+ relationship_name: name,
+ fields: AshAdmin.Resource.show_relationship_fields(assigns.resource, name)
)
~H"""
@@ -314,6 +316,7 @@ defmodule AshAdmin.Components.Resource.Show do
+ {attributes, flags, bottom_attributes}
+
+ _ ->
+ # An explicit field list keeps the given order in a single grid.
+ {Enum.map(fields, &Ash.Resource.Info.attribute(resource, &1)), [], []}
+ end
assigns =
assign(assigns,
diff --git a/lib/ash_admin/resource/resource.ex b/lib/ash_admin/resource/resource.ex
index c659e196..2a6ca110 100644
--- a/lib/ash_admin/resource/resource.ex
+++ b/lib/ash_admin/resource/resource.ex
@@ -124,6 +124,12 @@ defmodule AshAdmin.Resource do
doc:
"A list of calculations that can be loaded when this resource is shown. By default, no calculations are included."
],
+ show_relationship_fields: [
+ type: :keyword_list,
+ doc: """
+ The fields to render for each relationship on this resource's show page, keyed by relationship name. For example: `show_relationship_fields organizations: [:name, :slug]`. Fields are shown in the given order. Relationships that are not listed show every attribute.
+ """
+ ],
label_field: [
type: :atom,
doc:
@@ -145,7 +151,8 @@ defmodule AshAdmin.Resource do
AshAdmin.Resource.Transformers.AddPositionSortCalculation
],
verifiers: [
- AshAdmin.Resource.Verifiers.VerifyFileArgumentsExist
+ AshAdmin.Resource.Verifiers.VerifyFileArgumentsExist,
+ AshAdmin.Resource.Verifiers.VerifyShowRelationshipFields
]
@moduledoc """
@@ -271,6 +278,12 @@ defmodule AshAdmin.Resource do
Spark.Dsl.Extension.get_opt(resource, [:admin], :show_calculations, [], true)
end
+ def show_relationship_fields(resource, relationship) do
+ resource
+ |> Spark.Dsl.Extension.get_opt([:admin], :show_relationship_fields, [], true)
+ |> Keyword.get(relationship)
+ end
+
def fields(resource) do
Spark.Dsl.Extension.get_entities(resource, [:admin, :form])
end
diff --git a/lib/ash_admin/resource/verifiers/verify_show_relationship_fields.ex b/lib/ash_admin/resource/verifiers/verify_show_relationship_fields.ex
new file mode 100644
index 00000000..9c20640c
--- /dev/null
+++ b/lib/ash_admin/resource/verifiers/verify_show_relationship_fields.ex
@@ -0,0 +1,61 @@
+# SPDX-FileCopyrightText: 2020 Zach Daniel
+# SPDX-FileCopyrightText: 2020 ash_admin contributors
+#
+# SPDX-License-Identifier: MIT
+
+defmodule AshAdmin.Resource.Verifiers.VerifyShowRelationshipFields do
+ @moduledoc """
+ Ensures that every key of `show_relationship_fields` is a relationship of the
+ resource, and that every field is an attribute of the destination resource.
+ """
+
+ use Spark.Dsl.Verifier
+
+ alias Spark.Dsl.Verifier
+
+ def verify(dsl_state) do
+ dsl_state
+ |> Verifier.get_option([:admin], :show_relationship_fields, [])
+ |> Enum.reduce_while(:ok, fn {relationship_name, fields}, :ok ->
+ case check(dsl_state, relationship_name, fields) do
+ :ok -> {:cont, :ok}
+ {:error, message} -> {:halt, {:error, error(dsl_state, message)}}
+ end
+ end)
+ end
+
+ defp check(dsl_state, relationship_name, fields) do
+ case Ash.Resource.Info.relationship(dsl_state, relationship_name) do
+ nil ->
+ {:error, "`#{inspect(relationship_name)}` is not a relationship of this resource."}
+
+ %{destination: destination} when is_list(fields) ->
+ valid = destination |> Ash.Resource.Info.attributes() |> Enum.map(& &1.name)
+
+ case Enum.reject(fields, &(&1 in valid)) do
+ [] ->
+ :ok
+
+ invalid ->
+ {:error,
+ """
+ Invalid fields for relationship `#{inspect(relationship_name)}`: #{inspect(invalid)}
+ Only attributes of #{inspect(destination)} are allowed.
+ Valid fields: #{inspect(valid)}
+ """}
+ end
+
+ _ ->
+ {:error,
+ "Fields for relationship `#{inspect(relationship_name)}` must be a list of atoms."}
+ end
+ end
+
+ defp error(dsl_state, message) do
+ Spark.Error.DslError.exception(
+ message: message,
+ path: [:admin, :show_relationship_fields],
+ module: Verifier.get_persisted(dsl_state, :module)
+ )
+ end
+end
diff --git a/test/components/resource/show_relationship_fields_test.exs b/test/components/resource/show_relationship_fields_test.exs
new file mode 100644
index 00000000..30a29b60
--- /dev/null
+++ b/test/components/resource/show_relationship_fields_test.exs
@@ -0,0 +1,107 @@
+# SPDX-FileCopyrightText: 2020 ash_admin contributors
+#
+# SPDX-License-Identifier: MIT
+
+defmodule AshAdmin.Test.Components.Resource.ShowRelationshipFieldsTest do
+ use ExUnit.Case, async: false
+
+ import Phoenix.ConnTest
+ import Phoenix.LiveViewTest
+ @endpoint AshAdmin.Test.Endpoint
+
+ alias AshAdmin.Test.{
+ RelationshipFieldsArticle,
+ RelationshipFieldsAuthor,
+ RelationshipFieldsBook
+ }
+
+ setup do
+ favorite = Ash.create!(RelationshipFieldsBook, %{title: "Favorite", isbn: "978-1"})
+
+ author =
+ Ash.create!(RelationshipFieldsAuthor, %{name: "Ada", favorite_book_id: favorite.id})
+
+ Ash.create!(RelationshipFieldsBook, %{title: "Notes", isbn: "978-0", author_id: author.id})
+
+ Ash.create!(RelationshipFieldsArticle, %{
+ headline: "Analytical engine",
+ summary: "A summary",
+ author_id: author.id
+ })
+
+ {:ok, view, _html} = live(build_conn(), show_path(author))
+
+ %{view: view}
+ end
+
+ defp show_path(author) do
+ AshAdmin.Helpers.admin_path("/api/admin",
+ domain: AshAdmin.Domain.name(AshAdmin.Test.RelationshipFieldsDomain),
+ resource: "RelationshipFieldsAuthor",
+ primary_key: author.id,
+ action_type: "read"
+ )
+ end
+
+ defp load(view, relationship) do
+ tab = "#relationship-tab-#{relationship}"
+ if has_element?(view, tab), do: view |> element(tab) |> render_click()
+
+ view
+ |> element(~s|button[phx-click=load][phx-value-relationship="#{relationship}"]|)
+ |> render_click()
+ end
+
+ test "a has_many relationship only shows the configured columns", %{view: view} do
+ html = load(view, :books)
+
+ assert html =~ "Title"
+ assert html =~ "Notes"
+ refute html =~ "Isbn"
+ refute html =~ "978-0"
+ end
+
+ test "a belongs_to relationship only shows the configured fields", %{view: view} do
+ html = render(view)
+
+ assert html =~ "Favorite"
+ refute html =~ "Isbn"
+ refute html =~ "978-1"
+ end
+
+ test "a relationship without configuration shows every attribute", %{view: view} do
+ html = load(view, :articles)
+
+ assert html =~ "Headline"
+ assert html =~ "Summary"
+ assert html =~ "A summary"
+ end
+
+ test "the verifier rejects an unknown relationship" do
+ dsl =
+ put_in(
+ RelationshipFieldsAuthor.spark_dsl_config(),
+ [[:admin], :opts, :show_relationship_fields],
+ nope: [:title]
+ )
+
+ assert {:error, %Spark.Error.DslError{message: message}} =
+ AshAdmin.Resource.Verifiers.VerifyShowRelationshipFields.verify(dsl)
+
+ assert message =~ "is not a relationship"
+ end
+
+ test "the verifier rejects a field that is not an attribute of the destination" do
+ dsl =
+ put_in(
+ RelationshipFieldsAuthor.spark_dsl_config(),
+ [[:admin], :opts, :show_relationship_fields],
+ books: [:nope]
+ )
+
+ assert {:error, %Spark.Error.DslError{message: message}} =
+ AshAdmin.Resource.Verifiers.VerifyShowRelationshipFields.verify(dsl)
+
+ assert message =~ "Invalid fields"
+ end
+end
diff --git a/test/support/resources/relationship_fields.ex b/test/support/resources/relationship_fields.ex
new file mode 100644
index 00000000..b22e9b83
--- /dev/null
+++ b/test/support/resources/relationship_fields.ex
@@ -0,0 +1,102 @@
+# SPDX-FileCopyrightText: 2020 ash_admin contributors
+#
+# SPDX-License-Identifier: MIT
+
+# Fixtures for `show_relationship_fields`: the fields rendered for each
+# relationship on a show page.
+
+defmodule AshAdmin.Test.RelationshipFieldsDomain do
+ @moduledoc false
+ use Ash.Domain,
+ extensions: [AshAdmin.Domain]
+
+ admin do
+ show? true
+ end
+
+ resources do
+ resource(AshAdmin.Test.RelationshipFieldsAuthor)
+ resource(AshAdmin.Test.RelationshipFieldsBook)
+ resource(AshAdmin.Test.RelationshipFieldsArticle)
+ end
+end
+
+defmodule AshAdmin.Test.RelationshipFieldsAuthor do
+ @moduledoc false
+ use Ash.Resource,
+ domain: AshAdmin.Test.RelationshipFieldsDomain,
+ data_layer: Ash.DataLayer.Ets,
+ extensions: [AshAdmin.Resource]
+
+ attributes do
+ uuid_primary_key(:id)
+ attribute(:name, :string, public?: true)
+ end
+
+ actions do
+ default_accept(:*)
+ defaults([:read, :create])
+ end
+
+ admin do
+ show_relationship_fields(books: [:title], favorite_book: [:title])
+ end
+
+ relationships do
+ has_many(:books, AshAdmin.Test.RelationshipFieldsBook,
+ public?: true,
+ destination_attribute: :author_id
+ )
+
+ has_many(:articles, AshAdmin.Test.RelationshipFieldsArticle,
+ public?: true,
+ destination_attribute: :author_id
+ )
+
+ belongs_to(:favorite_book, AshAdmin.Test.RelationshipFieldsBook, public?: true)
+ end
+end
+
+defmodule AshAdmin.Test.RelationshipFieldsBook do
+ @moduledoc false
+ use Ash.Resource,
+ domain: AshAdmin.Test.RelationshipFieldsDomain,
+ data_layer: Ash.DataLayer.Ets
+
+ attributes do
+ uuid_primary_key(:id)
+ attribute(:title, :string, public?: true)
+ attribute(:isbn, :string, public?: true)
+ end
+
+ actions do
+ default_accept(:*)
+ defaults([:read, :create])
+ end
+
+ relationships do
+ belongs_to(:author, AshAdmin.Test.RelationshipFieldsAuthor, public?: true)
+ end
+end
+
+defmodule AshAdmin.Test.RelationshipFieldsArticle do
+ @moduledoc false
+ use Ash.Resource,
+ domain: AshAdmin.Test.RelationshipFieldsDomain,
+ data_layer: Ash.DataLayer.Ets
+
+ attributes do
+ uuid_primary_key(:id)
+ attribute(:headline, :string, public?: true)
+ attribute(:summary, :string, public?: true)
+ end
+
+ actions do
+ default_accept(:*)
+ defaults([:read, :create])
+ end
+
+ relationships do
+ belongs_to(:author, AshAdmin.Test.RelationshipFieldsAuthor, public?: true)
+ end
+end