diff --git a/guides/upgrading/v0.9.md b/guides/upgrading/v0.9.md index df276a24..08b270a4 100644 --- a/guides/upgrading/v0.9.md +++ b/guides/upgrading/v0.9.md @@ -12,7 +12,6 @@ Update Backpex to the latest version: end ``` - ## Refactor calls to [`Backpex.HTML.Form.field_input/1`]() We've refactored the [`Backpex.HTML.Form.field_input/1`]() component and renamed it to `Backpex.HTML.Form.input/1`. @@ -65,4 +64,9 @@ def render_form(assigns) do """ end -``` \ No newline at end of file +``` + +## `Backpex.LiveResource` function usage + +Although the change is relatively small, if you are using public functions of the `Backpex.LiveResource` directly, +check the updated function definitions in the module documentation. diff --git a/lib/backpex/adapters/ecto.ex b/lib/backpex/adapters/ecto.ex index b9d52007..d02742c7 100644 --- a/lib/backpex/adapters/ecto.ex +++ b/lib/backpex/adapters/ecto.ex @@ -122,7 +122,7 @@ defmodule Backpex.Adapters.Ecto do TODO: Should be private. """ def list_query(assigns, item_query, fields, criteria \\ []) do - %{schema: schema, full_text_search: full_text_search, live_resource: live_resource} = assigns + %{schema: schema, full_text_search: full_text_search} = assigns associations = associations(fields, schema) schema @@ -132,7 +132,7 @@ defmodule Backpex.Adapters.Ecto do |> maybe_preload(associations, fields) |> maybe_merge_dynamic_fields(fields) |> apply_search(schema, full_text_search, criteria[:search]) - |> apply_filters(criteria[:filters], live_resource.get_empty_filter_key()) + |> apply_filters(criteria[:filters], Backpex.LiveResource.empty_filter_key()) |> apply_criteria(criteria, fields) end diff --git a/lib/backpex/field.ex b/lib/backpex/field.ex index b0884f83..d2ef4dac 100644 --- a/lib/backpex/field.ex +++ b/lib/backpex/field.ex @@ -260,12 +260,12 @@ defmodule Backpex.Field do Handles index editable. """ def handle_index_editable(socket, value, change) do - if not Backpex.LiveResource.can?(socket.assigns, :edit, socket.assigns.item, socket.assigns.live_resource) do + %{assigns: %{item: item, live_resource: live_resource, fields: fields} = assigns} = socket + + if not live_resource.can?(assigns, :edit, item) do raise Backpex.ForbiddenError end - %{assigns: %{item: item, live_resource: live_resource, fields: fields} = assigns} = socket - opts = [ after_save_fun: fn item -> live_resource.on_item_updated(socket, item) diff --git a/lib/backpex/fields/belongs_to.ex b/lib/backpex/fields/belongs_to.ex index bf6e2b62..32853c37 100644 --- a/lib/backpex/fields/belongs_to.ex +++ b/lib/backpex/fields/belongs_to.ex @@ -30,7 +30,6 @@ defmodule Backpex.Fields.BelongsTo do import Ecto.Query - alias Backpex.LiveResource alias Backpex.Router @impl Phoenix.LiveComponent @@ -194,7 +193,7 @@ defmodule Backpex.Fields.BelongsTo do assigns link = - if Map.has_key?(field_options, :live_resource) and LiveResource.can?(assigns, :show, value, live_resource) do + if Map.has_key?(field_options, :live_resource) and live_resource.can?(assigns, :show, value) do Router.get_path(socket, Map.get(field_options, :live_resource), params, :show, value) else nil diff --git a/lib/backpex/fields/has_many.ex b/lib/backpex/fields/has_many.ex index 606bdfa6..8a6b87d0 100644 --- a/lib/backpex/fields/has_many.ex +++ b/lib/backpex/fields/has_many.ex @@ -39,7 +39,6 @@ defmodule Backpex.Fields.HasMany do import Ecto.Query import Backpex.HTML.Form alias Backpex.Adapters.Ecto, as: EctoAdapter - alias Backpex.LiveResource alias Backpex.Router @impl Phoenix.LiveComponent @@ -256,7 +255,8 @@ defmodule Backpex.Fields.HasMany do {field_name, field_options} = field validate_live_resource(field_name, field_options) - schema = field_options.live_resource.schema() + # TODO: do not rely on specific adapter + schema = field_options.live_resource.config(:adapter_config)[:schema] field_name_string = to_string(field_name) new_assocs = get_new_assocs(attrs, field_name_string, schema, repo, field_options, assigns) @@ -335,14 +335,12 @@ defmodule Backpex.Fields.HasMany do socket: socket, field_options: field_options, item: item, - live_resource: live_resource, params: params, link_assocs: link_assocs } = assigns link = - if link_assocs and Map.has_key?(field_options, :live_resource) and - LiveResource.can?(assigns, :show, item, live_resource) do + if link_assocs and field_options.live_resource.can?(assigns, :show, item) do Router.get_path(socket, Map.get(field_options, :live_resource), params, :show, item) else nil diff --git a/lib/backpex/html/resource.ex b/lib/backpex/html/resource.ex index 02130f08..2a302fab 100644 --- a/lib/backpex/html/resource.ex +++ b/lib/backpex/html/resource.ex @@ -105,7 +105,7 @@ defmodule Backpex.HTML.Resource do {_name, field_options} = field = Enum.find(fields, fn {field_name, _field_options} -> field_name == name end) readonly = - not LiveResource.can?(assigns, :edit, item, live_resource) or + not live_resource.can?(assigns, :edit, item) or Backpex.Field.readonly?(field_options, assigns) assigns = @@ -626,10 +626,7 @@ defmodule Backpex.HTML.Resource do def resource_buttons(assigns) do ~H"""
- <.link - :if={LiveResource.can?(assigns, :new, nil, @live_resource)} - patch={Router.get_path(@socket, @live_resource, @params, :new)} - > + <.link :if={@live_resource.can?(assigns, :new, nil)} patch={Router.get_path(@socket, @live_resource, @params, :new)}> @@ -686,8 +683,8 @@ defmodule Backpex.HTML.Resource do /> <.index_filter live_resource={@live_resource} - filter_options={LiveResource.get_filter_options(@live_resource, @query_options)} - filters={LiveResource.get_active_filters(@live_resource, assigns)} + filter_options={LiveResource.get_filter_options(@query_options)} + filters={LiveResource.active_filters(assigns)} />
""" @@ -703,7 +700,7 @@ defmodule Backpex.HTML.Resource do defp resource_actions(assigns, resource_actions) do Enum.filter(resource_actions, fn {key, _action} -> - LiveResource.can?(assigns, key, nil, assigns.live_resource) + assigns.live_resource.can?(assigns, key, nil) end) end @@ -712,7 +709,7 @@ defmodule Backpex.HTML.Resource do resource_actions = resource_actions(assigns, assigns.resource_actions) Enum.any?(index_item_actions) && - (Enum.any?(resource_actions) || LiveResource.can?(assigns, :new, nil, assigns.live_resource)) + (Enum.any?(resource_actions) || assigns.live_resource.can?(assigns, :new, nil)) end defp index_item_actions(item_actions) do @@ -729,7 +726,7 @@ defmodule Backpex.HTML.Resource do defp action_disabled?(assigns, action_key, items) do Enum.filter(items, fn item -> - LiveResource.can?(assigns, action_key, item, assigns.live_resource) + assigns.live_resource.can?(assigns, action_key, item) end) |> Enum.empty?() end @@ -758,7 +755,7 @@ defmodule Backpex.HTML.Resource do |> assign(:search_active?, get_in(assigns, [:query_options, :search]) not in [nil, ""]) |> assign(:filter_active?, get_in(assigns, [:query_options, :filters]) != %{}) |> assign(:title, Backpex.translate({"No %{resources} found", %{resources: assigns.plural_name}})) - |> assign(:create_allowed, LiveResource.can?(assigns, :new, nil, assigns.live_resource)) + |> assign(:create_allowed, assigns.live_resource.can?(assigns, :new, nil)) ~H"""
diff --git a/lib/backpex/html/resource/resource_index_table.html.heex b/lib/backpex/html/resource/resource_index_table.html.heex index 218ec39b..04e84c63 100644 --- a/lib/backpex/html/resource/resource_index_table.html.heex +++ b/lib/backpex/html/resource/resource_index_table.html.heex @@ -111,7 +111,7 @@ >
diff --git a/lib/backpex/live_components/form_component.ex b/lib/backpex/live_components/form_component.ex index 030677cd..870eb5c6 100644 --- a/lib/backpex/live_components/form_component.ex +++ b/lib/backpex/live_components/form_component.ex @@ -9,7 +9,6 @@ defmodule Backpex.FormComponent do import Backpex.HTML.Resource alias Backpex.Fields.Upload - alias Backpex.LiveResource alias Backpex.Resource alias Backpex.ResourceAction @@ -350,7 +349,7 @@ defmodule Backpex.FormComponent do {:ok, data} -> selected_items = Enum.filter(selected_items, fn item -> - LiveResource.can?(socket.assigns, action_key, item, socket.assigns.live_resource) + live_resource.can?(socket.assigns, action_key, item) end) {message, socket} = diff --git a/lib/backpex/live_resource.ex b/lib/backpex/live_resource.ex index 8b4a7742..cf1a2ec6 100644 --- a/lib/backpex/live_resource.ex +++ b/lib/backpex/live_resource.ex @@ -1,6 +1,3 @@ -# credo:disable-for-this-file Credo.Check.Refactor.LongQuoteBlocks -# credo:disable-for-this-file Credo.Check.Refactor.CyclomaticComplexity - defmodule Backpex.LiveResource do @moduledoc ~S''' A LiveResource makes it easy to manage existing resources in your application. It provides extensive configuration options in order to meet everyone's needs. In connection with `Backpex.Components` you can build an individual admin dashboard on top of your application in minutes. @@ -10,7 +7,12 @@ defmodule Backpex.LiveResource do > When you `use Backpex.LiveResource`, the `Backpex.LiveResource` module will set `@behavior Backpex.LiveResource`. Additionally it will create a LiveView based on the given configuration in order to create fully functional index, show, new and edit views for a resource. It will also insert fallback functions that can be overridden. ''' + use Phoenix.LiveView + import Backpex.HTML.Resource + alias Backpex.Adapters.Ecto, as: EctoAdapter alias Backpex.Resource + alias Backpex.ResourceAction + alias Backpex.Router @options_schema [ adapter: [ @@ -91,7 +93,8 @@ defmodule Backpex.LiveResource do default: false ], full_text_search: [ - type: :atom + type: :atom, + default: nil ] ] @@ -149,8 +152,7 @@ defmodule Backpex.LiveResource do The function has to return an `Ecto.Query`. It is recommended to build your `item_query` on top of the incoming query. Otherwise you will likely get binding errors. """ - @callback item_query(query :: Ecto.Query.t(), live_action :: atom(), assigns :: map()) :: - Ecto.Query.t() + @callback item_query(query :: Ecto.Query.t(), live_action :: atom(), assigns :: map()) :: Ecto.Query.t() @doc """ The function that can be used to add content to certain positions on Backpex views. It may also be used to overwrite content. @@ -214,11 +216,6 @@ defmodule Backpex.LiveResource do """ @callback resource_created_message() :: binary() - @doc """ - Returns the schema of the live resource. - """ - @callback schema() :: module() - @doc """ Uses LiveResource in the current module to make it a LiveResource. @@ -248,806 +245,28 @@ defmodule Backpex.LiveResource do use BackpexWeb, :html use Phoenix.LiveView, layout: @resource_opts[:layout] - import Backpex.HTML.Resource import Backpex.LiveResource import Phoenix.LiveView.Helpers import Ecto.Query - alias Backpex.Adapters.Ecto, as: EctoAdapter - alias Backpex.Resource - alias Backpex.ResourceAction - alias Backpex.Router - - require Logger + alias Backpex.LiveResource - @permitted_order_directions ~w(asc desc)a - @empty_filter_key :empty_filter - - def config(key) do - Keyword.fetch!(@resource_opts, key) - end + def config(key), do: Keyword.fetch!(@resource_opts, key) @impl Phoenix.LiveView - def mount(params, session, socket) do - subscribe_to_topic(socket, @resource_opts[:pubsub]) - - socket = - socket - |> assign(:live_resource, __MODULE__) - |> assign(:schema, @resource_opts[:adapter_config][:schema]) - |> assign(:repo, @resource_opts[:adapter_config][:repo]) - |> assign(:singular_name, singular_name()) - |> assign(:plural_name, plural_name()) - |> assign(:create_button_label, create_button_label()) - |> assign(:resource_created_message, resource_created_message()) - |> assign(:search_placeholder, search_placeholder()) - |> assign(:panels, panels()) - |> assign(:fluid?, @resource_opts[:fluid?]) - |> assign(:full_text_search, @resource_opts[:full_text_search]) - |> assign_active_fields(session) - |> assign_metrics_visibility(session) - |> assign_filters_changed_status(params) - - {:ok, socket} - end - - defp assign_active_fields(socket, session) do - fields = filtered_fields_by_action(fields(), socket.assigns, :index) - saved_fields = get_in(session, ["backpex", "column_toggle", "#{__MODULE__}"]) || %{} - - active_fields = - Enum.map(fields, fn {name, %{label: label}} -> - {name, - %{ - active: field_active?(name, saved_fields), - label: label - }} - end) - - socket - |> assign(:active_fields, active_fields) - end - - defp assign_metrics_visibility(socket, session) do - value = get_in(session, ["backpex", "metric_visibility"]) || %{} - - socket - |> assign(metric_visibility: value) - end - - defp assign_filters_changed_status(socket, params) do - %{assigns: %{live_action: live_action}} = socket - - socket - |> assign(:filters_changed, live_action == :index and params["filters_changed"] == "true") - end - - defp field_active?(name, saved_fields) do - case Map.get(saved_fields, Atom.to_string(name)) do - "true" -> - true - - "false" -> - false - - _other -> - true - end - end + def mount(params, session, socket), do: LiveResource.mount(params, session, socket) @impl Phoenix.LiveView - def handle_params(params, _url, socket) do - socket = - socket - |> assign(:params, params) - |> apply_item_actions(socket.assigns.live_action) - |> apply_action(socket.assigns.live_action) - - {:noreply, socket} - end - - def apply_action(socket, :index) do - socket - |> assign(:page_title, socket.assigns.plural_name) - |> apply_index() - |> assign(:item, nil) - end - - def apply_action(socket, :edit) do - %{ - live_resource: live_resource, - singular_name: singular_name - } = socket.assigns - - fields = filtered_fields_by_action(fields(), socket.assigns, :edit) - primary_value = URI.decode(socket.assigns.params["backpex_id"]) - item = Resource.get!(primary_value, socket.assigns, live_resource) - - unless can?(socket.assigns, :edit, item, __MODULE__), do: raise(Backpex.ForbiddenError) - - socket - |> assign(:fields, fields) - |> assign(:changeset_function, @resource_opts[:adapter_config][:update_changeset]) - |> assign(:page_title, Backpex.translate({"Edit %{resource}", %{resource: singular_name}})) - |> assign(:item, item) - |> assign_changeset(fields) - end - - def apply_action(socket, :show) do - %{ - live_resource: live_resource, - singular_name: singular_name - } = socket.assigns - - fields = filtered_fields_by_action(fields(), socket.assigns, :show) - primary_value = URI.decode(socket.assigns.params["backpex_id"]) - item = Resource.get!(primary_value, socket.assigns, live_resource) - - unless can?(socket.assigns, :show, item, __MODULE__), do: raise(Backpex.ForbiddenError) - - socket - |> assign(:page_title, singular_name) - |> assign(:fields, fields) - |> assign(:item, item) - |> apply_show_return_to(item) - end - - def apply_action(socket, :new) do - %{ - assigns: - %{ - schema: schema, - singular_name: singular_name, - create_button_label: create_button_label - } = assigns - } = socket - - unless can?(assigns, :new, nil, __MODULE__), do: raise(Backpex.ForbiddenError) - - fields = filtered_fields_by_action(fields(), assigns, :new) - empty_item = schema.__struct__() - - socket - |> assign(:changeset_function, @resource_opts[:adapter_config][:create_changeset]) - |> assign(:page_title, create_button_label) - |> assign(:fields, fields) - |> assign(:item, empty_item) - |> assign_changeset(fields) - end - - def apply_action(socket, :resource_action) do - id = - socket.assigns.params["backpex_id"] - |> URI.decode() - |> String.to_existing_atom() - - action = resource_actions()[id] - - unless can?(socket.assigns, id, nil, __MODULE__), do: raise(Backpex.ForbiddenError) - - socket = - socket - |> assign(:page_title, ResourceAction.name(action, :title)) - |> assign(:resource_action, action) - |> assign(:resource_action_id, id) - |> assign(:item, action.module.init_change(socket.assigns)) - |> apply_index() - |> assign(:changeset_function, &action.module.changeset/3) - |> assign_changeset(action.module.fields()) - end - - def apply_item_actions(socket, action) when action in [:index, :resource_action] do - item_actions = item_actions(default_item_actions()) - assign(socket, :item_actions, item_actions) - end - - def apply_item_actions(socket, _action), do: socket - - defp apply_index_return_to(socket) do - %{assigns: %{params: params, query_options: query_options} = assigns} = socket - - socket - |> assign( - :return_to, - Router.get_path(socket, __MODULE__, params, :index, query_options) - ) - end - - defp apply_show_return_to(socket, item) do - socket - |> assign( - :return_to, - Router.get_path(socket, __MODULE__, socket.assigns.params, :show, item) - ) - end - - defp apply_index(socket) do - %{ - live_resource: live_resource, - repo: repo, - schema: schema, - params: params - } = socket.assigns - - unless can?(socket.assigns, :index, nil, __MODULE__), do: raise(Backpex.ForbiddenError) - - fields = filtered_fields_by_action(fields(), socket.assigns, :index) - - per_page_options = @resource_opts[:per_page_options] - per_page_default = @resource_opts[:per_page_default] - init_order = @resource_opts[:init_order] - - filters = Backpex.LiveResource.get_active_filters(__MODULE__, socket.assigns) - valid_filter_params = Backpex.LiveResource.get_valid_filters_from_params(params, filters, @empty_filter_key) - - count_criteria = [ - search: search_options(params, fields, schema), - filters: filter_options(valid_filter_params, filters) - ] - - item_count = Resource.count(fields, socket.assigns, live_resource, count_criteria) - - per_page = - params - |> parse_integer("per_page", per_page_default) - |> value_in_permitted_or_default(per_page_options, per_page_default) - - total_pages = calculate_total_pages(item_count, per_page) - page = params |> parse_integer("page", 1) |> validate_page(total_pages) - - page_options = %{page: page, per_page: per_page} - - order_options = order_options_by_params(params, fields, init_order, socket.assigns, @permitted_order_directions) - - query_options = - page_options - |> Map.merge(order_options) - |> maybe_put_search(params) - |> Map.put(:filters, Map.get(valid_filter_params, "filters", %{})) - - socket - |> assign(:item_count, item_count) - |> assign(:query_options, query_options) - |> assign(:init_order, init_order) - |> assign(:total_pages, total_pages) - |> assign(:per_page_options, per_page_options) - |> assign(:filters, filters) - |> assign(:orderable_fields, orderable_fields(fields)) - |> assign(:searchable_fields, searchable_fields(fields)) - |> assign(:resource_actions, resource_actions()) - |> assign(:action_to_confirm, nil) - |> assign(:selected_items, []) - |> assign(:select_all, false) - |> assign(:fields, fields) - |> assign(:changeset_function, @resource_opts[:adapter_config][:update_changeset]) - |> maybe_redirect_to_default_filters() - |> assign_items() - |> maybe_assign_metrics() - |> apply_index_return_to() - end - - defp assign_changeset(socket, fields) do - %{ - item: item, - changeset_function: changeset_function, - live_action: live_action - } = socket.assigns - - metadata = Resource.build_changeset_metadata(socket.assigns) - changeset = changeset_function.(item, default_attrs(live_action, fields, socket.assigns), metadata) - - socket - |> assign(:changeset, changeset) - end - - defp default_attrs(:new, fields, %{schema: schema} = assigns) do - Enum.reduce(fields, %{}, fn - {name, %{default: default} = field_options} = field, attrs -> - if field_options.module.association?(field) && schema.__schema__(:association, name).cardinality == :one do - owner_key = schema.__schema__(:association, name).owner_key - - Map.put(attrs, owner_key, default.(assigns)) - else - Map.put(attrs, name, default.(assigns)) - end - - _field, attrs -> - attrs - end) - end - - defp default_attrs(:resource_action, fields, assigns) do - Enum.reduce(fields, %{}, fn - {name, %{default: default} = _field}, attrs -> - Map.put(attrs, name, default.(assigns)) - - _field, attrs -> - attrs - end) - end - - defp default_attrs(_live_action, _fields, _assigns), do: %{} - - defp maybe_redirect_to_default_filters(%{assigns: %{filters_changed: false}} = socket) do - %{ - assigns: - %{ - query_options: query_options, - params: params, - filters: filters - } = assigns - } = socket - - filters_with_defaults = - filters - |> Enum.filter(fn {_key, filter_config} -> - Map.has_key?(filter_config, :default) - end) - - # redirect to default filters if no filters are set and defaults are available - if Map.get(query_options, :filters) == %{} and Enum.count(filters_with_defaults) > 0 do - default_filter_options = - filters_with_defaults - |> Enum.map(fn {key, filter_config} -> - {key, filter_config.default} - end) - |> Enum.into(%{}, fn {key, value} -> - {Atom.to_string(key), value} - end) - - # redirect with updated query options - options = Map.put(query_options, :filters, default_filter_options) - to = Router.get_path(socket, __MODULE__, params, :index, options) - push_navigate(socket, to: to) - else - socket - end - end - - defp maybe_redirect_to_default_filters(socket) do - socket - end - - defp maybe_put_search(query_options, %{"search" => search} = _params) - when is_nil(search) or search == "", - do: query_options - - defp maybe_put_search(query_options, %{"search" => search} = _params), - do: Map.put(query_options, :search, search) - - defp maybe_put_search(query_options, params), do: query_options - - def assign_items(socket) do - %{ - live_resource: live_resource, - fields: fields - } = socket.assigns - - criteria = build_criteria(socket.assigns) - items = Resource.list(fields, socket.assigns, live_resource, criteria) - - assign(socket, :items, items) - end - - defp maybe_assign_metrics(socket) do - %{ - assigns: - %{ - repo: repo, - schema: schema, - live_action: live_action, - live_resource: live_resource, - params: params, - fields: fields, - query_options: query_options, - metric_visibility: metric_visibility - } = assigns - } = socket - - filters = Backpex.LiveResource.get_active_filters(__MODULE__, assigns) - - metrics = - metrics() - |> Enum.map(fn {key, metric} -> - query = - EctoAdapter.list_query( - assigns, - &item_query(&1, live_action, assigns), - fields, - search: search_options(query_options, fields, schema), - filters: filter_options(query_options, filters) - ) - - case Backpex.Metric.metrics_visible?(metric_visibility, live_resource) do - true -> - data = - query - |> Ecto.Query.exclude(:select) - |> Ecto.Query.exclude(:preload) - |> Ecto.Query.exclude(:group_by) - |> metric.module.query(metric.select, repo) - - {key, Map.put(metric, :data, data)} - - _visible -> - {key, metric} - end - end) - - socket - |> assign(metrics: metrics) - end + def handle_params(params, url, socket), do: LiveResource.handle_params(params, url, socket) @impl Phoenix.LiveView - def handle_event("close-modal", _params, socket) do - socket = - socket - |> push_patch(to: socket.assigns.return_to) - - {:noreply, socket} - end + def handle_event(event, params, socket), do: LiveResource.handle_event(event, params, socket) @impl Phoenix.LiveView - def handle_event("item-action", %{"action-key" => key, "item-id" => item_id}, socket) do - item = - Enum.find(socket.assigns.items, fn item -> - to_string(primary_value(item)) == to_string(item_id) - end) - - socket - |> assign(selected_items: [item]) - |> maybe_handle_item_action(key) - end - - def handle_event("item-action", %{"action-key" => key}, socket) do - maybe_handle_item_action(socket, key) - end - - defp maybe_handle_item_action(socket, key) do - key = String.to_existing_atom(key) - action = socket.assigns.item_actions[key] - items = socket.assigns.selected_items - - if has_modal?(action.module) do - open_action_confirm_modal(socket, action, key) - else - handle_item_action(socket, action, key, items) - end - end - - defp open_action_confirm_modal(socket, action, key) do - init_change = action.module.init_change(socket.assigns) - changeset_function = &action.module.changeset/3 - - metadata = Resource.build_changeset_metadata(socket.assigns) - - changeset = - init_change - |> Ecto.Changeset.change() - |> changeset_function.(%{}, metadata) - - socket = - socket - |> assign(:item_action_types, init_change) - |> assign(:changeset_function, changeset_function) - |> assign(:changeset, changeset) - |> assign(:action_to_confirm, Map.put(action, :key, key)) - - {:noreply, socket} - end - - defp handle_item_action(socket, action, key, items) do - items = Enum.filter(items, fn item -> can?(socket.assigns, key, item, __MODULE__) end) - - socket - |> assign(action_to_confirm: nil) - |> assign(selected_items: []) - |> assign(select_all: false) - |> action.module.handle(items, %{}) - end - - @impl Phoenix.LiveView - def handle_event("select-page-size", %{"select_per_page" => %{"value" => per_page}}, socket) do - %{assigns: %{query_options: query_options, params: params} = assigns} = socket - - per_page = String.to_integer(per_page) - - to = - Router.get_path( - socket, - __MODULE__, - params, - :index, - Map.merge(query_options, %{per_page: per_page}) - ) - - socket = push_patch(socket, to: to, replace: true) - - {:noreply, socket} - end - - @impl Phoenix.LiveView - def handle_event("index-search", %{"index_search" => %{"value" => search_input}}, socket) do - %{assigns: %{query_options: query_options, params: params} = assigns} = socket - - to = - Router.get_path( - socket, - __MODULE__, - params, - :index, - Map.merge(query_options, %{search: search_input}) - ) - - socket = push_patch(socket, to: to, replace: true) - - {:noreply, socket} - end - - @impl Phoenix.LiveView - def handle_event("change-filter", params, socket) do - query_options = socket.assigns.query_options - - empty_filter_name = Atom.to_string(@empty_filter_key) - - filters = - Map.get(query_options, :filters, %{}) - |> Map.merge(params["filters"]) - # Filter manually emptied filters and empty filter - |> Enum.filter(fn - {^empty_filter_name, _value} -> false - {_filter, ""} -> false - {_filter, %{"start" => "", "end" => ""}} -> false - _filter_params -> true - end) - - to = - Router.get_path( - socket, - __MODULE__, - socket.assigns.params, - :index, - Map.put(query_options, :filters, filters) - ) - - socket = - socket - |> assign(filters_changed: true) - |> push_patch(to: to) - - {:noreply, socket} - end - - @impl Phoenix.LiveView - def handle_event("clear-filter", %{"field" => field}, socket) do - %{assigns: %{query_options: query_options, params: params, live_resource: live_resource} = assigns} = socket - - new_query_options = - Map.put( - query_options, - :filters, - Map.get(query_options, :filters, %{}) - |> Map.delete(field) - |> Backpex.LiveResource.maybe_put_empty_filter(@empty_filter_key) - ) - - to = Router.get_path(socket, __MODULE__, params, :index, new_query_options) - - socket = - push_patch(socket, to: to) - |> assign(params: Map.merge(params, new_query_options)) - |> assign(query_options: new_query_options) - |> assign(filters_changed: true) - - {:noreply, socket} - end - - @impl Phoenix.LiveView - def handle_event("filter-preset-selected", %{"field" => field, "preset-index" => preset_index} = params, socket) do - query_options = socket.assigns.query_options - preset_index = String.to_integer(preset_index) - field_atom = String.to_existing_atom(field) - - get_preset_values = - socket.assigns - |> get_in([:filters, field_atom, :presets]) - |> Enum.at(preset_index) - |> Map.get(:values) - - filters = - Map.get(query_options, :filters, %{}) - |> Map.put(field, get_preset_values.()) - |> Map.drop([Atom.to_string(@empty_filter_key)]) - - to = - Router.get_path( - socket, - __MODULE__, - socket.assigns.params, - :index, - Map.put(query_options, :filters, filters) - ) - - socket = - socket - |> assign(filters_changed: true) - |> push_patch(to: to) - - {:noreply, socket} - end + def handle_info(msg, socket), do: LiveResource.handle_info(msg, socket) @impl Phoenix.LiveView - def handle_event("update-selected-items", %{"id" => id}, socket) do - selected_items = socket.assigns.selected_items - - item = Enum.find(socket.assigns.items, fn item -> to_string(primary_value(item)) == to_string(id) end) - - updated_selected_items = - if Enum.member?(selected_items, item) do - List.delete(selected_items, item) - else - [item | selected_items] - end - - select_all = length(updated_selected_items) == length(socket.assigns.items) - - socket = - socket - |> assign(:selected_items, updated_selected_items) - |> assign(:select_all, select_all) - - {:noreply, socket} - end - - @impl Phoenix.LiveView - def handle_event("toggle-item-selection", _params, socket) do - select_all = not socket.assigns.select_all - - selected_items = - if select_all do - socket.assigns.items - else - [] - end - - socket = - socket - |> assign(:select_all, select_all) - |> assign(:selected_items, selected_items) - - {:noreply, socket} - end - - @impl Phoenix.LiveView - def handle_info({"backpex:" <> unquote(@resource_opts[:pubsub][:event_prefix]) <> "created", item}, socket) - when socket.assigns.live_action in [:index, :resource_action] do - {:noreply, refresh_items(socket)} - end - - @impl Phoenix.LiveView - def handle_info({"backpex:" <> unquote(@resource_opts[:pubsub][:event_prefix]) <> "deleted", item}, socket) - when socket.assigns.live_action in [:index, :resource_action] do - if Enum.filter(socket.assigns.items, &(to_string(primary_value(&1)) == to_string(primary_value(item)))) != [] do - {:noreply, refresh_items(socket)} - else - {:noreply, socket} - end - end - - @impl Phoenix.LiveView - def handle_info({"backpex:" <> unquote(@resource_opts[:pubsub][:event_prefix]) <> "updated", item}, socket) - when socket.assigns.live_action in [:index, :resource_action, :show] do - {:noreply, update_item(socket, item)} - end - - @impl Phoenix.LiveView - def handle_info({:put_assoc, {key, value} = _assoc}, socket) do - changeset = Ecto.Changeset.put_assoc(socket.assigns.changeset, key, value) - assocs = Map.get(socket.assigns, :assocs, []) |> Keyword.put(key, value) - - socket = - socket - |> assign(:assocs, assocs) - |> assign(:changeset, changeset) - - {:noreply, socket} - end - - @impl Phoenix.LiveView - def handle_info({:put_embed, {key, value} = _assoc}, socket) do - changeset = Ecto.Changeset.put_embed(socket.assigns.changeset, key, value) - embeds = Map.get(socket.assigns, :embeds, []) |> Keyword.put(key, value) - - socket = - socket - |> assign(:embeds, embeds) - |> assign(:changeset, changeset) - - {:noreply, socket} - end - - @impl Phoenix.LiveView - def handle_info({:update_changeset, changeset}, socket) do - {:noreply, assign(socket, :changeset, changeset)} - end - - def get_empty_filter_key, do: @empty_filter_key - - defp primary_value(item) do - item - |> Map.get(@resource_opts[:primary_key]) - end - - defp update_item(socket, %{id: id} = _item) do - %{ - live_resource: live_resource, - live_action: live_action - } = socket.assigns - - fields = filtered_fields_by_action(fields(), socket.assigns, :show) - item = Resource.get(id, socket.assigns, live_resource) - - socket = - cond do - live_action in [:index, :resource_action] and item -> - items = Enum.map(socket.assigns.items, &if(primary_value(&1) == id, do: item, else: &1)) - - assign(socket, :items, items) - - live_action == :show and item -> - assign(socket, :item, item) - - true -> - socket - end - - socket - end - - defp refresh_items(socket) do - %{ - live_resource: live_resource, - schema: schema, - params: params, - fields: fields, - query_options: query_options - } = socket.assigns - - filters = Backpex.LiveResource.get_active_filters(__MODULE__, socket.assigns) - valid_filter_params = Backpex.LiveResource.get_valid_filters_from_params(params, filters, @empty_filter_key) - - count_criteria = [ - search: search_options(params, fields, schema), - filters: filter_options(valid_filter_params, filters) - ] - - item_count = Resource.count(fields, socket.assigns, live_resource, count_criteria) - %{page: page, per_page: per_page} = query_options - total_pages = calculate_total_pages(item_count, per_page) - new_query_options = Map.put(query_options, :page, validate_page(page, total_pages)) - - socket - |> assign(:item_count, item_count) - |> assign(:total_pages, total_pages) - |> assign(:query_options, new_query_options) - |> assign_items() - |> maybe_assign_metrics() - end - - @impl Phoenix.LiveView - def render(%{live_action: action} = assigns) when action in [:show, :show_edit] do - resource_show(assigns) - end - - @impl Phoenix.LiveView - def render(%{live_action: action} = assigns) when action in [:new, :edit] do - resource_form(assigns) - end - - @impl Phoenix.LiveView - def render(assigns) do - resource_index(assigns) - end + def render(assigns), do: LiveResource.render(assigns) @impl Backpex.LiveResource def can?(_assigns, _action, _item), do: true @@ -1073,9 +292,6 @@ defmodule Backpex.LiveResource do @impl Backpex.LiveResource def create_button_label, do: Backpex.translate({"New %{resource}", %{resource: singular_name()}}) - @impl Backpex.LiveResource - def schema, do: @resource_opts[:adapter_config][:schema] - @impl Backpex.LiveResource def resource_created_message, do: Backpex.translate({"New %{resource} has been created successfully.", %{resource: singular_name()}}) @@ -1092,17 +308,13 @@ defmodule Backpex.LiveResource do end end + # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity defmacro __before_compile__(_env) do quote do import Backpex.HTML.Layout import Backpex.HTML.Resource alias Backpex.Router - @impl Phoenix.LiveView - def handle_info(_message, socket) do - {:noreply, socket} - end - @impl Backpex.LiveResource def panels, do: [] @@ -1126,7 +338,7 @@ defmodule Backpex.LiveResource do @impl Backpex.LiveResource def return_to(socket, assigns, _action, _item) do - Map.get(assigns, :return_to, Router.get_path(socket, __MODULE__, %{}, :index)) + Map.get(assigns, :return_to, Router.get_path(socket, assigns.live_resource, %{}, :index)) end @impl Backpex.LiveResource @@ -1172,7 +384,7 @@ defmodule Backpex.LiveResource do <.main_title class="flex items-center justify-between"> <%= @singular_name %> <.link - :if={Backpex.LiveResource.can?(assigns, :edit, @item, @live_resource)} + :if={@live_resource.can?(assigns, :edit, @item)} class="tooltip hover:z-30" data-tip={Backpex.translate("Edit")} aria-label={Backpex.translate("Edit")} @@ -1231,6 +443,773 @@ defmodule Backpex.LiveResource do end end + @impl Phoenix.LiveView + def mount(params, session, socket) do + live_resource = socket.view + pubsub = live_resource.config(:pubsub) + subscribe_to_topic(socket, pubsub) + + # TODO: move these "config assigns" (and other global assigns) to where they are needed + adapter_config = live_resource.config(:adapter_config) + fluid? = live_resource.config(:fluid?) + full_text_search = live_resource.config(:full_text_search) + + socket = + socket + |> assign(:live_resource, live_resource) + |> assign(:schema, adapter_config[:schema]) + |> assign(:repo, adapter_config[:repo]) + |> assign(:singular_name, live_resource.singular_name()) + |> assign(:plural_name, live_resource.plural_name()) + |> assign(:create_button_label, live_resource.create_button_label()) + |> assign(:resource_created_message, live_resource.resource_created_message()) + |> assign(:search_placeholder, live_resource.search_placeholder()) + |> assign(:panels, live_resource.panels()) + |> assign(:fluid?, fluid?) + |> assign(:full_text_search, full_text_search) + |> assign_active_fields(session) + |> assign_metrics_visibility(session) + |> assign_filters_changed_status(params) + + {:ok, socket} + end + + defp assign_active_fields(socket, session) do + fields = + socket.assigns.live_resource.fields() + |> filtered_fields_by_action(socket.assigns, :index) + + saved_fields = get_in(session, ["backpex", "column_toggle", "#{socket.assigns.live_resource}"]) || %{} + + active_fields = + Enum.map(fields, fn {name, %{label: label}} -> + {name, + %{ + active: field_active?(name, saved_fields), + label: label + }} + end) + + socket + |> assign(:active_fields, active_fields) + end + + defp assign_metrics_visibility(socket, session) do + value = get_in(session, ["backpex", "metric_visibility"]) || %{} + + socket + |> assign(metric_visibility: value) + end + + defp assign_filters_changed_status(socket, params) do + %{assigns: %{live_action: live_action}} = socket + + socket + |> assign(:filters_changed, live_action == :index and params["filters_changed"] == "true") + end + + defp field_active?(name, saved_fields) do + case Map.get(saved_fields, Atom.to_string(name)) do + "true" -> true + "false" -> false + _other -> true + end + end + + def assign_items(socket) do + %{live_resource: live_resource, fields: fields} = socket.assigns + + criteria = build_criteria(socket.assigns) + items = Resource.list(fields, socket.assigns, live_resource, criteria) + + assign(socket, :items, items) + end + + defp maybe_assign_metrics(socket) do + %{ + assigns: + %{ + repo: repo, + schema: schema, + live_action: live_action, + live_resource: live_resource, + fields: fields, + query_options: query_options, + metric_visibility: metric_visibility + } = assigns + } = socket + + filters = active_filters(assigns) + + metrics = + socket.assigns.live_resource.metrics() + |> Enum.map(fn {key, metric} -> + query = + EctoAdapter.list_query( + assigns, + &socket.assigns.live_resource.item_query(&1, live_action, assigns), + fields, + search: search_options(query_options, fields, schema), + filters: filter_options(query_options, filters) + ) + + case Backpex.Metric.metrics_visible?(metric_visibility, live_resource) do + true -> + data = + query + |> Ecto.Query.exclude(:select) + |> Ecto.Query.exclude(:preload) + |> Ecto.Query.exclude(:group_by) + |> metric.module.query(metric.select, repo) + + {key, Map.put(metric, :data, data)} + + _visible -> + {key, metric} + end + end) + + socket + |> assign(metrics: metrics) + end + + @impl Phoenix.LiveView + def render(%{live_action: action} = assigns) when action in [:show] do + resource_show(assigns) + end + + @impl Phoenix.LiveView + def render(%{live_action: action} = assigns) when action in [:new, :edit] do + resource_form(assigns) + end + + @impl Phoenix.LiveView + def render(assigns) do + resource_index(assigns) + end + + @impl Phoenix.LiveView + def handle_params(params, _url, socket) do + socket = + socket + |> assign(:params, params) + |> apply_item_actions(socket.assigns.live_action) + |> apply_action(socket.assigns.live_action) + + {:noreply, socket} + end + + defp apply_action(socket, :index) do + socket + |> assign(:page_title, socket.assigns.plural_name) + |> apply_index() + |> assign(:item, nil) + end + + defp apply_action(socket, :edit) do + %{live_resource: live_resource, singular_name: singular_name} = socket.assigns + + fields = live_resource.fields |> filtered_fields_by_action(socket.assigns, :edit) + primary_value = URI.decode(socket.assigns.params["backpex_id"]) + item = Resource.get!(primary_value, socket.assigns, live_resource) + + if not live_resource.can?(socket.assigns, :edit, item), do: raise(Backpex.ForbiddenError) + + socket + |> assign(:fields, fields) + |> assign(:changeset_function, live_resource.config(:adapter_config)[:update_changeset]) + |> assign(:page_title, Backpex.translate({"Edit %{resource}", %{resource: singular_name}})) + |> assign(:item, item) + |> assign_changeset(fields) + end + + defp apply_action(socket, :show) do + %{live_resource: live_resource, singular_name: singular_name} = socket.assigns + + fields = live_resource.fields() |> filtered_fields_by_action(socket.assigns, :show) + primary_value = URI.decode(socket.assigns.params["backpex_id"]) + item = Resource.get!(primary_value, socket.assigns, live_resource) + + if not live_resource.can?(socket.assigns, :show, item), do: raise(Backpex.ForbiddenError) + + socket + |> assign(:page_title, singular_name) + |> assign(:fields, fields) + |> assign(:item, item) + |> apply_show_return_to(item) + end + + defp apply_action(socket, :new) do + %{live_resource: live_resource, schema: schema, create_button_label: create_button_label} = socket.assigns + + if not live_resource.can?(socket.assigns, :new, nil), do: raise(Backpex.ForbiddenError) + + fields = live_resource.fields() |> filtered_fields_by_action(socket.assigns, :new) + empty_item = schema.__struct__() + + socket + |> assign(:changeset_function, live_resource.config(:adapter_config)[:create_changeset]) + |> assign(:page_title, create_button_label) + |> assign(:fields, fields) + |> assign(:item, empty_item) + |> assign_changeset(fields) + end + + defp apply_action(socket, :resource_action) do + %{live_resource: live_resource} = socket.assigns + + id = + socket.assigns.params["backpex_id"] + |> URI.decode() + |> String.to_existing_atom() + + action = live_resource.resource_actions()[id] + + if not live_resource.can?(socket.assigns, id, nil), do: raise(Backpex.ForbiddenError) + + socket + |> assign(:page_title, ResourceAction.name(action, :title)) + |> assign(:resource_action, action) + |> assign(:resource_action_id, id) + |> assign(:item, action.module.init_change(socket.assigns)) + |> apply_index() + |> assign(:changeset_function, &action.module.changeset/3) + |> assign_changeset(action.module.fields()) + end + + defp apply_item_actions(socket, action) when action in [:index, :resource_action] do + item_actions = socket.assigns.live_resource.item_actions(default_item_actions()) + assign(socket, :item_actions, item_actions) + end + + defp apply_item_actions(socket, _action), do: socket + + defp apply_index_return_to(socket) do + %{live_resource: live_resource, params: params, query_options: query_options} = socket.assigns + + socket + |> assign( + :return_to, + Router.get_path(socket, live_resource, params, :index, query_options) + ) + end + + defp apply_show_return_to(socket, item) do + %{live_resource: live_resource, params: params} = socket.assigns + + socket + |> assign(:return_to, Router.get_path(socket, live_resource, params, :show, item)) + end + + defp apply_index(socket) do + %{ + live_resource: live_resource, + schema: schema, + params: params + } = socket.assigns + + if not live_resource.can?(socket.assigns, :index, nil), do: raise(Backpex.ForbiddenError) + + fields = live_resource.fields() |> filtered_fields_by_action(socket.assigns, :index) + + per_page_options = live_resource.config(:per_page_options) + per_page_default = live_resource.config(:per_page_default) + init_order = live_resource.config(:init_order) + + filters = active_filters(socket.assigns) + valid_filter_params = get_valid_filters_from_params(params, filters, empty_filter_key()) + + count_criteria = [ + search: search_options(params, fields, schema), + filters: filter_options(valid_filter_params, filters) + ] + + item_count = Resource.count(fields, socket.assigns, live_resource, count_criteria) + + per_page = + params + |> parse_integer("per_page", per_page_default) + |> value_in_permitted_or_default(per_page_options, per_page_default) + + total_pages = calculate_total_pages(item_count, per_page) + page = params |> parse_integer("page", 1) |> validate_page(total_pages) + + page_options = %{page: page, per_page: per_page} + + order_options = order_options_by_params(params, fields, init_order, socket.assigns) + + query_options = + page_options + |> Map.merge(order_options) + |> maybe_put_search(params) + |> Map.put(:filters, Map.get(valid_filter_params, "filters", %{})) + + socket + |> assign(:item_count, item_count) + |> assign(:query_options, query_options) + |> assign(:init_order, init_order) + |> assign(:total_pages, total_pages) + |> assign(:per_page_options, per_page_options) + |> assign(:filters, filters) + |> assign(:orderable_fields, orderable_fields(fields)) + |> assign(:searchable_fields, searchable_fields(fields)) + |> assign(:resource_actions, live_resource.resource_actions()) + |> assign(:action_to_confirm, nil) + |> assign(:selected_items, []) + |> assign(:select_all, false) + |> assign(:fields, fields) + |> assign(:changeset_function, live_resource.config(:adapter_config)[:update_changeset]) + |> maybe_redirect_to_default_filters() + |> assign_items() + |> maybe_assign_metrics() + |> apply_index_return_to() + end + + defp assign_changeset(socket, fields) do + %{item: item, changeset_function: changeset_function, live_action: live_action} = socket.assigns + + metadata = Resource.build_changeset_metadata(socket.assigns) + changeset = changeset_function.(item, default_attrs(live_action, fields, socket.assigns), metadata) + + socket + |> assign(:changeset, changeset) + end + + defp default_attrs(:new, fields, %{schema: schema} = assigns) do + Enum.reduce(fields, %{}, fn + {name, %{default: default} = field_options} = field, attrs -> + if field_options.module.association?(field) && schema.__schema__(:association, name).cardinality == :one do + owner_key = schema.__schema__(:association, name).owner_key + + Map.put(attrs, owner_key, default.(assigns)) + else + Map.put(attrs, name, default.(assigns)) + end + + _field, attrs -> + attrs + end) + end + + defp default_attrs(:resource_action, fields, assigns) do + Enum.reduce(fields, %{}, fn + {name, %{default: default} = _field}, attrs -> + Map.put(attrs, name, default.(assigns)) + + _field, attrs -> + attrs + end) + end + + defp default_attrs(_live_action, _fields, _assigns), do: %{} + + defp maybe_redirect_to_default_filters(%{assigns: %{filters_changed: false}} = socket) do + %{live_resource: live_resource, query_options: query_options, params: params, filters: filters} = socket.assigns + + filters_with_defaults = + filters + |> Enum.filter(fn {_key, filter_config} -> + Map.has_key?(filter_config, :default) + end) + + # redirect to default filters if no filters are set and defaults are available + if Map.get(query_options, :filters) == %{} and Enum.count(filters_with_defaults) > 0 do + default_filter_options = + filters_with_defaults + |> Enum.map(fn {key, filter_config} -> + {key, filter_config.default} + end) + |> Enum.into(%{}, fn {key, value} -> + {Atom.to_string(key), value} + end) + + # redirect with updated query options + options = Map.put(query_options, :filters, default_filter_options) + to = Router.get_path(socket, live_resource, params, :index, options) + push_navigate(socket, to: to) + else + socket + end + end + + defp maybe_redirect_to_default_filters(socket) do + socket + end + + defp maybe_put_search(query_options, %{"search" => search} = _params) + when is_nil(search) or search == "", + do: query_options + + defp maybe_put_search(query_options, %{"search" => search} = _params), + do: Map.put(query_options, :search, search) + + defp maybe_put_search(query_options, _params), do: query_options + + @impl Phoenix.LiveView + def handle_event("close-modal", _params, socket) do + socket = + socket + |> push_patch(to: socket.assigns.return_to) + + {:noreply, socket} + end + + @impl Phoenix.LiveView + def handle_event("item-action", %{"action-key" => key, "item-id" => item_id}, socket) do + item = + Enum.find(socket.assigns.items, fn item -> + to_string(primary_value(socket, item)) == to_string(item_id) + end) + + socket + |> assign(selected_items: [item]) + |> maybe_handle_item_action(key) + end + + @impl Phoenix.LiveView + def handle_event("item-action", %{"action-key" => key}, socket) do + maybe_handle_item_action(socket, key) + end + + @impl Phoenix.LiveView + def handle_event("select-page-size", %{"select_per_page" => %{"value" => per_page}}, socket) do + %{query_options: query_options, params: params} = socket.assigns + + per_page = String.to_integer(per_page) + + to = + Router.get_path( + socket, + socket.assigns.live_resource, + params, + :index, + Map.merge(query_options, %{per_page: per_page}) + ) + + socket = push_patch(socket, to: to, replace: true) + + {:noreply, socket} + end + + @impl Phoenix.LiveView + def handle_event("index-search", %{"index_search" => %{"value" => search_input}}, socket) do + %{query_options: query_options, params: params} = socket.assigns + + to = + Router.get_path( + socket, + socket.assigns.live_resource, + params, + :index, + Map.merge(query_options, %{search: search_input}) + ) + + socket = push_patch(socket, to: to, replace: true) + + {:noreply, socket} + end + + @impl Phoenix.LiveView + def handle_event("change-filter", params, socket) do + query_options = socket.assigns.query_options + + empty_filter_name = Atom.to_string(empty_filter_key()) + + filters = + Map.get(query_options, :filters, %{}) + |> Map.merge(params["filters"]) + # Filter manually emptied filters and empty filter + |> Enum.filter(fn + {^empty_filter_name, _value} -> false + {_filter, ""} -> false + {_filter, %{"start" => "", "end" => ""}} -> false + _filter_params -> true + end) + + to = + Router.get_path( + socket, + socket.assigns.live_resource, + socket.assigns.params, + :index, + Map.put(query_options, :filters, filters) + ) + + socket = + socket + |> assign(filters_changed: true) + |> push_patch(to: to) + + {:noreply, socket} + end + + @impl Phoenix.LiveView + def handle_event("clear-filter", %{"field" => field}, socket) do + %{live_resource: live_resource, query_options: query_options, params: params} = socket.assigns + + new_query_options = + Map.put( + query_options, + :filters, + Map.get(query_options, :filters, %{}) + |> Map.delete(field) + |> maybe_put_empty_filter(empty_filter_key()) + ) + + to = Router.get_path(socket, live_resource, params, :index, new_query_options) + + socket = + push_patch(socket, to: to) + |> assign(params: Map.merge(params, new_query_options)) + |> assign(query_options: new_query_options) + |> assign(filters_changed: true) + + {:noreply, socket} + end + + @impl Phoenix.LiveView + def handle_event("filter-preset-selected", %{"field" => field, "preset-index" => preset_index} = _params, socket) do + query_options = socket.assigns.query_options + preset_index = String.to_integer(preset_index) + field_atom = String.to_existing_atom(field) + + get_preset_values = + socket.assigns + |> get_in([:filters, field_atom, :presets]) + |> Enum.at(preset_index) + |> Map.get(:values) + + filters = + Map.get(query_options, :filters, %{}) + |> Map.put(field, get_preset_values.()) + |> Map.drop([Atom.to_string(empty_filter_key())]) + + to = + Router.get_path( + socket, + socket.assigns.live_resource, + socket.assigns.params, + :index, + Map.put(query_options, :filters, filters) + ) + + socket = + socket + |> assign(filters_changed: true) + |> push_patch(to: to) + + {:noreply, socket} + end + + @impl Phoenix.LiveView + def handle_event("update-selected-items", %{"id" => id}, socket) do + selected_items = socket.assigns.selected_items + + item = Enum.find(socket.assigns.items, fn item -> to_string(primary_value(socket, item)) == to_string(id) end) + + updated_selected_items = + if Enum.member?(selected_items, item) do + List.delete(selected_items, item) + else + [item | selected_items] + end + + select_all = length(updated_selected_items) == length(socket.assigns.items) + + socket = + socket + |> assign(:selected_items, updated_selected_items) + |> assign(:select_all, select_all) + + {:noreply, socket} + end + + @impl Phoenix.LiveView + def handle_event("toggle-item-selection", _params, socket) do + select_all = not socket.assigns.select_all + + selected_items = + if select_all do + socket.assigns.items + else + [] + end + + socket = + socket + |> assign(:select_all, select_all) + |> assign(:selected_items, selected_items) + + {:noreply, socket} + end + + @impl Phoenix.LiveView + def handle_info({:put_assoc, {key, value} = _assoc}, socket) do + changeset = Ecto.Changeset.put_assoc(socket.assigns.changeset, key, value) + assocs = Map.get(socket.assigns, :assocs, []) |> Keyword.put(key, value) + + socket = + socket + |> assign(:assocs, assocs) + |> assign(:changeset, changeset) + + {:noreply, socket} + end + + @impl Phoenix.LiveView + def handle_info({:put_embed, {key, value} = _assoc}, socket) do + changeset = Ecto.Changeset.put_embed(socket.assigns.changeset, key, value) + embeds = Map.get(socket.assigns, :embeds, []) |> Keyword.put(key, value) + + socket = + socket + |> assign(:embeds, embeds) + |> assign(:changeset, changeset) + + {:noreply, socket} + end + + @impl Phoenix.LiveView + def handle_info({:update_changeset, changeset}, socket) do + {:noreply, assign(socket, :changeset, changeset)} + end + + @impl Phoenix.LiveView + def handle_info({"backpex:" <> event, item}, socket) do + event_prefix = socket.assigns.live_resource.config(:pubsub)[:event_prefix] + ^event_prefix <> event_type = event + + handle_backpex_info({event_type, item}, socket) + end + + @impl Phoenix.LiveView + def handle_info(_msg, socket) do + {:noreply, socket} + end + + defp handle_backpex_info({"created", _item}, socket) when socket.assigns.live_action in [:index, :resource_action] do + {:noreply, refresh_items(socket)} + end + + defp handle_backpex_info({"deleted", item}, socket) when socket.assigns.live_action in [:index, :resource_action] do + %{items: items} = socket.assigns + + if Enum.filter(items, &(to_string(primary_value(socket, &1)) == to_string(primary_value(socket, item)))) != [] do + {:noreply, refresh_items(socket)} + else + {:noreply, socket} + end + end + + defp handle_backpex_info({"updated", item}, socket) + when socket.assigns.live_action in [:index, :resource_action, :show] do + {:noreply, update_item(socket, item)} + end + + defp refresh_items(socket) do + %{ + live_resource: live_resource, + schema: schema, + params: params, + fields: fields, + query_options: query_options + } = socket.assigns + + filters = active_filters(socket.assigns) + valid_filter_params = get_valid_filters_from_params(params, filters, empty_filter_key()) + + count_criteria = [ + search: search_options(params, fields, schema), + filters: filter_options(valid_filter_params, filters) + ] + + item_count = Resource.count(fields, socket.assigns, live_resource, count_criteria) + %{page: page, per_page: per_page} = query_options + total_pages = calculate_total_pages(item_count, per_page) + new_query_options = Map.put(query_options, :page, validate_page(page, total_pages)) + + socket + |> assign(:item_count, item_count) + |> assign(:total_pages, total_pages) + |> assign(:query_options, new_query_options) + |> assign_items() + |> maybe_assign_metrics() + end + + defp update_item(socket, item) do + %{live_resource: live_resource, live_action: live_action} = socket.assigns + + item_primary_value = primary_value(socket, item) + item = Resource.get(item_primary_value, socket.assigns, live_resource) + + socket = + cond do + live_action in [:index, :resource_action] and item -> + items = + Enum.map(socket.assigns.items, &if(primary_value(socket, &1) == item_primary_value, do: item, else: &1)) + + assign(socket, :items, items) + + live_action == :show and item -> + assign(socket, :item, item) + + true -> + socket + end + + socket + end + + defp maybe_handle_item_action(socket, key) do + key = String.to_existing_atom(key) + action = socket.assigns.item_actions[key] + items = socket.assigns.selected_items + + if has_modal?(action.module) do + open_action_confirm_modal(socket, action, key) + else + handle_item_action(socket, action, key, items) + end + end + + defp open_action_confirm_modal(socket, action, key) do + init_change = action.module.init_change(socket.assigns) + changeset_function = &action.module.changeset/3 + + metadata = Resource.build_changeset_metadata(socket.assigns) + + changeset = + init_change + |> Ecto.Changeset.change() + |> changeset_function.(%{}, metadata) + + socket = + socket + |> assign(:item_action_types, init_change) + |> assign(:changeset_function, changeset_function) + |> assign(:changeset, changeset) + |> assign(:action_to_confirm, Map.put(action, :key, key)) + + {:noreply, socket} + end + + defp handle_item_action(socket, action, key, items) do + %{live_resource: live_resource} = socket.assigns + items = Enum.filter(items, fn item -> live_resource.can?(socket.assigns, key, item) end) + + socket + |> assign(action_to_confirm: nil) + |> assign(selected_items: []) + |> assign(select_all: false) + |> action.module.handle(items, %{}) + end + + defp primary_value(socket, item) do + primary_key = socket.assigns.live_resource.config(:primary_key) + + Map.get(item, primary_key) + end + @doc """ Subscribes to pubsub topic. """ @@ -1245,14 +1224,14 @@ defmodule Backpex.LiveResource do ## Examples - iex> Backpex.LiveResource.order_options_by_params(%{"order_by" => "field", "order_direction" => "asc"}, [field: %{}], %{by: :id, direction: :asc}, %{}, [:asc, :desc]) + iex> Backpex.LiveResource.order_options_by_params(%{"order_by" => "field", "order_direction" => "asc"}, [field: %{}], %{by: :id, direction: :asc}, %{}) %{order_by: :field, order_direction: :asc} - iex> Backpex.LiveResource.order_options_by_params(%{}, [field: %{}], %{by: :id, direction: :desc}, %{}, [:asc, :desc]) + iex> Backpex.LiveResource.order_options_by_params(%{}, [field: %{}], %{by: :id, direction: :desc}, %{}) %{order_by: :id, order_direction: :desc} - iex> Backpex.LiveResource.order_options_by_params(%{"order_by" => "field", "order_direction" => "asc"}, [field: %{orderable: false}], %{by: :id, direction: :asc}, %{}, [:asc, :desc]) + iex> Backpex.LiveResource.order_options_by_params(%{"order_by" => "field", "order_direction" => "asc"}, [field: %{orderable: false}], %{by: :id, direction: :asc}, %{}) %{order_by: :id, order_direction: :asc} """ - def order_options_by_params(params, fields, init_order, assigns, permitted_order_directions) do + def order_options_by_params(params, fields, init_order, assigns) do init_order = resolve_init_order(init_order, assigns) order_by = @@ -1269,13 +1248,15 @@ defmodule Backpex.LiveResource do |> Map.get("order_direction") |> maybe_to_atom() |> value_in_permitted_or_default( - permitted_order_directions, + permitted_order_directions(), Map.get(init_order, :direction) ) %{order_by: order_by, order_direction: order_direction} end + defp permitted_order_directions, do: ~w(asc desc)a + @doc """ Returns all orderable fields. A field is orderable by default. @@ -1372,6 +1353,8 @@ defmodule Backpex.LiveResource do def filter_options(_no_filters_present, _filter_configs), do: %{} + def empty_filter_key, do: :empty_filter + @doc """ Checks whether a field is orderable or not. @@ -1389,10 +1372,7 @@ defmodule Backpex.LiveResource do def orderable?(field) when is_nil(field), do: false def orderable?({_name, field_options}), do: Map.get(field_options, :orderable, true) - @doc """ - TODO: make private? - """ - def build_criteria(assigns) do + defp build_criteria(assigns) do %{ schema: schema, fields: fields, @@ -1561,14 +1541,7 @@ defmodule Backpex.LiveResource do if value in permitted, do: value, else: default end - @doc """ - Checks whether user is allowed to perform provided action or not - """ - def can?(assigns, action, item, module) do - module.can?(assigns, action, item) - end - - def default_item_actions do + defp default_item_actions do [ show: %{ module: Backpex.ItemActions.Show, @@ -1585,35 +1558,35 @@ defmodule Backpex.LiveResource do ] end - def maybe_put_empty_filter(%{} = filters, empty_filter_key) when filters == %{} do + defp maybe_put_empty_filter(%{} = filters, empty_filter_key) when filters == %{} do Map.put(filters, Atom.to_string(empty_filter_key), true) end - def maybe_put_empty_filter(filters, _empty_filter_key) do + defp maybe_put_empty_filter(filters, _empty_filter_key) do filters end @doc """ Returns list of filter options from query options """ - def get_filter_options(module, query_options) do + def get_filter_options(query_options) do query_options |> Map.get(:filters, %{}) - |> Map.drop([Atom.to_string(module.get_empty_filter_key())]) + |> Map.drop([Atom.to_string(empty_filter_key())]) end @doc """ Returns list of active filters. """ - def get_active_filters(module, assigns) do - filters = module.filters(assigns) + def active_filters(assigns) do + filters = assigns.live_resource.filters(assigns) Enum.filter(filters, fn {key, option} -> - module.get_empty_filter_key() != key and option.module.can?(assigns) + empty_filter_key() != key and option.module.can?(assigns) end) end - def get_valid_filters_from_params(%{"filters" => filters} = params, valid_filters, empty_filter_key) do + defp get_valid_filters_from_params(%{"filters" => filters} = params, valid_filters, empty_filter_key) do valid_filters = Keyword.put(valid_filters, empty_filter_key, %{}) filters = @@ -1633,7 +1606,7 @@ defmodule Backpex.LiveResource do Map.put(params, "filters", filters) end - def get_valid_filters_from_params(_params, _valid_filters, _empty_filter_key), do: %{} + defp get_valid_filters_from_params(_params, _valid_filters, _empty_filter_key), do: %{} defp maybe_to_atom(nil), do: nil defp maybe_to_atom(value), do: String.to_existing_atom(value)