Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Select / typeahead combo for foreign key field #222

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ spark_locals_without_parens = [
field: 2,
format_fields: 1,
generic_actions: 1,
label_field: 1,
name: 1,
polymorphic_actions: 1,
polymorphic_tables: 1,
read_actions: 1,
relationship_display_fields: 1,
relationship_select_max_items: 1,
resource_group: 1,
resource_group_labels: 1,
show?: 1,
Expand Down
43 changes: 40 additions & 3 deletions assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Hooks.JsonEditor = {
target.dispatchEvent(
new Event("change", { bubbles: true, target: this.el.name }),
);
} catch (_e) {}
} catch (_e) { }
},
onChange: () => {
try {
Expand All @@ -37,7 +37,7 @@ Hooks.JsonEditor = {
target.dispatchEvent(
new Event("change", { bubbles: true, target: this.el.name }),
);
} catch (_e) {}
} catch (_e) { }
},
onModeChange: (newMode) => {
hook.mode = newMode;
Expand All @@ -63,7 +63,7 @@ Hooks.JsonEditorSource = {
} else {
}
}
} catch (_e) {}
} catch (_e) { }
},
};

Expand Down Expand Up @@ -163,6 +163,43 @@ Hooks.MaintainAttrs = {
},
};

Hooks.Typeahead = {
mounted() {
const target_id = this.el.getAttribute("data-target-id");
const target_el = document.getElementById(target_id);

switch (this.el.tagName) {
case "INPUT":
this.el.addEventListener("keydown", e => {
if (e.key === "Enter") {
e.preventDefault();
}
});
this.el.addEventListener("keyup", e => {
switch (e.key) {
case "Enter":
case "Escape":
this.el.blur();
window.setTimeout(function () { target_el.dispatchEvent(new Event("input", { bubbles: true })) }, 750);
break;
}
});
break;

case "LI":
this.el.addEventListener("click", e => {
window.setTimeout(function () { target_el.dispatchEvent(new Event("input", { bubbles: true })) }, 750);
});
break;
}
},
updated() {
if (this.el.tagName === "INPUT" && this.el.name.match(/suggest$/) && this.el.value.length === 0) {
this.el.focus();
}
}
};

function getCookie(name) {
var re = new RegExp(name + "=([^;]+)");
var value = re.exec(document.cookie);
Expand Down
5 changes: 5 additions & 0 deletions dev/resources/tickets/resources/customer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ defmodule Demo.Tickets.Customer do

admin do
relationship_display_fields [:id, :first_name]
label_field :full_name
end

resource do
Expand Down Expand Up @@ -46,6 +47,10 @@ defmodule Demo.Tickets.Customer do
attribute :representative, :boolean, public?: true
end

calculations do
calculate :full_name, :string, concat([:first_name, :last_name], " ")
end

relationships do
has_many :reported_tickets, Demo.Tickets.Ticket do
destination_attribute :reporter_id
Expand Down
10 changes: 9 additions & 1 deletion dev/resources/tickets/resources/organization.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
defmodule Demo.Tickets.Organization do
use Ash.Resource,
domain: Demo.Tickets.Domain,
data_layer: AshPostgres.DataLayer
data_layer: AshPostgres.DataLayer,
extensions: [
AshAdmin.Resource
]

postgres do
table "organizations"
Expand All @@ -13,6 +16,11 @@ defmodule Demo.Tickets.Organization do
defaults [:create, :read, :update, :destroy]
end

admin do
label_field :name
relationship_select_max_items 2
end

attributes do
uuid_primary_key :id

Expand Down
7 changes: 4 additions & 3 deletions dev/resources/tickets/resources/representative.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ defmodule Demo.Tickets.Representative do
AshAdmin.Resource
]

admin do
relationship_display_fields [:id, :first_name]
end
admin do
relationship_display_fields [:id, :first_name]
label_field :full_name
end

resource do
base_filter representative: true
Expand Down
2 changes: 2 additions & 0 deletions documentation/dsls/DSL-AshAdmin.Resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ 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 calculation that can be calculate when this resource is shown. By default, all calculations are included. |
| [`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` | | 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. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have a reasonable default value, like 50-100 I think.



### admin.form
Expand Down
65 changes: 63 additions & 2 deletions lib/ash_admin/components/resource/form.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ defmodule AshAdmin.Components.Resource.Form do
{:ok,
socket
|> assign(assigns)
|> assign(:typeahead_options, [])
|> assign_form()
|> assign(:initialized, true)}
end
Expand Down Expand Up @@ -197,7 +198,7 @@ defmodule AshAdmin.Components.Resource.Form do
class="block text-sm font-medium text-gray-700"
for={@form.name <> "[#{attribute.name}]"}
>
{to_name(attribute.name)}
{to_name(attribute)}
</label>
{render_attribute_input(assigns, attribute, @form)}
<.error_tag
Expand Down Expand Up @@ -352,7 +353,8 @@ defmodule AshAdmin.Components.Resource.Form do
opts: opts,
key: key,
hidden: hidden?,
exactly_fields: exactly_fields
exactly_fields: exactly_fields,
is_relationship_form: true
)

