Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ spark_locals_without_parens = [
show?: 1,
show_action: 1,
show_calculations: 1,
show_relationship_fields: 1,
show_resources: 1,
show_sensitive_fields: 1,
table_columns: 1,
Expand Down
3 changes: 2 additions & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ if config_env() == :test do
config :ash_admin,
ash_domains: [
AshAdmin.Test.Domain,
AshAdmin.Test.ManyToManyDomain
AshAdmin.Test.ManyToManyDomain,
AshAdmin.Test.RelationshipFieldsDomain
]
end
1 change: 1 addition & 0 deletions documentation/dsls/DSL-AshAdmin.Resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Configure the admin dashboard for a given resource.
| [`resource_group`](#admin-resource_group){: #admin-resource_group } | `atom` | | The group in the top resource dropdown that the resource appears in. |
| [`show_sensitive_fields`](#admin-show_sensitive_fields){: #admin-show_sensitive_fields } | `list(atom)` | | The list of fields that should not be redacted in the admin UI even if they are marked as sensitive. |
| [`show_calculations`](#admin-show_calculations){: #admin-show_calculations } | `list(atom)` | | A list of calculations that can be loaded when this resource is shown. By default, no calculations are included. |
| [`show_relationship_fields`](#admin-show_relationship_fields){: #admin-show_relationship_fields } | `keyword` | | 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`](#admin-label_field){: #admin-label_field } | `atom` | | The field to use as the label when the resource appears in a relationship select or typeahead field on another resource's form. |
| [`relationship_select_max_items`](#admin-relationship_select_max_items){: #admin-relationship_select_max_items } | `integer` | `50` | The maximum number of items to show in a select field when this resource is shown as a relationship on another resource's form. If the number of related resources is higher, a typeahead selector will be used. |

Expand Down
22 changes: 16 additions & 6 deletions lib/ash_admin/components/resource/show.ex
Original file line number Diff line number Diff line change
Expand Up @@ -257,12 +257,13 @@ defmodule AshAdmin.Components.Resource.Show do
name: name,
destination: destination,
context: context,
destination_domain: destination_domain
destination_domain: destination_domain,
fields: AshAdmin.Resource.show_relationship_fields(assigns.resource, name)
)

~H"""
<div class="mb-10">
{render_attributes(assigns, @record, @destination, @name)}
{render_attributes(assigns, @record, @destination, @name, @fields)}
<div class="px-4 py-3 text-right sm:px-6">
<.link
:if={AshAdmin.Resource.show_action(@destination)}
Expand Down Expand Up @@ -306,14 +307,16 @@ 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"""
<div class="mb-10 overflow-scroll">
<Table.table
data={@data}
resource={@destination}
attributes={@fields}
format_fields={@destination_format_fields}
domain={@destination_domain || Ash.Resource.Info.domain(@destination) || @domain}
table={@context[:data_layer][:table]}
Expand All @@ -328,9 +331,16 @@ defmodule AshAdmin.Components.Resource.Show do
"""
end

defp render_attributes(assigns, record, resource, relationship_name \\ nil) do
{attributes, flags, bottom_attributes, _} =
AshAdmin.Components.Resource.Form.attributes(resource, :show)
defp render_attributes(assigns, record, resource, relationship_name \\ nil, fields \\ nil) do
{attributes, flags, bottom_attributes} =
case AshAdmin.Components.Resource.Form.attributes(resource, :show) do
{attributes, flags, bottom_attributes, _} when is_nil(fields) ->
{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,
Expand Down
15 changes: 14 additions & 1 deletion lib/ash_admin/resource/resource.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 """
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# SPDX-FileCopyrightText: 2020 Zach Daniel
# SPDX-FileCopyrightText: 2020 ash_admin contributors <https://github.com/ash-project/ash_admin/graphs/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
107 changes: 107 additions & 0 deletions test/components/resource/show_relationship_fields_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# SPDX-FileCopyrightText: 2020 ash_admin contributors <https://github.com/ash-project/ash_admin/graphs/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
102 changes: 102 additions & 0 deletions test/support/resources/relationship_fields.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# SPDX-FileCopyrightText: 2020 ash_admin contributors <https://github.com/ash-project/ash_admin/graphs/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
Loading