# credo:disable-for-this-file Credo.Check.Design.DuplicatedCode defmodule Backpex.Fields.HasMany do @moduledoc """ A field for handling a `has_many` relation. This field can not be orderable or searchable. ## Options * `:display_field` - The field of the relation to be used for searching, ordering and displaying values. * `:display_field_form` - Optional field to be used to display form values. * `:live_resource` - The live resource of the association. Used to generate links navigating to the associations. * `:options_query` - Manipulates the list of available options in the multi select. Defaults to `fn (query, _field) -> query end` which returns all entries. * `:prompt` - The text to be displayed when no options are selected or function that receives the assigns. Defaults to "Select options...". * `:not_found_text` - The text to be displayed when no options are found. Defaults to "No options found". * `:live_resource` - Optional live resource module used to generate links to the corresponding show view for the item. * `:query_limit` - Optional limit passed to the query to fetch new items. Set to `nil` to have no limit. Defaults to 10. ## Example @impl Backpex.LiveResource def fields do [ posts: %{ module: Backpex.Fields.HasMany, label: "Posts", display_field: :title, options_query: &where(&1, [user], user.role == :admin), live_resource: DemoWeb.PostLive } ] end """ use BackpexWeb, :field import Ecto.Query import Backpex.HTML.Form alias Backpex.LiveResource alias Backpex.Resource alias Backpex.Router @impl Phoenix.LiveComponent def update(assigns, socket) do socket = socket |> assign(assigns) |> apply_action(assigns.type) {:ok, socket} end defp apply_action(socket, :form) do %{assigns: %{field_options: field_options} = assigns} = socket socket |> assign_new(:prompt, fn -> prompt(assigns, field_options) end) |> assign_new(:not_found_text, fn -> not_found_text(field_options) end) |> assign_new(:search_input, fn -> "" end) |> assign_new(:offset, fn -> 0 end) |> assign_new(:options_count, fn -> assigns |> options() |> length() end) |> assign_initial_options() |> assign_selected() |> assign_show_select_all() end defp apply_action(socket, _type), do: socket defp assign_show_select_all(%{assigns: %{show_select_all: _show_select_all}} = socket), do: socket defp assign_show_select_all(socket) do %{selected: selected, options_count: options_count} = socket.assigns show_select_all = length(selected) != options_count socket |> assign(:show_select_all, show_select_all) end defp assign_initial_options(%{assigns: %{options: _options}} = socket), do: socket defp assign_initial_options(socket), do: assign_options(socket) defp assign_options(socket, other_options \\ []) do %{assigns: %{field_options: field_options, search_input: search_input, offset: offset} = assigns} = socket limit = query_limit(field_options) options = other_options ++ options(assigns, offset: offset, limit: limit, search: search_input) show_more = count_options(assigns, search: search_input) > length(options) socket |> assign(:options, options) |> assign(:show_more, show_more) end defp options(assigns, opts \\ []) do %{repo: repo, schema: schema, field: field, field_options: field_options, name: name} = assigns %{queryable: queryable} = schema.__schema__(:association, name) display_field = display_field(field) schema_name = Resource.name_by_schema(queryable) from(queryable, as: ^schema_name) |> maybe_options_query(field_options, assigns) |> maybe_search_query(schema_name, field_options, display_field, Keyword.get(opts, :search)) |> maybe_offset_query(Keyword.get(opts, :offset)) |> maybe_limit_query(Keyword.get(opts, :limit)) |> repo.all() end defp maybe_limit_query(query, nil), do: query defp maybe_limit_query(query, limit), do: query |> limit(^limit) defp maybe_offset_query(query, nil), do: query defp maybe_offset_query(query, offset), do: query |> offset(^offset) defp maybe_options_query(query, %{options_query: options_query} = _field_options, assigns), do: options_query.(query, assigns) defp maybe_options_query(query, _field_options, _assigns), do: query defp maybe_search_query(query, _schema_name, _field_options, _display_field, nil), do: query defp maybe_search_query(query, schema_name, field_options, display_field, search_input) do if String.trim(search_input) == "" do query else search_input = "%#{search_input}%" select = Map.get(field_options, :select, nil) if select do where(query, ^dynamic(ilike(^select, ^search_input))) else where(query, [{^schema_name, schema_name}], ilike(field(schema_name, ^display_field), ^search_input)) end end end defp count_options(assigns, opts) do %{schema: schema, repo: repo, field: field, field_options: field_options, name: name} = assigns display_field = display_field(field) %{queryable: queryable} = schema.__schema__(:association, name) schema_name = Resource.name_by_schema(queryable) from(queryable, as: ^schema_name) |> maybe_options_query(field_options, assigns) |> maybe_search_query(schema_name, field_options, display_field, Keyword.get(opts, :search)) |> subquery() |> repo.aggregate(:count, :id) end defp assign_selected(%{assigns: %{selected: _selected}} = socket), do: socket defp assign_selected(socket) do %{assigns: %{name: name, form: form}} = socket values = case PhoenixForm.input_value(form, name) do value when is_binary(value) -> [value] value when is_list(value) -> value _value -> [] end assign(socket, :selected, values) end @impl Backpex.Field def render_value(assigns) do ~H"""