~H"""
Expand Down Expand Up @@ -737,6 +739,12 @@ defmodule AshAdmin.Components.Resource.Form do
_
)
when type in [Ash.Type.CiString, Ash.Type.String, Ash.Type.UUID, Ash.Type.Atom] do
attribute =
case form.source.type do
:read -> Map.put(attribute, :related_resource, assigns[:resource])
_ -> attribute
end

assigns =
assign(assigns,
attribute: attribute,
Expand Down Expand Up @@ -791,6 +799,15 @@ defmodule AshAdmin.Components.Resource.Form do
name={@name || @form.name <> "[#{@attribute.name}]"}
placeholder={placeholder(@default)}
/>
<% is_map(@attribute) and Map.has_key?(@attribute, :related_resource) && AshAdmin.Resource.label_field(@attribute.related_resource) -> %>
<.live_component
module={AshAdmin.Components.Resource.RelationshipField}
id={@id || "#{@form.name}-#{@attribute.name}"}
value={value(@value, @form, @attribute)}
resource={@attribute.related_resource}
form={@form}
attribute={@attribute}
/>
<% true -> %>
<.input
type={text_input_type(@resource, @attribute)}
Expand Down Expand Up @@ -1924,6 +1941,7 @@ defmodule AshAdmin.Components.Resource.Form do

def attributes(resource, %Ash.Resource.Calculation{arguments: arguments}, _exacly) do
sort_attributes(arguments, resource)
|> relate_attributes(resource)
end

def attributes(resource, %{type: :read, arguments: arguments}, exactly)
Expand All @@ -1934,30 +1952,35 @@ defmodule AshAdmin.Components.Resource.Form do
|> Enum.concat(arguments)
|> Enum.filter(&(&1.name in exactly))
|> sort_attributes(resource)
|> relate_attributes(resource)
end

def attributes(resource, %{type: :read, arguments: arguments}, _) do
sort_attributes(arguments, resource)
|> relate_attributes(resource)
end

def attributes(resource, nil, exactly) when not is_nil(exactly) do
resource
|> Ash.Resource.Info.attributes()
|> Enum.filter(&(&1.name in exactly))
|> sort_attributes(resource)
|> relate_attributes(resource)
end

def attributes(resource, :show, _) do
resource
|> Ash.Resource.Info.attributes()
|> Enum.reject(&(&1.name == :autogenerated_id))
|> sort_attributes(resource)
|> relate_attributes(resource)
end

def attributes(resource, %{type: :destroy} = action, _) do
action.arguments
|> Enum.filter(& &1.public?)
|> sort_attributes(resource, action)
|> relate_attributes(resource)
end

def attributes(resource, action, _) do
Expand All @@ -1978,6 +2001,7 @@ defmodule AshAdmin.Components.Resource.Form do
attributes
|> Enum.concat(arguments)
|> sort_attributes(resource, action)
|> relate_attributes(resource)
end

defp sort_attributes(attributes, resource, action \\ nil) do
Expand Down Expand Up @@ -2040,6 +2064,43 @@ defmodule AshAdmin.Components.Resource.Form do
{auto_sorted, flags, sorted_defaults, relationship_args}
end

defp relate_attributes({auto_sorted, flags, sorted_defaults, relationship_args}, resource) do
auto_sorted_with_relationships =
Enum.map(auto_sorted, fn
%Ash.Resource.Attribute{} = attribute ->
relationships = Ash.Resource.Info.relationships(resource)

if attribute.primary_key? do
case Enum.find(relationships, fn
%Ash.Resource.Relationships.BelongsTo{destination_attribute: dest_attr} ->
dest_attr == attribute.name

_other ->
false
end) do
%{source: source} -> Map.put(attribute, :related_resource, source)
_ -> attribute
end
else
case Enum.find(relationships, fn
%Ash.Resource.Relationships.BelongsTo{source_attribute: src_attr} ->
src_attr == attribute.name

_other ->
false
end) do
%{destination: destination} -> Map.put(attribute, :related_resource, destination)
_ -> attribute
end
end

attribute ->
attribute
end)

{auto_sorted_with_relationships, flags, sorted_defaults, relationship_args}
end

defp map_type?({:array, type}) do
map_type?(type)
end
Expand Down
Loading
Loading