Packages
ash_admin
1.0.0-rc.0
1.1.0
1.0.0-rc.0
0.14.0
0.13.26
0.13.25
0.13.24
0.13.23
0.13.22
0.13.21
0.13.20
0.13.19
0.13.18
0.13.17
0.13.16
0.13.15
0.13.14
0.13.13
0.13.12
0.13.11
0.13.10
0.13.9
0.13.8
0.13.7
0.13.6
0.13.5
0.13.4
0.13.3
0.13.2
0.13.1
0.13.0
0.12.6
0.12.5
0.12.4
0.12.3
0.12.2
0.12.1
0.12.0
0.11.11
0.11.10
0.11.9
0.11.8
0.11.7
0.11.6
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.12
0.10.11
0.10.10
retired
0.10.10-rc.1
0.10.10-rc.0
0.10.9
0.10.8
0.10.7
0.10.6
0.10.5
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.1
0.8.0
0.7.1
0.7.0
0.6.2
0.6.1
0.6.0-rc.2
0.6.0-rc.1
0.6.0-rc.0
0.5.2
0.5.1-rc.0
0.5.0
0.4.5-rc.0
0.4.4
0.4.2
0.4.0
0.3.0-rc.0
0.2.22
0.2.21
0.2.20
0.2.19
0.2.18
0.2.17
0.2.16-rc.1
0.2.16-rc.0
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.1
0.1.6
0.1.5
0.1.3
0.1.2
0.1.0
A super-admin UI for Ash Framework, built with Phoenix LiveView.
Current section
Files
Jump to
Current section
Files
lib/ash_admin/components/resource/managed_relationship_select_field.ex
defmodule AshAdmin.Components.Resource.ManagedRelationshipSelectField do
@moduledoc """
A LiveComponent for rendering a managed relationship field as a select/typeahead
instead of nested form inputs. Used when the destination resource has a `label_field` configured.
"""
use Phoenix.LiveComponent
import AshAdmin.CoreComponents
import Ash.Expr
require Ash.Query
def mount(socket) do
{:ok,
assign(socket,
suggestions: [],
search_term: "",
highlighted_index: -1
)}
end
def update(assigns, socket) do
destination = assigns.relationship.destination
pk_field = Ash.Resource.Info.primary_key(destination) |> List.first()
label_field = AshAdmin.Resource.label_field(destination)
max_items = AshAdmin.Resource.relationship_select_max_items(destination)
{:ok,
assign(socket, assigns)
|> assign(
pk_field: pk_field,
label_field: label_field,
max_items: max_items,
destination: destination
)}
end
def render(assigns) do
all_options = load_all_options(assigns)
selected_ids = MapSet.new(assigns.selected_ids, &to_string/1)
available_options =
Enum.reject(all_options, fn {_label, id} -> to_string(id) in selected_ids end)
field_type = if length(all_options) <= assigns.max_items, do: :select, else: :typeahead
assigns =
assign(assigns,
available_options: available_options,
field_type: field_type,
search_term: assigns.search_term
)
~H"""
<div class="mt-1">
<.input
:if={@field_type == :select && @available_options != []}
type="select"
options={@available_options}
prompt={"Add #{relationship_label(@relationship)}..."}
id={@id <> "-select"}
name={"_#{@id}_add"}
value=""
phx-change="select_item"
phx-target={@myself}
/>
<div
:if={@field_type == :typeahead}
id={@id}
class="autocomplete-combobox"
role="combobox"
aria-haspopup="listbox"
aria-owns={"#{@id}-listbox"}
aria-expanded={if @suggestions != [], do: "true", else: "false"}
>
<div class="relative">
<input
type="text"
id={"#{@id}-input"}
value={@search_term}
name={"_#{@id}_suggest"}
class="mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6 phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 border-zinc-300 focus:border-zinc-400 pr-9"
phx-keyup="suggest"
phx-debounce="300"
phx-target={@myself}
placeholder={"Search #{relationship_label(@relationship)}..."}
autocomplete="off"
onkeydown="if(event.key === 'Enter') event.preventDefault()"
/>
</div>
<ul
:if={Enum.count(@suggestions) > 0}
id={"#{@id}-listbox"}
role="listbox"
class="absolute z-10 mt-1 bg-white dark:bg-slate-800 shadow-lg rounded-md px-2 py-1 text-base ring-1 ring-slate-200 dark:ring-slate-700 overflow-auto focus:outline-none sm:text-sm"
>
<%= for {{suggestion_name, suggestion_id}, index} <- Enum.with_index(@suggestions) do %>
<li
id={"#{@id}-option-#{index}"}
class={[
"cursor-pointer select-none relative py-2 px-2 w-full truncate",
if(index == @highlighted_index,
do: "text-white bg-slate-700 dark:bg-slate-500",
else:
"text-slate-900 dark:text-slate-100 hover:bg-slate-100 dark:hover:bg-slate-700"
)
]}
role="option"
tabindex="0"
phx-click="select_suggestion"
phx-value-id={suggestion_id}
phx-target={@myself}
>
<% escaped_term = Regex.escape(@search_term) %>
<% suggestion_name =
String.replace(to_string(suggestion_name), ~r/(#{escaped_term})/i, "<b>\\0</b>") %>
{Phoenix.HTML.raw(suggestion_name)}
</li>
<% end %>
</ul>
</div>
</div>
"""
end
def handle_event("select_item", params, socket) do
id = params["_#{socket.assigns.id}_add"] || ""
if id != "" do
send_update(socket.assigns.form_component_module,
id: socket.assigns.form_component_id,
add_related: %{
path: socket.assigns.form_path,
pk_field: to_string(socket.assigns.pk_field),
id: id
}
)
end
{:noreply, socket}
end
def handle_event("suggest", %{"value" => search_term, "key" => key}, socket) do
cond do
key == "ArrowDown" and Enum.any?(socket.assigns.suggestions) ->
new_index =
min(socket.assigns.highlighted_index + 1, Enum.count(socket.assigns.suggestions) - 1)
{:noreply, assign(socket, highlighted_index: new_index)}
key == "ArrowUp" and Enum.any?(socket.assigns.suggestions) ->
new_index = max(socket.assigns.highlighted_index - 1, 0)
{:noreply, assign(socket, highlighted_index: new_index)}
key == "Enter" and socket.assigns.highlighted_index >= 0 ->
{_name, id} = Enum.at(socket.assigns.suggestions, socket.assigns.highlighted_index)
send_update(socket.assigns.form_component_module,
id: socket.assigns.form_component_id,
add_related: %{
path: socket.assigns.form_path,
pk_field: to_string(socket.assigns.pk_field),
id: to_string(id)
}
)
{:noreply, assign(socket, suggestions: [], search_term: "", highlighted_index: -1)}
key == "Escape" ->
{:noreply, assign(socket, suggestions: [], search_term: "", highlighted_index: -1)}
true ->
suggestions = fetch_suggestions(socket.assigns, search_term)
{:noreply,
assign(socket, suggestions: suggestions, search_term: search_term, highlighted_index: -1)}
end
end
def handle_event("select_suggestion", %{"id" => id}, socket) do
send_update(socket.assigns.form_component_module,
id: socket.assigns.form_component_id,
add_related: %{
path: socket.assigns.form_path,
pk_field: to_string(socket.assigns.pk_field),
id: id
}
)
{:noreply, assign(socket, suggestions: [], search_term: "", highlighted_index: -1)}
end
defp relationship_label(relationship) do
relationship.name |> to_string() |> String.replace("_", " ") |> String.capitalize()
end
defp load_all_options(assigns) do
resource = assigns.destination
label_field = assigns.label_field
limit = assigns.max_items + 1
resource
|> Ash.Query.new()
|> Ash.Query.load([label_field])
|> Ash.Query.limit(limit)
|> Ash.read!(
actor: assigns[:actor],
authorize?: assigns[:authorizing],
tenant: assigns[:tenant]
)
|> then(fn
%Ash.Page.Offset{results: results} -> results
results -> results
end)
|> Enum.map(&{to_string(Map.get(&1, label_field)), to_string(&1.id)})
end
defp fetch_suggestions(_assigns, "") do
[]
end
defp fetch_suggestions(assigns, search_term) do
assigns.destination
|> Ash.Query.new()
|> Ash.Query.load([assigns.label_field])
|> Ash.Query.filter(
contains(
^ref(assigns.label_field),
^%Ash.CiString{string: search_term}
)
)
|> Ash.Query.limit(assigns.max_items)
|> Ash.read!(
actor: assigns[:actor],
authorize?: assigns[:authorizing],
tenant: assigns[:tenant]
)
|> then(fn
%Ash.Page.Offset{results: results} -> results
results -> results
end)
|> Enum.map(&{to_string(Map.get(&1, assigns.label_field)), to_string(&1.id)})
|> Enum.reject(fn {_label, id} ->
to_string(id) in Enum.map(assigns.selected_ids, &to_string/1)
end)
end
end